Token Binding (Driver Layer)#

To bind authorization codes and refresh tokens to a DPoP key pair held on the device, configure token binding when constructing HaapiTokenManager. For the concept and security trade-offs, see Token Binding.

Configuration#

let tokenBoundConfiguration = BoundedTokenConfiguration(
    keyPairType: CryptoKeyType.secureEnclave
)

let haapiTokenManager = HaapiTokenManagerBuilder(
    tokenEndpoint: tokenEndpoint,
    clientId: clientId
)
.setTokenBoundConfiguration(config: tokenBoundConfiguration)
.build()

Use CryptoKeyType.secureEnclave when available; fall back to CryptoKeyType.p256 (software-backed) when Secure Enclave is not viable — some boot-state scenarios make the Secure Enclave key inaccessible.

val dpopTokenBinding = DpopTokenBindingConfiguration.Builder(context) {
    storage = StorageBuilder.encrypted(context) // default; or StorageBuilder.shared(context)
    keyType = DpopKeyPolicy.Auto                   // default; or Hardware / Software
}.build()

val haapiTokenManager = HaapiTokenManager.Builder(
    clientId = clientId,
    tokenEndpointUri = tokenEndpointUri
)
.setDpopTokenBinding(dpopTokenBinding)
.build()

DpopTokenBindingConfiguration.Builder(context) carries the key mode, storage, signing algorithm (ES256 by default), and time source in one object. With only an application Context it defaults to a hardware-backed key and an encrypted store — you no longer implement Storage yourself:

  • StorageBuilder — encrypted(context) (AES-GCM at rest under a non-exportable Keystore key; the default) or shared(context) (encrypted and readable across the app’s processes/components). Its Keystore key is provisioned on first construction, serialized across the app’s processes with a file lock so constructing the store from several processes at once still yields a single shared key.
  • DpopKeyPolicy — Auto (default; hardware-backed with a transparent software fallback), Hardware (fails closed with a typed error when no usable Keystore is present), or Software (encrypted software key).
  • haapiTokenManager.dpopKeyInUse — the key protection actually in use (Hardware, Software, or None), so an Auto fallback is observable rather than silent.

The legacy TokenBoundConfiguration / setTokenBoundConfiguration(...) path remains for backward compatibility but is deprecated in favor of the builder above. setDpopTokenBinding(...) and setTokenBoundConfiguration(...) are mutually exclusive — supplying both is rejected immediately. Switching an established session to the builder needs no re-authentication: a legacy key stored under the manager’s key-store alias is adopted in place automatically, and a custom legacy alias is adopted by setting migrateFromLegacyAlias on the builder.

Using the Bound Key at the Token Endpoint#

When binding is enabled, the OAuth token exchange must include a DPoP proof signed with the bound key. The Driver Layer surfaces the proof generator through haapiTokenManager.dpop (iOS) or haapiTokenManager.dpopHelper (Android), but the token-endpoint exchange itself happens at the SDK Layer through OAuthTokenManager. See OAuthTokenManager for the exchange call.

Omitting the DPoP proof on the token exchange when binding is enabled yields a server error invalid_dpop_proof. The client must always pass the active DPoP key when <issue-token-bound-authorization-code>true</issue-token-bound-authorization-code> is set on the server.

Was this helpful?