ExpireTimeSpan ignored after regenerateIdentity / validateInterval duration in MVC Identity (2.0.1)

GregTheDev picture GregTheDev · Jun 1, 2014 · Viewed 13.9k times · Source

Been scratching my head all day on this one. I'm trying to set up "very long" login sessions in MVC Identity 2.0.1. (30 days).

I use the following cookie startup:

      app.UseCookieAuthentication(new CookieAuthenticationOptions
        {

            SlidingExpiration = true,
            ExpireTimeSpan = System.TimeSpan.FromDays(30),
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/My/Login"),
            CookieName = "MyLoginCookie",
            Provider = new CookieAuthenticationProvider
            {                           
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),

                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });

Which on the whole, works fine. The cookie is set 30 days hence, all looks good.

If I close browser and come back after "validateInterval" duration has passed (30mins here) I'm still logged in, however the cookie is now re-issued as "session" only (correct cookie name still)! The 30 day expiration is gone.

If I now close browser/reopen again I'm no longer logged in.

I have tested removing the "Provider" and all works as expected then, I can come back several hours later and I'm still logged in fine. I read that it is best practice to use the stamp revalidation though, so am unsure how to proceed.

Answer

chrisg picture chrisg · Jun 19, 2014

When the SecurityStampValidator fires the regenerateIdentity callback, the currently authenticated user gets re-signed in with a non-persistent login. This is hard-coded, and I don't believe there is any way to directly control it. As such, the login session will continue only to the end of the browser session you are running at the point the identity is regenerated.

Here is an approach to make the login persistent, even across identity regeneration operations. This description is based on using Visual Studio MVC ASP.NET web project templates.

First we need to have a way to track the fact that a login session is persistent across separate HTTP requests. This can be done by adding an "IsPersistent" claim to the user's identity. The following extension methods show a way to do this.

public static class ClaimsIdentityExtensions
{
    private const string PersistentLoginClaimType = "PersistentLogin";

    public static bool GetIsPersistent(this System.Security.Claims.ClaimsIdentity identity)
    {
        return identity.Claims.FirstOrDefault(c => c.Type == PersistentLoginClaimType) != null;
    }

    public static void SetIsPersistent(this System.Security.Claims.ClaimsIdentity identity, bool isPersistent)
    {
        var claim = identity.Claims.FirstOrDefault(c => c.Type == PersistentLoginClaimType);
        if (isPersistent)
        {
            if (claim == null)
            {
                identity.AddClaim(new System.Security.Claims.Claim(PersistentLoginClaimType, Boolean.TrueString));
            }
        }
        else if (claim != null)
        {
            identity.RemoveClaim(claim);
        }
    }
}

Next we need to make the "IsPersistent" claim when the user signs in requesting a persistent session. For example, your ApplicationUser class may have a GenerateUserIdentityAsync method which can be updated to take an isPersistent flag parameter as follows to make such a claim when needed:

public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, bool isPersistent)
{
    var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
    userIdentity.SetIsPersistent(isPersistent);
    return userIdentity;
}

Any callers of ApplicationUser.GenerateUserIdentityAsync will now need to pass in the isPersistent flag. For example, the call to GenerateUserIdentityAsync in AccountController.SignInAsync would change from

AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, 
    await user.GenerateUserIdentityAsync(UserManager));

to

AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent },
    await user.GenerateUserIdentityAsync(UserManager, isPersistent));

Lastly, the CookieAuthenticationProvider.OnValidateIdentity delegate used in the Startup.ConfigureAuth method needs some attention to preserve the persistence details across identity regeneration operations. The default delegate looks like:

OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
    validateInterval: TimeSpan.FromMinutes(20),
    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))

This can be changed to:

OnValidateIdentity = async (context) =>
{
    await SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
        validateInterval: TimeSpan.FromMinutes(20),
        // Note that if identity is regenerated in the same HTTP request as a logoff attempt,
        // the logoff attempt will have no effect and the user will remain logged in.
        // See https://aspnetidentity.codeplex.com/workitem/1962
        regenerateIdentity: (manager, user) =>
            user.GenerateUserIdentityAsync(manager, context.Identity.GetIsPersistent())
    )(context);

    // If identity was regenerated by the stamp validator,
    // AuthenticationResponseGrant.Properties.IsPersistent will default to false, leading
    // to a non-persistent login session. If the validated identity made a claim of being
    // persistent, set the IsPersistent flag to true so the application cookie won't expire
    // at the end of the browser session.
    var newResponseGrant = context.OwinContext.Authentication.AuthenticationResponseGrant;
    if (newResponseGrant != null)
    {
        newResponseGrant.Properties.IsPersistent = context.Identity.GetIsPersistent();
    }
}