Demonstrating Proof of Possession (DPoP)
What is it?
The OAuth 2.0 Demonstrating Proof-of-Possession extension removes the security risk of stolen access tokens by binding the access token to a public/private key pair where the private key is known only to the client application server.
Use of DPoP allows the authorization server to create longer lived access tokens since the risk of theft and misuse is avoided.
How do I use it?
DPoP proofs are themselves a signed JWT that is sent with every API request in the special DPoP header. They
are single use tokens and must be recreated for every API call. It’s the responsibility of the resource
server to validate the DPoP proof.
When to Use DPoP
When added security and longer lived access tokens are needed. DPoP tokens can be used with both the Authorization Code Flow and Client Credentials Flow.
Creating a DPoP JWT
Create a public/private key pair for use with the obtained access token. The client application should store the keypair securely and ensure the private key is never transmitted to another server. The following JWT signing algorithms are supported:
- ES256 - ECDSA using P-256 and SHA-256
- ES256K - ECDSA using secp256k1 curve and SHA-256
- ES384 - ECDSA using P-384 and SHA-384
- ES512 - ECDSA using P-521 and SHA-512
- PS256 - RSASSA-PSS using SHA-256 and MGF1 with SHA-256
- PS384 - RSASSA-PSS using SHA-384 and MGF1 with SHA-384
Create a signed JWT with the following headers and claims:
jti: Unique id to prevent replay attacks. Use at least 96 bytes of random data base64url encoded or a v4 UUID.htm: The http method of the API call.htu: The API URL without query parameters or anchors/fragment parts (just the origin + pathname).ath: A sha256 hash of the access token value base64url encoded.iat: The unix timestamp (num of seconds since Jan 1, 1970 UTC) of the current time.typ: Always ‘dpop+jwt’.alg: The algorithm selected from step 1.jwk: The JWK variant of the public key (not the private key) generated in step 1.
const DPoPJwtHeaders = { typ: 'dpop+jwt', alg: SIGNING_ALGORITHM, jwk: DPOP_PUBLIC_KEY_JWK, }; const DPoPJwtClaims = { jti: RANDOM_ENCODED_STRING, htm: API_URL_METHOD, htu: API_URL_ORIGIN_AND_PATH, ath: ACCESS_TOKEN_SHA256_HASH_BASE64URL, iat: NUM_SECONDS_SINCE_EPOCH, };Sign the DPoP JWT with the private key created in step 1.
Call the API URL with the access token using the
DPoPauthorization scheme. The access token is sent to the server using theAuthorization: DPoP ey....header and does not use the typical “Bearer” scheme. You must also include aDPoPheader where the value is the JWT from step 2.
ath claim is omitted from the DPoP JWT
as an access token has not yet been obtained.Step-by-Step Obtaining a DPoP Bound Access Token
DPoP can be used with both Authorization Code and Client Credentials flows. A client application must send a DPoP proof to the TOKEN endpoint to obtain a bound token. The Client Credentials flow is used for illustration.
signed with private key Client_Backend->>Client_Backend: Generate DPoP JWT without the `ath` claim Client_Backend->>Auth_Server: Request token (POST)
Include the DPoP header with the value of the DPoP JWT
grant_type=client_credentials,
client_assertion,
client_assertion_type,
scope Auth_Server->>Client_Backend: Issue access token with `cnf` claim Client_Backend->>Client_Backend: Securely store access token end rect rgb(255, 245, 200) note over Client_Backend,Resource_Server: Accessing Resources Client_Backend->>Client_Backend: Generate DPoP JWT signed with private key Client_Backend->>Resource_Server: API call with Access Token
Authorization: DPoP ACCESS_TOKEN
DPoP: DPOP_JWT Resource_Server->>Client_Backend: Return protected resources end
1. Create a DPoP JWT
Follow the steps above to create a DPoP JWT for the https://API_ENVIRONMENT/TOKEN_ENDPOINT. Omit the ath claim.
2. Generate a client assertion JWT for authentication and Request an Access Token
To authenticate, you must generate a signed JWT (client_assertion) using your registered private key. The JWT must
contain the following claims:
jti: A unique identifier for the token, used as a nonce to prevent replay attacks.aud: The exact URL of the token endpoint (e.g.,https://login.jackhenry.com/a/oidc-provider/api/v0/token).sub: The Client ID of the application.iss: The Client ID of the application.exp: A timestamp (in seconds) indicating when the JWT will expire. Do not set this value to more than 5 minutes (300 seconds) from the time of issuance.
The JWT must be signed with your application’s private key. The following algorithms are supported by Jack Henry’s Authentication Framework:
ES256- ECDSA using P-256 and SHA-256PS256- RSASSA-PSS using SHA-256 with MGF1 and SHA-256RS256- RSASSA-PKCS1-v1_5 using SHA-256
Submit the JWT in a POST request to the token endpoint:
POST AUTHORIZATION_SERVER_URL/token
Content-Type: application/x-www-form-urlencoded
DPoP: DPOP_JWT
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_assertion=GENERATED_CLIENT_ASSERTION_JWT
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&scope=openid+profile
Note: AUTHORIZATION_SERVER_URL could be https://login.jackhenry.com/a/oidc-provider/api/v0 or
another authorization server.
3. Receive Access Token
The authorization server responds with a JSON object:
{
"access_token": "eyJhbGciOiJIUzI1NiIsIn...",
"expires_in": 600,
"token_type": "DPoP"
}
If you decode the JWT from the access_token, it will have a cnf claim indicating it is sender-constrained. To use
the token, you call APIs using the Authorization: DPoP ...token header instead of the standard “Bearer” scheme.
See validate an access token for the steps a resource
server must take to validate DPoP tokens.
Example DPoP Creation
Given a DPoP access token:
eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6InNpZy1lYzItMCJ9.eyJhdXRoVHlwZSI6ImMiLCJ2IjoiMCIsImNsaWVudF9pZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNsaWVudE5hbWUiOiJuYW1lIiwicGFydG5lck5hbWUiOiJudWxsIiwiaW5zdGl0dXRpb25JZCI6Imluc3RpdHV0aW9uSWQiLCJqdGkiOiJKRzlVTVBVZm1vdTlldm9IUGw0SUEiLCJzdWIiOiIxMDFlMTgwZS1kMTNjLTQwM2UtODIyNC05NTY3MGRiZThmYWEiLCJpYXQiOjE3NzIxMTg2ODYsImV4cCI6MTc3MjExOTU4Niwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCBhZGRyZXNzIGh0dHBzOi8vYXBpLmJhbm5vLmNvbS9jb25zdW1lci9hdXRoL29mZmxpbmVfYWNjZXNzIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QvYS9jb25zdW1lci9hcGkvdjAvb2lkYyIsImF1ZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNuZiI6eyJqa3QiOiItazJxejRENlpaSVZleVdjM1BXaEZEekt5azJhYWxVckY5WHVtSk94S3ZnIn19.RLM9zmnWFi-eJJU67SBG8KXdiBTW9QpBtXmcwGc0vDmmZFE04tHiK-TzGsiGQAfOovJEddvIHNexCsONQL0R6Q
DPoP Private Key:
{
"kty": "EC",
"x": "BeKQwFUrPw2f0_JdjwEauAG5qmzi9E4iyKU-AzV9Qvg",
"y": "vr8bQoTl3HtSYl7RA0Hva7r1CYo_AEe8xET_6Un8eHw",
"crv": "P-256",
"d": "JAeRh3fcAMi9cDK_wT5dDAoVvpLKkqI5BuvzMJKDrXk"
}
DPoP Public Key:
{
"kty": "EC",
"x": "BeKQwFUrPw2f0_JdjwEauAG5qmzi9E4iyKU-AzV9Qvg",
"y": "vr8bQoTl3HtSYl7RA0Hva7r1CYo_AEe8xET_6Un8eHw",
"crv": "P-256"
}
Construct the DPoP JWT Header:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"x": "BeKQwFUrPw2f0_JdjwEauAG5qmzi9E4iyKU-AzV9Qvg",
"y": "vr8bQoTl3HtSYl7RA0Hva7r1CYo_AEe8xET_6Un8eHw",
"crv": "P-256"
}
}
Construct the DPoP JWT Claims for calling the https://localhost/a/consumer/api/v0/oidc/me API:
{
"jti": "ab4a7c7b-0ad0-409a-bd64-09fe58b9216f",
"htm": "GET",
"htu": "https://localhost/a/consumer/api/v0/oidc/me",
"iat": 1772118686,
"ath": "9YHO_3qp-Zo4dp2LM52UWzFzw_c5QfFVkESfmRpeXVg"
}
Produce the signed DPoP JWT
eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6IkJlS1F3RlVyUHcyZjBfSmRqd0VhdUFHNXFtemk5RTRpeUtVLUF6VjlRdmciLCJ5IjoidnI4YlFvVGwzSHRTWWw3UkEwSHZhN3IxQ1lvX0FFZTh4RVRfNlVuOGVIdyIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJhYjRhN2M3Yi0wYWQwLTQwOWEtYmQ2NC0wOWZlNThiOTIxNmYiLCJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL2xvY2FsaG9zdC9hL2NvbnN1bWVyL2FwaS92MC9vaWRjL21lIiwiaWF0IjoxNzcyMTE4Njg2LCJhdGgiOiI5WUhPXzNxcC1abzRkcDJMTTUyVVd6Rnp3X2M1UWZGVmtFU2ZtUnBlWFZnIn0.s2ErmAe3SEnXe7DVr7dRiQLmy7J7mHUIMMhFkiP-XMPiQqyFdroSK_O_3ZpkqJ5LtN_oZWimJEJrdQi9rR-K6A
Call the endpoint:
HOST: localhost
GET /a/consumer/api/v0/oidc/me
Authorization: DPoP eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6InNpZy1lYzItMCJ9.eyJhdXRoVHlwZSI6ImMiLCJ2IjoiMCIsImNsaWVudF9pZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNsaWVudE5hbWUiOiJuYW1lIiwicGFydG5lck5hbWUiOiJudWxsIiwiaW5zdGl0dXRpb25JZCI6Imluc3RpdHV0aW9uSWQiLCJqdGkiOiJKRzlVTVBVZm1vdTlldm9IUGw0SUEiLCJzdWIiOiIxMDFlMTgwZS1kMTNjLTQwM2UtODIyNC05NTY3MGRiZThmYWEiLCJpYXQiOjE3NzIxMTg2ODYsImV4cCI6MTc3MjExOTU4Niwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCBhZGRyZXNzIGh0dHBzOi8vYXBpLmJhbm5vLmNvbS9jb25zdW1lci9hdXRoL29mZmxpbmVfYWNjZXNzIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QvYS9jb25zdW1lci9hcGkvdjAvb2lkYyIsImF1ZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNuZiI6eyJqa3QiOiItazJxejRENlpaSVZleVdjM1BXaEZEekt5azJhYWxVckY5WHVtSk94S3ZnIn19.RLM9zmnWFi-eJJU67SBG8KXdiBTW9QpBtXmcwGc0vDmmZFE04tHiK-TzGsiGQAfOovJEddvIHNexCsONQL0R6Q
DPOP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6IkJlS1F3RlVyUHcyZjBfSmRqd0VhdUFHNXFtemk5RTRpeUtVLUF6VjlRdmciLCJ5IjoidnI4YlFvVGwzSHRTWWw3UkEwSHZhN3IxQ1lvX0FFZTh4RVRfNlVuOGVIdyIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJhYjRhN2M3Yi0wYWQwLTQwOWEtYmQ2NC0wOWZlNThiOTIxNmYiLCJodG0iOiJHRVQiLCJodHUiOiJodHRwczovL2xvY2FsaG9zdC9hL2NvbnN1bWVyL2FwaS92MC9vaWRjL21lIiwiaWF0IjoxNzcyMTE4Njg2LCJhdGgiOiI5WUhPXzNxcC1abzRkcDJMTTUyVVd6Rnp3X2M1UWZGVmtFU2ZtUnBlWFZnIn0.s2ErmAe3SEnXe7DVr7dRiQLmy7J7mHUIMMhFkiP-XMPiQqyFdroSK_O_3ZpkqJ5LtN_oZWimJEJrdQi9rR-K6A
Creating a DPoP Proof in JavaScript Using the jose Library
import {Buffer} from 'node:buffer';
import crypto from 'node:crypto';
import {URL} from 'node:url';
import {SignJWT, calculateJwkThumbprint, decodeJwt} from 'jose';
// The privateKey should be securely stored along with the accessToken.
// It is generated before calling the /token endpoint.
const privateKeyJwk = {
kty: 'EC',
x: 'BeKQwFUrPw2f0_JdjwEauAG5qmzi9E4iyKU-AzV9Qvg',
y: 'vr8bQoTl3HtSYl7RA0Hva7r1CYo_AEe8xET_6Un8eHw',
crv: 'P-256',
d: 'JAeRh3fcAMi9cDK_wT5dDAoVvpLKkqI5BuvzMJKDrXk',
};
// Derive the public key from the private key
const publicKeyJwk = crypto.createPublicKey({key: privateKeyJwk, format: 'jwk'})
.export({format: 'jwk'});
const header = {
typ: 'dpop+jwt',
alg: 'ES256',
jwk: publicKeyJwk,
};
const uniqueId = crypto.randomBytes(96).toString('base64url');
// The accessToken should be securely stored.
const accessToken =
'eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6InNpZy1lYzItMCJ9.eyJhdXRoVHlwZSI6ImMiLCJ2IjoiMCIsImNsaWVudF9pZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNsaWVudE5hbWUiOiJuYW1lIiwicGFydG5lck5hbWUiOiJudWxsIiwiaW5zdGl0dXRpb25JZCI6Imluc3RpdHV0aW9uSWQiLCJqdGkiOiJKRzlVTVBVZm1vdTlldm9IUGw0SUEiLCJzdWIiOiIxMDFlMTgwZS1kMTNjLTQwM2UtODIyNC05NTY3MGRiZThmYWEiLCJpYXQiOjE3NzIxMTg2ODYsImV4cCI6MTc3MjExOTU4Niwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCBhZGRyZXNzIGh0dHBzOi8vYXBpLmJhbm5vLmNvbS9jb25zdW1lci9hdXRoL29mZmxpbmVfYWNjZXNzIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3QvYS9jb25zdW1lci9hcGkvdjAvb2lkYyIsImF1ZCI6IjgxMzE5MDIzLWFhN2QtNDIwOS1iYzFhLTUwOGYwZWMxMjY0YyIsImNuZiI6eyJqa3QiOiItazJxejRENlpaSVZleVdjM1BXaEZEekt5azJhYWxVckY5WHVtSk94S3ZnIn19.RLM9zmnWFi-eJJU67SBG8KXdiBTW9QpBtXmcwGc0vDmmZFE04tHiK-TzGsiGQAfOovJEddvIHNexCsONQL0R6Q';
const accessTokenHash = crypto.createHash('sha256')
.update(Buffer.from(accessToken, 'ascii'))
.digest('base64url');
// The URL object will normalize the url
const apiUrl = new URL('https://localhost/a/consumer/api/v0/oidc/me');
const currentTimestamp = Math.floor(Date.now() / 1000);
const claims = {
jti: uniqueId,
htm: 'GET',
htu: apiUrl.origin + apiUrl.pathname,
iat: currentTimestamp,
ath: accessTokenHash,
};
const dpopJwt = await new SignJWT(claims)
.setProtectedHeader(header)
.sign(privateKeyJwk);
console.log(dpopJwt);
// Let's check our work
// This section is just to demonstrate the binding was correct
// and is not needed when creating a DPoP JWT
const accessTokenJwt = decodeJwt(accessToken);
const publicKeyJwkThumbprint = await calculateJwkThumbprint(publicKeyJwk);
console.log(
'Access Token cnf claim validates:',
accessTokenJwt.cnf?.jkt === publicKeyJwkThumbprint,
);
- Have a how-to question? Seeing a weird error? Get help on StackOverflow.
- Register for the Developer Office Hours where we answer technical Q&A from the audience.