Swift iOS App using AppAuth

Swift iOS App using AppAuth

This tutorial shows how to run a code example that implements mobile OpenID Connect in a Swift App according to RFC8252, using the open source AppAuth iOS library. The demo app shows how to handle all of the OAuth lifecycle events and also how to use error details returned from the library.

Overview

The code example is a simple iOS App with two views, the first of which is an Unauthenticated View to handle processing related to signing the user in:

Unauthenticated View

Once signed in the app switches to an Authenticated View, to simulate screens in real apps that work with access tokens and call APIs. This view presents details about tokens and also allows token refresh and logout operations to be tested.

Authenticated View

Get the Code

First ensure that you are running an up-to-date version of Xcode, then clone the GitHub repository and open the app subfolder:

iOS Project

The code example is a SwiftUI app and will therefore only run on up to date devices, though the main OAuth integration is done in the AppAuthHandler class, which should be easy to adapt into older iOS platforms if needed. The example also uses the following iOS coding techniques to implement AppAuth with clean code:

AspectDescription
NavigationBasic SwiftUI navigation is used, to swap out the main fragment based on the current values of data
View ModelsView models are used to exclude processing code from views and to manage data values
HTTP RequestsSwift Coroutines are used to implement non blocking code when sending HTTP requests and receiving HTTP responses

Quick Start

The easiest way to run the code example is to point it to a deployed and preconfigured instance of the Curity Identity Server, running in Docker. This is done via a script included with the example that is explained in the Mobile Setup how-to:

./start-idsvr.sh

The result is to provide a working internet URL for the Curity Identity Server, such as https://baa467f55bc7.eu.ngrok.io, ready for the mobile app to connect to. The Admin UI is also available and can be accessed using the following details:

PropertyValue
URLhttps://localhost:6749/admin
Useradmin
PasswordPassword1

The deployed system also includes a user account of demouser / Password1 that can be used to sign in:

iOS Password Login

Configuration

The code example uses a configuration file at ./ios/config.json and contains the following OAuth settings. If you are using the above quick start, it will automatically be updated with the Curity Identity Server base URL, or you can provide the base URL of your own system if you prefer:

{
  "issuer": "https://baa467f55bc7.eu.ngrok.io/oauth/v2/oauth-anonymous",
  "clientID": "mobile-client",
  "redirectUri": "io.curity.client:/callback",
  "postLogoutRedirectUri": "io.curity.client:/logoutcallback",
  "scope": "openid profile"
}

The code example requires an OAuth client that uses the Authorization Code Flow (PKCE) and its full XML is shown below:

<client>
    <id>mobile-client</id>
    <client-name>mobile-client</client-name>
    <no-authentication>true</no-authentication>
    <redirect-uris>io.curity.client:/callback</redirect-uris>
    <proof-key>
        <require-proof-key>true</require-proof-key>
    </proof-key>
    <refresh-token-ttl>3600</refresh-token-ttl>
    <scope>openid</scope>
    <scope>profile</scope>
    <user-authentication>
        <allowed-authenticators>Username-Password</allowed-authenticators>
        <allowed-post-logout-redirect-uris>io.curity.client:/logoutcallback</allowed-post-logout-redirect-uris>
    </user-authentication>
    <capabilities>
        <code>
        </code>
    </capabilities>
    <validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
</client>

AppAuth Integration

AppAuth libraries are integrated into the code example using Swift Package Manager, and the Custom URI Scheme is registered in the info.plist file. This scheme is used by the code example for both login and logout redirects:

<dict>
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>io.curity.client</string>
			</array>
		</dict>
	</array>

AppAuth coding is based around a few key patterns that will be seen in the following sections and which are explained in further detail in the iOS AppAuth Documentation.

PatternDescription
BuildersBuilder classes are used to create OAuth request messages
CallbacksCallback functions are used to receive OAuth response messages
Error CodesError codes can be used to determine particular failure causes

OAuth Lifecycle

Login Redirects

When the login button is clicked, a standard OpenID Connect authorization redirect is triggered, which then presents a login screen from the Identity Server:

iOS Password Login

The login process follows these important best practices from RFC8252:

Best PracticeDescription
Login via System BrowserLogins use an ASWebAuthenticationSession window, meaning that the app itself never has access to the user's password
PKCEProof Key for Code Exchange prevents malicious apps being able to intercept redirect responses

Authorization redirects are triggered by building an OpenID Connect authorization request message and running it on an ASWebAuthenticationSession window:

var extraParams = [String: String]()
extraParams["acr_values"] = "urn:se:curity:authentication:html-form:Username-Password"

let scopesArray = self.config.scope.components(separatedBy: " ")
let request = OIDAuthorizationRequest(
    configuration: metadata,
    clientId: config.clientID,
    clientSecret: nil,
    scopes: scopesArray,
    redirectURL: redirectUri!,
    responseType: OIDResponseTypeCode,
    additionalParameters: extraParams)

let agent = OIDExternalUserAgentIOS(presenting: viewController)
self.userAgentSession = OIDAuthorizationService.present(request, handleAuthorizationResponse)

The message generated will have query parameters similar to those in the following table, and will include the code_challenge PKCE parameters:

Query ParameterExample Value
client_idmobile-client
redirect_uriio.curity.client:/callback
response_typecode
state_eB_JSonBtp9AasrJuQZWA
nonce_TT6iiN8eFeF7U6afzNNgQ
scopeopenid profile
code_challengewmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU
code_challenge_methodS256

When needed the library enables the app to customize OpenID Connect parameters. An example is to use the acr_values query parameter to specify a particular runtime authentication method.

Login Completion

After the user has successfully authenticated, an authorization code is returned in the response message, which is then redeemed for tokens. In the demo app this response is returned to the unauthenticated view, which then runs the following code to complete authorization:

let extraParams = [String: String]()
let request = authResponse.tokenExchangeRequest(withAdditionalParameters: extraParams)

OIDAuthorizationService.perform(
    request!,
    originalAuthorizationResponse: authResponse) { tokenResponse, handleTokenExchangeCallback }

This sends an authorization code grant message, which is a POST to the Curity Identity Server's token endpoint with these parameters, including the code_verifier PKCE parameter:

Form ParameterExample Value
grant_typeauthorization_code
client_idmobile-client
codeCg85vgCfElPckCgzPYDcZUrMekzA1iv5
code_verifierex_OaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdI_lNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ
redirect_uriio.curity.client:/callback

When login completes successfully, a property is set that results in the main SwiftUI view rendering the authenticated view. The user can potentially cancel the ASWebAuthenticationSession window, and the demo app handles this condition by remaining in the unauthenticated view so that the user can retry signing in.

OAuth State

The demo app stores the following information in an ApplicationStateManager helper class, which uses the AppAuth library's AuthState class:

DataContains
MetadataThe Identity Server endpoints that AppAuth uses when sending OAuth request messages from the app
Token ResponseThe access token, refresh token and ID token that are returned to the app

Using and Refreshing Access Tokens

Once the code is redeemed for tokens, most apps will then send access tokens to APIs as a message credential, in order for the user to be able to work with data. With default settings in the Curity Identity Server the access token will expire every 15 minutes. You can use the refresh token to silently renew an access token with the following code:

let request = OIDTokenRequest(
            configuration: metadata,
            grantType: OIDGrantTypeRefreshToken,
            authorizationCode: nil,
            redirectURL: nil,
            clientID: config.clientID,
            clientSecret: nil,
            scope: nil,
            refreshToken: refreshToken,
            codeVerifier: nil,
            additionalParameters: nil)

OIDAuthorizationService.perform(request, handleTokenResponse)

This results in a POST to the Curity Identity Server's token endpoint, including the following payload fields:

Form ParameterExample Value
grant_typerefresh_token
client_idmobile-client
refresh_token62a11202-4302-42e1-983e-b26362093b67

Eventually the refresh token will also expire, meaning the user's authenticated session needs to be renewed. This condition is detected by the code example, which checks for an invalid_grant error code in the token refresh error response:

if error.domain == OIDOAuthTokenErrorDomain && error.code == OIDErrorCodeOAuth.invalidGrant.rawValue {
}

End Session Requests

The user can also select the Sign Out button to end their authenticated session early. This results in an OpenID Connect end session redirect on the ASWebAuthenticationSession window, triggered by the following code:

let request = OIDEndSessionRequest(
    configuration: metadata,
    idTokenHint: idToken,
    postLogoutRedirectURL: postLogoutRedirectUri!,
    additionalParameters: nil)

let agent = OIDExternalUserAgentIOS(presenting: viewController)
self.userAgentSession = OIDAuthorizationService.present(request, externalUserAgent: agent!, handleEndSessionResponse)

The following query parameters are sent, which signs the user out at the Identity Server, removes the SSO cookie from the system browser, then returns to the app at the post logout redirect location:

Query ParameterExample Value
id_token_hinteyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl ...
post_logout_redirect_uriio.curity.client:/logoutcallback
stateIi8fYlMdVbX8fiMSmlI6SQ

Logout Alternatives

It can sometimes be difficult to get the exact behavior desired when using end session requests. A better option is usually to just remove tokens from the app and return the app to the unauthenticated view. Subsequent sign in behavior can then be controlled via the following OpenID Connect fields. This can also be useful when testing, in order to sign in as multiple users on the same device:

OpenID Connect Request ParameterUsage
promptSet prompt=login to force the user to re-authenticate immediately
max-ageSet max-age=N to specify the maximum elapsed time in seconds before which the user must re-authenticate

Extending Authentication

Once AppAuth has been integrated it is then possible to extend authentication by simply changing the configuration of the mobile client in the Curity Identity Server, without needing any code changes in the mobile app. Webauthn is an option worth exploring, where users authenticate via familiar mobile credentials, but strong security is used.

Techniques

Storing Tokens

In order to prevent the need for a user login on every app restart, an app can potentially use the device's features for secure storage, and save tokens from the AuthState class to mobile secure storage, such as the iOS Keychain.

Password Autofill

To avoid asking users to frequently type passwords on small mobile keyboards, you may want to use password autofill features, when the user has enabled it on the device. By default the ASWebAuthenticationSession window is abruptly dismissed after the user submits credentials, so the Save Password prompt cannot be selected. One way to resolve this is to activate user consent for the client, so that the browser remains active:

Save Password

Error Handling

AppAuth libraries provide good support for returning the standard OAuth error and error_description fields, and error objects also contain type and code numbers that correlate to the iOS Error Definitions File. The code example ensures that all four of these fields are captured, so that they can be displayed or logged in the event of unexpected failures:

Error Response

Logging

The example app writes some debug logs containing AppAuth response details. Of course a real app should not log secure fields in this manner, and the example only does so for educational purposes:

Example Logging

Financial Grade

The initial iOS code example would need extending in a couple of areas in order to fully meet Curity's Mobile Best Practices:

AreaDescription
Dynamic Client RegistrationAuthenticated DCR should be used, so that each instance of the mobile app gets a unique client ID and client secret
HTTPS Redirect ResponsesAn HTTPS URL should be used to guarantee that a malicious app cannot impersonate your app, as recommended in the FAPI Read Profile for Native Apps

Hypermedia Authentication API

See also the iOS HAAPI Mobile Sample for an alternative financial grade solution, which implements OpenID Connect with standard messages but also provides these features:

  • Client attestation strongly verifies the app's identity before allowing authentication attempts
  • For most forms of login the system browser is not used, so browser risks are eliminated
  • The app can render its own forms during the authentication workflow, for control over the user experience

Conclusion

OpenID Connect can be implemented fairly easily in an iOS app by integrating the AppAuth library, which manages OAuth requests and responses in the standard way. Once integration is complete, the app can potentially use many other forms of authentication and multiple factors, with zero code changes.