OAuthTokenManager#
OAuthTokenManager handles the OAuth lifecycle after the HAAPI flow reaches OAuthAuthorizationResponseStep: fetching the first access token, refreshing it before it expires, and revoking the refresh token on logout. Obtain an instance from HaapiAccessor (created via the SDK Layer builders) and call it from the same authentication context that ran the flow.
Fetching the Access Token#
When the HAAPI flow completes, exchange the authorization code for a TokenResponse. Success cases carry an access token, refresh token, and expiry; error cases surface as ErrorTokenResponse.
let code = authResponseStep.oauthAuthorizationResponseProperties.code!
oAuthTokenManager.fetchAccessToken(with: code) { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
}
private func handleTokenResponse(_ tokenResponse: TokenResponse) {
switch tokenResponse {
case let .successfulToken(success):
let accessToken = success.accessToken
let refreshToken = success.refreshToken
// persist tokens to secure storage
case let .errorToken(errorTokenResponse):
// handle OAuth-protocol error
break
case let .error(anError):
// handle framework or transport error
break
}
} val code = authResponseStep.properties.code
GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val tokenResponse = oAuthTokenManager.fetchAccessToken(
authorizationCode = code,
onCoroutineContext = this.coroutineContext,
additionalParameters = emptyMap()
)
handleTokenResponse(tokenResponse)
}
private fun handleTokenResponse(tokenResponse: TokenResponse) {
when (tokenResponse) {
is SuccessfulTokenResponse -> {
val accessToken = tokenResponse.accessToken
val refreshToken = tokenResponse.refreshToken
// persist tokens to secure storage
}
is ErrorTokenResponse -> {
// handle OAuth-protocol error
}
}
} On React Native, fetchAccessToken returns a Promise. SDK failures (network, attestation) reject the Promise — OAuth-protocol errors come back inside the resolved TokenResponse. Catch both surfaces in the same async block:
import { isHaapiError } from 'identityserver.haapi.reactnative.sdk'
import type { OAuthAuthorizationResponseStep, TokenResponse } from 'identityserver.haapi.reactnative.sdk'
const step = response.representation as OAuthAuthorizationResponseStep
const code = step.oauthAuthorizationResponseProperties.code
try {
const tokenResponse = await accessor.oauthTokenManager.fetchAccessToken(code)
handleTokenResponse(tokenResponse)
} catch (e) {
if (isHaapiError(e)) {
// Network / attestation / DPoP / bridge failure — see Error Handling.
}
}
function handleTokenResponse(tokenResponse: TokenResponse) {
if (tokenResponse.responseType === 'success') {
const accessToken = tokenResponse.accessToken
const refreshToken = tokenResponse.refreshToken
// Persist to a secure store (expo-secure-store, react-native-keychain).
} else {
// tokenResponse.error / errorDescription — RFC 6749 OAuth error
}
} Store accessToken and refreshToken in secure storage — Keychain on iOS, EncryptedSharedPreferences or the Keystore on Android.
Successful Token Response Fields#
The success branch carries six fields. Only accessToken and expiresIn are guaranteed non-null — the rest depend on what the server returned for the configured scopes and grant.
| Field | iOS (SuccessfulTokenResponse) | Android (SuccessfulTokenResponse) | React Native (SuccessfulTokenResponse) | Notes |
|---|---|---|---|---|
accessToken | String | String | string | Required. The OAuth access token to attach to authenticated requests. |
expiresIn | Int | Int | number | Required. Lifetime of accessToken in seconds, counted from token issuance. |
refreshToken | String? | String? | string? | Present when the client is configured for a refresh-capable grant. |
tokenType | String? | String? | string? | Typically "DPoP" when binding is enabled, "Bearer" otherwise. |
scope | String? | String? | string? | Space-delimited list of scopes the server actually granted. May differ from what was requested. |
idToken | String? | String? | string? | Present when openid is in the granted scopes. |
UI-Layer integrations receive the same six fields under different type names — OAuthTokenModel on iOS, OauthModel.Token on Android — delivered through the flow-result handler rather than through OAuthTokenManager.fetchAccessToken. The fields and their semantics are identical.
Refreshing the Access Token#
When the access token expires, present the refresh token to obtain a fresh pair:
oAuthTokenManager.refreshAccessToken(with: "refresh_token") { [weak self] tokenResponse in
self?.handleTokenResponse(tokenResponse)
} GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val tokenResponse = oAuthTokenManager.refreshAccessToken(
refreshToken = "refresh_token",
onCoroutineContext = this.coroutineContext
)
handleTokenResponse(tokenResponse)
} const tokenResponse = await accessor.oauthTokenManager.refreshAccessToken('refresh_token')
handleTokenResponse(tokenResponse)Pass optional extra OAuth parameters as a second argument:
await accessor.oauthTokenManager.refreshAccessToken('refresh_token', {
scope: 'openid profile',
}) On invalid_grant, the SDK automatically clears any stored DPoP key pair so the next flow starts cleanly. The recommended UX response is to discard the held tokens and prompt the user to re-authenticate.
Revoking the Refresh Token#
To log the user out cleanly, revoke the refresh token. The SDK clears any associated DPoP key pair as part of revocation:
oAuthTokenManager.revokeRefreshToken("refresh_token") { result in
// handle success or error
} GlobalScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
try {
oAuthTokenManager.revokeRefreshToken(
refreshToken = "refresh_token",
onCoroutineContext = this.coroutineContext
)
// success — SDK has deleted any token-bound key pair
} catch (e: OAuthUnrecoverableException) {
// server returned a 4xx with a parsed OAuth error
}
} Two revoke methods, one each for refresh and access tokens. Both return Promise<void> on success and reject with a HaapiError on transport or OAuth-protocol failure:
import { isHaapiError } from 'identityserver.haapi.reactnative.sdk'
try {
await accessor.oauthTokenManager.revokeRefreshToken('refresh_token')
// success — SDK has deleted any token-bound key pair
} catch (e) {
if (isHaapiError(e)) {
// Server returned an error, or transport failed.
}
}
// Access tokens can also be revoked explicitly:
await accessor.oauthTokenManager.revokeAccessToken('access_token') Related Topics#
The DPoP-proof requirement when token binding is enabled and the raw-response listener pattern each get their own page:
- Token Binding covers how to pass
haapiManager.dpop(iOS) or rely on the automatic Android threading when binding is on. - Token Endpoint Response Listener covers capturing raw HTTP headers and avoiding double-handling.
How to implement this: iOS SDK · Android SDK · Error Handling (SDK Layer) · Token Endpoint Response Listener · Token Binding · Error Handling