Token Binding (SDK Layer)#

To bind authorization codes and refresh tokens to a DPoP key pair, configure token binding on HaapiConfiguration. The SDK Layer threads the binding through both the HAAPI flow and the OAuth token exchange automatically. For the concept and security trade-offs, see Token Binding .

Configuration#

let tokenBoundConfiguration = BoundedTokenConfiguration(
    keyPairType: CryptoKeyType.secureEnclave
)

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

keyPairType — where the DPoP key pair lives:

  • CryptoKeyType.secureEnclave — hardware-backed. The key is generated and held inside the Secure Enclave; the private key never leaves the enclave and cannot be extracted even with root access. Preferred when available.
  • CryptoKeyType.p256 — software-backed P256 key pair stored in the Keychain. Use as a fallback when the Secure Enclave is unavailable: App Extensions cannot access the enclave, some device states temporarily lock it, and simulators have no enclave. Less hardware-resistant but more reliable across edge cases.

BoundedTokenConfiguration vs UnboundedTokenConfiguration. The framework exposes both an explicit “bind tokens” and an explicit “don’t bind tokens” configuration. Omit tokenBoundConfiguration to default to unbounded; pass UnboundedTokenConfiguration() to be explicit. The explicit form makes intent visible in code review and prevents accidental binding when the server changes its setting.

Recovering from key-creation failures. If Secure Enclave rejects key generation (rare; documented as HaapiError.dpopKeyCreationFailure and dpopProofFailure), the recommended recovery is to reconstruct the configuration with keyPairType: .p256 and re-authenticate. See Error Handling (SDK Layer) .

Choosing a Configuration#

The client binding configuration must match the server’s issue-token-bound-authorization-code setting. Mismatches produce specific errors:

Server issue-token-bound-authorization-codeClient configurationResult
trueBound (BoundedTokenConfiguration / TokenBoundConfiguration)✅ Refresh token bound to the DPoP key
trueNo binding❌ Server returns invalid_dpop_proof on token exchange
falseBound✅ Refresh token bound on the client side anyway
falseNo binding✅ Refresh token not bound

When binding is enabled on either side, the OAuth token exchange must include the active DPoP proof — see “Exchanging the Code” below.

Exchanging the Code#

When binding is enabled, the OAuth token exchange must include the DPoP proof. On iOS this is explicit: pass haapiManager.dpop into OAuthTokenManager.fetchAccessToken. On Android the SDK threads the DPoP material through automatically when TokenBoundConfiguration is set on the configuration.

// Provide haapiManager.dpop when binding is enabled.
let dPoP = haapiManager.dpop

oAuthTokenManager.fetchAccessToken(with: code, dpop: dPoP) { [weak self] tokenResponse in
    self?.handleTokenResponse(tokenResponse)
}

For the full OAuth lifecycle (fetch, refresh, revoke), see OAuthTokenManager .

Omitting the DPoP proof on the token exchange when binding is enabled yields a server error invalid_dpop_proof. iOS callers must pass haapiManager.dpop explicitly; Android does this automatically when TokenBoundConfiguration is configured.

Was this helpful?