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();
}
}
};
});
@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.
... 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 TimeSpan
s will sign the user off immediately (but not TimeSpan.Zero
).
services.AddOptions();
services.Configure<SecurityStampValidatorOptions>(options =>
{
// This is the key to control how often validation takes place
options.ValidationInterval = TimeSpan.FromMinutes(5);
});
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>
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With