Enhance OAuth 2.0 Token Security with Demonstration of Proof-of-Possession (DPoP)

Last week, I shared a topic in my team's knowledge-sharing session about an extension of OAuth 2.0 that enhances token security called Demonstration of Proof-of-Possession (DPoP). In this blog, I'll explain the concept of how the DPoP works.

What's Proof of Possession?

In real life, if someone steals your car and the police could recover it, you would show the police some documents proving you own the car. However, OAuth 2.0 doesn't provide a way to prove the possession of a client's token. This means when someone steals your access token, they can use it to access your restricted resources until the token is revoked or renewed.

With this concern of security, there are specifications to enhance security and one of them is Demonstrating Proof of Possession or we'll call it DPoP.

How to apply DPoP?

Since DPoP is an extension of OAuth 2.0 which means that you can extend it to the existing system. However, it requires updates to three main components

  1. Client: Generating DPoP proof
  2. Authorization server: Generating an access token
  3. Resource server: Verifying the access token

Client: Generating DPoP proof

On the client side, we need to generate a key pair (public and private keys) and store them securely.

For each request including the token request, the client needs to generate a DPoP proof which is a JSON web token (JWT). Here's an example of DPoP proof. Let's look into each field inside the JWT

Decoded DPoP Proof
12345678910111213141516171819
// Header
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"x": "l8tFrhx-34tV3hRICRDY9zCkDlpBhF42UQUfWVAWBFs",
"y": "9VE4jf_Ok_o64zbTTlcuNJajHmt6v9TDVrU0CdvGRDA",
"crv": "P-256"
}
}
// Payload
{
"jti": "-BwC3ESc6acc2lTc",
"htm": "POST",
"htu": "https://server.example.com/token",
"iat": 1562262616
}
  • Header
    • typ - Type of the token
    • alg - Encryption algorithm
    • jwk - JSON web key of the public key
  • Payload
    • jti - Generated token ID
    • htm - HTTP Method of the requesting resource
    • htu - HTTP URL of the requesting resource excludes query
    • iat - Timestamp at issuing token in second since UNIX epoch
  • Signature
    • Sign the token with the private key

The JWK public key is included in the header of JWT with the type dpop+jwt. The payload includes htm and htu for the endpoint used in requesting an access token, the ID of the token, and the issuing timestamp. The authorization server uses these fields to validate the request.
In the last part, we’ll use the generated private key to sign the token as the signature. Here is an example of a request for an access token from the authorization server.

12345678910
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwiaWF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg
grant_type=authorization_code\
&client_id=s6BhdRkqt\
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb\
&code_verifier=bEaL42izcC-o-xBk0K2vuJ6U-y1p9r_wW2dFWIWgjz-

Authorization server: Generating an access token

When the authorization server receives the above request, it verifies the DPoP proof before verifying the authorization code. The steps are:

  1. Verify the token signature using the public key in the token's header
  2. Verify if htm and htu match the request method and endpoint
  3. Verify if it's within an acceptable time window compared to iat
  4. (Optional) Verify if jti has been used before to prevent replay attacks

If these steps are verified, the authorization server generates a new access token. However, an additional step would be added to this step to bind the access token with the client.

The cnf or Confirmation field will be added to the access token's payload. The value of this field is the JWK Thumbprint of the public key sent with the DPoP header.

JWK Thumbprint = Base64(Sha256(PublicKey))
123456789101112131415161718
// Response
{
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IkJlQUxrYiJ9.eyJzdWIiOiJzb21lb25lQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJuYmYiOjE1NjIyNjI2MTEsImV4cCI6MTU2MjI2NjIxNiwiY25mIjp7ImprdCI6IjBaY09DT1JaTll5LURXcHFxMzBqWnlKR0hUTjBkMkhnbEJWM3VpZ3VBNEkifX0.3Tyo8VTcn6u_PboUmAOYUY1kfAavomW_YwYMkmRNizLJoQzWy2fCo79Zi5yObpIzjWb5xW4OGld7ESZrh0fsrA",
"token_type": "DPoP",
"expires_in": 2677,
"refresh_token": "QZp20...Ni4-g"
}
// Access Token Payload
{
"sub": "someone@example.com",
"iss": "https://server.example.com",
"nbf": 1562262611,
"exp": 1562266216,
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}

Resource server: Verifying the access token

As I mentioned, the client needs to generate a new DPoP proof for every request, including those to the resource server. The client sends both DPoP proof and access token to the resource server. Since the token type is now DPoP, the authorization header type changes accordingly.

1234
GET /protectedresource HTTP/1.1
Host: resource.example.org
Authorization: DPoP eyJhbGciOiJFUzI1NiIsImtpZCI6IkJlQUxrYiJ9.eyJzdWIiOiJzb21lb25lQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJuYmYiOjE1NjIyNjI2MTEsImV4cCI6MTU2MjI2NjIxNiwiY25mIjp7ImprdCI6IjBaY09DT1JaTll5LURXcHFxMzBqWnlKR0hUTjBkMkhnbEJWM3VpZ3VBNEkifX0.3Tyo8VTcn6u_PboUmAOYUY1kfAavomW_YwYMkmRNizLJoQzWy2fCo79Zi5yObpIzjWb5xW4OGld7ESZrh0fsrA
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJlMWozVl9iS2ljOC1MQUVCIiwiaHRtIjoiR0VUIiwiaHR1IjoiaHR0cHM6Ly9yZXNvdXJjZS5leGFtcGxlLm9yZy9wcm90ZWN0ZWRyZXNvdXJjZSIsImlhdCI6MTU2MjI2MjYxOCwiYXRoIjoiZlVIeU8ycjJaM0RaNTNFc05yV0JiMHhXWG9hTnk1OUlpS0NBcWtzbVFFbyJ9.2oW9RP35yRqzhrtNP86L-Ey71EOptxRimPPToA1plemAgR6pxHF8y6-yqyVnmcw6Fy1dqd-jfxSYoMxhAJpLjA

The resource server needs to verify the DPoP as the authorization server does. Additionally, it verifies if the client owns the access token by generating the JWK Thumbprint from the public key in DPoP proof the same way as authorization, and then compares it with the JWK Thumbprint in the access token's payload.

If they don't match, it means that the client doesn't own the access token and the server responds with a 401 Unauthorize error.

Let's assemble these as a diagram

sequenceDiagram
participant c as Client
participant ss as Secure Storage
participant as as Authorization Server
participant rs as Resouce Server

note over c: When get an access token
c ->> c: Generate key pairs
c ->> ss: Store key pairs
c ->> c: Generate DPoP Proof
c ->> as: Request Access Token with DPoP Proof
    alt DPoP is valid
    as ->> as: Generate acess token bind to the client
    as -->> c: Access Token
    else DPoP is invalid
    as -->> c: 401 Unauthorized
    end
note over c: When request resources
c ->> ss: Get key pairs
ss -->> c: Return key pairs
c ->> c: Generate DPoP Proof
c ->> rs: Request resources with DPoP and access token
    alt DPoP and access token is valid
    rs -->> c: Restricted resources
    else DPoP is invalid
    rs -->> c: 401 Unauthorized
    else access token is not bind to the client
    rs -->> c: 401 Unauthorized
    end

With DPoP, your OAuth 2.0 implementation has enhanced security. You can also apply this with other security extensions like RFC 7636: Proof Key for Code Exchange by OAuth Public Clients.

If you're interested in more details about DPoP, please refer to the specification RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP). See you in the next post! 👋🏼