Swift iOS App using AppAuth

Swift iOS App using AppAuth

Code Examples / mobile-integration

This tutorial shows how to run a code example that implements mobile OpenID Connect in a Swift App using the open source AppAuth iOS library. We will handle all of the OAuth lifecycle events and also show how to handle error conditions reliably.

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 Xcode 12.5 or later, then clone the GitHub repository and open its folder:

iOS Project

The code example is a SwiftUI app and will therefore only run on up to date devices. It uses the following modern iOS coding techniques to implement AppAuth with simple code and good reliability:

AspectDescription
NavigationThe code example is a SwiftUI App with simple navigation to swap out the main fragment
UI UpdatesView models are used in a basic way to make reading and writing UI elements simpler
HTTP RequestsSwift Coroutines can simplify making HTTP requests and processing HTTP callbacks

Note however that the main OAuth integration is in the AppAuthHandler class and it should be able to adapt this code into any other style of iOS app.

Setup

Connectivity from Mobile Devices

If you are running the Curity Identity Server on your development computer over port 8443, it is recommended to use ngrok to create a working connection from an emulator or device. This can be done via the following simple script:

ngrok http 8443 -log=stdout &
sleep 5
BASE_URL=$(curl -s http://localhost:4040/api/tunnels | jq -r '.tunnels[] | select(.proto == "https") | .public_url')
echo $BASE_URL

A generated URL such as https://ef5540f40573.eu.ngrok.io can then be configured as the Base URL in the Admin UI. Also set the protocol under Deployments to HTTP, as discussed in the Exposing Curity Using ngrok article.

Mobile ngrok

The OAuth settings for the demo app are configured in the ApplicationConfig source file and this will need updating with the ngrok URL:

struct ApplicationConfig {
    issuer = "https://bae971bb9a64.eu.ngrok.io/oauth/v2/oauth-anonymous"
    redirectUri = "io.curity.client:/callback"
    let postLogoutRedirectUri = "io.curity.client:/logoutcallback"
    let scope = "openid profile"

Enable Dynamic Client Registration

In the Curity Identity Server, ensure that Dynamic Client Registration is configured with default options. For this initial mobile sample, select the No Authentication option for non-templatized clients:

Dynamic Client Setup

AppAuth Integration

Redirect Scheme Registration

AppAuth libraries are integrated into the code example using Swift Package Manager, and the Custom URI Scheme is registered in the info.plist file:

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

AppAuth Patterns

AppAuth usage 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

Dynamic Client Registration

Using DCR is recommended as a security best practice, in order to associate a unique instance of the app with each user. It potentially enables you to detect invalid usage patterns, such as multiple users signing in from the same device.

When the app is first run it downloads metadata from the Curity Identity Server’s metadata endpoint and then verifies that registration is enabled. The AppAuth builder classes then create the dynamic client registration request:

var extraParams = [String: String]()
extraParams["scope"] = self.config.scope
extraParams["requires_consent"] = "true"
extraParams["post_logout_redirect_uris"] = self.config.postLogoutRedirectUri

let nonTemplatizedRequest = OIDRegistrationRequest(
    configuration: metadata,
    redirectURIs: [redirectUri!],
    responseTypes: nil,
    grantTypes: [OIDGrantTypeAuthorizationCode],
    subjectType: nil,
    tokenEndpointAuthMethod: nil,
    additionalParameters: extraParams)

OIDAuthorizationService.perform(nonTemplatizedRequest, handleRegistrationResponseCallback)

This code results in a request message being sent with the following parameters. Additional request parameters can be sent to further refine behavior, as described in the Using Dynamic Client Registration article.

{
    "redirect_uris":["io.curity.client:/callback"],
    "post_logout_redirect_uris":["io.curity.client:/logoutcallback"],
    "application_type":"native",
    "grant_types":["authorization_code"],
    "scope":"openid profile"
}

The Identity Server then returns a payload that includes the Client ID and Client Secret the application instance should use for subsequent logins:

{
	"default_acr_values": ["urn:se:curity:authentication:html-form:Username-Password"],
	"application_type": "native",
	"registration_client_uri": "https://ef5540f40573.eu.ngrok.io/token-service/oauth-registration/87f1d9de-de2f-4e96-b27b-882cce0a3352",
	"registration_access_token_expires_in": 31536000,
	"registration_access_token": "e99ab41e-52a1-4fa3-b21b-fb95a398c33a",
	"client_id": "87f1d9de-de2f-4e96-b27b-882cce0a3352",
	"token_endpoint_auth_method": "client_secret_basic",
	"scope": "openid profile",
	"client_id_issued_at": 1624011134,
	"client_secret": "tfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc",
	"id_token_signed_response_alg": "RS256",
	"grant_types": ["authorization_code", "refresh_token"],
	"subject_type": "public",
	"redirect_uris": ["io.curity.client:/callback"],
    "post_logout_redirect_uris":["io.curity.client:/logoutcallback"],
	"client_secret_expires_at": 0,
	"token_endpoint_auth_methods": ["client_secret_basic", "client_secret_post"],
	"response_types": ["code", "id_token"],
	"refresh_token_ttl": 3600
}

The client is then created in a database record rather than being managed in the Admin UI. With the Curity Identity Server it is also possible to use Templatized DCR if you prefer an option that reduces duplication:

Database Clients

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. You can then create an account or sign in as an existing user:

iOS Password Login

The login process follows a number of best practices from RFC8252:

Best PracticeDescription
Login via System BrowserLogins use a 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
User Specific Client SecretEach instance of the mobile app uses its own client secret from the dynamic client registration response

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

let 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: registrationResponse.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 PKCE parameters:

Query ParameterExample Value
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
redirect_uriio.curity.client:/callback
response_typecode
state_eB_JSonBtp9AasrJuQZWA
nonce_TT6iiN8eFeF7U6afzNNgQ
scopeopenid profile
code_challengewmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU
code_challenge_methodS256

When needed it is possible to include additional runtime parameters, such as the acr_values query parameter to specify a particular 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:

var extraParams = [String: String]()
extraParams["client_secret"] = registrationResponse.clientSecret
let request = authResponse.tokenExchangeRequest(withAdditionalParameters: extraParams)

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

This results in a POST to the Curity Identity Server’s token endpoint, which includes PKCE parameters and the application instance’s unique client secret:

Form ParameterExample Value
grant_typeauthorization_code
codeCg85vgCfElPckCgzPYDcZUrMekzA1iv5
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
client_secrettfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc
code_verifierex_OaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdI_lNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ
redirect_uriio.curity.client:/callback

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

OAuth State

The demo app stores all of the following data in an instance of AppAuth’s AuthState class:

DataContains
MetadataAll of the endpoint locations that AppAuth uses when sending OAuth request messages
Registration DataThe response from the DCR request, containing the app instance’s client ID and secret
Token DataAn access token, refresh token and id token are all returned to the app

When the app exits it saves its registration data to the iOS Keychain so that the same Client ID and Client Secret are used when the app is restarted. Token data can also potentially be saved to secure storage, to avoid logins on every app restart.

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: registrationResponse.clientID,
            clientSecret: registrationResponse.clientSecret,
            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, which again includes the app instance’s unique client secret:

Form ParameterExample Value
grant_typerefresh_token
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
client_secrettfs9nad3dweFAa1CqpUj5p6NC4-092hb5Gg4SVRhOkc
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 == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.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
client_id87f1d9de-de2f-4e96-b27b-882cce0a3352
post_logout_redirect_uriio.curity.client:/logoutcallback
stateIi8fYlMdVbX8fiMSmlI6SQ
id_token_hinteyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl …

Techniques

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

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 their 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, then looked up to find the underlying cause:

Error Response

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

There are other options in the Curity Identity Server that can improve usability for mobile users, and Webauthn is an option worth exploring, where users authenticate via familiar mobile credentials, but strong security is used.

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, and these will be covered in separate code examples:

AreaDescription
Dynamic Client RegistrationAuthenticated DCR should be used, so that an access token is sent with regstration requests, and there are a few different ways to implement this
Receiving Redirect ResponsesAn HTTPS URL should be used to prevent a malicious app being able to impersonate your app, as recommended in the FAPI Read Profile for Native Apps

Hypermedia Authentication API

See 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 libraries, which manage 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.

Let’s Stay in Touch!

Get the latest on identity management, API Security and authentication straight to your inbox.

Keep up with our latest articles and how-tos using RSS feeds