Token Binding#

Token binding cryptographically associates an authorization code and its resulting refresh token with a DPoP key pair held on the device. Once bound, a leaked token cannot be exchanged elsewhere — only a request that signs a fresh DPoP proof with the original key is accepted. This prevents token-replay attacks even if the device storage holding the refresh token is compromised in isolation.

How It Works#

The HAAPI access token is always DPoP-bound. When token binding is enabled for authorization codes and refresh tokens, the bind chain extends through the OAuth flow:

  1. The client generates a DPoP key pair on first use, typically backed by Secure Enclave on iOS or the Android Keystore.
  2. The HAAPI flow signs each request with a DPoP proof derived from that key.
  3. On reaching the authorization response, the client receives an authorization code bound to the same key.
  4. The token exchange must include a DPoP proof. The issued refresh token inherits the binding, as do all subsequent refresh tokens.

Server-Side Configuration#

Enable binding via the HAAPI client capability:

<capabilities>
    <haapi>
        <issue-token-bound-authorization-code>true</issue-token-bound-authorization-code>
    </haapi>
</capabilities>

Setting this requires use-legacy-dpop to be false (the default). When the server issues bound codes, the client must include the DPoP proof on the token request, or the server responds with invalid_dpop_proof.

Trade-Offs#

Bound tokens are dramatically harder to misuse, but the binding lifecycle adds complexity. Keys backed by Secure Enclave or StrongBox can become inaccessible after boot events, OS upgrades, or certain device states. When that happens, the refresh token must be discarded and the user re-authenticated. A fallback BoundedTokenConfiguration with a non-hardware key pair (such as P256 in software) trades some assurance for resilience.

Was this helpful?