Skip to content

Sender-constrained tokens

A bearer token opens what it opens for whoever presents it. A sender-constrained token is bound to something its client proves again every time it presents the token: a key it signs a proof with, DPoP (RFC 9449), or the certificate of its TLS connection, mutual TLS (RFC 8705). Whoever takes the token without what it is bound to holds a token that UserInfo refuses, and so does every resource server that checks the binding.

The two bindings

DPoP Mutual TLS
What the client proves A key, with a proof it signs for each request and sends in the DPoP header The certificate it presented at the TLS handshake
Which clients are bound Every client whose token request carries a proof A client that authenticates with its certificate, and a client that registered tls_client_certificate_bound_access_tokens
What the token names in cnf (RFC 7800 §3.1) jkt, the JWK SHA-256 thumbprint of the public key (RFC 7638 §3) x5t#S256, the SHA-256 thumbprint of the DER encoding of the certificate (RFC 8705 §3.1)
token_type of the token response and of introspection DPoP Bearer
How the token is presented Authorization: DPoP <token>, with a new proof that names the token in ath Authorization: Bearer <token>, on a connection that presents the same certificate

The binding is made at the token endpoint, by every grant. The access token of an authorization code, a refresh token, client credentials, a device code, a backchannel authentication, a token exchange and a JWT bearer grant is bound to what the token request proved. An ID token carries no binding.

Both shapes of access token carry it. An at+jwt carries cnf among its claims, and an opaque token keeps the same member in its record, which introspection returns. A resource server that verifies an at+jwt offline reads the binding in the token; one that introspects reads it in the answer.

A token is bound to one thing. A token request that proves a certificate the tokens would be bound to, and a DPoP key as well, is refused with invalid_dpop_proof, status 400, rather than arbitrated: a token names one thing in its cnf, and the provider does not choose for the client. A certificate presented only because the listener asked for one binds nothing (see mutual TLS), so a client that authenticates another way may send a DPoP proof over that connection.

Refresh tokens

The refresh token of a public client is bound to what the token request proved, key or certificate, and it is refreshed only by a request that proves the same thing again: any other request is answered invalid_grant. Such a token is not rotated when it is used, the binding being what tells it from a copy. A public client that binds nothing has its refresh token rotated at every use instead.

The refresh token of a confidential client is bound to nothing, since the client authenticates every time it refreshes. The access token a refresh issues is bound to what that refresh request proves, which may be a key the earlier tokens were not bound to.

Refresh tokens

A client that must bind

Two members of a registration refuse a token request rather than answer it with bearer tokens. Both are read at the token endpoint, after the client has authenticated and before the grant is read, and both refuse with invalid_request, status 400.

config/packages/beffroi.yaml
beffroi:
    clients:
        payments-app:
            # ...
            require_sender_constrained_tokens: true
Member What a token request of that client must prove Who sets it
require_sender_constrained_tokens A certificate its tokens are bound to, or a DPoP key, whichever (FAPI 2.0 Security Profile §5.3.1) The deployment, in the declaration of the client. A client that registers itself and sends it is refused with invalid_client_metadata
tls_client_certificate_bound_access_tokens The certificate of its connection, whatever way it authenticates (RFC 8705 §3.4) The declaration of the client, or the client itself when it registers, RFC 8705 §3.4 making it a member of a registration

Both default to false, and neither is a setting of the deployment as a whole.

Where the binding is checked

At a resource of the provider, which is UserInfo: a bound token is honoured only when the request proves again what it is bound to, and refused with invalid_token, status 401, otherwise. A resource server of your own makes the same check against the cnf it reads, as RFC 9449 §7.1 and RFC 8705 §3.2 describe; DPoP and mutual TLS say what each one compares.

What the provider announces. The discovery document always carries dpop_signing_alg_values_supported, the signature algorithms the provider is wired with. It carries tls_client_certificate_bound_access_tokens set to true because a client authentication method that proves a certificate is wired, which the bundle does. The bare challenge of UserInfo, answered to a request that carries no token at all, is what is read from the bindings themselves, both wired in the bundle: WWW-Authenticate: Bearer, DPoP.