Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

Binding a token to a key

The printer's phone app keeps your photo connection on your phone. It holds an access token for the photo API and a refresh token for getting the next one. Token theft and replay followed the ways a token like that can escape, and ended with the alternative to a bearer token: a sender-constrained token, which works only for the party that can prove it holds a particular key.

Demonstrating Proof of Possession, usually shortened to DPoP and published as RFC 9449, is one standard way to build that binding. It works entirely in ordinary HTTP requests, which makes it practical for an app on a phone.

A token that needs its key

The outline is the one Token theft and replay sketched. The app creates a key pair before it asks for tokens. With its token request it sends a small signed JWT, called a DPoP proof, that contains its public key, and the authorization server records which public key the token belongs to. From then on, every request that uses the token must arrive with a fresh proof signed by the matching private key. It is the proof of possession from Secrets and key pairs, repeated on every call.

The private key never travels. It does not go to the authorization server or the photo API, and it does not appear in any token. Only the public key is sent, inside each proof, and a public key is no help to anyone trying to sign.

Giving the app a key pair

All domains, codes, and tokens in these examples are fictional. When you connect your photo account in the app, the app generates an elliptic curve key pair on the P-256 curve, which it will use with the ES256 signature algorithm. It keeps the private key in the platform's protected key storage, such as the Keychain on Apple devices or the Android Keystore, and marks it non-exportable where the platform allows. As Storing and using keys explained, the app can then ask for signatures but cannot read out the key itself, and neither can anything that copies the app's files.

The public half, written as a JSON Web Key, looks like this:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "DboN9G_lcWgZ3rxckKu35JwH2OYBLWJgxcT_-KEXK8Y",
  "y": "rj69LkdPmdnAbPoBq6mpcpbJPWLEQ6Qs2SaMMyKWBSk"
}

This public key was generated for these examples, and its private half was discarded, so nobody can sign with it.

Nothing about this key is registered in advance. Unlike a key registered for private_key_jwt, which the authorization server knows before any request arrives, a DPoP key can be created by any installation at any moment. That is why DPoP suits public clients. There is no secret to hide in the app package, because each copy of the app makes its own key on its own device.

It also means DPoP is not client authentication. Anyone can generate a key pair and send a proof. A valid proof shows that the request comes from the same holder as the one the token was issued to. It does not show that the holder is the genuine printer app, and the app remains a public client.

Asking for a bound token

After you approve the connection, the app exchanges its authorization code as usual, with one addition: a DPoP header carrying a proof, shortened here for display:

POST /token HTTP/1.1
Host: auth.photos.example
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYi...

grant_type=authorization_code
&code=demo-app-code-1
&redirect_uri=https%3A%2F%2Fapp.printer.example%2Foauth%2Fcallback
&code_verifier=demo_app_verifier_created_for_one_attempt_only_2026
&client_id=photo-printer-app

The authorization server checks the proof, then binds the tokens it issues to the public key inside it:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "demo-app-access-token-1",
  "token_type": "DPoP",
  "expires_in": 600,
  "refresh_token": "demo-app-refresh-token-1",
  "scope": "photos.read"
}

The token_type is the signal. DPoP tells the app that the access token is bound to its key and must be sent with a proof on every request. If the server answers Bearer instead, it has chosen not to bind the access token, and the token is an ordinary bearer token. A client that relies on the binding for its security must discard that response rather than carry on unprotected.

Because the app is a public client, the refresh token is bound to the same key. A refresh token copied from the phone is as useless without that key as the access token is.

Recording the key in the token

The photo API needs to know which key the token belongs to, without asking the app. The authorization server tells it inside the token. As in earlier lessons, demo-app-access-token-1 is a readable stand-in for an encoded JWT. Decoded, its claims are:

{
  "iss": "https://auth.photos.example",
  "sub": "user-2048",
  "aud": "https://api.photos.example",
  "client_id": "photo-printer-app",
  "scope": "photos.read",
  "iat": 1790845200,
  "exp": 1790845800,
  "jti": "demo-app-token-id-1",
  "cnf": {
    "jkt": "BVohHgHi0D9O5iBFRpVSFAukBjzgVbIRUjYuTsfJER8"
  }
}

The cnf claim is the confirmation claim that Token theft and replay mentioned: it names the key that a presenter of this token must prove it holds. Its jkt member is the JWK thumbprint of the app's public key: a SHA-256 hash of a fixed, canonical form of the key, defined in RFC 7638. A thumbprint is short, the same size for every key, and changes completely if any part of the key changes. Creating a DPoP proof builds the proof itself and computes this thumbprint step by step.

When the access token is opaque rather than a JWT, the API learns the same value from the authorization server instead. A token introspection response for a bound token carries the same cnf member with the same thumbprint.

Put the pieces together and a stolen token looks very different. Someone who copies demo-app-access-token-1 also needs a proof signed by a key whose thumbprint is BVohHgHi0D9O5iBFRpVSFAukBjzgVbIRUjYuTsfJER8. They can make a proof with a key of their own, but its thumbprint will not match, and the photo API will reject the request. The token has become one half of a credential, and the other half stays in the phone's key storage.

Try it in the Lab

PUT IT INTO PRACTICE

Check your understanding

Try these questions before moving on. If an answer isn't right, use the feedback and try again.

0 of 2 answered correctly

Enable JavaScript to answer these questions and save progress in this browser.

QUESTION 1 OF 2A debugging log captured the phone app's DPoP-bound access token. Why can't the person reading the log use it at the photo API?

QUESTION 2 OF 2Someone writes their own app, generates a key pair, and sends valid DPoP proofs to the photo service. What has DPoP established about them?

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

Learn identity