Demonstration of proof-of-possession

Dealing with compromised Bearer tokens

OAuth Bearer tokens are used to access protected resources. They are often used to offer sufficient proof to gain access through the use of scopes or finer-grained details in claims, such as authorization_details (Ref: Rich authorization request). This implies that if an unauthorized actor or system gains access to the Bearer token, they can impersonate the user (or system) and get access to the protected resources. There is no way for a resource server to verify that the sender of the Bearer token is legitimate as long as the token is valid.

A solution is to require the use of Proof-of-Possession Tokens that allows a resource server to verify that the Bearer token is being used by a legitimate client. Demonstrating proof-of-possession at the application layer (DPoP) is another such approach that is more appropriate for clients that cannot use mutual TLS, such as single page applications that do not have a backend.

📘

Note

Demonstrating proof-of-possession in the application layer (DPoP) is a draft standard. However, this has been implemented in IBM Security Verify Access OIDC Provider
due to the obvious benefits and in preparation for Financial Grade API (FAPI) 2.0.

DPoP-bound tokens

DPoP-bound tokens are bound to an client-generated private key that is held by the client. The private key must not be leaked and should be stored in a manner that prevents its parameters from being accessed by application code. On browsers, for example, this can be achieved using SubtleCrypto, where the key that is generated is marked non-extractable and prevents the JavaScript application code from exporting the private key parameters from the browser.

There are a number of optional steps that can be performed in a flow that is requesting for a DPoP-bound token. However, for the sake of simplicity, the steps below are performed on the client prior to calling the token endpoint on the OAuth authorization server.

  1. Generate a key-pair
// Generates a key pair. The private key is marked non-extractable to prevent
// any application code from exporting private key parameters.
async function generateKey() {
  const generatedKeyPair: CryptoKeyPair = await crypto.subtle.generateKey({
        name: 'RSASSA-PKCS1-v1_5',
        modulusLength: 4096,
        publicExponent: new Uint8Array([1, 0, 1]),
        hash: 'SHA-256',
    },
    false,
    ['sign', 'verify']
  );

  return generatedKeyPair;
}
  1. Create a DPoP proof: The DPoP proof is a JSON Web Token that embeds the public parameters of the signing key in JWK format in the JWT header and is signed by the private key generated in step 1. Standard claims, such as the issued time (iat), expiry (exp) and JWT identifier (jti), are included. In addition, the proof must contain the HTTP method (htm) and the HTTP URL (htu) of the token endpoint
{
  "alg": "RS256",
  "typ": "dpop+jwt",
  "jwk": {
    "kty": "RSA",
    "n": "o5Fiw7GSdTDrO61ivks7KM2M7bLar4HF9DWLcIRDGcQqNu0aRMkWLD4QEBtqkyV8Uu30WZ4g8sZxgSGLVoSH9JGc270vWqtA0fYx7AhFi1JPHM-v3Kz3PtLHCIXTRFi-Cj-uDNn31RMduMVevtjmuPz99_qvQU4lDGhQsyAjONNEjYQ5wJp_iYVYPXXRpP3rGg2avoTrsvtFzEABecmIKWGh556M7qSFwdboIUKG-Q6DdBYD9aq3tm0A8JiFATA3RONVF8dSIPl1dfUkwRsosZI2Fr-OT51x6J5f0Kz8J6DUj_UHr0ecwtn25sLZHEN-fCxZ1LeEK-ZeUgIrxZLagw",
    "e": "AQAB",
    "kid": "HjFAbEgNeDnFbLWHh3cR3B63wI2U0xm0ZTuIV_8I8EU"
  }
}
{
  "jti": "e63dcda8-f374-4701-aa94-55664c973b0e",
  "iat": 1678798289,
  "exp": 1678800089,
  "htm": "POST",
  "htu": "https://harbinger.verify.ibm.com/oauth2/token"
}
  1. Make the token request: As part of this request, the DPoP proof is included in the dpop header.

  2. If the DPoP proof is valid, the authorization server (IBM Security Verify) issues a token of type "DPoP". As part of this process, the thumbprint is generated from the jwk embedded in the DPoP proof and added to the authorization grant as part of the "confirmation" claim (cnf). This is then made available as part of token introspection response or embedded within a JWT-formatted access token.

The following shows an example of an introspection response. The payload includes the confirmation claim.

{
  "active": true,
  "aud": [
      "177cb3ec-b112-4c29-8b08-94717f7bc4b6"
  ],
  "client_id": "177cb3ec-b112-4c29-8b08-94717f7bc4b6",
  "cnf": {
      "jkt": "PJ4wWW_nkvnKbKHPrgyjr7DiCBrTL4JbwDGBlBFKOCw"
  },
  "exp": 1678945818,
  "grant_id": "c7836eff-7451-4b67-8bef-633e3a7d647b",
  "iat": 1678942218,
  "iss": "https://harbinger.verify.ibm.com/oauth2",
  "nbf": 1678942218,
  "preferred_username": "[email protected]",
  "scope": "openid payment",
  "sub": "671000CCGT",
  "token_type": "DPoP",
  "token_use": "access_token"
}

Now that the token has been issued, it can be used to access a protected resource.

Using the DPoP-bound token

When the client accesses a protected resource API authorized using the DPoP token, it generates a new DPoP proof signed by the same private key. Unlike Bearer tokens, the "Authorization" header must start with DPoP (in place of "Bearer"). This indicates the use of a DPoP-bound token and requires the proof to be included as part of the "dpop" header. The DPoP proof must contain the standard JWT claims, such as "iat", "jti" and "exp". In addition, the following must be added:

ClaimDescriptionExample
htmHTTP methodGET
htuHTTP URLhttps://jke.com/photos
athComputed as the BASE64URL-encoded SHA-256 hash of the access token valueYSm0UcYpuIR6HZfVU9dnIg

The API (or gateway) introspects the token (if it is not a JWT) and does the following:

  1. Validate the DPoP proof signature.
// Importing node-jose module
const jose  = require('node-jose');

// DPoP proof, usually extracted from the request object
const dpopProof = '...';

// Validate the JWT
let dpopProofUnpacked = await jose.JWS.createVerify().
  verify(dpopProof, { allowEmbeddedKey: true });
  1. Compare the access token hash to verify that the DPoP proof used is associated with the token.
// Importing crypto module
const { createHash } = require('crypto');

// Access token, usually extracted from the Authorization header
const token = '...';

// Compute the expected access token hash (ath) claim in the proof
let digest = createHash('sha256').update(token).digest();
let atHash = encode(digest.slice(0, digest.length / 2));

// Compare the atHash with the 'ath' claim in the DPoP proof
  1. Generate the encoded thumbprint from the embedded JWK.
// Compute the thumbprint hash
let thumbprint = await dpopProofUnpacked.key.thumbprint('SHA-256');
let computedFingerprint = jose.util.base64url.encode(thumbprint);
  1. Validate that the "htm" and "htu" claims match the resource endpoint's HTTP method and HTTP URL.

  2. Introspect the token or validate the JWT (if formatted in this manner).

  3. Extract the cnf.jkt value and compare with the thumbprint hash computed in step 3. If they match, perform other authorization decisions, such as comparing scopes granted, etc.

Conclusion

DPoP-bound tokens add a strong layer of assurance by requiring proof of possession through the use of the signed DPoP proof. They mitigate the misuse of tokens by unauthorized parties. Unauthorized parties include bad actors, resource APIs with access to the token that re-use it to access other endpoints, etc. Unlike certificate-bound access tokens that require Mutual TLS, the DPoP proof is generated on the application layer and is suitable for clients that cannot be issued client certificates. For example, single page applications written using React or Angular.js that do not have a backend.