Understand ASP Net core: Cookie authentication

Posted by jusitnm on Sat, 05 Mar 2022 12:43:44 +0100

Understand ASP Net core: Cookie authentication

ASP.NET Core provides built-in Cookie based authentication support. When using Cookie authentication, there are three related elements;

  • Authentication mode name; CookieAuthenticationDefaults.AuthenticationScheme

    namespace Microsoft.AspNetCore.Authentication.Cookies
    {
        public static class CookieAuthenticationDefaults
        {
            public const string AuthenticationScheme = "Cookies";
    

    See: Check the source code of CookieAuthenticationDefaults in GitHub

  • Authentication processor: CookieAuthenticationHandler

    public class CookieAuthenticationHandler : Microsoft.AspNetCore.Authentication.SignInAuthenticationHandler<Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationOptions>
    

    See: Check the source code of CookieAuthenticationHandler in GitHub

    You can see that it is derived from SignInAuthenticationHandler, so it supports login and logout operations.

  • Authentication configuration: CookieAuthenticationOptions

    public class CookieAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions
    

    See: View the source code of CookieAuthenticationOptions in GitHub

Cookie authentication configuration

Cookie basic settings

Cookie based authentication naturally needs to maintain and process authentication information through cookies. The settings related to cookies are completed through cookies. It is a cookie builder type, which is used to build cookies. Cookie builder provides several properties about the cookie itself, which are used to configure cookies.

public class CookieBuilder
{
    private string? _name;
    public virtual string? Name
    {
        get => _name;
        set => _name = !string.IsNullOrEmpty(value)
            ? value
            : throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value));
    }

    public virtual string? Path { get; set; }
    public virtual string? Domain { get; set; }
    public virtual bool HttpOnly { get; set; }
    public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified;
    public virtual CookieSecurePolicy SecurePolicy { get; set; }
    public virtual TimeSpan? Expiration { get; set; }
    public virtual TimeSpan? MaxAge { get; set; }
    public virtual bool IsEssential { get; set; }

View the source code of CookieBuilder in GitHub

Main attributes:

  • Name, the name of the Cookie

  • Path, the role path of the Cookie

  • Domain, the scope of the Cookie

  • HttpOnly

  • SameSite

  • SecurePolicy

  • IsEssential, do you need user license

  • Expiration, which cannot be set, will get an exception

    Cookie Expiration is ignored, use ExpireTimeSpan instead.

The source code for throwing exceptions is as follows. You can see that setting Expiration will cause exceptions to be thrown.

public static AuthenticationBuilder AddCookie(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action<CookieAuthenticationOptions> configureOptions)
{          
  builder.Services
    .TryAddEnumerable(
    	ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, 
    	PostConfigureCookieAuthenticationOptions>());
  builder.Services.AddOptions<CookieAuthenticationOptions>(authenticationScheme)
    .Validate(o => o.Cookie.Expiration == null, "Cookie.Expiration is ignored, use ExpireTimeSpan instead.");
  return builder
    .AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(
    	authenticationScheme, 
    	displayName, 
    	configureOptions);
}

View the source code of CookieExtensions in GitHub

The default Cookie name is

public static readonly string CookiePrefix = ".AspNetCore.";

Check the source code of CookieAuthenticationDefaults in GitHub

For example:

services.AddAuthentication(
  Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme)
  .AddCookie(options =>
  {
  	// Cookie settings
  	options.Cookie.HttpOnly = true;

However, Expiration cannot be set together with ExpireTimeSpan to implement persistent cookies. Setting at the same time will cause the following exceptions:

Cookie.Expiration is ignored, use ExpireTimeSpan instead.

If you want to get persistent cookies, you must set them using AuthenticationProperties.

var authProperties = new AuthenticationProperties
{
	IsPersistent = true,
  ExpiresUtc = DateTime.UtcNow.AddMinutes(5),
  AllowRefresh = true
};

await HttpContext.SignInAsync(
  CookieAuthenticationDefaults.AuthenticationScheme, 
  new ClaimsPrincipal(claimsIdentity), 
  authProperties);

See clarify behavior of cookieauthentication options Cookie. Discussion on expiration

Life cycle configuration of authentication information in cookies

public bool SlidingExpiration { get; set; }
public TimeSpan ExpireTimeSpan { get; set; }
ExpireTimeSpan

The expiration time of the verification bill is 14 days by default. Note that the expiration time of cookies is not set here.

Controls how much time the cookie will remain valid from the point it is created. The expiration information is in the protected cookie ticket. Because of that an expired cookie will be ignored even if it is passed to the server after the browser should have purged it.

The expiration time of cookies needs to be set through AuthenticationProperties.

SlidingExpiration

Whether sliding expiration is supported. The default is True.

Cookie Manager

Cookies can also be further managed through Cookie manager.

public ICookieManager CookieManager { get; set; } = default!;

The Cookie manager property provides the management of how to extract the value saved in the authentication Cookie from the context of request processing, how to append, and delete the authentication Cookie in the context.

public interface ICookieManager
{
    string? GetRequestCookie(HttpContext context, string key);
    void AppendResponseCookie(HttpContext context, string key, string? value, CookieOptions options);
    void DeleteCookie(HttpContext context, string key, CookieOptions options);
}

View ICookieManager source code in GitHub

Its default implementation is the ChunkingCookieManager class,

public class ChunkingCookieManager : ICookieManager
{
 // ...... 
}

Check the source code of ChunkingCookieManager in GitHub

You can see in CookieAuthenticationOptions,

/// <summary>
/// The component used to get cookies from the request or set them on the response.
///
/// ChunkingCookieManager will be used by default.
/// </summary>
public ICookieManager CookieManager { get; set; } = default!;

View the source code in GitHub

Persistent cookies and expiration time

For cookies, the default Expiration time is Session, which is cleared after closing the browser. A remember me option is usually provided when users log in to ensure that cookies are not cleared when browsing is closed. Therefore, it is necessary to declare the absolute Expiration time of an Expiration in the generated Cookie. As we have seen earlier, this cannot be achieved by setting the Expiration property of CookieBuilder.

In the SignInAsync method, receive a parameter of AuthenticationProperties type, which can be used to specify whether the Cookie is persistent and the expiration time.

await HttpContext.SignInAsync("MyCookieAuthenticationScheme", principal, new AuthenticationProperties
{
    // Persistent preservation
    IsPersistent = true

    // Specify expiration time
    ExpiresUtc = DateTime.UtcNow.AddMinutes(20)
});

Take a look at the SignInAsync method in CookieAuthenticationHandler. About the implementation of this configuration:

if (!signInContext.Properties.ExpiresUtc.HasValue)
{
    signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
}
if (signInContext.Properties.IsPersistent)
{
    var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
    signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
}

Check the source code of CookieAuthenticationHandler in GitHub

Expires is specified when writing cookies only if IsPersistent is True. It should be noted that the Cookie expiration time in the browser is only used to specify whether the browser deletes the Cookie, and the value stored in the Cookie will also include the release time and expiration time of the Cookie authentication, which will be verified in the HandleAuthenticateAsync method. It does not mean that you can pass the authentication as long as you have a Cookie.

View the source code of AuthenticationProperties in GitHub

Virtual path related to Cookie authentication

  • LoginPath login path
  • LogoutPath logout path
  • AccessDeniedPath prohibits access to the path
  • ReturnUrlParameter the Url parameter used for the jump
public PathString LoginPath { get; set; }
public PathString LogoutPath { get; set; }
public PathString AccessDeniedPath { get; set; }
public string ReturnUrlParameter { get; set; }

Their default values are defined in CookieAuthenticationDefaults

public static readonly PathString LoginPath = new PathString("/Account/Login");
public static readonly PathString LogoutPath = new PathString("/Account/Logout");
public static readonly PathString AccessDeniedPath = new PathString("/Account/AccessDenied");
public static readonly string ReturnUrlParameter = "ReturnUrl";

Check the source code of CookieAuthenticationDefaults in GitHub

Cookie authentication event

On the Cookie authentication options, two event based extensions are provided for the Cookie authentication process. In fact, they come from the base class AuthenticationSchemeOptions.

public Type EventsType { get; set; }
public object Events { get; set; }

See: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/Core/src/AuthenticationSchemeOptions.cs

By default, an Events object is provided and can be used directly. If EventsType is provided, it will be used directly to process Events.

Four callback points for login and logout are defined:

  • OnSigningIn
  • OnSignedIn
  • OnSigningOut
  • OnValidatePricipal

Another 4 callback points for redirection

  • OnRedirectToLogin
  • OnRedirectToAccessDenied
  • OnRedirectToLogout
  • OnRedirectToReturnUrl

These callback points are defined on an event object of type: CookieAuthenticationEvents. On this base class, default callback processing is provided by default. If you provide your own EventsType, it is usually derived from it.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Authentication.Cookies
{
    public class CookieAuthenticationEvents
    {
        public Func<CookieValidatePrincipalContext, Task> OnValidatePrincipal { get; set; } 
      			= context => Task.CompletedTask;
        public Func<CookieSigningInContext, Task> OnSigningIn { get; set; } = context => Task.CompletedTask;

        public Func<CookieSignedInContext, Task> OnSignedIn { get; set; } = context => Task.CompletedTask;

        public Func<CookieSigningOutContext, Task> OnSigningOut { get; set; } = context => Task.CompletedTask;

        public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } 
      		= context =>
        {
            if (IsAjaxRequest(context.Request))
            {
                context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
                context.Response.StatusCode = 401;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        };

        public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToAccessDenied { get; set; }
      		= context =>
        {
            if (IsAjaxRequest(context.Request))
            {
                context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
                context.Response.StatusCode = 403;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        };

        public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogout { get; set; } = context =>
        {
            if (IsAjaxRequest(context.Request))
            {
                context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        };

        public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToReturnUrl { get; set; } = context =>
        {
            if (IsAjaxRequest(context.Request))
            {
                context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        };

        private static bool IsAjaxRequest(HttpRequest request)
        {
            return string.Equals(request.Query[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal) ||
                string.Equals(request.Headers[HeaderNames.XRequestedWith], "XMLHttpRequest", StringComparison.Ordinal);
        }

        public virtual Task ValidatePrincipal(CookieValidatePrincipalContext context) => OnValidatePrincipal(context);

        public virtual Task SigningIn(CookieSigningInContext context) => OnSigningIn(context);

        public virtual Task SignedIn(CookieSignedInContext context) => OnSignedIn(context);

        public virtual Task SigningOut(CookieSigningOutContext context) => OnSigningOut(context);

        public virtual Task RedirectToLogout(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogout(context);

        public virtual Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToLogin(context);

        public virtual Task RedirectToReturnUrl(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToReturnUrl(context);

        public virtual Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context) => OnRedirectToAccessDenied(context);
    }
}

See: https://github.com/dotnet/aspnetcore/blob/release/5.0/src/Security/Authentication/Cookies/src/CookieAuthenticationEvents.cs

ASP.NET Core to realize cross site login redirection

As NET programmers, one of the pain is since ASP NET until the latest ASP NET core cannot directly realize cross site login redirection (such as access) https://q.cnblogs.com , jump to https://passport.cnblogs.com Login), you can only jump to the current site.

Take ASP Net core is cookieauthenticationoptions Loginpath can only specify the path and cannot specify the full url containing the host name, ASP Net core will automatically add the host name of the current request when redirecting.

services.AddAuthentication()
.AddCookie(options =>
{
    options.LoginPath = "/account/signin";
});

The ReturnUrl query parameter will only contain the path, not the complete url.

I read ASP. Net yesterday Net core authenticaion Source code After that, we found a new antidote - modify cookieauthenticationevents Onredirecttologin delegates cross site login redirection.

The following is the preparation method of the new antidote.

At startup Add the following configuration code to AddCookie in configureservices to redirect with the modified url:

services.AddAuthentication()
.AddCookie(options =>
{
    var originRedirectToLogin = options.Events.OnRedirectToLogin;
    options.Events.OnRedirectToLogin = context =>
    {
        return originRedirectToLogin(RebuildRedirectUri(context));
    };
});

The implementation code of RebuildRedirectUri is as follows:

private static RedirectContext<CookieAuthenticationOptions> RebuildRedirectUri(
    RedirectContext<CookieAuthenticationOptions> context)
{
    if (context.RedirectUri.StartsWith(ACCOUNT_SITE))
        return context;

    var originUri = new Uri(context.RedirectUri);
    var uriBuilder = new UriBuilder(ACCOUNT_SITE);
    uriBuilder.Path = originUri.AbsolutePath;
    var queryStrings = QueryHelpers.ParseQuery(originUri.Query);
    var returnUrlName = context.Options.ReturnUrlParameter;
    var returnUrl = originUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped) + queryStrings[returnUrlName];
    uriBuilder.Query = QueryString.Create(returnUrlName, returnUrl).ToString();
    context.RedirectUri = uriBuilder.ToString();
    return context;
}

The above pile of code is used to realize url conversion. See Bowen for details https://q.cnblogs.com/q/108087/

This long suffering is finally based on ASP Net core's powerful expansion and configuration capabilities are relatively gracefully eliminated.

Check whether the ticket in the Cookie is consistent with the background security information

When the Cookie saved in the user's browser returns to the server with the request again, the Cookie authentication processor will not automatically check whether the current user is valid in the background, but directly use the user information parsed from the Cookie. For example, after Tom logs in, the user has been disabled in the background database, but because Tom has logged in, a valid Cookie has been saved in the browser of the client, so Tom can continue to access the system.

To avoid this, the Cookie authentication processor provides an extension point OnValidatePrincipal to allow you to check whether the current credentials are available. After the user passes the authentication, the registered callback method will be called to check.

You can register the callback method of OnValidatePrincipal as follows.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(options =>
    {
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
        .AddCookie(options =>
        {
            options.Events.OnValidatePrincipal = PrincipalValidator.ValidateAsync;
        });
}

Custom inspector.

public static class PrincipalValidator
{
    public static async Task ValidateAsync(CookieValidatePrincipalContext context)
    {
        if (context == null) throw new System.ArgumentNullException(nameof(context));

        var userId = context.Principal.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value;
        if (userId == null)
        {
            context.RejectPrincipal();
            return;
        }

        // Get an instance using DI
        var dbContext = context.HttpContext.RequestServices.GetRequiredService<IdentityDbContext>();
        var user = await dbContext.Users.FindByIdAsync(userId);
        if (user == null)
        {
            context.RejectPrincipal();
            return;
        }
    }
}

https://www.meziantou.net/validating-user-with-cookie-authentication-in-asp-net-core-2.htm

See react to back end changes , in this example, the event processing object derived from CookieAuthenticationEvents is customized, and the ValidatePrincipal() method is rewritten.

Storage of authentication tickets

In addition to completely saving the authentication information into the Cookie, you can also save the authentication information into a storage of authentication information. Its type is ITicketStore.

When a large amount of information is saved in the authentication ticket, there will be too much content in the cookie. It can be considered to save the authentication information in the storage on a server. Each authentication information provides a mark, and only the mark is saved in the cookie. This can not only compress the length of the content in the cookie, but also provide security.

/// <summary>
/// An optional container in which to store the identity across requests. When used, only a session identifier is sent
/// to the client. This can be used to mitigate potential problems with very large identities.
/// </summary>
public ITicketStore? SessionStore { get; set; }

The interface is defined as follows:

public interface ITicketStore
{
    /// <summary>
    /// Store the identity ticket and return the associated key.
    /// </summary>
    /// <param name="ticket">The identity information to store.</param>
    /// <returns>The key that can be used to retrieve the identity later.</returns>
    Task<string> StoreAsync(AuthenticationTicket ticket);

    /// <summary>
    /// Tells the store that the given identity should be updated.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="ticket"></param>
    /// <returns></returns>
    Task RenewAsync(string key, AuthenticationTicket ticket);

    /// <summary>
    /// Retrieves an identity from the store for the given key.
    /// </summary>
    /// <param name="key">The key associated with the identity.</param>
    /// <returns>The identity associated with the given key, or if not found.</returns>
    Task<AuthenticationTicket> RetrieveAsync(string key);

    /// <summary>
    /// Remove the identity associated with the given key.
    /// </summary>
    /// <param name="key">The key associated with the identity.</param>
    /// <returns></returns>
    Task RemoveAsync(string key);
}

View the definition of ITicketStore in GitHub

Manage authentication information using distributed cache

Referring to the principle of Session, save the Claims information in the server and set an ID for it, and only save the ID in the Cookie, so that the complete Claims information can be retrieved from the server through the ID. Note, however, that this is not using ASP The Session in net core refers to its storage mode only.

So how? In the AddCookie method used in the previous registration of Cookie authentication, its Cookie authenticationoptions parameter can also set a SessionStore attribute of ITicketStore type. We can customize the Cookie access method by implementing this interface. The following is a demonstration using local cache. In the case of multiple servers, distributed cache processing is required.

First add Microsoft Extensions. Caching. Package reference of memory:

dotnet add package Microsoft.Extensions.Caching.Memory

Then, define the MemoryCacheTicketStore class

public class MemoryCacheTicketStore : ITicketStore
{
    private const string KeyPrefix = "CSS-";
    private IMemoryCache _cache;

    public MemoryCacheTicketStore()
    {
        _cache = new MemoryCache(new MemoryCacheOptions());
    }
    
    public async Task<string> StoreAsync(AuthenticationTicket ticket)
    {
        var key = KeyPrefix + Guid.NewGuid().ToString("N");
        await RenewAsync(key, ticket);
        return key;
    }
    
    public Task RenewAsync(string key, AuthenticationTicket ticket)
    {
        var options = new MemoryCacheEntryOptions();
        var expiresUtc = ticket.Properties.ExpiresUtc;
        if (expiresUtc.HasValue)
        {
            options.SetAbsoluteExpiration(expiresUtc.Value);
        }
        options.SetSlidingExpiration(TimeSpan.FromHours(1));
        _cache.Set(key, ticket, options);
        return Task.FromResult(0);
    }
    
    public Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        _cache.TryGetValue(key, out AuthenticationTicket ticket);
        return Task.FromResult(ticket);
    }
    
    public Task RemoveAsync(string key)
    {
        _cache.Remove(key);
        return Task.FromResult(0);
    }
}

Configure MemoryCacheTicketStore into CookieAuthenticationOptions:

.AddCookie(options =>
{
    options.SessionStore = new MemoryCacheTicketStore();
});

Topics: .NET