Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh token not updated in Cache #3160

Open
michiproep opened this issue Dec 4, 2024 · 12 comments
Open

Refresh token not updated in Cache #3160

michiproep opened this issue Dec 4, 2024 · 12 comments

Comments

@michiproep
Copy link

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

3.4.0

Web app

Sign-in users and call web APIs

Web API

Protected web APIs (validating tokens)

Token cache serialization

Distributed caches

Description

We face a serious issue while calling downstream Api for a user.
When a user logs in into the web app (2nd time) after the refresh token has expired, he gets a new access token which is valid for one hour. The token gets written to the cache (in our case: redis) but the corresponding refresh token only gets cached on the first login of the user but never gets updated on new logins nor on refresh_token_exchange.
We can see that on refresh_token_exchange the access token gets updated in the cache.

Error: After the first refresh token has expired the user can never - except for one hour after each login - call the downstream api since the used refresh token for that user is always expired.

Reproduction steps

  1. web app: sign in and call downstream api using IDownstreamApi.CallApiForUserAsync
  2. check cache: access token and refresh token OK
  3. wait until access token expires and call downstream API again => access token in cache exchanged but not refresh token

Error message

"error":"invalid_grant","error_description":"AADB2C90129: The provided grant has been revoked. Please reauthenticate and try again.\r\nCorrelation ID: ....

Id Web logs


Relevant code snippets

(I can provide this if needed)

Regression

dontKnow

Expected behavior

with each refreshTokenExchange, the cache should contain the new refresh token

@odin568
Copy link

odin568 commented Dec 5, 2024

interesting point. I tried to replicate and yes, I see the same behavior. Wonder that this is not more of an issue for others.

@bgavrilMS
Copy link
Member

Hi @odin568 ... The refresh token can be invalidated not just through expiration, but in a variety of situations - user changes password, MFA is enabled in your app, the refresh token gets invalidated. This is just a signal for the app to re-authenticate the user interactively.

The error message that you mention The provided grant has been revoked suggests that the user has revoked access to the app. It does not suggest that the refresh token has expired. Btw default RT expiration is 3 months I believe.

The correct way of handling these exceptions, is to challenge the user again. Are you doing this? And after the user logs a second time, does the error not go away?

Note that the SDK saves a blob containing all 3 tokens - access token, id token, refresh token - in the cache. It doesn't save a single item to Redis.

One way to be sure is to use a tool like Fiddler to capture traffic and to monitor Redis.

  • after a "refresh_token" grant, the token response will contain a new access token, a new id token and a new refresh token
  • observe the value in Redis. The cache key is formed by the "oid" and "tid" claims in the id token. The cache value is in json format, easy to read.

Alternatively, please get verbose logs and send them over. You can send them over email if you like to avoid putting them here - bogavril at microsoft com

@michiproep
Copy link
Author

michiproep commented Dec 5, 2024

@bgavrilMS
You are right with refresh tokens can get invalid in serveral different ways. And yes, the users have to be challengened again in these cases.
But the issue which is decribed here is different:
Even though the user has been challengend again (or the user got a new valid refresh token while running an refresh token exchange) the cache still contains the old invalid/expired refresh token.
So, there is no way to get out of this situation.

Note that the SDK saves a blob containing all 3 tokens - access token, id token, refresh token - in the cache. It doesn't save a single item to Redis.

This is true but doesn't matter. That is where I did verfiy the issue. This single cache entry gets an updated AccessToken but no updated RefreshToken after a refreshTokenGrant

@michiproep
Copy link
Author

The correct way of handling these exceptions, is to challenge the user again. Are you doing this? And after the user logs a second time, does the error not go away?

I already did describe this situation. After a new challange the user is able to work for one more hour (access token expiry) since the access token gets replaced in the cache but not the refresh token

@jmprieur
Copy link
Collaborator

jmprieur commented Dec 5, 2024

@jmprieur jmprieur added question Further information is requested answered and removed bug Something isn't working P2 more info needed labels Dec 5, 2024
@jmprieur
Copy link
Collaborator

jmprieur commented Dec 5, 2024

Closing as answered.

@jmprieur jmprieur closed this as completed Dec 5, 2024
@michiproep
Copy link
Author

michiproep commented Dec 6, 2024

@jmprieur:
Sorry to say but this cannot be closed.
We do handle incremental consent but this leads to the fact that users are now challenged every hour after their very first refresh token expired.
The main question to this issue is:

  • why is the refresh token never exchanged in the cache if a new one is available? Is there any reason for this?
  • why is only the accessToken exchanged?

Additionaly, within the first 30 days (where their first refresh token is still valid) we face another issue
An accessToken aquired via refresh token may contain completly outdated claims (email, roles) since the corresponding refreshtoken got not exchanged with the lasted login/challenge

@michiproep
Copy link
Author

I found another hint for this issue.
At ther very bottom at https://github.com/AzureAD/microsoft-identity-web/wiki/Token-Cache-Troubleshooting#my-users-get-prompted-for-mfa-often-even-after-they-completed-mfa

Note: a similar incident was reported where refresh tokens were expiring, prompting users to re-auth repeatedly

@michiproep
Copy link
Author

Ok, disabling the L1 cache might do the trick but I'm still not happy with this!

builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
    options.DisableL1Cache = true;
});

@michiproep
Copy link
Author

@jmprieur : Do we expect a changing behaviour when the new HybridCache feature gets implemented?

@bgavrilMS
Copy link
Member

bgavrilMS commented Dec 10, 2024

Hi @michiproep - thanks for troubleshooting further and finding this.

Do you have a suggestion on how to better deal with this ? We recommend using session affinity settings to ensure that the same user lands on the same server. Or disable memory caches.

I am not sure that hybrid cache fixes this problem. I see this in their docs
image

Since this error occurs when hitting the IdP, we could have Id.Web catch it and, in case memory + distributed cache is enabled try to get the token from the distributed cache only, i.e. ignore the local memory cache on retry. Thoughts ?

I'll mark it as an enhacement.

CC @jmprieur

@bgavrilMS bgavrilMS added feature request and removed question Further information is requested answered b2c feature request labels Dec 10, 2024
@michiproep
Copy link
Author

Hi @bgavrilMS ,

Do you have a suggestion on how to better deal with this ? We recommend using session affinity settings to ensure that the same user lands on the same server. Or disable memory caches.

I think the problem is not really due to running multiple replicas, it is really "after refresh token exchange, the new refresh token also needs to find it's way to the L2 cache". Because the described scenario is also happening if you run only a single replica of your app.

New finding: It seems like after a fresh login (challenge) the refresh token gets exchanged. It only happens for refreshToken grants.

Additional info: All our apps provide 2 logout buttons:
Sing out from app => just clears app's session cookie
Sing out from all apps => additionally signs out from B2C => in this case, the cache entry is completly removed.
So, for now we told our users in case they run into an error to "completly sign out". But this is just a workaround and it is annoying for them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants