Swift iOS App using AppAuth
On this page
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.
Note
The example uses the Curity Identity Server, but you can run the code against any standards-based authorization server.
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:
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.
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:
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:
Aspect | Description |
---|---|
Navigation | Basic SwiftUI navigation is used, to swap out the main fragment based on the current values of data |
View Models | View models are used to exclude processing code from views and to manage data values |
HTTP Requests | Swift 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:
Property | Value |
---|---|
URL | https://localhost:6749/admin |
User | admin |
Password | Password1 |
The deployed system also includes a user account of demouser / Password1
that can be used to sign in:
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.
Pattern | Description |
---|---|
Builders | Builder classes are used to create OAuth request messages |
Callbacks | Callback functions are used to receive OAuth response messages |
Error Codes | Error 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:
The login process follows these important best practices from RFC8252:
Best Practice | Description |
---|---|
Login via System Browser | Logins use an ASWebAuthenticationSession window, meaning that the app itself never has access to the user's password |
PKCE | Proof 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 Parameter | Example Value |
---|---|
client_id | mobile-client |
redirect_uri | io.curity.client:/callback |
response_type | code |
state | _eB_JSonBtp9AasrJuQZWA |
nonce | _TT6iiN8eFeF7U6afzNNgQ |
scope | openid profile |
code_challenge | wmIZzT7QMPBLICXlvm19orboBMQnHKXGbMyyhfN8gPU |
code_challenge_method | S256 |
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 Parameter | Example Value |
---|---|
grant_type | authorization_code |
client_id | mobile-client |
code | Cg85vgCfElPckCgzPYDcZUrMekzA1iv5 |
code_verifier | ex_OaauEnB0cLdBwXUXypYxr4j2CrkPNfWOsdI_lNrKAdgL1c-bx-Uizzsgb-0Eio58ohD85zKjWqWQc2lvjSQ |
redirect_uri | io.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:
Data | Contains |
---|---|
Metadata | The Identity Server endpoints that AppAuth uses when sending OAuth request messages from the app |
Token Response | The 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 Parameter | Example Value |
---|---|
grant_type | refresh_token |
client_id | mobile-client |
refresh_token | 62a11202-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 Parameter | Example Value |
---|---|
id_token_hint | eyJraWQiOiIyMTg5NTc5MTYiLCJ4NXQiOiJCdEN1Vzl ... |
post_logout_redirect_uri | io.curity.client:/logoutcallback |
state | Ii8fYlMdVbX8fiMSmlI6SQ |
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 Parameter | Usage |
---|---|
prompt | Set prompt=login to force the user to re-authenticate immediately |
max-age | Set 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:
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:
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:
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:
Area | Description |
---|---|
Dynamic Client Registration | Authenticated DCR should be used, so that each instance of the mobile app gets a unique client ID and client secret |
HTTPS Redirect Responses | An 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.
Join our Newsletter
Get the latest on identity management, API Security and authentication straight to your inbox.
Start Free Trial
Try the Curity Identity Server for Free. Get up and running in 10 minutes.
Start Free Trial