Sender-constrained tokens — Versola Docs
VersolaVersola/docs
Pre-1.00.7.1versola.kzGitHub

Sender-constrained tokens

How DPoP and mTLS certificate-bound access tokens work in Versola, which fields and config keys control them, and how edge enforces them.

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:

RequestHeaders
Token request to authDPoP: <proof>. The access token in the response is bound to the proof’s key, as cnf.jkt
API call through edgeAuthorization: DPoP <access_token> and DPoP: <proof>. The proof carries htm, htu and ath (hash of the access token)

How it is checked:

  • Algorithms. ES256 and PS256 by default. RS256 is supported but not in the default set. RSA keys must be at least 2048 bits.
  • htu. auth builds the expected URL from the configured issuer, and edge from edge-url. The Host header is never used. On the mTLS alias listener, htu must match the alias address.
  • Replay. A proof’s jti is remembered. Reusing it is refused.
  • Nonce. When a nonce is required, a proof without a fresh one gets use_dpop_nonce and a DPoP-Nonce header. The client retries with the nonce in the proof. auth requires it per tenant (requireDpopNonce, default false, in Challenges & Security, DPoP). edge requires it when its Edges record has Require DPoP nonce enabled.
  • Where. auth verifies proofs at /token and /userinfo. edge verifies them on proxied calls.

Configuration keys:

WhereKeyDefaultMeaning
authdpop.iat-leeway60sAllowed distance between a proof’s iat and now
authdpop.nonce-ttl300sHow long an issued nonce is accepted
edgedpop.iat-leewayset in edge configSame, at the proxy
edgedpop.nonce-ttlset in edge configSame, at the proxy
edgedpop.nonce-saltset in edge configSecret input for nonce generation

Client fields (Central API, Clients in the console):

FieldMeaning
dpopBoundAccessTokensThe client always uses DPoP. A token request without a proof is refused with invalid_dpop_proof
dpopSigningAlgsNarrows 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
dpopMinRsaKeySizeMinimum 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. auth serves /token, /introspect, /revoke, /par and /userinfo a second time on a separate TLS listener on MPORT (default 8083) that requires a client certificate. The listener starts only when auth config has a mutual-tls block with certificate, private-key, trusted-certificates and external-url. trusted-certificates has no default: without it, no certificate would be trusted.
  • Discovery. When the listener is on, the discovery document adds mtls_endpoint_aliases pointing to external-url. Clients must call the alias endpoints for certificate-authenticated requests.
  • Proxy-terminated mTLS. If a reverse proxy terminates mTLS instead, auth reads the certificate from a header. Configure it per tenant in Challenges & Security, Mutual TLS: mtlsCertificateHeader and mtlsCertificateEncoding (urlEncodedPem or base64Der). Both are set or cleared together. A client that registers certificate authentication on a tenant without this setting is refused.

Client fields:

FieldMeaning
authMethodtls_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
mtlsAuthFor tls_client_auth: subjectType (subject_dn, san_dns, san_uri, san_ip, san_email) and subjectValue
certificateBoundAccessTokensBinds 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

SituationUse
Mobile or desktop appDPoP. The key lives in the device keystore
Service calling your API from a backend you controltls_client_auth with certificate-bound tokens, or private_key_jwt with DPoP
Browser app behind edgeNothing 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 proxyDPoP

How edge treats them

edge decides from the token’s cnf claim, not from the scheme the caller picked.

TokenPresented asResult
cnf.jktAuthorization: DPoP with a valid proofAccepted
cnf.jktAuthorization: BearerRefused (dpop_downgrade). This holds even when edge has no dpop block
cnf.x5t#S256EDGE_SESSION cookieAccepted. The certificate is edge’s own
cnf.x5t#S256Authorization headerRefused. edge cannot verify the caller’s certificate
cnf.jkt and cnf.x5t#S256DPoP proofRefused. A valid proof is not enough when the certificate half cannot be shown
no cnfAuthorization: DPoPRefused. 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.