ASP.Net identity slidingexpiration set to true does not re-issues cookie

12k Views Asked by At

I'm using ASP.Net identity authentication to control my application authorization, i need to terminate users sessions after specified minutes of inactivity, I tried to achivieve this by doing the following aproach

public void ConfigureAuth(IAppBuilder app) {
    app.CreatePerOwinContext<UserStore>(() => new UserStore());
    app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

    app.UseCookieAuthentication(new CookieAuthenticationOptions {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/login"),
        LogoutPath = new PathString("/logout"),
        CookieDomain = ConfigurationManager.AppSettings["CookieDomain"],
        Provider = new CookieAuthenticationProvider {
            // Enables the application to validate the security stamp when the user logs in.
            // This is a security feature which is used when you change a password or add an external login to your account.  
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                validateInterval: TimeSpan.FromMinutes(2),
                regenerateIdentity: (manager, user) => manager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie)
            )
        },
        SlidingExpiration = true,
    });
}

And I also tried this aproach

app.UseCookieAuthentication(new CookieAuthenticationOptions {
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/login"),
    LogoutPath = new PathString("/logout"),
    CookieDomain = ConfigurationManager.AppSettings["CookieDomain"],
    ExpireTimeSpan = TimeSpan.FromMinutes(2),
    SlidingExpiration = true,
});

Using those aproches user cookie session expired after 2 minutes no matter if the user was active in the site. I read in the documentation that by setting SlidingExpiration = true the cookie would be re-issued on any request half way through the ExpireTimeSpan. For example, if the user logged in and then made a second request 16 minutes later the cookie would be re-issued for another 30 minutes. If the user logged in and then made a second request 31 minutes later then the user would be prompted to log in.

I don't know why it is not working, any ideas?

2

There are 2 best solutions below

0
On

To be clear: The CookieHandler is checking if the time remaining until cookie expiration is less than the time elapsed since issue (meaning it's > half expired) before requesting a refresh. This is in the dll for Microsoft.AspNetCore.Authentication.Cookies.

Personally, I would prefer to have an option to change the percentage of elapse. When you're working with very small timeouts (15-mins or less) for more secure apps, having the user timeout after only 7-mins of inactivity, because the cookie never refreshed for the first 6mins they were active, is quite annoying.
Perhaps adding an option to use a remaining timespan check against a constant instead. For example, issue a refresh request when the cookie has less than {TimeSpan} remaining.

private void CheckForRefresh(AuthenticationTicket ticket)
{
  DateTimeOffset utcNow = this.get_Clock().get_UtcNow();
  DateTimeOffset? issuedUtc = ticket.get_Properties().get_IssuedUtc();
  DateTimeOffset? expiresUtc = ticket.get_Properties().get_ExpiresUtc();
  bool? allowRefresh = ticket.get_Properties().get_AllowRefresh();
  bool flag = !allowRefresh.HasValue || allowRefresh.GetValueOrDefault();
  if (((!issuedUtc.HasValue || !expiresUtc.HasValue ? 0 : (this.get_Options().SlidingExpiration ? 1 : 0)) & (flag ? 1 : 0)) == 0)
    return;
  TimeSpan timeSpan = utcNow.Subtract(issuedUtc.Value);
  if (!(expiresUtc.Value.Subtract(utcNow) < timeSpan))
    return;
  this.RequestRefresh(ticket);
}
0
On

I suspect the problem is that the refresh behaviour is being suppressed despite SlidingExpiration being true. Looking at the Katana source code, CookieAuthenticationHandler class, AuthenticateCoreAsync() method, we see:

bool? allowRefresh = ticket.Properties.AllowRefresh;
if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration
    && (!allowRefresh.HasValue || allowRefresh.Value))
{
    TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value);
    TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc);

    if (timeRemaining < timeElapsed)
    {
    _shouldRenew = true;
    _renewIssuedUtc = currentUtc;
    TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
    _renewExpiresUtc = currentUtc.Add(timeSpan);
    }
}

I.e. the refresh only occurs if ticket.Properties.AllowRefresh is null or true, and depending on which middleware you are using this may be set to false. E.g. in OpenIdConnectAuthenticationHandler and WsFederationAuthenticationHandler we have this...

private ClaimsPrincipal ValidateToken(...)
{
    ...
    if (Options.UseTokenLifetime)
    {
        // Override any session persistence to match the token lifetime.
        DateTime issued = jwt.ValidFrom;
        if (issued != DateTime.MinValue)
        {
            properties.IssuedUtc = issued.ToUniversalTime();
        }
        DateTime expires = jwt.ValidTo;
        if (expires != DateTime.MinValue)
        {
            properties.ExpiresUtc = expires.ToUniversalTime();
        }
        properties.AllowRefresh = false;
    }
}

Noting that UseTokenLifetime defaults to true. So the deault behaviour is to use the expiry time from the authentiaction token we got from the IdP server, and to end the session when that expiry time is reached, regardless of whether the user is active or not.