I am coming back to this, and I have a solution now, but I do have a question with regards to it (see bottom paragraph for question).
I'll start by saying a refresh token attack using a refresh token generated from a PKCE flow is somewhat the fault of the PKCE spec and/or ISVA. The reason for this, if you are using a true authorization code grant flow (without PKCE), you cannot exchange the refresh token without authenticating the client (via client ID or mTLS). However, this breaks for PKCE, because the API client is not authenticated.
With PKCE, the API client has no secret and the token_endpoint_auth_method is none, and hence the API client does not require authentication. This creates the ability of someone to compromise the refresh token that was generated for a PKCE flow and reuse it without providing any authentication to the authorization server. However, because the token was generated for a PKCE flow, the token should be bound to the subject user that initially generated the refresh (and obviously access) token rather than ISVA just blindly exchanging the refresh token.
Now, I can see a counter argument to this. One could argue that the user does not need to be logged into the IAM system where the authorization server resides to refresh their token. For example, say the user's client pulls down a single page application, get a token from ISVA, then the user goes on their merry way of using the SPA for several hours while not interacting with the ISVA infrastructure, except, to refresh their token. One could argue perhaps that the client SHOULD be allowed to refresh the token without requiring the user to have a valid session with the authorization server. So perhaps that is why there is somewhat a risk here both in the PKCE spec and hence in ISVA.
I suspect this counter argument I mention is why the PKCE spec works the way it does, because if you didn't care about this risk, you'd simply deny refresh tokens (which ISVA v10 can correctly do now by simply not registering the PKCE client with the refresh_token grant type) for PKCE clients and tell the developers their app needs to just get a new access token when it is expired, because after all, the user needs to be logged in anyway otherwise we can't protect the refresh tokens like we want to. However, when I tried to explain this to our developers, they said their code required the use of refresh tokens, hence why I am in this pickle. My guess is that down the road, someone will complain about what I've implemented below, and I'll have to put a switch that can be set as an attribute on the client registrations to override this restriction, hence allowing the refresh token refresh without a valid user session present putting the functionality back to how it is today. What fun!
Here is my solution (note we use a lot of mTLS here implemented to RFC8705 spec, so I have this at hand as well for extra protection):
- In post mapping rule, when an access token request for an authorization code grant is created, and there is a code_verifier present in the oauth body parameters, I associate an attribute to say something isPkce=true to the token's state id
- Note for mTLS I also associate the cert fingerprint for all tokens' state id (hence every token we record this where mTLS authentication is used by an unattended API client or attended user)
- In the pre mapping rule, for a request to refresh the token, I look to see if that isPkce association is present for the token's state id and if that association value is set to true. If it does exist and it is set to true, we know this token was generated during a PKCE flow when it was initially requested, so we go into further restriction logic, otherwise we just get out of this logic block I just added and continue normally.
- In this new restriction logic, I grab the current user id (stsuu.getPrincipalName()), the user ID stored on the token (using token.getUsername()), the mTLS cert fingerprint used for the current call (if present), and the mTLS cert fingerprint stored for the original access token request (if present)
- I then compare all this, and if the username and/or mTLS fingerprint (if both original OR current mTLS fingerprints are present) are different, I call throwSTSCustomUserMessageException and throw 400 with an invalid_grant so no access token is returned.
Now, here's the thing. All this works great. However, because the refresh tokens can only be used a single time, immediately after the refresh token is replayed without the correct subject or mTLS fingerprint, the refresh token is removed by the ISVA logic and hence no longer valid during a legitimate refresh. For example, if the refresh token was compromised or somehow the flow was broken, such as if the user was not logged in when refreshing the token.
Is there any way to stop the refresh token from being removed so if it is compromised, it would still be valid for the original user? Or is this not possible? Honestly the API developers wanted it to be removed as per the spec I mentioned previously in this thread. However, I think this is a headache on the original user, since with what I have implemented, the token is worthless to someone that compromises it now. So if I could prevent it from being removed, I think it would be best for end user experience.
Thanks for any input!
------------------------------
Matt Jenkins
------------------------------
Original Message:
Sent: Wed June 01, 2022 10:25 AM
From: Shane Weeden
Subject: Preventing OAuth refresh token replay attacks
For the most part, associate attributes such as DPoP keys or other MTLS/x509 info with the *grant* rather than a specific token. I think you probably meant this anyway. By this I mean use the state_id of the grant as an index, not the individual ATs or RTs that are issued through the lifetime of that grant (unless of course there is a specific use case for doing so).
While on terminology, OAuth grants (which include refresh tokens) are issued to a client, not a user. So what you are trying to ensure is that the same client to whom the grant was issued is subsequently presenting the RT during a refresh operation at the /token endpoint.
In the case of confidential clients (those that use MTLS or a client secret), this whole topic should be largely irrelevant as you already surmised. Technologies like DPoP add value on refresh token flows when you have public clients - those which do not have a unique client authentication credential. DPoP also has a place in resource requests using an access token where client credentials are not provided (as does MTLS), but that's out of scope for this question.
Such public clients are typically either native mobile apps or SPAs (single page web apps), and while these should also use PKCE, PKCE itself isn't the catalyst for device-bound proofs on refresh token flows - public clients are. I don't quite follow what you mean by "attended flows", but you most certainly can tell if a client is confident or public. When implementing a scheme like DPoP what you really care about is that the same proof of possession key is presented during a refresh flow (or use of an access token on a protected resource) as was presenting during the initial establishment of the grant (typically exchanging an initial authorization code for RT+AT+id_token, etc).
I advocate for a slight variation of the last DPoP draft I read during refresh token flows - which includes an rt_hash in DPoP jwt to bind the RT value to the DPoP included in the request. Either way just make sure you are comfortable that your scheme provides some kind of guarantee that the presenter of a refresh token in a public client context is the actual client to which it was issued. Schemes like DPoP can help do this.
------------------------------
Shane Weeden
IBM
------------------------------
Original Message:
Sent: Wed June 01, 2022 09:09 AM
From: Matt Jenkins
Subject: Preventing OAuth refresh token replay attacks
Shane, thanks very much for the reply and mentioning device bound proofs. We already implemented RFC8705 mTLS certificate bound access tokens. I even implemented this for PKCE flows, although that wasn't part of the spec, I store the x509 fingerprint of the attended user into the JWT or into the database (if using opaque tokens, we support both but steer towards JWT). However, we do not check the mTLS during refresh token exchange, but if I did, that would eliminate the issue I believe, at least where mTLS was involved. I don't think, however, I am associating the fingerprint with the refresh token.
Further, however, I was thinking this out for PKCE after reading your message and meditating on it some. PKCE flows are attended flows, so we should always know the subject. If we just record with the refresh token it was issued from an attended flow, then when we go to exchange it, we just check that the subject matches the current authenticated user. If they don't match, then something is amiss and we do not allow the exchange. This doesn't protect the access tokens, which DPoP would protect (when we don't have mTLS authentication involved), but this solution would protect the refresh tokens for attended flows. For confidential unattended flows, the client id+secret must be present for the refresh token exchange from my understanding, so they would be protected by the API client credentials in that case.
Any thoughts on just protecting the refresh tokens for these attended flows? Is there any way to tell the refresh token was generated for a PKCE attended flow today, or will I likely have to associate some attribute during the pre-mapping rule with it?
This doesn't exactly give the same functionality as what was asked, to remove all refresh tokens if there is a compromise, but it would prevent a replay attack, which is the goal of revoking them in the first place. Aside, this would provide a better user experience, as if their token was compromised, and they went to refresh their token in the future, their flow would not be broken because someone compromised their token (which the attacker could then do nothing with).
Thanks Shane!
PS: For others interested, here were two great points of reading for DPoP that Shane mentioned.
------------------------------
Matt Jenkins
Original Message:
Sent: Wed June 01, 2022 04:00 AM
From: Shane Weeden
Subject: Preventing OAuth refresh token replay attacks
I personally believe this is pretty much impossible to implement effectively, let alone efficiently. A better approach is to use device-bound proofs on RT flows, such as DPOP. This would mean a lift-and-shift approach of using a stolen RT would not work. I'd also argue that aside from a brute force attack, how did the RT get compromised? If that was easily possible, then perhaps there are bigger problems in play?
The DPOP standard is still evolving, and whilst not currently natively supported in ISVA, I have implemented a prototype via some creative additions to mapping rules and have a sample client that runs in a well-known API test tool. It might be best to reach out to your customer advocate, and suggest they arrange a time with me to take you through that sample if you want to be an early adopter. Otherwise, I have a fair idea of your capabilities and believe you might know how to do this as well.... either way, at least consider the merits of DPOP over trying to implement that SBP. If you cannot do this because the clients are out of your control and incapable of DPOP, then everything you've suggested about using mapping rules to store [hashes of] previously presented RT's is the only viable approach. I believe you should still be able to get to a pre-token mapping rule even if the RT is unknown to ISVA - that is how we support "custom implementations of ATs and RTs such as JWTs".
------------------------------
Shane Weeden
IBM