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
- Client: Generating DPoP proof
- Authorization server: Generating an access token
- 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
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.
12345678910POST /token HTTP/1.1Host: server.example.comContent-Type: application/x-www-form-urlencodedDPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCRnMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JEQSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwiaWF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lggrant_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:
- Verify the token signature using the public key in the token's header
- Verify if htm and htu match the request method and endpoint
- Verify if it's within an acceptable time window compared to iat
- (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.
1234GET /protectedresource HTTP/1.1Host: resource.example.orgAuthorization: DPoP eyJhbGciOiJFUzI1NiIsImtpZCI6IkJlQUxrYiJ9.eyJzdWIiOiJzb21lb25lQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJuYmYiOjE1NjIyNjI2MTEsImV4cCI6MTU2MjI2NjIxNiwiY25mIjp7ImprdCI6IjBaY09DT1JaTll5LURXcHFxMzBqWnlKR0hUTjBkMkhnbEJWM3VpZ3VBNEkifX0.3Tyo8VTcn6u_PboUmAOYUY1kfAavomW_YwYMkmRNizLJoQzWy2fCo79Zi5yObpIzjWb5xW4OGld7ESZrh0fsrADPoP: 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! 👋🏼