Oauth2-server: Refresh token revoked before we're sure the new tokens were received successfully

Created on 2 Jul 2019  路  11Comments  路  Source: thephpleague/oauth2-server

We're using this package, via Laravel Passport, to build the API for a mobile app. Having mobile clients comes with problems surrounding unstable connections. In this case, the problem seems to be that the client can successfully send a request to the server, but can't receive the server's response. For most requests and responses, this isn't too much of a problem. Except when you can't try again.

This is the case when trying to exchange a refresh token for a new access token; a new access token and refresh token are generated and the old tokens are revoked. But if the client hasn't been able to receive the new tokens, it can't try again. As with many apps, most of the users of this app are not registered, resulting in being locked out if the token exchanges fails.

What I think should happen is not immediately revoking the old tokens (or at least not revoking the refresh token) when the refresh_token grant is called. The old tokens should only be revoked when the new one are successfully used for the first time. When the client tries again with the old refresh token, the new tokens could be returned again or they could be revoked while returning a new and third set of tokens.

What do you think about this? And what's your take on the way to change this behaviour? I'm very much willing to make a PR, but based on your replies I might find a different path if that leads to a better result.

Future Version Improvement Idea

Most helpful comment

I believe this is a very real problem! And to solve it (plus, not have the possibility of a replay attack), here is what I thought of:

  • Let's say the current refresh token is R1 and current access token is A1.
  • Now, A1 has expired, so we use R1 to issue a new R2 and A2. We keep R1 in the DB, and send R2 and A2 to the client.
  • If the client get's R2 and A2, they will use A2, at which point we can remove R1 from the DB.
  • However, if the client does not get R2, A2, they will use R1 again, at which point, your backend should generate R3, A3 and send those over to your client. At this point, we need to keep track of R1, R2 and R3 since we do not know which one the client TRULY has.
  • If the client gets R3, and they use A3, then we can remove R1 and R2.

This method will guarantee that this issue doesn't happen and that you can still use rotating refresh tokens to detect token theft!

However, this method has a problem of having to keep track of these additional tokens. To solve this, I thought that you can create R2 in a way that has R1 in it. Let me elaborate:

  • Let's say R1 is some random string.
  • When we create R2, it is initially treated as a child of R1. So we create it like R2 = R1 + nonce Then we do not store R2 in the db.
  • When we create R3, it is initially also treated as a child of R1. So we create it like R3 = R1 + nonce Then we do not store R3 in the db.
  • Now when the client uses either R2, or R3, then we extract the R1 out of it and check that R1 is still in the db. If it is, we "promote" the child to being the parent and get rid of R1. This also means we will now store R2 in the db. If R1 is not in the db, it means R2 cannot be used.

Note that we don't actually form R2 using just R1, but the hash of R1. And we store the hash of hash of R1 in the db

Above is somewhat a rough idea of the parent-child structure.. In fact, I have implemented this for Laravel and NodeJS here: https://supertokens.io

All 11 comments

I agree completely I鈥檝e been running into this same issue myself.

This is something that has been brought to my attention before and I'm a bit stumped on how to resolve this.

The reason we revoke your refresh token is to make the server more secure but it does have the draw back, as you have seen, that if you have a break in connection, you might not receive the new code.

One way this could be changed is to allow you to use the same refresh token over and over again. Every time you use the refresh token, we revoke the old associated access token and generate a new one. However, this would require adding another option to the server.

Out of interest, why can't your clients request a new access token?

Thank you for your understanding response.

To avoid having to ask for personal data as much as possible, the app uses anonymous users by default. When you first use the app, it calls an endpoint on our API, which creates an empty user and returns the access and refresh tokens. Just the few users that wish to use multiple devices (including users migrating to an other or new device) enter a username and password to register their account. This means that most users don't have credentials to request a new access token when their refresh token is revoked.

Interesting. I can see how that works. You could use something like dynamic client registration for that, which would provide the client with a client id and secret to use in perpetuity, saving user registration and determining credentials for when the user actually wants to register.

Aside from that I get that requesting user interactions in other scenarios can be a hassle with unstable internet so regardless issues can occur.

To be honest I get the use-cases, but it may be hard to fix. You could associate a new access token with the refresh token used to create it and then invalidate it when it is used, but the question would be how to do that only once. Especially in a distributed setting where the authorization server and resource server are not the same systems.

What if the client provides feedback that they have received the tokens? So the flow would look something like this:
1) Client鈥檚 access token expires.
2) Client sends request to refresh tokens.
3) Client receives new access and refresh tokens back from step 2.
4) Client sends request to server that we have received the new tokens and the server expires the old refresh token.

Step 4 is the only thing that would need to be added. In step 4, we could pass in the original refresh token and the server could expire it. This way, we know for a fact that we can revoke the original refresh token since it has been used.

I'd prefer not to add extra steps into the flow. I think the only way we can resolve this is to allow users to have a non-expiring refresh token. It would probably be best if we implement this as the same time as the token revokation RFC. I have no timescales for this at present as we've only just released version 8 which modified the PKCE support for the library.

Didn't know about the "Token Revocation RFC". That's a more formal version of my Step 4 above. Looks like a good way to handle this issue.

I believe this is a very real problem! And to solve it (plus, not have the possibility of a replay attack), here is what I thought of:

  • Let's say the current refresh token is R1 and current access token is A1.
  • Now, A1 has expired, so we use R1 to issue a new R2 and A2. We keep R1 in the DB, and send R2 and A2 to the client.
  • If the client get's R2 and A2, they will use A2, at which point we can remove R1 from the DB.
  • However, if the client does not get R2, A2, they will use R1 again, at which point, your backend should generate R3, A3 and send those over to your client. At this point, we need to keep track of R1, R2 and R3 since we do not know which one the client TRULY has.
  • If the client gets R3, and they use A3, then we can remove R1 and R2.

This method will guarantee that this issue doesn't happen and that you can still use rotating refresh tokens to detect token theft!

However, this method has a problem of having to keep track of these additional tokens. To solve this, I thought that you can create R2 in a way that has R1 in it. Let me elaborate:

  • Let's say R1 is some random string.
  • When we create R2, it is initially treated as a child of R1. So we create it like R2 = R1 + nonce Then we do not store R2 in the db.
  • When we create R3, it is initially also treated as a child of R1. So we create it like R3 = R1 + nonce Then we do not store R3 in the db.
  • Now when the client uses either R2, or R3, then we extract the R1 out of it and check that R1 is still in the db. If it is, we "promote" the child to being the parent and get rid of R1. This also means we will now store R2 in the db. If R1 is not in the db, it means R2 cannot be used.

Note that we don't actually form R2 using just R1, but the hash of R1. And we store the hash of hash of R1 in the db

Above is somewhat a rough idea of the parent-child structure.. In fact, I have implemented this for Laravel and NodeJS here: https://supertokens.io

@rishabhpoddar While I can see how your proposed logic could work, but it would not fit how refresh tokens are persisted in this library currently. The library requires the full refresh token to persist it, and we cannot encode the full refresh token in an access tokens for obvious reasons (super-insecure). Just a refresh token id might be safe and acceptable though.

Maybe I am understanding it incorrectly. Could you create a sequence diagram explaining the interactions between systems maybe? Keep in mind that the resource owner, client, authorization server and resource servers are different entities.

Besides that, there is the larger issue with how/when the authorization server would detect an access token is used for the first time. The reason we use JWT access tokens is so that resources servers can validate them without having to rely on the authorization server. So there is no trigger for the authorization server to update whether the old refresh token should be revoked.

Token introspection (RFC7662) could facilitate the option for resource servers to validate access tokens with the authorization server, but adding that as a requirement for resource server implementations should be avoided I think.

Even if we did that though, invalidating the old refresh token at that point would incur checks with persistent storage on every validation request, because there is no way of knowing the old refresh token is already invalidated or the new one is activated. Or how could we prevent this?

I have read through the latest draft specification on this to see how this might be allowed. It outlines two ways for securing a refresh token from replay attacks, one of which is token rotation. This library might implement 'sender constrained refresh tokens' however. This would add two techniques to the list of subjects to dive into, but this approach might be more suitable than rotating refresh tokens in this use-case and circumvent problems associating with the proposed revoking after first access token use.
https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-4.12

Regardless, this should not be an issue for any client / resource owners other than those automatically generated when an app is first installed - as other grant types must always be usable as a fallback according to specifications. This would thus only really cover a small number of use-cases.

@sg3s Thanks for your quick reply! (Apologies for my initial response being very unclear and brief.)

"we cannot encode the full refresh token in an access tokens for obvious reasons" -> you encode the hashed value of a refresh token in the access / refresh token you are sending over to the client. And in the database, you store the double hashed version..

"Could you create a sequence diagram explaining the interactions between systems maybe" -> If I do, I shall post it here for sure :) Thanks for the suggestion.

Response to your third paragraph -> The resource server does not necessarily need to communicate with the authorisation server upon when it receives a new access token. The auth server will still only have to store the parent refresh token (and none of the child ones). When the child refresh token is used next, the parent one is replaced with the child. The new parent refresh token gets a new child refresh token (and a new access token) which is then sent over to the client.

"invalidating the old refresh token at that point would incur checks with persistent storage on every validation request" -> this can be kept "in state" by changing a flag in the JWT access token and telling the client to use that. However, this is not necessary. As suggested above, the old (parent) refresh token need not be removed from the database since we are not storing its children in the db.

I have written pseudo code for how I think session management should work with rotating refresh tokens: https://github.com/supertokens/home/wiki/Implementation-logic

It would be great if you could have a look at it and let me know your thoughts :)

Hi all. I have given this a lot of thought and I think this is something that should be fixed by the implementer.

I recently made a change for a client who was having such an issue. I modified the revoke function so that it revokes a ticket after X minutes. I also modified the implementation so that a client could have at most, one set of tokens associated with them (so if the refresh token is used again, all previously issued refresh tokens are revoked immediately).

I think this is the best way to resolve this problem. I will also be changing the server for version 9 to remove expiry dates for refresh tokens as this does not conform to the OAuth spec.

Closing this issue as I don't think there is anything to change in this lib.

Was this page helpful?
0 / 5 - 0 ratings