Identityserver4: Code flow & forced to login after app was closed for ~ 1 hour

Created on 10 May 2020  路  7Comments  路  Source: IdentityServer/IdentityServer4

Hello,

I would like to implement a persistent login with refresh tokens without prompting the login view to the user but i have to login again after the app was closed for like 1 hour +.

Steps to reproduce

  • login into the app via navigating to a restricted area like dashboard
  • close the app
  • open it after 1 hour+
  • click again on dashboard

issue: the user gets redirected to the identity server and has to login again
expected: user can continue without the login prompt

Do i have to implement the refresh of the accesstoken via refreshtoken by myself when the accesstoken is already expired? or is the identity server doing that by its own? Probably thats the missing part. The refresh is working when im using the app (probably from silent-refresh.html i think). But if i reopen the app after 1 hour and navigating to an restricted area the refresh token should be still valid.

I tested it mainly on a installed pwa on android but it happens on desktop as well.

Testing (currently deployed for demo, before it was both on 3600):

  • Access Token expiration on 2 min
  • Identity Token expiration on 2 min

On inspecting the network i can see that the access and refresh token gets refreshed every min or so. This is working like expected, it seems like when im using the app it works properly.

The problem occurs when im closing the app like for 1 hour and open it again, navigating on a restricted area my app redirects me to the login screen.
If i close my app only for some minutes everything works without prompt the login.

Thats my Config

ClientId = "angular_spa",
                    ClientName = "angular_spa",
                    AccessTokenType = AccessTokenType.Jwt,
                    AccessTokenLifetime = 120,
                    IdentityTokenLifetime = 120,
                    RefreshTokenExpiration = TokenExpiration.Sliding,
                    RefreshTokenUsage = TokenUsage.OneTimeOnly,
                    AbsoluteRefreshTokenLifetime = 0,
                    SlidingRefreshTokenLifetime = 2592000,
                    RequireClientSecret = false,
                    AllowedGrantTypes = GrantTypes.Code,
                    RequirePkce = true,
                    AllowAccessTokensViaBrowser = true,
                    AllowedScopes = new List<string> {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.OfflineAccess,
                        "spa",
                        "role"},
                    RedirectUris = new List<string>(settings.RedirectUrl),
                    PostLogoutRedirectUris = new List<string>(settings.PostLogoutRedirectUri),
                    AllowedCorsOrigins = new List<string>(settings.AllowedCorsOrigin),
                    RequireConsent = false,
                    EnableLocalLogin = true,
                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowOfflineAccess = true,
                    AlwaysSendClientClaims = true,
                    UpdateAccessTokenClaimsOnRefresh = true,
                    UserSsoLifetime = 31104000

My Identity registration:

services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
                options.UserInteraction.LoginUrl = "/Account/Login";
                options.UserInteraction.LogoutUrl = "/Account/Logout";
                options.Authentication = new AuthenticationOptions()
                {
                    CookieLifetime = TimeSpan.FromSeconds(31104000),
                    CookieSlidingExpiration = true
                };
                if (appsettings.IssuerUri != string.Empty)
                {
                    options.IssuerUri = appsettings.IssuerUri;

                    if (appsettings.EnablePublicOrigin)
                    {
                        options.PublicOrigin = appsettings.IssuerUri;
                    }
                }
            })
            .AddDeveloperSigningCredential()
            .AddAspNetIdentity<ApplicationUser>()
            .AddProfileService<IdentityWithAdditionalClaimsProfileService>()
            .AddConfigurationStore(options =>
            {
                if (appsettings.AuthDbType == DatabaseTypes.MSSQL)
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
                }
                if (appsettings.AuthDbType == DatabaseTypes.Postgre)
                {
                    options.ConfigureDbContext = b => b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
                }
            })
            .AddOperationalStore(options =>
            {
                if (appsettings.AuthDbType == DatabaseTypes.MSSQL)
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
                }
                if (appsettings.AuthDbType == DatabaseTypes.Postgre)
                {
                    options.ConfigureDbContext = b => b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly));
                }
            });

Thats the angular part, not sure if its relevant but just for completion:

import { Injectable } from '@angular/core';
import { UserManager, UserManagerSettings, User } from 'oidc-client';
import { Router } from '@angular/router';
import { UserRoles } from '../../models/UserRoles';
import { environment } from '../../../environments/environment';

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    public manager: UserManager = new UserManager(getClientSettings());
    public user: User = null;

    constructor(private router: Router) {
        this.manager.getUser().then(user => {
            this.user = user;
        });

        this.manager.events.addUserLoaded(x => {
            this.user = x;
        });

        this.manager.events.addAccessTokenExpired(x => {
            this.manager.signinSilent().then(function (user) {
            }).catch(function (e) {
            })
        });
    }

    isLoggedIn(): boolean {
        return this.user != null && !this.user.expired && this.user.profile.role != null;
    }

    isStandardUser() {
        return this.user != null &&
            !this.user.expired &&
            this.user.profile.role != null &&
            this.user.profile.role.some(x => x === UserRoles.Standard);
    }

    isAdministrator() {
        return this.user != null &&
            !this.user.expired &&
            this.user.profile.role != null &&
            this.user.profile.role.some(x => x === UserRoles.Administrator);
    }

    getClaims(): any {
        if (this.user) {
            return this.user.profile;
        } else {
            return { name: 'Guest' }
        }
    }

    getAuthorizationHeaderValue(): string {
        if (this.user) {
            return `${this.user.token_type} ${this.user.access_token}`;
        }
        return '';
    }

    startAuthentication(): Promise<void> {
        return this.manager.signinRedirect();
    }

    completeAuthentication(): Promise<void> {
        return this.manager.signinRedirectCallback().then(user => {
            this.user = user;
        });
    }

    logout(): Promise<any> {
        return this.manager.signoutRedirect();
    }
}

export function getClientSettings(): UserManagerSettings {
    return {
        authority: environment.authority,
        client_id: 'angular_spa',
        redirect_uri: environment.redirect_uri,
        response_type: "code",
        scope: "openid profile spa role offline_access",
        filterProtocolClaims: true,
        loadUserInfo: true,
        automaticSilentRenew: true,
        silent_redirect_uri: environment.silent_redirect_uri
    };
}
import { Injectable } from '@angular/core';
import {
    HttpRequest,
    HttpHandler,
    HttpEvent,
    HttpInterceptor
} from '@angular/common/http';

import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    constructor(
        public authService: AuthService) {
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (request.url.indexOf('/token') == -1) {
            request = request.clone({
                setHeaders: {
                    Authorization: this.authService.getAuthorizationHeaderValue(),
                    Platform: 'default',
                    Accept: 'application/json',
                    'Cache-Control': 'no-cache',
                    'Pragma': 'no-cache'
                },
                setParams: {
                    'platform': 'default'
                }
            });
        }

        return next.handle(request);
    }
}
import { Injectable } from '@angular/core';
import { Router, CanActivate, CanLoad, ActivatedRouteSnapshot, Route } from '@angular/router';
import { AuthService } from '../core/services/auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

    constructor(
        private authService: AuthService,
        private router: Router) { }

    canLoad(route: Route): boolean {
        return this.check(route.data.expectedRoles);
    }

    canActivate(route: ActivatedRouteSnapshot): boolean {
        return this.check(route.data.expectedRoles);
    }

    private check(expectedRoles: string[]): boolean {
        if (this.authService.isLoggedIn()) {
            const roles = this.authService.getClaims().role as string[];
            if (roles &&
                expectedRoles &&
                expectedRoles.some(x => roles.some(r => r == x))) {
                return true;
            }
            this.router.navigate(['/not-authorized']);
            return false;
        }
        this.authService.startAuthentication();
        return false;
    }
}

and the silent-refresh.html (i dont need it propably but should be okay with code flow as well right?)

<head>
  <title></title>
</head>
<body>
  <script src="es6-promise.min.js"></script>
  <script src="es6-promise.auto.min.js"></script>
  <script>
    __Zone_enable_cross_context_check = true;
  </script>
  <script src="oidc-client.min.js"></script>
  <script>
    new UserManager().signinSilentCallback()
      .catch((err) => {
        console.log(err);
      });
  </script>
</body>

question

All 7 comments

when this behaviour happens i see following message on the auth service:
"Identity.Application was not authenticated. Failure message: Unprotect ticket failed"

Seems like its working on local completly fine. But the weird thing is, it cant be heroku right? even if i restart the services on my local machine its continuee to work.

does someone have a idea why its not working?

Might be an issue with asp.net core data protection changing keys on production environment.
https://identityserver4.readthedocs.io/en/latest/topics/deployment.html#asp-net-core-data-protection

Might be an issue with asp.net core data protection changing keys on production environment.
https://identityserver4.readthedocs.io/en/latest/topics/deployment.html#asp-net-core-data-protection

Hm yes maybe thats the issue. I will look at it later today. Not sure what needs to be done for this data protection topic for now.

Might be an issue with asp.net core data protection changing keys on production environment.
https://identityserver4.readthedocs.io/en/latest/topics/deployment.html#asp-net-core-data-protection

i had to implement a store for data protection keys like a database one right? I deployed this right now. not sure if this is the right way.

its fixed by adding a context and migrations for the data protection keys.

configure:

services.AddDbContext<KeysDbContext>(options => options.UseSqlServer(connectionString));
services.AddDataProtection().PersistKeysToDbContext<KeysDbContext>();

and start migration:
serviceScope.ServiceProvider.GetRequiredService<KeysDbContext>().Database.Migrate();

thanks for the hint with the data protection crazyfx1.

The issue can be closed

Just want to say thanks to @crazyfx1 and @SergejSachs for hints.

A few more links for those who might have similar problem:

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

krgm03 picture krgm03  路  3Comments

wangkanai picture wangkanai  路  3Comments

leastprivilege picture leastprivilege  路  3Comments

garymacpherson picture garymacpherson  路  3Comments

mackie1001 picture mackie1001  路  3Comments