Demonstrating Proof of Possession (DPoP)
Demonstrating Proof of Possession (DPoP) in OAuth 2.0 ensures that authorization codes and access tokens can only be used by the original client to which they were issued for.
It prevents intercepted tokens/codes from being reused by attackers (replay attacks), which is especially useful for public clients like SPAs or mobile apps.
This security mechanism is defined in RFC 9449.
How it works
The DPoP mechanism enables a client to prove possession of a private key when obtaining and using both authorization codes and access tokens in a FAPI 2.0 flow.
At the start of each authorization request, the client generates an ephemeral signing key pair. Using this key pair, the client creates a signed JWT (the DPoP Proof JWT) and sends it in the DPoP header to the authorization server. Presenting this proof demonstrates that the client holds the private key it claims to and that it is initiating the request directly.
The authorization server binds the client’s presented public key to the resulting credential.
For an authorization code grant, the server binds the key to the issued authorization code, ensuring that only the client holding the private key can redeem it at the token endpoint.
For an access token, the server embeds the client’s public key into the token, restricting its use to the legitimate client with the private key.
When the client later redeems the authorization code or invokes protected resource endpoints with an access token, it must again present a new DPoP proof JWT alongside its credential. The server verifies the proof and ensures that the bound credential is only usable by the same client instance that created the original request.
Corppass expects the DPoP proof to be provided for requests made to the Pushed Authorization Request endpoint, Token endpoint and other protected endpoints (eg. Userinfo endpoint /userinfo ).
Generating a DPoP Proof JWT
First, clients need to generate an ephemeral signing key pair. Currently, Corppass only supports EC keys.
Use the same ephemeral key pair for all subsequent requests stemming from the same authentication request.
Next, clients need to construct the signed JWT as per the following specifications:
DPoP Request Header
The DPoP proof JWT should be base64-encoded and sent with the DPoP request header.
DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6IkVDIiwieCI6IlBFZHFyLTFSemZ2QkZsazE0U085UkZSSm1uV2FGNlc3YlZPZ2Y3Wk9KUVkiLCJ5IjoieGVBeklzcjlTbmk1RWVnVE5xZHFZblg1V08yTm5wTHBOTXFHTEoxSjRDWSIsImNydiI6IlAtMjU2In19.eyJpYXQiOjE3NDQxODQ5ODgsImp0aSI6IjVlNmU1NTA5LTcwZTMtNGJiNy1hZDMwLTRlMTc2ZmZkM2NhOCIsImh0bSI6IlBPU1QiLCJodHUiOiJodHRwczovL2lkLmNvcnBwYXNzLmdvdi5zZy9tZ2Evc3BzL29hdXRoL29hdXRoMjAvdG9rZW4ifQ.fggiYkc6lBwIKgpZDJjSA2c3_vkrxWHxJ_7WN9RMpyqjfgOCB1Cwlvdcw7AsGNF1nDsi59s8DHUHnM6a9warbADPoP JWT Header Structure
Example of a DPoP proof JWT header:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}Fields in a DPoP proof JWT header:
typ
Type of token. Always set to "dpop+jwt".
alg
Signing algorithm. The supported values are ES256, ES256K, ES384, ES512.
jwk
The public key corresponding to the private key used to sign the JWT. Must match the key used in the DPoP-bound token.
DPoP JWT Payload Structure
Example of a DPoP proof JWT payload:
{
"htm": "POST",
"htu": "https://id.corppass.gov.sg/mga/sps/oauth/oauth20/token",
"iat": 1744184988,
"jti": "5e6e5509-70e3-4bb7-ad30-4e176ffd3ca8",
"ath": "hKzf0cQEXAMPLEUXxKXJvHr_F34Pp7pg6LFaP_LZ7Jw"
}Fields in a DPoP proof JWT payload:
htm
Yes
The HTTP method of the request (e.g., POST)
htu
Yes
The full HTTP URI of the request (e.g., https://id.corppass.gov.sg/mga/sps/oauth/oauth20/token).
iat
Yes
Issued-at time (epoch timestamp). Used to detect replayed proofs.
jti
Yes
A unique identifier for the proof. Prevents replay attacks. Must be unique for each JWT.
ath
No
Required only when accessing protected resource endpoints (e.g. /userinfo).
This claim contains the base64url-encoded SHA-256 hash of the DPoP-bound access token and is used to bind the proof to that specific token. Read section below on how to compute this value.
How to compute the Access Token Hash (ath) claim
The ath (Access Token Hash) claim is a mandatory field in the DPoP proof JWT for requests to protected resource endpoints (eg. /userinfo).
It cryptographically binds the proof to the access token being used in a request, preventing replay attacks and misuse of tokens.
Steps:
Start with the raw access token string you received from the Token endpoint.
Compute a SHA-256 hash of the token.
Base64URL-encode the hash (no padding).
Example (Pseudo-code)
const crypto = require("crypto");
function computeATH(accessToken) {
const accessToken = "eyJhbGciOi...";
const ath = Buffer.from(crypto.createHash("sha256")
.update(accessToken).digest()
).toString("base64url");
return ath;
}Example Output
"ath": "3rEbqHhURQcbfV3zM9sl1rsBzAcjT9TKqX3akHLZ9Nc"Example payload with ath
{
"htu": "https://id.corppass.gov.sg/authorization-info",
"htm": "POST",
"iat": 1712681904,
"jti": "6ef56d85-e469-4bc3-a508-05d8f94f780e",
"ath": "3rEbqHhURQcbfV3zM9sl1rsBzAcjT9TKqX3akHLZ9Nc"
}Last updated