aspnetcore: JWT token silent renew breaks and causes crash on React template with authentication

Describe the bug

The JWT silent renew in the React template with authentication can fail in some scenarios. What happens is the token is listed as expired (user.expires_in is negative and outside the 5 minute clock skew grace period). As a result, the API request fails with a 401, the token is never renewed, and the user can no longer get into [Authorize] marked areas. The only way to solve this I found (and the workaround isn’t even confirmed), is to explicitly press the logout button and restart my application (presumably to reset authentication/authorization on the client side SPA).

If you don’t explicitly log out, this issue will persist even on restarts of the ASP.NET application presumably because it’s a client side issue. There are a lot of steps to the below so some may or may not actually contribute to the issue but it is a consistent repro.

To Reproduce

  • Create a new React based ASP.NET application with Individual User authentication
  • Reduce the AccessTokenLifetime parameter by modifying the call .AddApiAuthorization to:
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(opt =>
    {
        foreach (var c in opt.Clients)
            c.AccessTokenLifetime = 65;
    });
  • The default is an hour but that’s way too long to repro.
  • Start the application, register a new account and let it auto-login for you
  • Now explicitly log out
  • Then login again with your newly created account and make sure to tick the Remember Me box.
  • Now stop the application.
  • Wait for at least 65 seconds (ie. wait until the JWT access token expires).
  • Start the application again with debugging on (if you want to be able to see the expired token live forever; see the last image in this report)
  • Wait for 5 minutes while the clock skew grace period is consumed.
  • Try to access the Fetch Data page which requires authentication/authorization.

Expected outcome: You get to the Fetch Data page with a renewed JWT. Actual outcome: A crash with a JWT that hasn’t and apparently won’t renew.

image

You can break into AuthorizeService.js and inspect the user value to see that the token is expired. You can actually see the token work and count more and more negative until it gets to 300 at which point it’ll then fail.:

image

Further technical details

  • ASP.NET Core 3
  • VSCode

.NET Core SDK (reflecting any global.json): Version: 3.0.100 Commit: 04339c3a26

Runtime Environment: OS Name: Windows OS Version: 10.0.18363 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\3.0.100\

Host (useful for support): Version: 3.0.0 Commit: 7d57652f33

.NET Core SDKs installed: 3.0.100 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed: Microsoft.AspNetCore.All 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 2.1.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

To install additional .NET Core runtimes or SDKs: https://aka.ms/dotnet-download

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Comments: 19 (6 by maintainers)

Most upvoted comments

You can work around it quickly in the meantime by triggering a silent sign in for your user when you receive an expired token.

While delay isn’t ideal, it does stop the site crashing.

async getAccessToken() {
    await this.ensureUserManagerInitialized()
    const user = await this.userManager.getUser()
    if (user.expired === true) {
      try {
        //Attempt to sign in our user silently
        const silentUser = await this.userManager.signinSilent(this.createArguments(LoginMode.Silent))
        //Return refreshed access token
        return silentUser && silentUser.access_token
      } catch (err) {
        //Failed to sign in user silently
        //Sign out user or user other authentication method
      }
    }
    return user && user.access_token
}

Unfortunately when getting our user we don’t appear to get the refresh token. Otherwise I’d look at modifying startup.cs, and creating a new tokenController as per https://www.blinkingcaret.com/2018/05/30/refresh-tokens-in-asp-net-core-web-api/