How to Configure Token Binding#

You want to make a refresh-token theft useless without the corresponding private key on the original device. Token binding pins each authorization code and refresh token to a DPoP key pair on the device, so a token lifted from one client cannot be replayed from another.

For the concept and trade-offs, see Token Binding . For per-parameter depth, see Token Binding (SDK Layer) . This page is the end-to-end task walkthrough.

Prerequisites#

  • A working HAAPI integration (per Quickstart — iOS or Quickstart — Android ).
  • On the Curity Identity Server, the OAuth profile has issue-token-bound-authorization-code = true. The client and server token-binding settings must match; mismatches surface as invalid_dpop_proof at the token-exchange step.

Step-by-Step#

1. Pick the binding parameters#

  • iOS — choose keyPairType: .secureEnclave (hardware-backed; preferred) or .p256 (software-backed, App-Extension-safe fallback).
  • Android — choose keyPairAlgorithmConfig (ES256 is the recommended default), pick a stable keyAlias (e.g., "token-bound-key"), and supply a Storage implementation (SharedPreferences, EncryptedSharedPreferences, or a custom-keystore-backed store).
  • React Native — pick the same parameters per platform (CryptoKeyType for iOS, KeyPairAlgorithmConfig + StorageOption for Android) and pass them in the platform config records.

2. Wire it onto your builder#

The same binding type works from either layer — wire it at the layer where your app’s configuration lives.

SDK Layer#

let tokenBoundConfiguration = BoundedTokenConfiguration(
    keyPairType: CryptoKeyType.secureEnclave
)

let haapiConfiguration = HaapiConfiguration(
    // ...other configuration
    tokenBoundConfiguration: tokenBoundConfiguration
)

UI Layer#

HaapiUIKitConfigurationBuilder(...)
    .setTokenBoundConfiguration(
        configuration: BoundedTokenConfiguration(
            keyPairType: CryptoKeyType.secureEnclave
        )
    )
    .build()

3. (iOS, SDK Layer only) Pass the DPoP proof on token exchange#

At the SDK Layer on iOS, the token-exchange call is explicit — pass haapiManager.dpop to OAuthTokenManager.fetchAccessToken:

oAuthTokenManager.fetchAccessToken(with: code, dpop: haapiManager.dpop) { tokenResponse in
    // handle the response
}

On Android (SDK Layer) and at the UI Layer on either platform, the SDK threads the DPoP material through automatically once tokenBoundConfiguration is set.

Verify It Works#

Inspect the refresh token returned by a successful flow:

  • A bound refresh token carries "token_type": "DPoP" in the token-endpoint response.
  • The token exchange succeeds. If the server has issue-token-bound-authorization-code = true and the client omitted the binding, the exchange fails with invalid_dpop_proof.

For the full server/client matrix and what each combination produces, see the table in Token Binding (SDK Layer) .

Pitfalls#

  • Server-client mismatch. Setting issue-token-bound-authorization-code = true on the server but omitting tokenBoundConfiguration on the client returns invalid_dpop_proof at token exchange — silently, after the user authenticates. Configure both sides together.
  • Secure Enclave unavailable. App Extensions, simulators, and some device-locked states cannot access the Secure Enclave. Catch HaapiError.dpopKeyCreationFailure / dpopProofFailure and reconstruct with keyPairType: .p256 for the fallback path.
  • Android Storage is mandatory. Omitting it causes the SDK to lose its key-pair metadata across OS upgrades or biometric resets. EncryptedSharedPreferences is the recommended production choice; plain SharedPreferences works for development.
  • iOS App Extension binding. Use the .p256 software key on iOS App Extensions — DCAppAttestService (and thus the enclave-binding path) is unavailable in extension contexts.

Was this helpful?