Search code examples
asp.net-coreasp.net-identity-3

ASP.NET 5 Identity 3 users get signed out after some time


I'm, using RC1 bits and external (Google) authentication, no Identity.EntityFramework.

During login, I set 'Remember me' flag.

Logged-in user survives browser restart (I see cookie set to expire in 14 days) and website restart.

But after some time of inactivity (about 15 min), no matter browser/site were restarted or not, refreshing page lead to signing out, logs says:

info: Microsoft.AspNet.Authentication.Cookies.CookieAuthenticationMiddleware:
    AuthenticationScheme: Microsoft.AspNet.Identity.Application signed out.
    AuthenticationScheme: Microsoft.AspNet.Identity.External signed out.
    AuthenticationScheme: Microsoft.AspNet.Identity.TwoFactorUserId signed out.

This looks like "sessions" in previous ASP, but I do not use any sessions here.

This is my local developer machine, no IIS, direct Kestrel connection to 5000 port, so this is not data-protection problem

Why user forced to sign out?

Update: my Startup.cs file:

public void ConfigureServices(IServiceCollection services) 
{
    ....
    var identityBuilder = services
        .AddIdentity<User, UserRole>(options =>
        {
            options.User.AllowedUserNameCharacters = null;
            options.Cookies.ApplicationCookie.LoginPath = "/user/login";
            options.Cookies.ApplicationCookie.LogoutPath = "/user/logout";
        });
    identityBuilder.Services
        .AddScoped<IUserStore<User>, SportCmsDb>(serviceProvider => serviceProvider.GetService<SportCmsDb>())
        .AddScoped<IRoleStore<UserRole>, SportCmsDb>(serviceProvider => serviceProvider.GetService<SportCmsDb>());
    identityBuilder
        .AddDefaultTokenProviders();
    ....

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 
{
    ....
    app.UseIdentity();
    app.UseGoogleAuthentication(options =>
    {
        options.ClientId = Configuration["OAuth:Google:ClientId"];
        options.ClientSecret = Configuration["OAuth:Google:Secret"];
    });
    ....

SportCmsDb is DbContext and also implements IUserStore<User>, IUserLoginStore<User>, IUserEmailStore<User>, IRoleStore<UserRole>, IUserClaimStore<User>

Update 2

I enabled detailed (debug) logging and found that when user get signed out - prior to this my IUserStore<User>.FindByIdAsync is called. With real/existing user id, and function returning correct non-null User. Everything seems good. But my loaded-from-db User is "rejected" and forced to sign out. There is not additional log messages that can reveal why/where.


Solution

  • Wow, I solved it!

    TL;DR

    I need to implement IUserSecurityStampStore<User> on my custom UserManager (aka SportCmsDb).

    Details

    During AddIdentity call (in Startup.cs ConfigureServices method) IdentityOptions are configured with default instance of IdentityCookieOptions. In constructor of IdentityCookieOptions instance of ApplicationCookie (of type CookieAuthenticationOptions) is created with handler CookieAuthenticationEvents.OnValidatePrincipal set to SecurityStampValidator.ValidatePrincipalAsync static method.

    During UseIdentity call (in Startup.cs Configure method) CookieAuthenticationMiddleware is configured with IdentityOptions.Cookies.ApplicationCookie options.

    CookieAuthenticationHandler (created by CookieAuthenticationMiddleware) in it's HandleAuthenticateAsync method reads ticket from cookie and call Options.Events.ValidatePrincipal handler for validation.

    Effectively, SecurityStampValidator.ValidatePrincipalAsync is called. This method checks that enough time has elapsed since cookie was issued (30 min by default) and calls ISecurityStampValidator.validateAsync (lines 81-82).

    Default implementation of ISecurityStampValidator is SecurityStampValidator<TUser>. It calls SignInManager<TUser>.ValidateSecurityStampAsync and when null is returned - rejects principal and forces user to sign out (lines 30-40).

    SignInManager<TUser> in its ValidateSecurityStampAsync method tries to read security stamp from User and returns null if it can't (if UserManager<User> does not supports this interface) or stamp does not match saved one (in cookie).

    My custom UserManager does not implement IUserSecurityStampStore<User>. Bingo.