3

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.

Community
  • 1
  • 1
Dmitry
  • 16,110
  • 4
  • 61
  • 73
  • Could you include your call to services.AddIdentity in ConfigureServices, or did you remove that entirely? Are you still using `app.UseIdentity();` in the Startup.Configure method? – Ron DeFreitas May 11 '16 at 19:35
  • Updated my post with parts of `Startup.cs` – Dmitry May 11 '16 at 21:35

3 Answers3

12

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.

Dmitry
  • 16,110
  • 4
  • 61
  • 73
  • Hi, I implemented `IUserSecurityStampStore` interface in my custom UserStore and just stored the stamp in the user (`SetSecurityStampAsync`) and returned it (`GetSecurityStampAsync`) but the Set method has never been called ? In the `ValidateSecurityStampAsync` method from the asp.net code I saw that they retrieve a claim with name `SecurityStampClaimType`? Who is responsible to set that claim ? Me ? In my case I don't have custom UserManager or SignInManager, only the store is custom and there I am only getting the user, no creating users at all. – Dilyan Dimitrov May 17 '16 at 13:26
  • 1
    You should manually call `SetSecurityStampAsync` only when you need user to relogin (say, after changing it's roles/rights from admin page). Claim `SecurityStampClaimType` is set "internally" (with user id and name claims), don't worry about this. But you should have some initial security stamp value - `GetSecurityStampAsync` should not return null. – Dmitry May 17 '16 at 13:34
  • Okey, I thought everything should be right, but no success :( I put the stamp value in the claims, but the `GetSecurityStampAsync` (return the same value I put in the claim) never called.. 30 mins waiting for a test is harsh :) – Dilyan Dimitrov May 17 '16 at 14:33
  • 1
    Set `options.SecurityStampValidationInterval = TimeSpan.FromMinutes(1)` in `Startup.cs` when configuring `IdentityOptions` – Dmitry May 17 '16 at 15:22
  • Okey.. everything works now! Now I understand the whole idea of this. Thank you so very much for the help, you save my day :) – Dilyan Dimitrov May 17 '16 at 15:39
  • Thanks. This problem drove me crazy. – Oliver Weichhold Nov 01 '16 at 12:44
1

Thank you very much for the previous answers, I worked with this problem today and I resolve with this:

1.- Custom UserStore.cs:

public class UserStore : IUserStore<User>,                                                                                              
    IUserPasswordStore<User>,                                                                                                           
    IUserEmailStore<User>,                                                                                                              
    IUserRoleStore<User>,                                                                                                               
    IUserSecurityStampStore<User>                                                                                                       
{  
    //omitted...

    public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken = default(CancellationToken))
    {
        user.SecurityStamp = stamp;
        return Task.FromResult(0);
    }

    public Task<string> GetSecurityStampAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (user.SecurityStamp == null) {
            return Task.FromResult("AspNet.Identity.SecurityStamp");
        }
        return Task.FromResult(user.SecurityStamp);
    }
}

2.- In User.cs And DB Table User add SecurityStamp as string.

For TEST change default 30m to 1m in Startup.cs:

services.Configure<SecurityStampValidatorOptions>(options => {        
    options.ValidationInterval = TimeSpan.FromMinutes(1);                  
});
Uriel
  • 129
  • 3
  • This solved my 'logout' problem in ASPNETCORE 2.1. My problem was using a custom user store which didn't implement 'IUserSecurityStampStore' (who knew?!) and thought the fault was with the sliding expiration cookies stuff. The only difference in my code to Uriel's is the 'User user' was 'CustomUser user'. – Sid James Jan 23 '19 at 06:14
0

In my case I was using identity server and I faced the same issue. The problem was that I was not adding the SecurityStamp claim to the cookie in the first place, so when the SecurityStampValidator was invoked it always returned false.

Code before:

var user = await this.loginService.GetOrCreateUser(claimsPrincipal, provider);    
    
var localSignInProps = this.GetAuthenticationProperties(result);

// issue authentication cookie for user
var identityServerUser = new IdentityServerUser(user.Id)
{
    DisplayName = user.UserName,
    IdentityProvider = provider
};

await this.HttpContext.SignInAsync(identityServerUser, localSignInProps);

Code after:

var user = await this.loginService.GetOrCreateUser(claimsPrincipal, provider);    
var principal = await this.signInManager.CreateUserPrincipalAsync(user);
    
var localSignInProps = this.GetAuthenticationProperties(result);

// issue authentication cookie for user
var identityServerUser = new IdentityServerUser(user.Id)
{
    DisplayName = user.UserName,
    IdentityProvider = provider,
    AdditionalClaims = principal.Claims.ToList(),
};

await this.HttpContext.SignInAsync(identityServerUser, localSignInProps);

Principal contains a claim that looks like this:

{AspNet.Identity.SecurityStamp: BBDVFD2WCAZTMA75O3IQKPUWNKKOZOL5}
Mariano Soto
  • 136
  • 10