loading
Preparing LoginRadius developer resources
Mission: Help enterprises accelerate digital transformation with our fully-managed Customer IAM technology.
Skip to main content

Demonstrating Proof-of-Possession (DPoP)

LoginRadius implements RFC 9449 Demonstrating Proof-of-Possession (DPoP) for OAuth 2.0 and OpenID Connect applications. A DPoP-bound access token is sender-constrained: the client proves possession of the private key that matches the token's cnf.jkt claim on every token and resource request.

A leaked Bearer token can be replayed by anyone who holds it. A DPoP-bound token cannot be used without a fresh, request-bound proof signed by that key.


Why it matters

Bearer access tokens are bearer credentials. If a token appears in a log, a Referer header, or a compromised client, an attacker can call your APIs as that user. DPoP binds the token to a public key so the authorization server and resource server accept the token only with a matching proof.

DPoP complements other OAuth protections. It does not replace them.

ControlWhat it protectsLoginRadius docs
PKCEAuthorization code interception on public clientsPKCE Flow
Resource IndicatorsToken audience (aud) so one API cannot accept a token issued for anotherResource Indicators
DPoPToken sender: the caller must hold the bound private keyThis page

Use PKCE and DPoP together on public clients (SPAs, mobile, Auth Studio). Use Resource Indicators when the token must be valid for a specific API only.


How it works

The client generates an asymmetric key pair, sends the public-key thumbprint on /authorize, proves possession on /token, and proves possession again on every resource call.

Key concepts

These terms appear on the wire and in the Admin Console. Use them when you read the request and response examples below.

ConceptDescription
dpop_jktJWK thumbprint (RFC 7638) of the client's public key, sent on /authorize so the authorization code is bound to that key.
DPoP proofCompact JWT (typ: dpop+jwt) sent in the DPoP HTTP header. Each request gets a new proof.
jktSHA-256 thumbprint of the proof's public JWK.
cnf.jktConfirmation claim on the access token. The resource server matches this to the proof's jkt.
token_typeDPoP when the access token is bound. Use the DPoP HTTP scheme, not Bearer.
athHash of the access token, required on resource-request proofs (RFC 9449 §4.2).
DPoP-NonceServer-issued nonce header. Required in the next proof when Require DPoP nonce is on.
use_dpop_nonceToken-endpoint error telling the client to retry once with the nonce from DPoP-Nonce.

Enable DPoP in the Admin Console

DPoP is configured per OAuth 2.0 or OIDC application on the Tokens tab. The tenant DPoP feature must be on before the card appears.

Enable the DPoP feature

DPoP requires the tenant feature enable_dpop. Contact LoginRadius Support if the Demonstrating Proof-of-Possession (DPoP) card is not visible on the app Tokens tab.

  1. In the Admin Console, open Applications and select your OAuth 2.0 or OIDC app.
  2. Open the Tokens tab and find Demonstrating Proof-of-Possession (DPoP).
  3. Turn on Enable DPoP. The app can issue DPoP-bound access tokens (token_type DPoP with cnf.jkt). Discovery advertises dpop_signing_alg_values_supported.
  4. Optionally turn on Require DPoP. Token requests without a valid DPoP proof are rejected. Dynamic clients (DCR and CIMD) follow this parent setting at request time.
  5. Optionally turn on Require DPoP nonce. LoginRadius challenges clients with use_dpop_nonce and requires proofs to include the server-issued DPoP-Nonce.
SettingJSON fieldEffect
Enable DPoPDPoP.IsEnabledApp may issue DPoP-bound tokens. Clients that send a proof receive token_type DPoP.
Require DPoPDPoP.IsRequiredToken requests without a valid proof are rejected.
Require DPoP nonceDPoP.NonceRequiredProofs must include a server nonce. Missing or stale nonce returns use_dpop_nonce.
warning

Turn on Require DPoP only for applications whose clients send DPoP proofs. A token request with no DPoP header is rejected. Create a dedicated app for DPoP clients if other clients still send Bearer-only token requests.


Discovery

When DPoP is enabled for the app, LoginRadius advertises the supported proof algorithms on both metadata documents.

https://{SiteURL}/service/oidc/{OIDCAppName}/.well-known/openid-configuration
https://{SiteURL}/.well-known/oauth-authorization-server/service/oidc/{OIDCAppName}

Replace oidc with oauth and {OIDCAppName} with {OAuthAppName} for OAuth 2.0 apps. Copy the exact URLs from the app Endpoints tab.

{
"dpop_signing_alg_values_supported": ["ES256", "RS256"]
}

LoginRadius accepts DPoP proofs signed with ES256 (P-256) or RS256. Hosted Auth Studio and typical public clients use ES256.


Step 1: Authorization request (dpop_jkt)

Send dpop_jkt on the authorization request so the issued code is bound to your key. Do not send a DPoP proof on /authorize. The proof is used on /token and on resource requests.

GET https://{SiteURL}/service/oidc/{OIDCAppName}/authorize

Example request

GET /service/oidc/MyApp/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback
&scope=openid%20profile
&state=xyz123
&code_challenge=BASE64URL_SHA256_VERIFIER
&code_challenge_method=S256
&dpop_jkt=YOUR_JWK_THUMBPRINT HTTP/1.1
Host: your-app.hub.loginradius.com
ParameterRequiredDescription
client_idRequiredApplication Client ID.
response_typeRequiredcode for Authorization Code.
redirect_uriRequiredWhitelisted callback URL.
scopeRequired for OIDCInclude openid for OIDC.
code_challenge / code_challenge_methodRequired when PKCE is enforcedS256. See PKCE Flow.
dpop_jktRequired when DPoP is requiredRFC 7638 SHA-256 thumbprint of the client's public JWK (EC crv/x/y or RSA n/e, lexicographic JSON).
stateRecommendedCSRF correlation value.

After authentication, LoginRadius redirects to redirect_uri with code and state. Exchange that code on /token with a DPoP proof whose jkt matches dpop_jkt.


Step 2: Token request (DPoP header)

POST the token endpoint with a DPoP proof JWT. The proof htu is the token URL with no query or fragment. The proof htm is POST.

POST https://{SiteURL}/api/oidc/{OIDCAppName}/token
POST /api/oidc/MyApp/token HTTP/1.1
Host: your-app.hub.loginradius.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiLi4uIiwieSI6Ii4uLiJ9fQ...

grant_type=authorization_code
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback
&client_id=YOUR_CLIENT_ID
&code_verifier=YOUR_CODE_VERIFIER

Proof JWT

Header and payload of a token-endpoint proof:

{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
{
"jti": "unique-proof-id",
"htm": "POST",
"htu": "https://your-app.hub.loginradius.com/api/oidc/MyApp/token",
"iat": 1778675881
}
Claim / headerRequiredDescription
typRequiredMust be dpop+jwt.
algRequiredES256 or RS256.
jwkRequiredPublic JWK only. Private parameters (d, p, q, and similar) are rejected.
jtiRequiredUnique proof ID. Reuse of the same jti for the same htu and jkt is rejected.
htmRequiredHTTP method of this request (POST on /token).
htuRequiredHTTP URI of this request: scheme, host, and path only.
iatRequiredUnix timestamp. Proofs older than 120 seconds are rejected.
nonceWhen nonce is requiredValue from the DPoP-Nonce response header. See Nonce challenge.
athResource requests onlyBase64url SHA-256 of the ASCII access token. Omit on /token.

Token response

{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "DPoP",
"expires_in": 3600,
"refresh_token": "def50200..."
}

Decoded access token (DPoP-relevant claims):

{
"iss": "https://your-app.hub.loginradius.com/service/oidc/MyApp",
"sub": "e2a19b161ba241559319dc4c47e5ef75",
"azp": "your-client-id",
"cnf": {
"jkt": "YOUR_JWK_THUMBPRINT"
},
"iat": 1778675881,
"exp": 1778679481
}

Present this access token with Authorization: DPoP <token>, not Bearer. Encrypted (JWE) access tokens carry cnf.jkt inside the ciphertext. Resource servers that decrypt the JWE still enforce DPoP.

See Access Token Claims for the full claim set.


Step 3: Resource request

Every call that uses a DPoP-bound access token sends a new proof. The proof ath claim is the hash of that access token. The Authorization scheme is DPoP.

GET /service/oidc/MyApp/userinfo HTTP/1.1
Host: your-app.hub.loginradius.com
Authorization: DPoP eyJhbGciOiJSUzI1NiIs...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7fX0...

LoginRadius enforces this on its own resource endpoints, including:

ResourceMethod and pathNotes
OIDC UserInfoGET /service/oidc/{OIDCAppName}/userinfoUse Authorization: DPoP plus a proof whose htu is the userinfo_endpoint from discovery.
Identity accountGET /identity/v2/auth/accountSame scheme and proof. htu is the Identity URL (scheme, host, and path, no query). Send the API Key as X-LoginRadius-ApiKey.

A DPoP-bound token presented as Authorization: Bearer or as ?access_token= is treated as a downgrade. LoginRadius returns 401 and a WWW-Authenticate challenge:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP algs="ES256 RS256", error="invalid_dpop_proof", error_description="DPoP-bound access token must be presented with the DPoP authorization scheme"
DPoP-Nonce: ServerIssuedNonceValue

DPoP-Nonce is included when Require DPoP nonce is on. Unbound Bearer tokens continue to use the Bearer scheme and do not require a proof.

Your own APIs must perform the same checks: if cnf.jkt is present, require Authorization: DPoP, validate the proof, and match jkt to cnf.jkt. See Access Token Claims.


Nonce challenge

When Require DPoP nonce is on, LoginRadius issues a DPoP-Nonce and expects that value in the next proof's nonce claim. This is the RFC 9449 challenge-response against proof replay.

  1. The client sends a token or resource request with a proof that has no nonce, or a stale nonce.
  2. LoginRadius responds with use_dpop_nonce (token endpoint, HTTP 400) or 401 (resource) and sets DPoP-Nonce.
  3. The client retries once with a new proof that includes nonce equal to that header.

Token-endpoint nonces are scoped per client_id. A mismatched nonce returns use_dpop_nonce with the existing nonce. LoginRadius does not rotate the stored nonce on a bad request, so concurrent clients of the same app keep a stable challenge.

Cross-origin browser clients must be able to read DPoP-Nonce and WWW-Authenticate. LoginRadius exposes those headers in CORS (Access-Control-Expose-Headers).

If a nonce cannot be issued or looked up, LoginRadius rejects the request. It does not skip the nonce check.


Auth Studio

Auth Studio hosted login can start a DPoP authorization request for you. When the selected app has Enable DPoP (and when Require DPoP is on, the preview always includes it), the Admin Console preview URL adds dpopEnabled=true.

The v3 before-script reads dpopEnabled from the query string. Workflow URLs lowercase query keys, so the same flag is dpopenabled. The script then calls:

buildAuthorizeUrl({ pkce, dpop: true })

That call generates an ES256 key in the browser and adds dpop_jkt to /authorize. Token exchange sends the matching DPoP proof.

Keep the key in the same browser session as the authorization. A new tab without that key cannot mint proofs for a DPoP-bound token.


Error handling

Token-endpoint DPoP failures return OAuth JSON errors. Resource-server failures return 401 with WWW-Authenticate: DPoP.

ScenarioHTTP statuserrorWhere
Missing or invalid proof when DPoP is required400invalid_dpop_proof/token
Proof jkt does not match the bound key400invalid_grant/token (refresh and code exchange)
Missing or stale nonce400use_dpop_nonce/token (retry with DPoP-Nonce)
DPoP-bound token sent as Bearer or query401invalid_dpop_proofuserinfo, Identity, your API
Valid token, invalid or missing resource proof401invalid_dpop_proofuserinfo, Identity, your API
Replay of the same jti for the same htu and jkt400 or 401invalid_dpop_proof/token or resource

Token-endpoint example

{
"error": "use_dpop_nonce",
"error_description": "The DPoP nonce is invalid or expired"
}
HTTP/1.1 400 Bad Request
DPoP-Nonce: ServerIssuedNonceValue
Content-Type: application/json

Security considerations

DPoP reduces the impact of a leaked access token. It still depends on how you store the key, which algorithms you accept, and whether resource servers enforce cnf.jkt.

  • Pair DPoP with PKCE on public clients. DPoP constrains the access token. PKCE protects the authorization code.
  • Supported proof algorithms are ES256 and RS256. Symmetric algorithms and alg: none are rejected.
  • Each proof is single-use for a given htu and jkt (jti replay window is 120 seconds).
  • Refresh stays bound to the original jkt. A different key on refresh returns invalid_grant.
  • Do not put the DPoP private key in localStorage or a cookie. Treat it like a device-bound credential.
  • Resource servers must check cnf.jkt. LoginRadius Identity and userinfo already do. Your APIs must reject a DPoP-bound JWT presented as Bearer.
  • NonceRequired fail-closed. When nonce is required and the nonce store is unavailable, requests fail instead of skipping the check.