OAuth DPoP: Lifting the Hood on 'Demonstrating Proof of Possession'
— OAUTH, oidc, DPOP, demonstrating proof of possession — 7 min read
As we continue to explore the OAuth framework, the common trend is that it’s perpetually under construction 🚧. Today we’re going to lift the hood on DPoP, and hopefully by the end of this you’ll have a better understanding on how it works at a high level and technical level.
Links:
GitHub - Identity Citizen's Open Source DPoP Client (Using raw JS)
So DPoP is Demonstrating Proof of Possession. It’s an extension to the OAuth 2.0 framework published in September 2023. At its core, it’s a simple concept that can be described with a building access card analogy (which loosely follows an OAuth flow).
Greg has landed himself a great new job at XYZCORP. On his first day at the high-rise headquarters he is directed down to the security office to get his building access card.
The Security staff issue Greg with an access card which allows him to get past the electronic security gates in the XYZCORP HQ lobby. Consider this swipe card an ‘Access Token’ in the OAuth flow. It’s what determines if you’re authorized to access a resource, e.g. building, website, data, etc.
Everything is working fine at this point. However, what happens if a criminal is able to duplicate Greg’s access card without him knowing? This is the equivalent of someone stealing an OAuth Access Token.
Now, this criminal is able to enter the XYZCORP building using Greg’s duplicate access card and have the exact same level of authorization as him. Much like someone could access websites and other resources directly using a stolen Access Token.
This is where DPoP defends against these levels of attacks. The attacker is simply in possession of a ‘thing’, whether it’s a physical access card or an OAuth Access Token, it’s a single 'thing' which when held, authorizes the holder to something private. DPoP adds a second layer of security at the metaphorical security gate to ensure that if someone presents Greg’s access card, a verification takes place to ensure it is actually Greg that is trying to access the building and not an imposter with a duplicate access card. If that verification fails, the gates simply won’t open.
Demonstrating Proof of Possession (DPoP) uses Public-Key Cryptography to link an Access Token to a Client. At a very simplified and high level view, here’s what happens:
- Using an Authorization Code grant flow, our Client directs us to an Authorization Server, which issues us an Authorization Code.
- Before we exchange this Authorization Code for an Access Token, our Client locally generates a Public and Private Key Pair.
- Along with the Authorization Code, we also send the Public Key of our Key Pair.
- The Authorization Server issues us with a DPoP flavoured Access Token.
Now that we have our Access Token, if we plan to use it against a Resource Server (or the User Info Endpoint), we can’t just use our Access Token on its own. We need to perform an additional step.
- Before requesting a protected resource, we have to create a ‘proof’ to send alongside our Access Token. This is a token that contains a hash of the Access Token, a timestamp and some information about the resource we're requesting (such as the HTTP Method and URL of the resource).
- Using the Private Key of the Key Pair we sign this token and include it in a separate “DPoP” header along with the Access Token in its usual Authorization header (albeit with a DPoP scheme instead of the usual Bearer scheme).
- We now rely on the Resource Server to properly validate this DPoP enabled request by cross checking the following:
- Does a Hash of the Access Token match the Token Hash in the DPoP?
- Does the HTTP Method and URI match with what was requested from the resource server?
- Can we validate the DPoP token signature with the Public Key provided during the Access Token request?
- Is the timestamp in the DPoP header within an allowable time-skew (e.g. +/- 5 minutes)?
- (Optional) Ensure that the DPoP JWT has not been replayed.
The safechecks that DPoP provides makes it infuriatingly difficult for an attacker to do much (or any) damage with a stolen Access Token. Even if they manage to capture a DPoP header as well, you’d be limited to just the request endpoint for a very limited amount of times, assuming that replay prevention isn’t implemented.
OAuth was revolutionary in the way it allowed Resource Servers to grant access to people without ever needing to see their credentials. The fewer times an application needs to handle a password the better.
However, DPoP takes this a step further and wraps a safety blanket around the Access Token itself. Tokens have to traverse many software layers to get to their destination (think browsers, proxies, web servers, etc), so each layer opens an opportunity for compromise.
By binding Access Tokens with Clients using cryptography, we improve the integrity of the entire authorization process.
Looking Under the Hood
Let's have a look at some raw requests in a sample DPoP transaction. The scenario here is requesting an Access Token, and using this to request user information from the User Info endpoint.
- The Authorization Request is unchanged from any usual Authorization Code flow (this one uses PKCE as well to align with best practices):
GET /as/authorization.oauth2?client_id=DPOP&redirect_uri=http://localhost&scope=openid&response_type=code&state=tMqvkxPUKemM&code_challenge=B-GL4-WvJES1RX5iJgmthvRqEfyipputNPsS81T6zCI&code_challenge_method=S256 HTTP/2Host: auth.identitycitizen.local:9031
- The Authorization returns to my callback URL (localhost) with an Authorization Code
GET /?code=hbW0zk0kvGn86y_wp2DzoyGCGxRgZVbvv4tlwr38&state=tMqvkxPUKemM
At this point, the Client generates a Key Pair for kicking off the DPoP request for Tokens.
- To exchange my Authorization Code for an Access Token, my Client creates a signed DPoP JWT, which includes the Public Key so the Authorization Server (and Resource Server) can validate request in the future.
POST /as/token.oauth2 HTTP/2Host: auth.identitycitizen.local:9031User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0Accept: */*Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflate, brReferer: http://localhost/Content-Type: application/x-www-form-urlencodedDPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Ind4ci0wSUViQ0p1UmVCY093dkhLdGpqTFp3ckZSM3ZwMzVuaHgzU0xSMTgiLCJ5IjoiN3cxdENYZXJMcXhOaURlZUgxVU1HSExaMnZBVEpSVG82bjNkYm1Ldjh2USIsImNydiI6IlAtMjU2In19.eyJqdGkiOiI4Q2s1SDZWMVkzIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vYXV0aC5pZGVudGl0eWNpdGl6ZW4ubG9jYWw6OTAzMS9hcy90b2tlbi5vYXV0aDIiLCJpYXQiOjE3MDgzMzQ2MDl9.nzirDi70IUae9LuNW1pGSlXhQT45ztkE_PVftGHlYTLjN3mjMaHzMX8Pybb8bv6yZFXPZgPjbsYAnoMtMxrtdAContent-Length: 195Origin: http://localhost
If we extract the JWT DPoP header and decode it, we are have two components.
The JWT Header:
{ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "x": "wxr-0IEbCJuReBcOwvHKtjjLZwrFR3vp35nhx3SLR18", "y": "7w1tCXerLqxNiDeeH1UMGHLZ2vATJRTo6n3dbmKv8vQ", "crv": "P-256" }}
The header contains the Public Key information in the jwk
claim.
The JWT Body:
{ "jti": "8Ck5H6V1Y3", "htm": "POST", "htu": "https://auth.identitycitizen.local:9031/as/token.oauth2", "iat": 1708334609}
This contains information about the Authorization Server to which I'm making the request.
This JWT is signed by the Private Key of the generated Key Pair.
The Authorization returns an Access Token at this point.
- Now we have our Access Token, we can request the User Info Endpoint to retrieve additional user information.
GET /idp/userinfo.openid HTTP/2Host: auth.identitycitizen.local:9031User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0Accept: */*Accept-Language: en-US,en;q=0.5Accept-Encoding: gzip, deflate, brReferer: http://localhost/Content-Type: application/x-www-form-urlencodedAuthorization: DPoP eyJhbGciOiJ....ODM0MTgxMH0.LqHREaITc9S91z // shortenedDPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwieCI6Ind4ci0wSUViQ0p1UmVCY093dkhLdGpqTFp3ckZSM3ZwMzVuaHgzU0xSMTgiLCJ5IjoiN3cxdENYZXJMcXhOaURlZUgxVU1HSExaMnZBVEpSVG82bjNkYm1Ldjh2USIsImNydiI6IlAtMjU2In19.eyJqdGkiOiJHODhHTTkyTzhHIiwiaHRtIjoiR0VUIiwiaHR1IjoiaHR0cHM6Ly9hdXRoLmlkZW50aXR5Y2l0aXplbi5sb2NhbDo5MDMxL2lkcC91c2VyaW5mby5vcGVuaWQiLCJpYXQiOjE3MDgzMzQ2MTAsImF0aCI6IklpeW9lM25ZaURxc3EteFhCX1FoU0lVbUdXSkdwVHpVSzVHT1ZFUXpQUG8ifQ.J4H9SaOuxKwW-A_0A-svNGyV_3jDrvsEMWaGx5x3aXTGzS1FDXV4EA_ilJCmCnX3I63mS6_d84-tpQqdUyPBPgOrigin: http://localhost
Notice the Authorization header containing the Access Token uses the 'DPoP' scheme as opposed to the usual 'Bearer' scheme.
The DPoP header is also in JWT format, again signed by the Private Key, and also contains the Public Key in the header.
{ "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "x": "wxr-0IEbCJuReBcOwvHKtjjLZwrFR3vp35nhx3SLR18", "y": "7w1tCXerLqxNiDeeH1UMGHLZ2vATJRTo6n3dbmKv8vQ", "crv": "P-256" }}
The resource server must ensure the Public Key matches the one provided to the Authorization Server at Token issue time.
The DPoP JWT Body contains information about the Resource Server URI, as well as the ath
claim, which is a SHA256 Hash of the Access Token to ensure integrity between the DPoP header and the Access Token.
{ "jti": "G88GM92O8G", "htm": "GET", "htu": "https://auth.identitycitizen.local:9031/idp/userinfo.openid", "iat": 1708334610, "ath": "Iiyoe3nYiDqsq-xXB_QhSIUmGWJGpTzUK5GOVEQzPPo"}
On successful validation by the Resource Server, the protected information becomes accessible to the client.
DPoP is a great new offering in the OAuth framework, and one that should be seriously considered for those who take security very seriously. Reach out with your experiences!
If you'd like to get even deeper with DPoP, check out our open-source OAuth DPoP client. It uses raw JavaScript to build the DPoP payloads/headers/etc so hopefully showcase the flow easier from a programmers point of view.
GitHub - Identity Citizen's Open Source DPoP Client (Using raw JS)