Search code examples
c#asp.net-coreasp.net-identityidentityserver4

Cookie does not slide on server but does when run locally


I have an Identity server 4 application with asp .net identity. I have the cookies set up to slide.

services.ConfigureApplicationCookie(opts =>
                        {
                            opts.Cookie.Expiration = TimeSpan.FromDays(30);
                            opts.SessionStore = new RedisCacheTicketStore(new RedisCacheOptions()
                            {
                                Configuration = configuration["Redis:HostPort"]
                            }, logger, configuration);
                            opts.Cookie.SameSite = SameSiteMode.None;
                            opts.SlidingExpiration = true;
                            opts.ExpireTimeSpan = TimeSpan.FromDays(30);
                        }
                    );

Not Sliding

Localhost: When the user logs in .AspNetCore.Idenitty.Application gets an expiration time. When the page is refreshed the expiration is updated i can see the timestamp change.

Production: However if i check this when its up on the server the user logs in and .AspNetCore.Idenitty.Application gets an expiration time with a time stamp of when the logged in. However when the page is refreshed the time stamp does not change. It remains the same as it was when the user logged in.

enter image description here

User kicked out after 30 minutes

Production: The second issue is that as you can see the expiration time is set for a month in advance yet when on the server in 30 minutes this user will be kicked out and forced to login again. I cant keep a user logged in for more then 30 minutes even if they are active.

Security stamp

I have checked the users security stamp has not changed and the token contains "AspNet.Identity.SecurityStamp": "[users actual key]"

Update

So after some digging i finally decided to over ride the security stamp validation. I did that by over riding the following methods in my ApplicationSignInManager

 public override async Task<ApplicationUser> ValidateSecurityStampAsync(ClaimsPrincipal principal)
        {
            if (principal == null)
            {
                Logger.LogError(LoggingEvents.ApplicationSignInManagerSecurityTokenValidation, "ClaimsPrincipal is null");
                return null;
            }
            var user = await UserManager.GetUserAsync(principal);
            if (await ValidateSecurityStampAsync(user, principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType)))
            {
                return user;
            }

            if(user == null)
                Logger.LogError(LoggingEvents.ApplicationSignInManagerSecurityTokenValidation, "User not found [principal {principal}]", principal);

            var principalSecurityStamp = principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType);  // Security stamp from claims
            var userManagerSecurityStamp = user.SecurityStamp;                                                     // Security Stamp from usermanager
            var getSecurityStampAsyncResults = await UserManager.GetSecurityStampAsync(user);                      // Security stamp from GetSecurityStampAsync
            Logger.LogError(LoggingEvents.ApplicationSignInManagerSecurityTokenValidation,
                "Security stamp Validation Failed: [principalSecurityStamp {principalSecurityStamp}] != [getSecurityStampAsyncResults {getSecurityStampAsyncResults}] also ([userManagerSecurityStamp {userManagerSecurityStamp}] )", principalSecurityStamp, getSecurityStampAsyncResults, userManagerSecurityStamp);

            return null;
        }

        public virtual async Task<bool> ValidateSecurityStampAsync(ApplicationUser user, string securityStamp)
            => user != null &&
               // Only validate the security stamp if the store supports it
               (!UserManager.SupportsUserSecurityStamp || securityStamp == await UserManager.GetSecurityStampAsync(user));

This resulted in some very interesting information showing up in my log instantly.

Security stamp Validation Failed: [principalSecurityStamp (null)] != [getSecurityStampAsyncResults 83270b3f-a042-4a8f-b090-f5e1a084074e] also ([userManagerSecurityStamp 83270b3f-a042-4a8f-b090-f5e1a084074e] )

So principal.FindFirstValue(Options.ClaimsIdentity.SecurityStampClaimType) appears to be null. Why I dont know. I also dont know how to fix it as there are a number of third party applications calling this identity server.

update2:

I can now verify that GenerateClaimsAsync does set the SecurityStampClaim. However the CookieValidatePrincipalContext in ValidateAsync does not contain the claim in question which is strange as the comment on the method says.

/// <param name="context">The context containing the <see cref="System.Security.Claims.ClaimsPrincipal"/>

Solution

  • It has taken quite some time to get to the root of this issue. I am going to try to explain it here in the event someone else runs across this issue.

    First off the problem was with the securty token. The security token is stored on the user table, When the identity.application cookie is created this token is stored within the cookie. Every five minutes an application contacts the identity server and checks if the token needs to be validated. If its older than thirty minutes then the security token will be validated. (Note both the five minute and thirty minutes times are configurable this is just the defaults)

    This is used for something called sign out ever where. If you change your password the security token on your row in the user table will be updated. There by making it different than the one stored in the cookie on all of your devices. This will force you to be logged out ever where.

    Issue nr one

    SignInManager.cs#L260 validates the security token but does not test if it is null.

    So if there is something wrong with the cookie and the token is null for some reason either in the database or in my case it had been over written by another cookie then the user will be logged in for thirty minutes then be kicked out the first time it tries to validate the security token. which lead to issue request #7055. The cookie should be tested each time to assure that a security token is in it.

    Issue nr 2

    The following line of code signs in a user and creates the cookie storing the secure token within said cookie

    var signInUserResult = await _signInManager.PasswordSignInAsync(userName, password, rememberMe, true);
    

    After much digging and debugging i found the following line which was over writing the original cookie with a new one that did not contain the security token.

    await HttpContext.SignInAsync(user.Id.ToString(), user.UserName, props);