jasontaylordev / NorthwindTraders

Northwind Traders is a sample application built using ASP.NET Core and Entity Framework Core.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SecurityTokenExpiredException - Refresh Access token does not work (silent renew never called?)

kondelik opened this issue · comments

I am not expert with IdentityServer, thats why i downloaded this (otherwise great!) example app. But i think i found a bug:

I logged in as "andrew@northwind" (found this ApplicationUser in SampleDataSeeder.cs)
It was few minutes after midnight, so i let app run and goes to sleep.

At the morning, Andrew's access token was expired:

Visual Studio Output:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/2.0 GET https://localhost:44376/api/Customers/GetAll  
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[1]
      Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]', Current time: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
   at Microsoft.IdentityModel.Tokens.Validators.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateLifetime(Nullable`1 notBefore, Nullable`1 expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
   at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
   at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[7]
      IdentityServerJwtBearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]', Current time: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[7]
      IdentityServerJwtBearer was not authenticated. Failure message: IDX10223: Lifetime validation failed. The token is expired. ValidTo: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]', Current time: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
      AuthenticationScheme: IdentityServerJwtBearer was challenged.
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 58.169200000000004ms 401 
Chrome console:

VM1481:1 GET https://localhost:44376/api/Customers/GetAll 401

It looked like i was still logged in as Andrew (i see his name in "Hello andrew@northwind" from LoginMenuComponent) but cant access any protected endpoint (see log).

Have to manually logout & login. (That would really suck IRL - i take my time filling ultra complicated enterprise form and blam, have to do it again but faster 😄)

I tried it again (let the access token expire) with same result - Angular thinking i am still logged in (i see Andrew at right top, still getting TokenExpired)

Possible problems:
I dont see any silent renew... I think you are supposed to call it yourself?

Not in AuthorizeInterceptor and truth to be said, I have no idea where to look next... AFAIK (and one more time: i am not an expert, i literally downloaded this repo to see "how it should be done" 😃 ) there should be something like HttpErrorInterceptor which will call AuthorizeService.signIn(?) (thats the only place where i found usage of (oidc-client package) UserManager.signinSilent()) when intercept 401 response status code + ? (And of course some magic to not create endless cycle when you try to access endpoint you really shouldnt be accessing)

App have hidden iFrame with src to https://localhost:44376/connect/checksession (that should be used for Silent renew, shouldnt it?)

Ok, i think i got this wrong, there is settings.automaticSilentRenew = true; in AuthorizeService but callback (silent_redirect_uri) is null... maybe thats the problem? Still investigating...

Ok, i think i nailed the problem... Its kind of schrödinbug.

I added SilentRenewError callback

AuthorizeService.ensureUserManagerInitialized():
(...)

this.userManager.events.addSilentRenewError(function (e) {
      console.error('silent renew error', e.message);
    });

(and lowered AccessTokenLifetime to 2 minutes)

Now, when i keep app open in active window and tab, everything is all right. Access token is refreshed every now and then, no error is printed.

but when i open new tab or go to an another browser window, silent authentication fail:

Chrome dev console:

silent renew error Frame window timed out

Googling around, thats not exactly new problem:
IdentityModel/oidc-client-js#955

Maybe another bug:

AuthorizeService.userManager is created twice:

just add breakpoint after

private async ensureUserManagerInitialized(): Promise<void> {
  if (this.userManager !== undefined) {
    return;
  }
// there
(...)
}

the reason is:

LoginMenuComponent

ngOnInit() {
    this.isAuthenticated = this.authorizeService.isAuthenticated(); // returns promise with this.userManager initialisation.
    this.userName = this.authorizeService.getUser().pipe(map(u => u && u.name)); // return ANOTHER promise with this.userManager initialisation.
}

Well, thats it.

The root of the problem is second initialisation of AuthorizeService.userManager

Solution

If i modify AuthorizeService.getUserFromStorage() to not return new Observable<IUser> every time it is called, AuthorizeService.userManager is initialised only once and problem with Expired token goes away:

private _userStorage$: Observable<IUser>;
private getUserFromStorage(): Observable<IUser> {
  if (this._userStorage$ === undefined) {
    this._userStorage$ = from(this.ensureUserManagerInitialized())
      .pipe(
        mergeMap(() => this.userManager.getUser()),
        map(u => u && u.profile));
  }
  return this._userStorage$;
}

I have no idea if i do not break something else with this modification, so i am not going to do Pull Request.

or even better

private _userStorage$ = from(this.ensureUserManagerInitialized())
      .pipe(
        mergeMap(() => this.userManager.getUser()),
        map(u => u && u.profile)
);

and get rid of getUserFromStorage() method...

(i am still learning angular & rxjs)

Thank you for your interest in this project. This repository has been archived and is no longer actively maintained or supported. We appreciate your understanding. Feel free to explore the codebase and adapt it to your own needs if it serves as a useful reference. If you have any further questions or concerns, please refer to the README for more information.