In Asp.Net Core, cookies are validated every 30 minutes by default. This means that when the user logs in to the site and enters their information, the user's current claims are stored in the cookie and can access the pages they want, and the cookies are not validated for up to 30 minutes, even if the user changes their password. Or change the user role by the site admin. However, until the user leaves the site and does not re-enter the site, he can use cookies and stay on the site. But if the user's password is stolen and the user wants to change his password, it still does not matter, because the cookie is active on the website of the user who stole the password and entered the site before the user changed his password. . To solve this problem, we can put settings in the cookie event that will validate the cookie every time a request is sent to the site.

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
 {
     options.AccessDeniedPath = "/Auth/SignIn";
     options.Cookie.HttpOnly = true;
     options.ExpireTimeSpan = TimeSpan.FromDays(15);
     options.LoginPath = "/Auth/SignIn";
     options.ReturnUrlParameter = "returnUrl";
     options.SlidingExpiration = true;
     options.Cookie.IsEssential = true;// ignore GDPR
     options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
     options.Events = new CookieAuthenticationEvents
     {
         OnValidatePrincipal = ValidateAsync
     };
 });

ValidateAync method

private static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
    context = context ?? throw new ArgumentNullException(nameof(context));
    var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
    if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
    {
        await RejectPrincipal();
        return;
    }
    UserManager<Users> userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<Users>>();
    var user = await userManager.FindByNameAsync(context.Principal.FindFirstValue(ClaimTypes.NameIdentifier));
    if (user == null || user.SecurityStamp != context.Principal.FindFirst(new ClaimsIdentityOptions().SecurityStampClaimType)?.Value)
    {
        await RejectPrincipal();
        return;
    }
    async Task RejectPrincipal()
    {
        context.RejectPrincipal();
        await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    }
}

In this method, it is first checked whether Claim exists or not. If it does not exist, the user becomes a SignOut and has to log in again, but if there is Claim in the relevant context, we use ClaimTypes.NameIdentifier to get the unique name of the user and read the user and it from the database and then the value of SecurityStamp with SecurityStampClaimType Inside the cookie we check that if the user is null or the SecurityStamp value of the user is against the SecurityStampClaimType value in the cookie the user becomes a SignOut and has to log in again because if the user is null it means that the NameIdentifier field value is empty and no user has logged in. And if the value of the SecurityStamp field is different from the value inside the cookie, it means that the user has edited his profile and has to log in again. The SecurityStamp field should be updated when the user changes their password or makes any edits to the user. You can update this value using the UpdateSecurityStampAsync method in the UserManager class. As follows

 await _userManager.UpdateSecurityStampAsync(user);

Then if you run the program and log in to the site in two different browsers (for example, log in with Chrome and incognito) and in the normal Chrome tab change your password (SecurityStamp value also changes) and then go to the incognito page and Refresh the page to see that you do not need to login and is still an Authenticate user. Because by default cookies are validated every 30 minutes. To solve this problem, we must do the following

services.Configure<SecurityStampValidatorOptions>(options =>
{
    options.ValidationInterval = TimeSpan.Zero;
});

This way, every time a request is sent to the server, first the cookie is validated and then the rest of the work is done. Doing so may have a detrimental effect on site performance, but site security will increase somewhat.

 You can download a sample project written for this purpose along with the Identity implementation from GitHub ;)

Powered by Froala Editor

Comments