-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathREADME.md
232 lines (185 loc) · 12.6 KB
/
README.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# Migrating away from [WebView](https://developer.apple.com/documentation/webkit/wkwebview) for iOS Mobile app Singpass Logins
Usage of WebViews for web logins are not recommended due to security and usability reasons documented in [RFC8252](https://www.rfc-editor.org/rfc/rfc8252). Google has done the [same](https://developers.googleblog.com/2021/06/upcoming-security-changes-to-googles-oauth-2.0-authorization-endpoint.html) for Google Sign-in in 2021.
> This best current practice requires that only external user-agents
like the browser are used for OAuth by native apps. It documents how
native apps can implement authorization flows using the browser as
the preferred external user-agent as well as the requirements for
authorization servers to support such usage.
*Quoted from RFC8252.*
This repository has codes for a sample iOS application implementing the recommended [Proof Key for Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636) for Singpass logins. The application will demonstrate the Singpass login flow with PKCE leveraging on the iOS [AppAuth](https://github.com/openid/AppAuth-iOS) library.
# Sequence Diagram

<br>
*RP stands for **Relying Party**
- 1a) Call **RP Backend** to obtain backend generate `code_challenge`, `code_challenge_method` along with `state` and `nonce` if required. #
<br><br>
- 1b) **RP Backend** responds with the requested parameters. (`code_challenge`, `code_challenge_method`, `state`, `nonce`) #
<br><br>
- 2a) Open the Authorization endpoint in web browser via [AppAuth](https://github.com/openid/AppAuth-iOS) providing query params of `redirect_uri`*, `client_id`, `scope`, `code_challenge`, `code_challenge_method` along with `state` and `nonce` if required. There can be other query params provided if needed. e.g. (`purpose_id` for myInfo use cases)
<br><br>
- 2b) The `authorization code` will be delivered back to **RP Mobile App**.
<br><br>
- 3a) **RP Mobile App** Upon reception of `authorization code`, proceed to relay the Authorization code back to **RP Backend**. #
<br><br>
- 3b) **RP Backend** will use the `authorization code` along with the generated `code_verifier` along with `state` and `nonce` if required, and do client assertion to call the token endpoint to obtain ID/access tokens.
<br><br>
- 3c) Token endpoint responds with the token payload to **RP Backend**.
<br><br>
- 3d) **RP Backend** process the token payload and does its required operations and responds to **RP Mobile App** with the appropriate session state tokens or data. #
<br><br>
​* - Take note that the `redirect_uri` should be a non-https url that represents the app link of the **RP Mobile App** as configured in the [AppAuth](https://github.com/openid/AppAuth-iOS) library.
​# - It is up to the RP to secure the connection between **RP Mobile App** and **RP Backend**
# Potential changes/enhancements for RP Backend
1. Implement endpoint to serve `code_challenge`, `code_challenge_method`, `state`, `nonce` and other parameters needed for **RP Mobile App** to initiate the login flow.
<br><br>
2. Implement endpoint in receive `authorization code`, `state` and other required parameters.
<br><br>
4. Register your new `redirect_uri` for your OAuth client_id
# Potential changes/enhancements for RP Mobile App
1. Integrate [AppAuth](https://github.com/openid/AppAuth-iOS) library to handle launching of authorization endpoint webpage in an in app browser.
<br><br>
1. Implement api call to **RP Backend** to request for `code_challenge`, `code_challenge_method`, `state` and `nonce` if required and other parameters.
<br><br>
1. Implement api call to send `authorization code`, `state` and other needed parameters back to **RP Backend**.
# Other Notes
- Please use the query param `app_launch_url` when opening the authorization endpoint webpage for iOS to enable Singpass App to return to RP mobile app automatically.
<br><br>
- Do **NOT** use the query param `app_launch_url` if an external web browser is used instead of in app browser when opening the authorization endpoint webpage for iOS.
<br><br>
- Strongly recommended to use either [Android DeepLinks](https://developer.android.com/training/app-links#deep-links) or [iOS URL Schemes](https://support.apple.com/en-sg/guide/shortcuts/apd621a1ad7a/6.0/ios/16.0) for your `redirect_uri`. This will prevent usability issues when external web browser redirects back to the RP Mobile App. An example of such a URI is: `sg.gov.singpass.app://ndisample.gov.sg/rp/sample`.
<br><br>
- Although the sample mobile application code in this repository provides an example of how to receive the token endpoint response from the RP Backend, RPs will need to cater for their own processing of the token response instead.
<br><br>
- In the case where using use either [Android DeepLinks](https://developer.android.com/training/app-links#deep-links) or [iOS URL Schemes](https://support.apple.com/en-sg/guide/shortcuts/apd621a1ad7a/6.0/ios/16.0) as the `redirect_uri` is not possible, an additional query parameter, `redirect_uri_https_type=app_claimed_https` should be added to the authorization endpoint when launching in the in-app browser. This applies only to direct Singpass logins, and not to Myinfo logins. An example of such a URI is: `https://stg-id.singpass.gov.sg/auth?redirect_uri=https%3A%2F%2Fapp.singpass.gov.sg%2Frp%2Fsample&client_id=ikivDlY5OlOHQVKb8ZIKd4LSpr3nkKsK&response_type=code&state=9_fVucO3cHJIIjR50wr2ctFPYIJLMt_NV6rvLBNQxlztWSCCWbCYMkesXdBC93lX&nonce=7d0c9f09-1c1a-400e-b026-77cc7bc89cd0&scope=openid&code_challenge=ZnRSoTcoIncnebg0mCqNT-E5fbRNQ8zcYkly52-qWxw&code_challenge_method=S256&redirect_uri_https_type=app_claimed_https`.
<br><br>
- Do contact us if you face any issues adding your `redirect_uri`.
# Implementation Details
## Required dependencies
AppAuth iOS Library
> pod 'AppAuth'
## Implementation
### In the Info.plist
Configure a custom URL scheme for your app in Info.plist with `redirect_uri`.
```xml
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>sg.ndi.sample</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sg.gov.singpass.app</string>
</array>
</dict>
</array>
</dict>
```
### In the ViewController
Set the necessary endpoints such as the `redirect_uri` and service configuration endpoints `issuer`, `authorizationEndpoint` and `tokenEndpoint`.
```swift
let kRedirectURI: String = "sg.gov.singpass.app://ndisample.gov.sg/rp/sample"
let serviceConfigEndpoints: [String: String] = [
"issuer": "https://test.api.myinfo.gov.sg",
"authorizationEndpoint": "https://test.api.myinfo.gov.sg/com/v4/authorize",
"tokenEndpoint": "https://test.api.myinfo.gov.sg/com/v4/token"
]
```
<br>
###
The below code snippets OAuth authorization flow with [AppAuth](https://github.com/openid/AppAuth-iOS)
<br>
Create the Oauth service configuration
```swift
// This is the dictionary that describes the current Oauth service
// This example is using the test environment for MyInfo Singpass login
let configuration = OIDServiceConfiguration(authorizationEndpoint: authURL, tokenEndpoint: tokenURL, issuer: issuerURL)
```
<br>
Create the OAuth authorization request
```swift
// code_challenge and code_challenge_method generated from RP Backend
// Set code_challenge for code_verifier as AppAuth library
// Set code_verifier as nil
// as we are not calling token endpoint from the mobile app
var request: OIDAuthorizationRequest {
var dict: [String: String] = [appLaunchURL: appLinkURL]
if myInfo {
// MyInfo Singpass login does not need nonce and state
// It needs purpose_id and has different scope values
dict["purpose_id"] = "demonstration"
return OIDAuthorizationRequest(configuration: configuration, // from the above section
clientId: clientID, // RP client_id
clientSecret: nil,
scope: "name", // myinfo_scope
redirectURL: redirectURI, // redirect_uri
responseType: OIDResponseTypeCode, // code
state: nil,
nonce: nil,
codeVerifier: nil,
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod,
additionalParameters: dict)
} else {
return OIDAuthorizationRequest(configuration: configuration, // from the above section
clientId: clientID, // RP client_id
clientSecret: nil,
scope: OIDScopeOpenID, // scope: openid
redirectURL: redirectURI, // redirect_uri
responseType: OIDResponseTypeCode, // code
state: state, // state generated from RP Backend
nonce: nonce, // nonce generated from RP Backend
codeVerifier: nil,
codeChallenge: codeChallenge,
codeChallengeMethod: codeChallengeMethod,
additionalParameters: dict)
}
}
```
<br>
Create the OAuth authorization service to perform authorization code exchange.
Upon reception of authorization code, proceed to relay the Authorization code back to the RP backend.
```swift
OIDAuthorizationService.present(request, presenting: self) { (response, error) in
if let response = response {
let authState = OIDAuthState(authorizationResponse: response)
self.setAuthState(authState)
printd("Authorization response with code: \(response.authorizationCode ?? "DEFAULT_CODE")")
self.sampleView.setAuthCode(response.authorizationCode)
if self.myInfo {
self.postAuthCode()
} else {
self.postAuthCode(nonce: request.nonce, state: request.state)
}
} else {
printd("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")")
}
}
```
## Permissions
Include camera permission in **info.plist** to allow Singpass Face Verification(SFV)
```
<key>NSCameraUsageDescription</key>
<string>To enable face verification</string>
```
<br>
## Demo Video/s
| MyInfo Mockpass Demo | Singpass Demo |
|---|---|
| <img src="myinfo_pkce.gif" alt="Myinfo Mockpass flow video" width="300px" height="600px"></img> | <img src="singpass_pkce.gif" alt="Singpass flow video" width="300px" height="600px"></img> |
## FAQ
- How do I know if I am using [Safari](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller), external web browser or [WebView](https://developer.apple.com/documentation/webkit/wkwebview)?
You can tell if the Singpass login page is being open in [Safari](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) by looking at the action sheet. In-app browsers using [Safari](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) includes features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking.
Based on Apple's documentation:
<br>
`The view controller includes Safari features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking. In iOS 9 and 10, it shares cookies and other website data with Safari. The user's activity and interaction with SFSafariViewController are not visible to your app, which cannot access AutoFill data, browsing history, or website data. You do not need to secure data between your app and Safari. If you would like to share data between your app and Safari in iOS 11 and later, so it is easier for a user to log in only one time, use ASWebAuthenticationSession instead`
| Safari In-app Browser | Webview |
|---|---|
| <img src="safari_reader_and_content_blocking.jpeg" alt="Safari in-app browser" width="300px" height="600px"></img> | <img src="webview.png" alt="Webview" width="300px" height="600px"></img> |
<br>
You can tell if the Singpass login page is opened in a external web browser by looking for the editable address bar. Below are 2 examples.
| Safari Browser | Chrome Browser |
|----------------|----------------|
| <img src="safari_browser.png" alt="Safari browser" width="300px" height="600px"></img> | <img src="chrome_browser.jpeg" alt="Chrome browser" width="300px" height="600px"></img> |