Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validate authentication cookie with ASP.NET Core 2.1 / 3+ Identity

When using Cookie Authentication in ASP.NET Core 2 (with or without Identity) it might happen, that a user's email or name is changed, or even the account is deleted during the cookie's lifetime. That's why the docs point out, that the cookie should be validated. The example in the docs is commented with

The approach described here is triggered on every request. This can result in a large performance penalty for the app.

So I am wondering what is the best pattern to validate the cookie principal. What I did in Startup.cs is to subscribe to the OnValidatePrincipal event and check the pricipal's validity e.g. every 5 minutes by appending a LastValidatedOn claim to the cookie like so:

services.ConfigureApplicationCookie(options =>
{
    // other cookie options go here

    options.Events.OnValidatePrincipal = async context =>
    {
        const string claimType = "LastValidatedOn";
        const int reValidateAfterMinutes = 5;

        if (!(context.Principal?.Identity is ClaimsIdentity claimIdentity)) return;

        if (!context.Principal.HasClaim(c => c.Type == claimType) ||
            DateTimeOffset.Now.UtcDateTime.Subtract(new DateTime(long.Parse(context.Principal.Claims.First(c => c.Type == claimType).Value))) > TimeSpan.FromMinutes(reValidateAfterMinutes))
        {
            var mgr = context.HttpContext.RequestServices.GetRequiredService<SignInManager<ApplicationUser>>();
            var user = await mgr.UserManager.FindByNameAsync(claimIdentity.Name);
            if (user != null && claimIdentity.Claims.FirstOrDefault(c => c.Type == "AspNet.Identity.SecurityStamp")?.Value == await mgr.UserManager.GetSecurityStampAsync(user))
            {
                claimIdentity.FindAll(claimType).ToList().ForEach(c => claimIdentity.TryRemoveClaim(c));
                claimIdentity.AddClaim(new Claim(claimType, DateTimeOffset.Now.UtcDateTime.Ticks.ToString(), typeof(long).ToString()));
                context.ShouldRenew = true;
            }
            else
            {
                context.RejectPrincipal();
                await mgr.SignOutAsync();
            }
        }
    };
});
like image 581
axuno Avatar asked Jul 22 '18 19:07

axuno


1 Answers

@MarkG pointed me into the right direction, thanks. After having a closer look at the source code for SecurityStampValidator and Identity things became clear to me. Actually, the sample code I posted with my question is unnecessary, because ASP.NET Core Identity brings the feature in a better fashion out-of-the-box.

As I didn't find a summary like this yet, maybe it will be helpful to others, too.

What has nothing to do with authentication cookie validation

... but still good to know...

services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Expiration = TimeSpan.FromDays(30);
    options.ExpireTimeSpan = TimeSpan.FromDays(30);
    options.SlidingExpiration = true;
});

ExpireTimeSpan

Defaults to TimeSpan.FromDays(14)

The issue time of the authentication ticket is part of the cookie (CookieValidatePrincipalContext.Properties.IssuedUtc). When the cookie is sent back to the server, the current time minus the issue time must be greater than ExpireTimeSpan. If it is not, the user will be signed out without further investigation. In practice, setting the ExpireTimeSpan, mostly goes together with SlidingExpiration set to true. This is a means to ensure that the user is actively working with the app, and did not e.g. leave the device back unattended. Negative TimeSpans will sign the user off immediately (but not TimeSpan.Zero).

What is needed to control authentication cookie validation

services.AddOptions();
services.Configure<SecurityStampValidatorOptions>(options =>
{
    // This is the key to control how often validation takes place
    options.ValidationInterval = TimeSpan.FromMinutes(5);
});

ValidationInterval

Defaults to TimeSpan.FromMinutes(30)

This determines the time span after which the validity of the authentication cookie will be checked against persistent storage. It is accomplished by calling the SecurityStampValidator for every request to the server. If the current time minus the cookie's issue time is less or equal to ValidationInterval, a call to ValidateSecurityStampAsync will occur. This means ValidationInterval = TimeSpan.Zero leads to calling the ValidateSecurityStampAsync for each request.

Note UserManager must support getting security stamps or it will fail. For a custom user manager or user store, both must properly implement IUserSecurityStampStore<TUser>.

Sequence of loading services in Startup

The thing to be aware of is: services. AddIdentity() also sets defaults for the authentication cookie. If you add it after services.ConfigureApplicationCookie() this will override the previous settings. I called services.Configure<SecurityStampValidatorOptions>() after the previous ones above.

Thanks again to @MarkG for showing the way.

like image 161
axuno Avatar answered Sep 29 '22 20:09

axuno