ASP.NET Implementation of core authentication

Time:2021-2-13

Tracing the origin, starting from the use

First of all, let’s take a look at how we usually use Microsoft’s own authentication. Generally, we configure our required dependency authentication service in startup. Here we will explain it through JWT authentication

public void ConfigureServices(IServiceCollection services)
{
  services.AddAuthentication(authOpt =>
  {
    authOpt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    authOpt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  })
  .AddJwtBearer(o =>
  {
    o.TokenValidationParameters = new TokenValidationParameters
    {
      //Configure the parameters you want to verify
      
    };
  });
}

Let’s take a look at what addauthentication does


public static class AuthenticationServiceCollectionExtensions
 {
  public static AuthenticationBuilder AddAuthentication( this IServiceCollection services, Action<AuthenticationOptions> configureOptions)
  {
   if (services == null)
    throw new ArgumentNullException(nameof (services));
   if (configureOptions == null)
    throw new ArgumentNullException(nameof (configureOptions));
   AuthenticationBuilder authenticationBuilder = services.AddAuthentication();
   services.Configure<AuthenticationOptions>(configureOptions);
   return authenticationBuilder;
  }

  public static AuthenticationBuilder AddAuthentication( this IServiceCollection services)
  {
   if (services == null)
    throw new ArgumentNullException(nameof (services));
   services.AddAuthenticationCore();
   services.AddDataProtection();
   services.AddWebEncoders();
   services.TryAddSingleton<ISystemClock, SystemClock>();
   return new AuthenticationBuilder(services);
  }

  public static AuthenticationBuilder AddAuthentication(
   this IServiceCollection services,
   string defaultScheme)
  {
   return services.AddAuthentication((Action<AuthenticationOptions>) (o => o.DefaultScheme = defaultScheme));
  } 

 .....
}

The configuration services method is basically service registration. Based on the style of Microsoft, the addauthentication core here must be our authentication service registration method. Let’s take a look


public static class AuthenticationCoreServiceCollectionExtensions
 {
  /// <summary>
  /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />.
  /// </summary>  
  public static IServiceCollection AddAuthenticationCore(
   this IServiceCollection services)
  {
   if (services == null)
    throw new ArgumentNullException(nameof (services));
   services.TryAddScoped<IAuthenticationService, AuthenticationService>();
   services.TryAddSingleton<IClaimsTransformation, NoopClaimsTransformation>();
   services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
   services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
   return services;
  }

  /// <summary>
  /// Add core authentication services needed for <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationService" />.
  /// </summary>  
  public static IServiceCollection AddAuthenticationCore(
   this IServiceCollection services,
   Action<AuthenticationOptions> configureOptions)
  {
   if (services == null)
    throw new ArgumentNullException(nameof (services));
   if (configureOptions == null)
    throw new ArgumentNullException(nameof (configureOptions));
   services.AddAuthenticationCore();
   services.Configure<AuthenticationOptions>(configureOptions);
   return services;
  }
 }

We can see that authentication service, authentication handler provider and authentication scheme provider are registered here. As mentioned at the beginning of the article, tracing the origin and starting from the use, let’s take a look at how these three objects are used in the authentication system and how they play a role.

Starting from use

Take a look at our certification pipeline construction


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
  {
    ...
    app.UseAuthentication();
    ...
  }


 public static class AuthAppBuilderExtensions
 {
  public static IApplicationBuilder UseAuthentication( this IApplicationBuilder app)
  {
   if (app == null)
    throw new ArgumentNullException(nameof (app));
   return app.UseMiddleware<AuthenticationMiddleware>();
  }
 }

Here we use the agreed registration method usemiddleware, and specify to use the middleware authentication middleware

public class AuthenticationMiddleware
 {
  private readonly RequestDelegate _next;

  public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
  {
   if (next == null)
    throw new ArgumentNullException(nameof (next));
   if (schemes == null)
    throw new ArgumentNullException(nameof (schemes));
   this._next = next;
   this.Schemes = schemes;
  }

  public IAuthenticationSchemeProvider Schemes { get; set; }

  public async Task Invoke(HttpContext context)
  {
   context.Features.Set<IAuthenticationFeature>((IAuthenticationFeature) new AuthenticationFeature()
   {
    OriginalPath = context.Request.Path,
    OriginalPathBase = context.Request.PathBase
   });
   IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
   foreach (AuthenticationScheme authenticationScheme in await this.Schemes.GetRequestHandlerSchemesAsync())
   {
    IAuthenticationRequestHandler handlerAsync = await handlers.GetHandlerAsync(context, authenticationScheme.Name) as IAuthenticationRequestHandler;
    bool flag = handlerAsync != null;
    if (flag)
     flag = await handlerAsync.HandleRequestAsync();
    if (flag)
     return;
   }
   AuthenticationScheme authenticateSchemeAsync = await this.Schemes.GetDefaultAuthenticateSchemeAsync();
   if (authenticateSchemeAsync != null)
   {
    AuthenticateResult authenticateResult = await  context.AuthenticateAsync (aut henticateSchemeAsync.Name ); // actual authentication service
    if (authenticateResult?.Principal != null)
     context.User = authenticateResult.Principal;
   }
   await this._next(context);
  }
 }

Before going on, let’s take a look at the result of the authentication middleware. When the authentication is passed, the user attribute (claim principal) of httpcontext is given an identity. Therefore, in subsequent request pipelines, authentication is based on the identity in the authentication result, which we will refer to later in the actual operation.

To get back to the point, we introduce our two objects, authentication handler provider and authentication scheme provider.

Explanation of important objects

  IAuthenticationSchemeProvider

From the name, iauthentication scheme provider should be used to provide scheme, which is also the role of provider in Microsoft style (similar to factory mode).

What is the scheme? Obviously, in the framework era, there are different schemes based on authentication, such as bearer and cookie. Different schemes defined in the aspnet core represent different authentication processing methods. Specifically, each scheme contains a handler of the corresponding iauthenticationhandler type, which is used to complete the authentication processing related to its own scheme. What if there is no definition? If you look at the above source code carefully, authentication will be performed only when the authentication scheme is not empty. Otherwise, once the authentication label [authorize] is marked on the controller, 401 will be returned directly, so we must specify our own scheme.

So where do we specify that our scheme is similar? Let’s go back to the addjwtbearer of configureservice first. The friends who have used it must know that the scheme obtained here is the scheme type that we specified in configureservice through addxxx scheme. Here we use JWT

Here, we specify that toptions is jwtbeareroptions and thandler is jwtbearerhandler.


public virtual AuthenticationBuilder AddScheme<TOptions, THandler>(
   string authenticationScheme,
   string displayName,
   Action<TOptions> configureOptions)
   where TOptions : AuthenticationSchemeOptions, new()
   where THandler : AuthenticationHandler<TOptions>
  {
   return this.AddSchemeHelper<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
  }


  private AuthenticationBuilder AddSchemeHelper<TOptions, THandler>(
   string authenticationScheme,
   string displayName,
   Action<TOptions> configureOptions)
   where TOptions : class, new()
   where THandler : class, IAuthenticationHandler
  {
   this.Services.Configure<AuthenticationOptions>((Action<AuthenticationOptions>) (o => o.AddScheme(authenticationScheme, (Action<AuthenticationSchemeBuilder>) (scheme =>
   {
    scheme.HandlerType = typeof (THandler);
    scheme.DisplayName = displayName;
   }))));
   if (configureOptions != null)
    this.Services.Configure<TOptions>(authenticationScheme, configureOptions);
   this.Services.AddTransient<THandler>();
   return this;
  }

Note that toptions need to inherit the authenticationschemeoptions, here jwtbeareroptions, and thandler is the authenticationhandler < toptions > type handler, here jwtbearerhandler.

Let’s go back to the analysis of scheme and go on. First, let’s look at the definition of authentication scheme


public class AuthenticationScheme
 {
  /// <summary>Constructor.</summary>  
  public AuthenticationScheme(string name, string displayName, Type handlerType)
  {
   if (name == null)
    throw new ArgumentNullException(nameof (name));
   if (handlerType == (Type) null)
    throw new ArgumentNullException(nameof (handlerType));
   if (!typeof (IAuthenticationHandler).IsAssignableFrom(handlerType))
    throw new ArgumentException("handlerType must implement IAuthenticationHandler.");
   this.Name = name;
   this.HandlerType = handlerType;
   this.DisplayName = displayName;
  }

  /// <summary>The name of the authentication scheme.</summary>
  public string Name { get; }

  /// <summary>
  /// The display name for the scheme. Null is valid and used for non user facing schemes.
  /// </summary>
  public string DisplayName { get; }

  /// <summary>
  /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandler" /> type that handles this scheme.
  /// </summary>
  public Type HandlerType { get; }
 }

As you can see here, if you want to use aspnet core’s own authentication system, you need to register scheme first, and the scheme must specify a handler of type iauthenticationhandler, otherwise an exception will be thrown. (in fact, the authenticationhandler has been specified in addxxscheme.)

Let’s look at what the getrequesthandlerschemesasync method of iauthentication schemeprovider does


  public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
  {
   return Task.FromResult<IEnumerable<AuthenticationScheme>>((IEnumerable<AuthenticationScheme>) this._requestHandlers);
  }

It’s back_ Request handlers, what is this? Look at the code


public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider
 {
  private readonly object _lock = new object();
  private readonly AuthenticationOptions _options;
  private readonly IDictionary<string, AuthenticationScheme> _schemes;
  private readonly List<AuthenticationScheme> _requestHandlers;

  /// <summary>
  /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" />
  /// using the specified <paramref name="options" />,
  /// </summary>  
  public AuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
   : this(options, (IDictionary<string, AuthenticationScheme>) new Dictionary<string, AuthenticationScheme>((IEqualityComparer<string>) StringComparer.Ordinal))
  {
  }

  /// <summary>
  /// Creates an instance of <see cref="T:Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider" />
  /// using the specified <paramref name="options" /> and <paramref name="schemes" />.
  /// </summary>  
  protected AuthenticationSchemeProvider(
   IOptions<AuthenticationOptions> options,
   IDictionary<string, AuthenticationScheme> schemes)
  {
   this._options = options.Value;
   IDictionary<string, AuthenticationScheme> dictionary = schemes;
   if (dictionary == null)
    throw new ArgumentNullException(nameof (schemes));
   this._schemes = dictionary;
   this._requestHandlers = new List<AuthenticationScheme>();
   foreach (AuthenticationSchemeBuilder scheme in this._options.Schemes)
    this.AddScheme(scheme.Build());
  }

  public virtual void AddScheme(AuthenticationScheme scheme)
  {
   if (this._schemes.ContainsKey(scheme.Name))
    throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
   lock (this._lock)
   {
    if (this._schemes.ContainsKey(scheme.Name))
     throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
    if (typeof (IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
     this._requestHandlers.Add(scheme);
    this._schemes[scheme.Name] = scheme;
   }
  }
.....
}

This thing loads the scheme specified in the authentication and registration service through the constructor of the authentication schemeprovider, and then returns a series of list < authentication scheme >. OK, what’s the use of getting these schemes? This brings us to our second object, authenticationhandlerprovider. Let’s take a look at it.

  IAuthenticationHandlerProvider

We see that the gethandlerasync method of iauthenticationhandlerprovider is used in authentication middleware. Let’s take a look at the function of this method first


public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider
 {
  private Dictionary<string, IAuthenticationHandler> _handlerMap = new Dictionary<string, IAuthenticationHandler>((IEqualityComparer<string>) StringComparer.Ordinal);

  /// <summary>Constructor.</summary>
  public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes)
  {
   this.Schemes = schemes;
  }

  /// <summary>
  /// The <see cref="T:Microsoft.AspNetCore.Authentication.IAuthenticationHandlerProvider" />.
  /// </summary>
  public IAuthenticationSchemeProvider Schemes { get; }

  /// <summary>Returns the handler instance that will be used.</summary>  
  public async Task<IAuthenticationHandler> GetHandlerAsync( HttpContext context, string authenticationScheme)
  {
   if (this._handlerMap.ContainsKey(authenticationScheme))
    return this._handlerMap[authenticationScheme];
   AuthenticationScheme schemeAsync = await this.Schemes.GetSchemeAsync(authenticationScheme);
   if (schemeAsync == null)
    return (IAuthenticationHandler) null;
   IAuthenticationHandler handler = (context.RequestServices.GetService(schemeAsync.HandlerType) ?? ActivatorUtilities.CreateInstance(context.RequestServices, schemeAsync.HandlerType)) as IAuthenticationHandler;
   if (handler != null)
   {
    await handler.InitializeAsync(schemeAsync, context);
    this._handlerMap[authenticationScheme] = handler;
   }
   return handler;
  }
 }

When creating a handler, it is first obtained from the authentication scheme. If it does not exist, it is created through activator utilities. After getting the handle, it will be placed in the_ In the handlermap dictionary, the next time you get the handler, you will get it directly from the cache.

  IAuthenticationService

This object is the last one used in authentication middleware, and it is called based on httpcontext extension


public static class AuthenticationHttpContextExtensions
{
  public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) =>
    context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);

 ....     
}

Here we mainly call the authenticateasync method of iauthenticationservice to see what this method does


public class AuthenticationService : IAuthenticationService
{
  public IAuthenticationSchemeProvider Schemes { get; }
  public IAuthenticationHandlerProvider Handlers { get; }
  public IClaimsTransformation Transform { get; }

  public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
  {
    if (scheme == null)
    {
      var scheme = (await this.Schemes.GetDefaultAuthenticateSchemeAsync())?.Name;
      if (scheme == null)
        throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found.");
    }

    var handler = await Handlers.GetHandlerAsync(context, scheme);
    if(handler == null)
      throw await this.CreateMissingHandlerException(scheme);
    AuthenticateResult result = await handler.AuthenticateAsync();
    if (result != null && result.Succeeded)      
      return AuthenticateResult.Success(new AuthenticationTicket(await Transform.TransformAsync(result.Principal), result.Properties, result.Ticket.AuthenticationScheme));

    return result;
  }
}

This is actually what we talked about before getting the corresponding AuthenticationHandler from Scheme, and then calling the AuthenticateAsync () method, which calls the core method HandleAuthenticateOnceAsync and then calls HandleAuthenticateAsync () the core authentication method.

As you can see from the figure above, handleauthenticateasync is an abstract method. All our subclasses need to implement the action of this method. Based on the example in this article, let’s take a look at an actual authentication of jwtbearerhandler.


public class JwtBearerHandler : AuthenticationHandler<JwtBearerOptions>
{
  protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
  {
   JwtBearerHandler jwtBearerHandler = this;
   string token = (string) null;
   object obj;
   AuthenticationFailedContext authenticationFailedContext;
   int num;
   try
   {
    MessageReceivedContext messageReceivedContext = new MessageReceivedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options);
    await jwtBearerHandler.Events.MessageReceived(messageReceivedContext);
    if (messageReceivedContext.Result != null)
     return messageReceivedContext.Result;
    token = messageReceivedContext.Token;
    if (string.IsNullOrEmpty(token))
    {
     string header = (string) jwtBearerHandler.Request.Headers["Authorization"];
     if (string.IsNullOrEmpty(header))
      return AuthenticateResult.NoResult();
     if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
      token = header.Substring("Bearer ".Length).Trim();
     if (string.IsNullOrEmpty(token))
      return AuthenticateResult.NoResult();
    }
    if (jwtBearerHandler._configuration == null && jwtBearerHandler.Options.ConfigurationManager != null)
    {
     OpenIdConnectConfiguration configurationAsync = await jwtBearerHandler.Options.ConfigurationManager.GetConfigurationAsync(jwtBearerHandler.Context.RequestAborted);
     jwtBearerHandler._configuration = configurationAsync;
    }
    TokenValidationParameters validationParameters1 = jwtBearerHandler.Options.TokenValidationParameters.Clone();
    if (jwtBearerHandler._configuration != null)
    {
     string[] strArray = new string[1]
     {
      jwtBearerHandler._configuration.Issuer
     };
     TokenValidationParameters validationParameters2 = validationParameters1;
     IEnumerable<string> validIssuers = validationParameters1.get_ValidIssuers();
     object obj1 = (validIssuers != null ? (object) validIssuers.Concat<string>((IEnumerable<string>) strArray) : (object) null) ?? (object) strArray;
     validationParameters2.set_ValidIssuers((IEnumerable<string>) obj1);
     TokenValidationParameters validationParameters3 = validationParameters1;
     IEnumerable<SecurityKey> issuerSigningKeys = validationParameters1.get_IssuerSigningKeys();
     IEnumerable<SecurityKey> securityKeys = (issuerSigningKeys != null ? issuerSigningKeys.Concat<SecurityKey>((IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys()) : (IEnumerable<SecurityKey>) null) ?? (IEnumerable<SecurityKey>) jwtBearerHandler._configuration.get_SigningKeys();
     validationParameters3.set_IssuerSigningKeys(securityKeys);
    }
    List<Exception> exceptionList = (List<Exception>) null;
    foreach (ISecurityTokenValidator securityTokenValidator in (IEnumerable<ISecurityTokenValidator>) jwtBearerHandler.Options.SecurityTokenValidators)
    {
     if (securityTokenValidator.CanReadToken(token))
     {
      SecurityToken securityToken;
      ClaimsPrincipal claimsPrincipal;
      try
      {
       claimsPrincipal = securityTokenValidator.ValidateToken(token, validationParameters1, ref securityToken);
      }
      catch (Exception ex)
      {
       jwtBearerHandler.Logger.TokenValidationFailed(ex);
       if (jwtBearerHandler.Options.RefreshOnIssuerKeyNotFound && jwtBearerHandler.Options.ConfigurationManager != null && ex is SecurityTokenSignatureKeyNotFoundException)
        jwtBearerHandler.Options.ConfigurationManager.RequestRefresh();
       if (exceptionList == null)
        exceptionList = new List<Exception>(1);
       exceptionList.Add(ex);
       continue;
      }
      jwtBearerHandler.Logger.TokenValidationSucceeded();
      TokenValidatedContext validatedContext = new TokenValidatedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options);
      validatedContext.Principal = claimsPrincipal;
      validatedContext.SecurityToken = securityToken;
      TokenValidatedContext tokenValidatedContext = validatedContext;
      await jwtBearerHandler.Events.TokenValidated(tokenValidatedContext);
      if (tokenValidatedContext.Result != null)
       return tokenValidatedContext.Result;
      if (jwtBearerHandler.Options.SaveToken)
       tokenValidatedContext.Properties.StoreTokens((IEnumerable<AuthenticationToken>) new AuthenticationToken[1]
       {
        new AuthenticationToken()
        {
         Name = "access_token",
         Value = token
        }
       });
      tokenValidatedContext.Success();
      return tokenValidatedContext.Result;
     }
    }
    if (exceptionList == null)
     return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
    authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options)
    {
     Exception = exceptionList.Count == 1 ? exceptionList[0] : (Exception) new AggregateException((IEnumerable<Exception>) exceptionList)
    };
    await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext);
    return authenticationFailedContext.Result == null ? AuthenticateResult.Fail(authenticationFailedContext.Exception) : authenticationFailedContext.Result;
   }
   catch (Exception ex)
   {
    obj = (object) ex;
    num = 1;
   }
   if (num == 1)
   {
    Exception ex = (Exception) obj;
    jwtBearerHandler.Logger.ErrorProcessingMessage(ex);
    authenticationFailedContext = new AuthenticationFailedContext(jwtBearerHandler.Context, jwtBearerHandler.Scheme, jwtBearerHandler.Options)
    {
     Exception = ex
    };
    await jwtBearerHandler.Events.AuthenticationFailed(authenticationFailedContext);
    if (authenticationFailedContext.Result != null)
     return authenticationFailedContext.Result;
    Exception source = obj as Exception;
    if (source == null)
     throw obj;
    ExceptionDispatchInfo.Capture(source).Throw();
    authenticationFailedContext = (AuthenticationFailedContext) null;
   }
   obj = (object) null;
   token = (string) null;
   AuthenticateResult authenticateResult;
   return authenticateResult;
  }
}

This method is a bit long, mainly from the Request.Headers In addjwtbearer, you can get the authorized bearer to parse it, and then pass in the token validation parameters attribute of jwtbeareroptions in addjwtbearer as the basis for comparison to determine whether the authentication is passed or not.

summary

In this paper ASP.NET Core authentication process to do a source code analysis process, because it is the source code analysis, so it may be more boring and bitter difficult to understand. In the later real use process, and then combined with a summary process of this article, I believe you will gradually be cheerful.

  • The configureservices method in the startup class registers our three main objects: authenticationservice, authenticationhandlerprovider and authenticationschemeprovider by adding addauthentication
  • The authentication builder returned through addauthentication specifies the scheme type and parameters to be verified through addjwtbearer (or addcookie)
  • The configure method in the startup class registers the authentication middleware by adding useauthentication
  • In the authentication process, the correct scheme is obtained through the authenticationschemeprovider. In the authentication service, the correct authenticationhandler is obtained through the scheme and the authenticationhandlerprovider. Finally, the authentication process is carried out through the corresponding authenticationhandler’s authenticateasync method

This is about ASP.NET This is the article about the implementation method of core authentication, and more about it ASP.NET Core authentication authentication content please search previous articles of developer or continue to browse the following related articles. I hope you can support developer more in the future!