Tokens & Sender Constraint
Token formats
| Token | Format | Default lifetime |
|---|---|---|
| Access token | Signed JWT, typ: at+jwt (RFC 9068) | issuer.access_token_ttl, 10m |
| ID token | Signed JWT | issuer.id_token_ttl, 10m |
| Refresh token | Opaque, server-side grant | issuer.refresh_token_ttl, 720h |
| Authorization code | Opaque, single use | issuer.auth_code_ttl, 60s |
A client can override the access, refresh and ID token lifetimes. Every token the server verifies must carry an expiry, and the server enforces the token's audience on its own resources.
Refresh tokens
A refresh token is issued only when the client may refresh and the user granted
offline_access. Refresh tokens rotate on every use. Presenting an already-used
refresh token revokes the whole family descended from it, and the refresh token's
binding to its client is checked before the token is consumed. Narrowing the scope on a
refresh narrows that token, not the underlying grant.
Revocation and introspection
/revoke (RFC 7009) revokes refresh tokens only; a revoked access token stays valid
until it expires (#91). /introspect (RFC 7662) reports a token's
state, including cnf and token_type for DPoP-bound tokens. Introspection is
caller-bound: only the confidential client a token was issued to can introspect it.
Public clients are refused, and a token belonging to another client is reported inactive.
A separate resource server therefore cannot use /introspect; it should validate the JWT
locally against the JWKS. Logging out and deleting
a user also retire the refresh tokens tied to them.
DPoP: sender-constrained tokens
DPoP (RFC 9449) binds a token to a key the client holds, so a stolen token is useless without that key.
- Proofs are verified once per request: signature,
htm,htu, theiatwindow, a server-issued nonce when the deployment uses nonces, and a replay cache forjti. Accepted proof algorithms are ES256, PS256/384/512 and RS256/384/512. - Binding. If the token request carries a proof, the issued access token carries
cnf.jkt(the key's thumbprint). This works for every grant: authorization code, refresh, client credentials and device code. - Early binding.
dpop_jkton/authorize,/parand/device_authorizationbinds the authorization code to the key before it exists. ADPoPheader sent to/paris ignored (#133); use thedpop_jktparameter there. The key requested at/authorizeis carried through sign-in and consent. - Enforcement. A bound token presented without a matching proof is refused at UserInfo, the admin API and client registration (RFC 9449 §7.1).
A client cannot yet require that all of its own tokens be sender-constrained
(dpop_bound_access_tokens, #138). The remaining DPoP work is tracked in #34.
Authorization request integrity
| Feature | Specification | Notes |
|---|---|---|
| Pushed authorization requests | RFC 9126 | POST /par returns a single-use request_uri. Set fapi.require_pushed_authorization_requests: true to require PAR for every client. |
| Signed request objects | RFC 9101 | A request passed by value, signed with ES256 or RS256 by a key at the client's jwks_uri. none and HMAC are refused. request_uri accepts PAR references only. |
| Rich authorization requests | RFC 9396 | Not usable yet. authorization_details is validated against the detail types a client is registered for, but no registration path can set those types, so every request carrying it is refused. Tracked in #36 and #59. |
| Resource indicators | RFC 8707 | resource at /authorize and /token restricts the token's audience. |
| Issuer identification | RFC 9207 | iss is returned on the authorization response. |
Signing keys
The server signs with ES256 (EC P-256) and RS256. EdDSA and PS256 are not offered
for signing. Configure keys with keys.provider:
memorygenerates keys at startup. It is for development only and is refused for an issuer that is not loopback.filereads PEM keys fromkeys.signing_key_path(EC) andkeys.rsa_signing_key_path(RSA). If only the EC key is configured, the deployment is ES256-only and discovery says so.
Each organization other than the main one has its own signing keys. They are stored in
the database, encrypted under keys.encryption_key.
Rotation is manual today. To rotate, publish the previous public keys through
keys.verification_key_paths and keys.rsa_verification_key_paths so tokens already
issued still verify, then swap the signing key. Rotating a tenant key without a restart
is tracked in #300.
FAPI 2.0 mode
An organization created with fapi_mode applies part of the FAPI 2.0 Security Profile:
- pushed authorization requests are required,
- PKCE with S256 is required,
- signing and request-object processing use ES256 only, and
- the authorization code lifetime is capped at 60 seconds.
It does not yet refuse shared-secret client authentication (#33) or require sender-constrained tokens (#54). The full profile is tracked in #35, and naming the switch after what it currently enforces is tracked in #57.