Developer Programs

Learn

Docs

Demonstrating Proof of Possession (DPoP)

Concepts > Advanced Topics > 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

  1. 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
  2. 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.

  3. Call the API URL with the access token using the DPoP authorization scheme. The access token is sent to the server using the Authorization: DPoP ey.... header and does not use the typical “Bearer” scheme. You must also include a DPoP header where the value is the JWT from step 2.

Initial Token Call
For the initial call to the Authorization Server TOKEN endpoint, the 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.

sequenceDiagram participant Client_Backend as Confidential Client Backend participant Auth_Server as Jack Henry Authorization Server participant Resource_Server as Jack Henry API rect rgb(200, 255, 200) note over Client_Backend,Auth_Server: JWT Generation & Token Request Client_Backend->>Client_Backend: Generate JWT (client_assertion)
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-256
  • PS256 - RSASSA-PSS using SHA-256 with MGF1 and SHA-256
  • RS256 - 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 Question?

Did this page help you?

Last updated Sat Mar 21 2026