DPoP-bound tokens in your tenant
A bearer token works for anyone who holds it. With DPoP (RFC 9449), a token works only together with a fresh proof signed by the client's own key.
The same controls apply to BTL's own organization under BTL Admin, OAuth Management.
Proofs at the token endpoint
The client keeps a key pair of its own (ES256, PS256, RS256 or EdDSA (Ed25519); the lab toolkit makes one with btl-lab jwk). For each request it signs a proof, a JWT whose header has typ: dpop+jwt, the algorithm and the public key in jwk, and sends it in the DPoP header:
{"jti": "<random, new each time>", "htm": "POST", "htu": "<the URL called, without query>", "iat": <now>,
"nonce": "<when the tenant requires one>", "ath": "<at an API: the access token's SHA-256, base64url>"}A token request with a valid proof receives "token_type": "DPoP", and the access token is bound to the key's thumbprint (jkt, RFC 7638): a JWT access token carries cnf: {"jkt": ...}, and introspection reports the same cnf with token_type: DPoP. Every grant works this way. A request without a proof gets a bearer token, unless the client or tenant requires DPoP.
What is bound
- Access tokens issued with a proof are bound to its key.
- Authorization codes. Send
dpop_jkt(the key's thumbprint) in the authorization request, or send a proof with a pushed request, and the code can be redeemed only with a proof from that key. - Refresh tokens of public clients are bound to the key used when they were issued, so each refresh needs a proof from it. A confidential client already authenticates, so its refresh tokens are not bound, but each new access token is bound to the key of that request's proof.
- Token exchange. A bound subject or actor token is exchanged only with a proof from its key, so a stolen bound token cannot be traded for an unbound one.
- JWT bearer assertions. An assertion bound to a key with
cnf.jktis accepted only with a DPoP proof from that key; otherwise it is refused withinvalid_grant(assertion_dpop_mismatch). Other kinds of binding are refused, since they cannot be checked here.
Using a bound token
Send it as Authorization: DPoP <token> with a new proof for that request, whose ath is the token's hash. UserInfo, SCIM, the decision endpoints and the sample photo API all check it: a bound token sent as Bearer, a missing or wrong proof, or a proof from another key is refused with HTTP 401 and a WWW-Authenticate: DPoP challenge naming the error. A token that is not bound cannot use the DPoP scheme. Your own APIs check cnf.jkt against the proof's key the same way.
Tenant, client and resource settings
In Flow Policy, DPoP-bound tokens are Allowed by default; choose Required from every client, or Off, which ignores proofs and issues bearer tokens (tokens already bound stay bound). Required from every client leaves out the built-in Token Decoder, which cannot sign proofs. It does apply to every other connection, including the HR Simulator's SCIM client, which sends no proofs and so stops provisioning while DPoP is required for everyone. Longest proof age sets how old a proof's iat may be, from 30 to 600 seconds (300 by default). Require a server nonce makes the token endpoint and this organization's APIs answer a proof without the current nonce with use_dpop_nonce and a DPoP-Nonce header; the client puts that value in its next proof. Nonces change every five minutes.
Each client in Clients can Require DPoP-bound tokens (dpop_bound_access_tokens). Where DPoP is required, responses that return an access token straight from the authorization endpoint (token and the hybrid types that include it) are refused with unauthorized_client, since no proof travels with the browser, and such a client cannot be saved with those response types. dpop_jkt is refused while DPoP is Off, except from a client that requires DPoP itself or follows FAPI 2.0: those clients get only bound tokens whatever the Flow Policy says. Each resource in Resources can Accept only DPoP-bound tokens, which its metadata publishes as dpop_bound_access_tokens_required and the sample photo API enforces. A point-in-time restore keeps a client's or the tenant's DPoP requirement added after the restore point, but returns this resource setting to its value at that point, so check it after a restore. A client or resource cannot start requiring DPoP while Flow Policy has it Off, and Flow Policy cannot turn it Off while a client or resource requires it. Discovery and resource metadata list dpop_signing_alg_values_supported.
Refusals and history
The token endpoint answers invalid_dpop_proof (HTTP 400) or use_dpop_nonce. Audit records why, with the reasons dpop_proof_required, dpop_proof_invalid, dpop_proof_signature_invalid, dpop_proof_mismatch (method or URL), dpop_proof_expired, dpop_proof_replayed, dpop_nonce_required, dpop_key_mismatch and, at APIs, dpop_ath_mismatch, dpop_token_as_bearer, token_not_dpop_bound and dpop_required. Each proof works once: up to 100,000 used proofs are remembered until they could no longer be accepted, shown on the tenant's Usage view. When that store is full, the token and pushed request endpoints answer HTTP 429 temporarily_unavailable with Retry-After, and APIs answer the same way; each names the limit in Logs.