Skip to content

This repository has codes for a sample iOS application implementing the recommended Proof Key for Code Exchange (PKCE) for Singpass logins.

Notifications You must be signed in to change notification settings

singpass/iOS-Singpass-in-app-browser-login-demo

Repository files navigation

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.

Sequence Diagram

Sequence Diagram


*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. #

  • 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 with state and nonce 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 generated code_verifier along with state and nonce 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

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.

  2. Implement endpoint in receive authorization code, state and other required parameters.

  3. Register your new redirect_uri for your OAuth client_id

Potential changes/enhancements for RP Mobile App

  1. Integrate AppAuth library to handle launching of authorization endpoint webpage in an in app browser.

  2. Implement api call to RP Backend to request for code_challenge, code_challenge_method, state and nonce if required and other parameters.

  3. 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.

  • 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.

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.

<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.

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")")
    }
}

Permissions

Include camera permission in info.plist to allow Singpass Face Verification(SFV)

<key>NSCameraUsageDescription</key>
<string>To enable face verification</string>

Demo Video/s

MyInfo Mockpass Demo Singpass Demo
Myinfo Mockpass flow video Singpass flow video

FAQ

  • How do I know if I am using Safari, external web browser or WebView?

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
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
Safari browser Chrome browser

About

This repository has codes for a sample iOS application implementing the recommended Proof Key for Code Exchange (PKCE) for Singpass logins.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages