A sender-constrained access token is bound to a key or a certificate that the client holds. A thief who copies the token cannot use it without that key or certificate. Versola supports two mechanisms: DPoP (RFC 9449) and mTLS certificate-bound tokens (RFC 8705).
Why bearer tokens are risky
A bearer token works for whoever presents it. If it leaks from a log, a proxy, a browser extension or a crash report, the holder can call your API until it expires. Binding the token to a key moves the proof from “I have the string” to “I have the string and the private key”.
Tenants on the fapi2 security profile require every client to use one of the two mechanisms.
DPoP
With DPoP the client holds a key pair and signs a short proof JWT for each request.
What the client sends:
| Request | Headers |
|---|---|
Token request to auth | DPoP: <proof>. The access token in the response is bound to the proof’s key, as cnf.jkt |
API call through edge | Authorization: DPoP <access_token> and DPoP: <proof>. The proof carries htm, htu and ath (hash of the access token) |
How it is checked:
- Algorithms.
ES256andPS256by default.RS256is supported but not in the default set. RSA keys must be at least 2048 bits. htu.authbuilds the expected URL from the configured issuer, andedgefromedge-url. TheHostheader is never used. On the mTLS alias listener,htumust match the alias address.- Replay. A proof’s
jtiis remembered. Reusing it is refused. - Nonce. When a nonce is required, a proof without a fresh one gets
use_dpop_nonceand aDPoP-Nonceheader. The client retries with the nonce in the proof.authrequires it per tenant (requireDpopNonce, defaultfalse, in Challenges & Security, DPoP).edgerequires it when its Edges record has Require DPoP nonce enabled. - Where.
authverifies proofs at/tokenand/userinfo.edgeverifies them on proxied calls.
Configuration keys:
| Where | Key | Default | Meaning |
|---|---|---|---|
auth | dpop.iat-leeway | 60s | Allowed distance between a proof’s iat and now |
auth | dpop.nonce-ttl | 300s | How long an issued nonce is accepted |
edge | dpop.iat-leeway | set in edge config | Same, at the proxy |
edge | dpop.nonce-ttl | set in edge config | Same, at the proxy |
edge | dpop.nonce-salt | set in edge config | Secret input for nonce generation |
Client fields (Central API, Clients in the console):
| Field | Meaning |
|---|---|
dpopBoundAccessTokens | The client always uses DPoP. A token request without a proof is refused with invalid_dpop_proof |
dpopSigningAlgs | Narrows the proof algorithms for this client. An empty list adds no narrowing. It can only remove algorithms the server advertises in dpop_signing_alg_values_supported |
dpopMinRsaKeySize | Minimum RSA modulus in bits. Values below 2048 are refused at registration |
mTLS certificate-bound tokens
With mTLS the client authenticates with an X.509 certificate, and the access token carries the certificate’s SHA-256 thumbprint as cnf.x5t#S256. Introspection returns the same claim.
- Listener.
authserves/token,/introspect,/revoke,/parand/userinfoa second time on a separate TLS listener onMPORT(default8083) that requires a client certificate. The listener starts only whenauthconfig has amutual-tlsblock withcertificate,private-key,trusted-certificatesandexternal-url.trusted-certificateshas no default: without it, no certificate would be trusted. - Discovery. When the listener is on, the discovery document adds
mtls_endpoint_aliasespointing toexternal-url. Clients must call the alias endpoints for certificate-authenticated requests. - Proxy-terminated mTLS. If a reverse proxy terminates mTLS instead,
authreads the certificate from a header. Configure it per tenant in Challenges & Security, Mutual TLS:mtlsCertificateHeaderandmtlsCertificateEncoding(urlEncodedPemorbase64Der). Both are set or cleared together. A client that registers certificate authentication on a tenant without this setting is refused.
Client fields:
| Field | Meaning |
|---|---|
authMethod | tls_client_auth (certificate subject matched against the registered value) or self_signed_tls_client_auth (certificate key matched against the client’s jwks). Immutable after creation |
mtlsAuth | For tls_client_auth: subjectType (subject_dn, san_dns, san_uri, san_ip, san_email) and subjectValue |
certificateBoundAccessTokens | Binds tokens to the certificate for a client that authenticates another way. A client that authenticates with a certificate always gets bound tokens |
Exception: for a native client fronted by edge, the certificate belongs to edge and is shared by every installation, so its tokens are bound to the device DPoP key only and certificateBoundAccessTokens must be false.
How to choose
| Situation | Use |
|---|---|
| Mobile or desktop app | DPoP. The key lives in the device keystore |
| Service calling your API from a backend you control | tls_client_auth with certificate-bound tokens, or private_key_jwt with DPoP |
Browser app behind edge | Nothing to configure on the client. Tokens stay inside edge, which holds them in an EDGE_SESSION cookie session |
| You cannot run a client-certificate listener or proxy | DPoP |
How edge treats them
edge decides from the token’s cnf claim, not from the scheme the caller picked.
| Token | Presented as | Result |
|---|---|---|
cnf.jkt | Authorization: DPoP with a valid proof | Accepted |
cnf.jkt | Authorization: Bearer | Refused (dpop_downgrade). This holds even when edge has no dpop block |
cnf.x5t#S256 | EDGE_SESSION cookie | Accepted. The certificate is edge’s own |
cnf.x5t#S256 | Authorization header | Refused. edge cannot verify the caller’s certificate |
cnf.jkt and cnf.x5t#S256 | DPoP proof | Refused. A valid proof is not enough when the certificate half cannot be shown |
no cnf | Authorization: DPoP | Refused. A proof says nothing about an unbound token |
Removing the dpop block from edge config disables DPoP verification, but key-bound tokens over Bearer are still refused. A native app that goes through edge also requires the dpop block.