Light-OAuth2 Refresh Tokens live forever? (Must be explictly deleted)

279 Views Asked by At

I have a question about light-oauth2 refresh tokens. It appears that they never expire so must be manually revoked, which IMO causes maintenance and security issues, and also seems to contradict the light-oauth2 docs.

(Note: My ears are open to explanations about this behaviour, as there may be very good reasons behind it. It is simply (at the moment) counterintuitive to me.)

More Details

The refresh tokens accumulate indefinitely in the refresh_token table. There does not appear to be any code in CacheStartupHookProvider that gives them a TTL. As you can see, the code that would seem to evict refresh tokens after 1 day has been commented out (lines 108-119):

        // fresh token map with near cache and evict. A new refresh token will
        // be generated each time refresh token is used. This token only lives
        // for 1 day and it will be removed from the cache automatically.
        MapConfig tokenConfig = new MapConfig();
        tokenConfig.setName("tokens");
        NearCacheConfig tokenCacheConfig = new NearCacheConfig();
        /*
        tokenCacheConfig.setTimeToLiveSeconds(24 * 60 * 60 * 1000); // 1 hour TTL
        tokenCacheConfig.setMaxIdleSeconds(24 * 60 * 60 * 1000);    // 30 minutes max idle seconds
        tokenCacheConfig.setInMemoryFormat(InMemoryFormat.OBJECT);
        tokenCacheConfig.setCacheLocalEntries(true); // this enables the local
        */

Also, the light-oauth2 docs state that:

"The authorization server issues a new refresh token and the client MUST discard the old refresh token and replace it with the new refresh token. The authorization server revoke the old refresh token after issuing a new refresh token to the client." (emphasis mine)

But my own tests show that this isn't happening. For example:

  1. Call the light-oauth2 code service (GET), and use the authorization code that was sent to the redirect uri.
  2. Call the light-oauth2 (POST: authorization code grant type) with the authorization code. It will return an authorization token T1 and a refresh token R1.
  3. Call the light-oauth2 token service (POST: refresh token grant type) with refresh token R1. It will return an authorization token T2 and refresh token R2.
  4. Call the light-oauth2 token service again (again, the POST: refresh token grant type) with refresh token R1. It will return an authorization token T3 and refresh token R3.

Step 4 seems to contradict the docs, which say that The authorization server revoke the old refresh token after issuing a new refresh token to the client. From step 4 above, however, it does not appear that refresh token R1 has been revoked even after the new refresh token R2 has been issued. R1 is still able to get new authorization tokens.

So now, R1, R2, and R3 can all be used to get new authorization tokens, and the set of useable refresh tokens keeps growing indefinitely.

My question is whether this is an omission or by design (and perhaps the docs should be updated). And if it is by design, what is the rationale for this? It would seem to me that

  • its insecure to have so many never-expiring refresh tokens issued
  • it creates a maintenance complication to have to manually remove refresh tokens from the database, rather than have them automatically expire and be evicted.
  • adding so many new refresh tokens to the database is slower and seemingly unnecessary (e.g.: why is new refresh token needed every time a new authorization token is issued? It would seem more efficient if a new refresh token was issued ONLY in the authorization code grant type)

Thanks for any help

2

There are 2 best solutions below

0
On

These are all good questions and I think you have identified a defect in the implementation.

First, let me explain why we issue a new refresh token every time the old refresh token is used.

As we all know, the access token we issued is a JWT token and it cannot be revoked once it is issued. To mitigate the risk of the stolen access token, we have to make sure that the access will be expired within a short period. Normally, itis between 5 minutes to 15 minutes depending on the implementation. This is where the refresh token has emerged. It can be last longer and most OAuth 2.0 implementation provides an API to revoke it upon the report that the refresh token is stolen from the user. Depending on the security level of the web application, sometimes, the refresh token needs to last 24 hours to several months. We don't want to limit it so we remove the TTL for the refresh token so that it will never expire. To maximize the security, we don't want to reuse the refresh token again and again, so a new refresh token is issued along with the access token. This makes the refresh token a one-time token so it won't last long enough once it is stolen as the active session will have to renew the access token every 5 to 15 minutes unless the application is closed.

What you found that the previous refresh token still can be used to get access token is strange and it might be a defect. If you look at this line of the code https://github.com/networknt/light-oauth2/blob/master/token/src/main/java/com/networknt/oauth/token/handler/Oauth2TokenPostHandler.java#L393

The old refresh token is removed from the database and cache. I don't know if the Hazelcast will cache the data longer or not. We need more testing and investigation for this issue. Would you be able to open an issue on the light-oauth2 repo?

As this is about security, we need to pay more attention to all the details. Let's figure this out and get it fixed together. Thanks a lot for raising the issue.

1
On

I believe I found the error. In line 423 of com.networknt.oauth.token.handler.Oauth2TokenPostHandler, we have

String newRefreshToken = UUID.randomUUID().toString();

But then in line 431 we have

tokens.put(refreshToken, newToken);.

This should be

tokens.put(newRefreshToken, newToken);

In other words, the code was using the old refresh token as the key to the new refresh token.