Closed szalapski closed 3 years ago
@szalapski thanks for contacting us.
This indeed looks like an issue, I'm not 100% positive I understand the scenario, from what I get it happens when the refresh token has expired.
The question that I have is about the refresh, does it error out while trying to authenticate the user to visit a page, or does it error out when the app is trying to provision a token to talk to an API?
From what I can see in the call stack I think is the first scenario, but I want to confirm.
Thanks for contacting us.
We're moving this issue to the Next sprint planning
milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.
"The question that I have is about the refresh, does it error out while trying to authenticate the user to visit a page, or does it error out when the app is trying to provision a token to talk to an API?"
Great question. I believe we are doing both, but of course we can't really see explicit code that does each part. However, it seems right that this is the first scenario--we are trying to render the results of the AuthorizeView markup in the main page's .razor component, not doing any API call yet.
The 400 invalid_grant with AADSTS700081 seems to be in attempting to refresh the expired ID token (which seems to live for 1 hour 5 minutes), but it cannot because the refresh token is also expired (lifetime seems to be 1 day, set on the AD side). When this happened, I have not yet attempted any API call that requires authorization/authentication.
(By the way, we also get similar symptoms when attempting an authorization-required XHR request, but there we can catch a AccessTokenNotAvailableException and redirect to login as a mitigation, as in this example. When just trying to get the user's info on the client only, we cannot catch an exception, as it is all black-box magic inside <AuthorizeView>
. I suspect fixing the core problem here would let us avoid that exception that occurs later, as the client UI would first take care of signing in the user and their tokens would all be valid before attempting a authorization-required XHR request.)
Could this be considered a bug? I doubt it was designed so that an expired refresh token would result in an exception thrown, and an uncatchable one at that. Not sure, just curious.
If this helps, here are all the page and XHR requests this makes, from the Network tab in Firefox developer tools. Hopefully this helps you narrow it down, with the "invalid_grant" request and response selected.
Hmm, I realize that I can't even see a workaround--how do I prompt the user to re-login? If I try to redirect them to sign-in, I just get the same error again. I have to make them sign out, perhaps? If so, this makes things a little more urgent to fix.
Accidentally clicked close.
@captainsafia can you please handle this? Let's target 5.0.2.
@mkArtakMSFT Sure. I'll take a look at this. Behavior is a little surprising because we have some logic that falls back to authenticate via a pop-up if silent sign-in fails but there might be something funky afoot.
Update: I see where the issue is. We don't have any error handling in the token acquisition in our getUser
method which causes exceptions that bubble up from it to be fatal.
We didn't catch this error in our validations/testing because we don't run into the experienced tokens often based on our test scripts and dev loops.
The fix here is to add some error handling to the getUser
method.
Great.
Am I right to assume that the fix would apply to <AuthorizeView>
particularly, or is it broader than that?
Particularly, will the behavior change for XHR requests from an httpClient in a Blazor component? Obviously an XHR request ought never redirect to login. Currently we have to catch an AccessTokenNotAvailableException e
in our component and then call e.Redirect()
in the catch block so that the login sequence happens in the user's browser, not in the XHR request, as in this example. My guess is that the fix you are working on here won't strictly affect that behavior? (This will be hard for us to test so I hope I have explained it adequately; if not, please let's dialog to be sure it is clear to both of us.)
(Of course, if we have good use of <AuthorizeView>
it would prevent most refresh-token-expired XHR requests from being attempted, but that is beside the point here.)
@szalapski It applies to any scenario where the GetUser
API so it shouldn't affect the error handling you're using around HTTP requests.
I have learned that this symptom happens not only when I use <AuthorizeView>
as above, but also when I use <RemoteAuthenticatorView>
, e.g. to redirect to logout. So I cannot even provide a workaround to log out and then back in because beginning the logout process fails before it even starts. I get the same AADSTS700081: The refresh token has expired due to maximum lifetime
error, but I then subsequently get Unhandled exception rendering component: state_mismatch: State mismatch error. Please check your network. Continued requests may cause cache overflow.
as well. This strikes me as a related but separate error, perhaps?
We'd like to go live on our app soon, but this is a potential blocker. I really appreciate that you are already working on it. Can you suggest any kind of workaround? So far the only way I can see to recover is to have the user clear their local storage.
I can prompt the user via a timed HTML fragment that only appears if the user stays on the "Authentication" page for more than 5 seconds (which isn't likely to happen except for this error). I would like the user to click on a link or button to recover from this state, but I cannot see what link destination could possibly help, given that <RemoteAuthenticatorView>
is broken too. Is there any way on the client to totally invalidate their sign-in state?
We'd like to go live on our app soon, but this is a potential blocker. I really appreciate that you are already working on it. Can you suggest any kind of workaround? So far the only way I can see to recover is to have the user clear their local storage.
You can try programmatically calling window.AuthenticationService.signIn
via JS interop from your application code when the user clicks on the button.
Great, I got the workaround working. In case anyone else wants to do the same, here is the code:
Part of MainLayout.razor:
@inject IJSRuntime JS
@* ... *@
<AuthorizeView>
<NotAuthorized>
@* ... *@
</NotAuthorized>
<Authorized>
@* ... *@
</Authorized>
<Authorizing>
@* Mitigation for https://github.com/dotnet/aspnetcore/issues/28151 - can remove when fixed*@
<div class="m-4 authentication-unhandled-failure-message">
If this gets stuck, just
<a class="cursor-pointer" @onclick="RefreshTokenExpiredFailureWorkaround">sign in again</a>.
</div>
</Authorizing>
</AuthorizeView>
@code
{
// see also https://github.com/dotnet/aspnetcore/issues/28151
private async Task RefreshTokenExpiredFailureWorkaround() =>
await JS.InvokeVoidAsync("window.AuthenticationService.signIn");
}
CSS:
/* Mitigation for https://github.com/dotnet/aspnetcore/issues/28151 - can remove when fixed*/
.authentication-unhandled-failure-message {
// Since we cannot detect some errors in authenticating, using animation to show after a delay
animation: fade-in 5s step-end;
animation-fill-mode: both;
opacity: 0;
}
.cursor-pointer {
cursor: pointer;
}
Is there anyway of capturing the InteractionRequiredAuthError so that we can either automate the redirect to window.AuthenticationService.signIn or prevent the default blazor-error-ui from being displayed?
@szalapski Glad the workaround resolved the issue.
A fix for this will be shipped in 5.0.2.
Describe the bug
MSAL on Blazor WebAssembly fails to initiate sign-in when an invalid_grant or AADSTS700081 error occurs--as in when the refresh token is expired
To Reproduce
My MSAL on the client is configured as:
I sign in to my Blazor Web Assembly app, then wait till my refresh token expires (for me, 1 day). Then I try to refresh the page, which includes a component like this:
Expected behavior
The page should show "Authorizing", then the code in MSAL that AuthorizeView triggers should automatically initiate a redirect to sign-in, so that the user can go through authentication and thus get a new refresh token and ID token. (Once signed in, the user should redirect back to the same page, which should show the content within the
<Authorized>
fragment.)Actual behavior
The page shows "Authorizing", and the HTTP request
POST https://login.microsoftonline.com/0c33cce8-883c-4ba5-b615-34a6e2b8ff38/oauth2/v2.0/token
returns HTTP 400 withThen Blazor allows an exception to be thrown with
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: login_required: AADSTS50058: A silent sign-in request was sent but no user is signed in.
and further detail. The error is written to the browser console and Blazor shows the standard "An unhandled error has occurred. Reload" bottom banner.`Possible Solution
Isn't there some way to configure MSAL to initiate the interactive sign-in process on invalid_grant, rather than having it fail fatally? Or is this just a big bug? Any such action would have to navigate/redirect or popup on the user's browsing page, not naively redirect an XHR request, of course.
This seems to be similar to: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/2219 , though I am not using MSAL.js directly.
This looks like my situation exactly, but I don't see how I can mitigate. My code never explicitly calls AcquireTokenSilent.
Additional context/ Logs / Screenshots
Here's the end of the stack trace: https://gist.github.com/szalapski/942baf9b8da7b5bdb68ebd7f9e2f5544
(Thought I'd post first on MSAL repo, but they say it is a aspnetcore issue.)