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, on Security, Tokens & keys, 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 on Security, Tokens & keys, 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 |
Service or bank calling an API behind edge | A certificate-bound token on the edge mutual-TLS listener, or DPoP on the plain port. See the how-to |
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: Bearer on the mutual-TLS listener, certificate thumbprint equals the claim | Accepted |
cnf.x5t#S256 | Authorization: Bearer on the mutual-TLS listener, another certificate | Refused (certificate_bound_token_outside_session) |
cnf.x5t#S256 | Authorization header on the plain port | Refused (certificate_bound_token_outside_session). edge cannot see the caller’s certificate there |
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.
A token bound to a certificate is accepted from the caller only when edge runs the mutual-TLS listener (the mutual-tls block, MPORT), which reads the certificate off the handshake. The plain port keeps refusing it. Setup is on Calling an API with a certificate-bound token.
How auth treats edge at /userinfo
When an endpoint has fetchUserInfo on, edge calls auth’s /userinfo with the caller’s token. auth enforces the token’s binding there as strictly as edge does, and edge can supply neither the caller’s DPoP proof (it was addressed to edge) nor the caller’s certificate. So edge says that it has already verified the holder, and says it with its own certificate on the mutual-TLS listener of auth (native.auth-mutual-tls-url), not with a header.
auth takes such a call as an edge’s, and skips the DPoP and holder-certificate checks for that call only, when the certificate of the TLS session:
- names exactly one edge, as the URI subject alternative name
urn:versola:edge:<id>, - was signed by a certificate listed in
mutual-tls.edge-trusted-certificatesofauth(certificate authorities, valid now, checked at startup), and is a leaf, not a CA, - belongs to an edge that
centralstill lists (authreads edge ids and their assigned tenants with the rest of its configuration), and - belongs to an edge assigned to the tenant of the token’s client.
If any of these fails, the call is judged as any client’s.
The name alone proves nothing, because an issuer of client certificates can put any name in a certificate it signs. That is why the issuer must be a dedicated CA that issues edge certificates and nothing else, and why edge-trusted-certificates must also be among the listener’s trusted-certificates, so the handshake succeeds. Without edge-trusted-certificates, no certificate is an edge’s and /userinfo behaves as for any caller.
What is still checked for such a call: the token’s signature, its revocation and the openid scope. A token bound to both a key and a certificate is refused (invalid_token) even then. A certificate that arrives in a forwarded header is never taken for an edge’s. Every other caller, including every client, is held to the full binding checks.
The previous mechanism, a short-lived edge assertion in the Versola-Edge-Assertion header, is removed. central still serves the edge registry to auth, but with ids and assigned tenants only, no keys.
Security. The private key of an edge’s certificate is the most sensitive key in the deployment. Whoever holds it can call
/userinfoas that edge, for any token of the tenants assigned to that edge, without proof of possession: that is the blast radius of a stolen edge certificate. What limits it:
- The edge CA must be dedicated, never the CA of your clients. If
edge-trusted-certificatesnames a CA that also issues client certificates (docker-local, or aclient-certificatesbackend that shares a CA), the only thing that keeps a client from presenting an edge name iscentralrefusingurn:versola:edge:names in client certificates.authlogs a warning at startup when the listener trusts nothing else.- Short lifetimes. A certificate’s own validity cannot be shortened after issuance. The
vpsbundle issues edge certificates for 30 days andedgerenews them.- Deleting an edge in
central, or unassigning its tenants, cuts it off atauthafter the next configuration refresh (configuration-cache-refresh-intervalinauth.conf: one minute indocker-local, five elsewhere), not at expiry. Ifcentralis unreachableauthkeeps the last registry it had; with none loaded no certificate is an edge’s.- Make the mutual-TLS listener of
authreachable only from the network the edges run in, and rotate the edge CA like any other.
Configuration of the auth side is under mutual-tls. For a token that works behind edge, see DPoP or certificate-bound tokens for an API behind edge.