Migrating away from WebView for iOS Mobile app Singpass Logins
Usage of WebViews for web logins are not recommended due to security and usability reasons documented in RFC8252. Google has done the same 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) for Singpass logins. The application will demonstrate the Singpass login flow with PKCE leveraging on the iOS AppAuth library.
*RP stands for Relying Party
- 1a) Call RP Backend to obtain backend generate
code_challenge
,code_challenge_method
along withstate
andnonce
if required. # - 1b) RP Backend responds with the requested parameters. (
code_challenge
,code_challenge_method
,state
,nonce
) # - 2a) Open the Authorization endpoint in web browser via AppAuth providing query params of
redirect_uri
*,client_id
,scope
,code_challenge
,code_challenge_method
along withstate
andnonce
if required. There can be other query params provided if needed. e.g. (purpose_id
for myInfo use cases) - 2b) The
authorization code
will be delivered back to RP Mobile App. - 3a) RP Mobile App Upon reception of
authorization code
, proceed to relay the Authorization code back to RP Backend. # - 3b) RP Backend will use the
authorization code
along with the generatedcode_verifier
along withstate
andnonce
if required, and do client assertion to call the token endpoint to obtain ID/access tokens. - 3c) Token endpoint responds with the token payload to RP Backend.
- 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. #
* - 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 library.
# - It is up to the RP to secure the connection between RP Mobile App and RP Backend
- Implement endpoint to serve
code_challenge
,code_challenge_method
,state
,nonce
and other parameters needed for RP Mobile App to initiate the login flow. - Implement endpoint in receive
authorization code
,state
and other required parameters. - Register your new
redirect_uri
for your OAuth client_id
- Integrate AppAuth library to handle launching of authorization endpoint webpage in an in app browser.
- Implement api call to RP Backend to request for
code_challenge
,code_challenge_method
,state
andnonce
if required and other parameters. - Implement api call to send
authorization code
,state
and other needed parameters back to RP Backend.
- 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. - 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. - Strongly recommended to use either Android DeepLinks or iOS URL Schemes 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
. - 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.
- In the case where using use either Android DeepLinks or iOS URL Schemes 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
. - Do contact us if you face any issues adding your
redirect_uri
.
AppAuth iOS Library
pod 'AppAuth'
Configure a custom URL scheme for your app in Info.plist with redirect_uri
.
<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>
Set the necessary endpoints such as the redirect_uri
and service configuration endpoints issuer
, authorizationEndpoint
and tokenEndpoint
.
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"
]
The below code snippets OAuth authorization flow with AppAuth
Create the Oauth service configuration
// 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)
Create the OAuth authorization request
// 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)
}
}
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.
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")")
}
}
Include camera permission in info.plist to allow Singpass Face Verification(SFV)
<key>NSCameraUsageDescription</key>
<string>To enable face verification</string>
MyInfo Mockpass Demo | Singpass Demo |
---|---|
You can tell if the Singpass login page is being open in Safari by looking at the action sheet. In-app browsers using Safari includes features such as Reader, AutoFill, Fraudulent Website Detection, and content blocking.
Based on Apple's documentation:
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 |
---|---|
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 |
---|---|