/images/resources/tutorials/haapi/haapi-mobile-security-lifecycle.png

HAAPI Mobile Security Lifecycle

On this page

HAAPI Security

This tutorial provides a summary of the OAuth lifecycle for a mobile app that uses the Hypermedia Authentication API (HAAPI). Some example messages are also provided. For the complete and definitive reference on the security design, see instead the HAAPI Whitepaper.

Zero Code Required

When integrating HAAPI into a mobile app, the UI SDK will handle all of the OAuth messages for you. The information provided here is only to promote understanding of your app's authentication behavior.

Mobile OAuth Threats

The traditional way for mobile apps to implement OAuth based user authentication is published in RFC8252. This involves the app running a code flow that uses the system browser, then listening for a response. The user can then authenticate in many ways, and this ensures that passwords are not exposed to applications.

AppAuth has some security concerns however. A malicious mobile app could potentially impersonate your app by using its client ID, then trick a user into signing in. The malicious app may be able to listen for responses in the same way as your app, then receive an authorization code and swap it for tokens. In addition, the browser itself is a source of threats that you need to protect against.

Client Attestation

When using HAAPI, the mobile app first proves its identity before authentication is allowed to begin. This prevents malicious apps from using your app's client ID. The flow starts when the UI SDK requests a challenge from the HAAPI endpoints of the Curity Identity Server:

http
1234
GET /oauth/v2/oauth-token/cat HTTP/1.1
Accept: application/json
 
client_id=haapi-android-ui-client

A challenge response is then received, containing some state for the client to sign:

json
123
{
"challenge": "eyJzdGF0ZSI6ImV5SnJhV1FpT2lJeE5UQXpPRGM..."
}

The SDK then takes the challenge and produces a challenge response. This uses the attestation features built into modern devices and the security involved is described in detail in the whitepaper. The end result is a challenge response that a malicious app could not produce. The challenge response is then sent to the API, where it can be cryptographically verified:

http
123456
POST /oauth/v2/oauth-token/cat HTTP/1.1
Accept: application/json
Content-Type: application/x-www-form-urlencoded
 
challenge=eyJzdGF0ZSI6ImV5SnJhV1FpT2lJeE5UQXpPRGM...
&challenge_response=...

At this point the API performs some simple validations, after which it issues a client attestation token (CAT) based on the challenge response. For a compliant device, the CAT payload will look similar to the following, to indicate that the device's boot state has been verified to be secure, and that it is using the expected hardware backed signing key.

text
123456789101112131415161718192021
{
"iat": 1674054131,
"exp": 1674054171,
"iss": "https://login.example.com/oauth/v2/oauth-anonymous",
"sub": "haapi-android-ui-client,
"aud": "https://login.example.com/oauth/v2/oauth-token",
"purpose": "cat",
"type": "android",
"data": {
"packageNames": [
"io.curity.demoapp"
],
"signatureKeys": [
"LVY9TfP6bKjNxCqyqYC2ayItIUtZk9rBkCTmyc219WY="
],
"verifiedBootState": true,
"securityLevel": 1,
"attestationErrors": []
},
"jkt": "YypaAy4bxzuRi0y8s8x4NhEjRFtFie42DDclBekhygg"
}

Client Authentication

Next, the CAT is used to authenticate the client, in a client assertion request to the token endpoint. At this point the SDK also uses the Demonstration of Proof-of-Possession (DPoP) standard to send proof of ownership of the same key that was used to sign the challenge response:

http
12345678910
POST /oauth/v2/oauth-token HTTP/1.1
Accept: application/json
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkV...
Content-Type: application/x-www-form-urlencoded
 
client_id=haapi-android-ui-client&
scope=urn%3Ase%3Acurity%3Ascopes%3Ahaapi&
grant_type=client_credentials&
client_assertion_type=urn%3Ase%3Acurity%3Aattestation%3Aclient&
client_assertion=eyJraWQiOiIxNTAzODc3OTgiLCJ4NXQiOiJ1SGdqS3...

The API then performs the detailed cryptographic validation described in the whitepaper. The exact checks depend on the configured authorization policy. In development environments, a low security policy will enable emulators to complete this request successfully. In production environments, an attempt by a malicious app to authenticate would be rejected with an error response similar to this:

json
1234
{
"error": "unsupported_device",
"error_description": "Invalid certificate"
}

An authentication access token (AAT) is then issued, as a JWT. An example payload is shown next. The JWT contains the special urn:se:curity:scopes:haapi scope, and also a cnf (confirmation) claim, representing a SHA-256 thumbprint of the attestation public key.

json
12345678910111213141516171819202122232425262728293031
{
"dpl": 0,
"sub": "haapi-android-ui-client",
"attestation": {
"packageNames": [
"io.curity.demoapp"
],
"signatureKeys": [
"LVY9TfP6bKjNxCqyqYC2ayItIUtZk9rBkCTmyc219WY="
],
"verifiedBootState": false,
"securityLevel": 0,
"attestationErrors": [],
"type": "android"
},
"purpose": "haapi",
"iss": "https://login.example.com/oauth/v2/oauth-anonymous",
"cal": "by-policy",
"aud": [
"https://login.example.com/oauth/v2/oauth-anonymous",
"https://login.example.com/authn/authentication"
],
"cap": "android-dev-policy",
"scope": "urn:se:curity:scopes:haapi",
"cnf": {
"jkt": "YypaAy4bxzuRi0y8s8x4NhEjRFtFie42DDclBekhygg"
},
"exp": 1674054731,
"iat": 1674054131,
"jti": "7f345398-b6bd-4d62-bfdf-77bd844f676d"
}

Authentication API Requests

The main authentication workflow begins when the UI SDK sends an OpenID Connect request to the authorization server. Some example parameters are shown below. The flow runs in an API driven manner, which eliminates man-in-the-browser (MITB) threats. There are no browser redirects, so a state parameter is not used. Similarly, no PKCE parameters are required, since an attacker cannot intercept the authorization code in an API driven flow.

http
123456789
GET /oauth/v2/oauth-authorize HTTP/1.1
Authorization: DPoP eyJraWQiOiIxNTAzODc3OTgiLCJ4...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6I...
Accept: application/vnd.auth+json
 
client_id=haapi-android-ui-client&
response_type=code&
redirect_uri=app%3A%2F%2Fhaapi&
scope=openid%20profile

A valid HAAPI access token must be sent, in the HTTP Authorization header, on every request from the UI SDK to the API endpoints. A new DPoP proof is also sent on every request, to prove that the caller still owns the attestation private key. The API then validates the DPoP proof against the cnf claim in the access token. If an attacker somehow intercepts the AAT they will be unable to use it, since they do not own the DPoP key.

User Authentication

A number of authentication steps will then be followed, to capture the user's identity. The Curity Identity Server provides many ways to authenticate, along with authentication actions for controlling data and behavior when working with identities. The following request shows credentials for a username and password flow being posted to the server:

http
1234567
POST /authn/authentication/Username-Password HTTP/1.1
Authorization: DPoP eyJraWQiOiIxNTAzODc3OTgiLCJ4NXQi...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2Iiwian...
Accept: application/vnd.auth+json
 
userName=demouser&
password=Password1

Eventually, when all authentication factors and logic have been applied, and you have proven the user's identity to the required level, an authorization code will be returned from the API, in a final hypermedia response:

json
1234567891011121314
{
"links": [{
"href": "app://haapi?code\u003dMjhFMsV8HrAmGvqEJUk5fE5Gx6hKOZ3i\u0026session_state\u003dSTpf8O%2B3fc1VjJVhpoaLKxbXDug0fbSE4AugtcdhmSM%3D.a6lzBi1jczar",
"rel": "authorization-response"
}],
"metadata": {
"viewName": "templates/oauth/success-authorization-response"
},
"type": "oauth-authorization-response",
"properties": {
"code": "MjhFMsV8HrAmGvqEJUk5fE5Gx6hKOZ3i",
"session_state": "STpf8O+3fc1VjJVhpoaLKxbXDug0fbSE4AugtcdhmSM\u003d.a6lzBi1jczar"
}
}

Once the SDK receives the authorization code, it stops using HAAPI until the next authentication flow. The UI SDK proceeds to swap the code for tokens, using an authorization code grant request. This HAAPI SDK also sends a DPoP proof, which it signs with the same key that was used during the HAAPI flow:

http
123456789
POST /oauth/v2/oauth-token HTTP/1.1
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2...
Content-Type: application/x-www-form-urlencoded
accept: application/json
 
client_id=haapi-android-ui-client&
grant_type=authorization_code&
code=MjhFMsV8HrAmGvqEJUk5fE5Gx6hKOZ3i&
redirect_uri=app%3A%2F%2Fhaapi

The mobile app then receives a set of tokens. The final access token issued to the mobile app is a normal opaque access token, used to call OAuth secured APIs in the standard way:

json
12345678
{
"id_token": "eyJraWQiOiIxNTAzODc3OTgiLCJ4NX ...",
"token_type": "bearer",
"access_token": "_0XBPWQQ_cb09730f-993f-4d3a-96bb-c1c651b17c92",
"refresh_token": "_1XBPWQQ_f34ae0b6-6036-4403-9a4a-14290bee70bc",
"scope": "openid profile",
"expires_in": 300
}

Token Refresh

The app can renew access tokens using a refresh token grant POST to the token endpoint. The HAAPI SDK also sends a DPoP proof, which it signs with the same key that it used during the HAAPI flow:

http
1234567
POST https://login.example.com/oauth/v2/oauth-token HTTP/1.1
DPoP: eyJqd2siOnsiY3J2IjoiUC0yNTYiLCJ5IjoiUm...
Content-Type: application/x-www-form-urlencoded
 
client_id=haapi-android-ui-client&
grant_type=refresh_token&
refresh_token=_1XBPWQQ_197003c2-704f-4475-923c-2b40e5f5d696

A HAAPI mobile client remains a public client, so no client credential is supplied when refreshing access tokens. Yet the DPoP proof protects the refresh token flow. If an attacker somehow steals a refresh token it will not be possible to successfully replay it, since the attacker will not be able to produce a cryptographically correct DPoP proof.

Mobile Connectivity

The Curity Identity Server defaults to the highest security behavior for token refresh, by also issuing a new refresh token. The next token refresh must then use the most recently issued refresh token. To ensure this behavior you should leave the Reuse Refresh Tokens option deactivated by default.

Yet if a user's mobile device has low connectivity the app could sometimes fail to receive the refresh token response. Upon retrying, the current refresh token stored in the app may be rejected, leading to a new user login. If your users experience this connectivity issue frequently when using a HAAPI-secured mobile app, you can safely disable refresh token rotation. The app's refresh token will then have a longer lifetime, but will remain protected by the DPoP proof.

DPoP Proofs

The Curity Identity Server requires Authorization Server Provided Nonces, as a mechanism to ensure short-lived DPoP proofs. For instances of the mobile client that pass attestation checks, the client attestation response includes a DPop-Nonce header that provides the nonce value to the client:

text
1
Dpop-Nonce: "1708333357#rjhudQ8RiN9UayluNozL7qwvHjbsEH4nr6FOd2kFj9M"

The HAAPI UI SDK issues this value as a nonce to DPoP JWTs for the duration of the HAAPI flow:

json
1234567
{
"jti": "3e6e28bce811ab4aee749f86ce212487",
"htm": "POST",
"htu": "https://login.example.com/oauth/v2/oauth-token",
"iat": 1708333359,
"nonce": "1708333357#rjhudQ8RiN9UayluNozL7qwvHjbsEH4nr6FOd2kFj9M"
}

Once the HAAPI flow completes, the same nonce value can be included in DPoP JWTs sent in grant requests for OAuth tokens. The nonce is a short-lived value that expires a few minutes after it is issued. When the authorization server receives a missing or expired nonce it returns a JSON response with the following fields and an HTTP 400 status. This response also provides a new server nonce to the client in the Dpop-Nonce response header.

json
1234
{
"error": "use_dpop_nonce",
"error_description": "use provided DPoP nonce"
}

The HAAPI UI SDK then retries the token request with the new nonce. These HTTP 400 responses are a normal part of the flow and not an error or warning event.

DPoP Nonce HTTP 400 Responses

Any log entries for HTTP 400 responses that return a new DPoP nonce to the client should not be treated as warnings or errors by monitoring systems.

Logout

The app must implement logout. Since authentication is API driven, no Single Sign On (SSO) cookie is issued when a user signs in. Therefore, to logout, the app simply needs to remove tokens from storage, then return the user to an unauthenticated view.

Password Considerations

When using HAAPI, a distinction is made between first-party and third-party applications. The former are used when the organization providing the mobile app also provides the authorization server and stores usernames and passwords there. In this case, exposing a username and password within the application does not reveal any new information.

Third-party passwords, such as a user's Google or Azure AD credentials, continue to be captured in the system browser, and the party providing the mobile app will never have access to them. The tutorials on password authentication flows and advanced authentication flows provide screenshots to show how the different types of password are managed.

Attestation Fallback

The attestation process summarized in this tutorial will fail for older Android devices, that were issued before version 8, since they lack the required hardware support. If you run into this scenario, and if there is sufficient user impact, you can provide a workaround via attestation fallback. See the following resources for further information:

Conclusion

This tutorial explained how your mobile app's OAuth lifecycle will work. Further information for developers is available in the HAAPI UI SDK documentation. The next tutorial focuses on the configuration and code changes needed for attestation fallback, which may be required if you need to support old or non-standard Android devices.

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