Asp.Net Core Authorize those things you don't know (source code interpretation)

1, Foreword

IdentityServer4 has shared some practical application articles. From the architecture to the landing application of the authorization center, it has also mastered some use rules of IdentityServer4, but it still knows a little about many principles. Therefore, I will continue to take you to read its relevant source code, This article first looks at why adding an Authorize filter in the Controller or Action or an Authorize filter in the global can protect the resource through access_ Can a token pass the relevant authorization? Today, I'll take you to understand the relationship between AuthorizeAttribute and AuthorizeFilter and code interpretation.

2, Code interpretation

Before reading, let's take a look at the following two codes marking authorization methods:

Annotation method
 [Authorize]
 [HttpGet]
 public async Task<object> Get()
 {
      var userId = User.UserId();
      return new
      {
         name = User.Name(),
         userId = userId,
         displayName = User.DisplayName(),
         merchantId = User.MerchantId(),
      };
 }

In the code, access to the api resource is restricted by [Authorize] annotation

Global mode
public void ConfigureServices(IServiceCollection services)
{
     //Add AuthorizeFilter filter globally
     services.AddControllers(options=>options.Filters.Add(new AuthorizeFilter()));

     services.AddAuthorization();
     services.AddAuthentication("Bearer")
         .AddIdentityServerAuthentication(options =>
         {
             options.Authority = "http://localhost:5000 "; / / configure the authorized address of Identityserver
             options.RequireHttpsMetadata = false;           //https is not required    
             options.ApiName = OAuthConfig.UserApi.ApiName;  //The name of api should be the same as that of config
         });
}

The global api resources are limited by adding an AuthorizeFilter filter

AuthorizeAttribute

Let's take a look at the source code of AuthorizeAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
    /// <summary>
    /// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class. 
    /// </summary>
    public AuthorizeAttribute() { }

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthorizeAttribute"/> class with the specified policy. 
    /// </summary>
    /// <param name="policy">The name of the policy to require for authorization.</param>
    public AuthorizeAttribute(string policy)
    {
       Policy = policy;
    }

    /// <summary>
    ///Collection strategy
    /// </summary>
    public string Policy { get; set; }

    /// <summary>
    ///Authorization role
    /// </summary>
    public string Roles { get; set; }

    /// <summary>
    ///Authorization Schemes
    /// </summary>
    public string AuthenticationSchemes { get; set; }
}

It can be seen in the code that the AuthorizeAttribute inherits the IAuthorizeData abstract interface, which is mainly the constraint definition of authorization data and defines three data attributes

  • Prolicy: authorization policy
  • Roles: authorization role
  • AuthenticationSchemes: support for authorization Schemes
    Asp. The http Middleware in net core will obtain the authorization filters according to IAuthorizeData to intercept the filters and execute relevant codes.
    Let's look at the AuthorizeAttribute code as follows:
public interface IAuthorizeData
{
        /// <summary>
        /// Gets or sets the policy name that determines access to the resource.
        /// </summary>
        string Policy { get; set; }

        /// <summary>
        /// Gets or sets a comma delimited list of roles that are allowed to access the resource.
        /// </summary>
        string Roles { get; set; }

        /// <summary>
        /// Gets or sets a comma delimited list of schemes from which user information is constructed.
        /// </summary>
        string AuthenticationSchemes { get; set; }
}

Let's look at the core code of use authorization middleware:

public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app)
{
    if (app == null)
    {
        throw new ArgumentNullException(nameof(app));
    }

    VerifyServicesRegistered(app);

    return app.UseMiddleware<AuthorizationMiddleware>();
}

The authorization middleware is registered in the code. The source code of the authorization middleware is as follows:

 public class AuthorizationMiddleware
 {
        // Property key is used by Endpoint routing to determine if Authorization has run
        private const string AuthorizationMiddlewareInvokedWithEndpointKey = "__AuthorizationMiddlewareWithEndpointInvoked";
        private static readonly object AuthorizationMiddlewareWithEndpointInvokedValue = new object();

        private readonly RequestDelegate _next;
        private readonly IAuthorizationPolicyProvider _policyProvider;

        public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
        {
            _next = next ?? throw new ArgumentNullException(nameof(next));
            _policyProvider = policyProvider ?? throw new ArgumentNullException(nameof(policyProvider));
        }

        public async Task Invoke(HttpContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var endpoint = context.GetEndpoint();

            if (endpoint != null)
            {
                // EndpointRoutingMiddleware uses this flag to check if the Authorization middleware processed auth metadata on the endpoint.
                // The Authorization middleware can only make this claim if it observes an actual endpoint.
                context.Items[AuthorizationMiddlewareInvokedWithEndpointKey] = AuthorizationMiddlewareWithEndpointInvokedValue;
            }

            // The AuthorizeAttribute for is obtained through the endpoint routing element IAuthorizeData and associated to the AuthorizeFilter
            var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
            var policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
            if (policy == null)
            {
                await _next(context);
                return;
            }

            // Policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor
            var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();

            var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);

            // Allow Anonymous skips all authorization
            if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
            {
                await _next(context);
                return;
            }

            // Note that the resource will be null if there is no matched endpoint
            var authorizeResult = await policyEvaluator.AuthorizeAsync(policy, authenticateResult, context, resource: endpoint);

            if (authorizeResult.Challenged)
            {
                if (policy.AuthenticationSchemes.Any())
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ChallengeAsync(scheme);
                    }
                }
                else
                {
                    await context.ChallengeAsync();
                }

                return;
            }
            else if (authorizeResult.Forbidden)
            {
                if (policy.AuthenticationSchemes.Any())
                {
                    foreach (var scheme in policy.AuthenticationSchemes)
                    {
                        await context.ForbidAsync(scheme);
                    }
                }
                else
                {
                    await context.ForbidAsync();
                }

                return;
            }

            await _next(context);
        }
    }

The core of the code intercepts and obtains the code of the AuthorizeFilter filter

var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();

I shared an article about Asp. Interpretation of the working principle of net core endpoint routing It is explained in the article that the Attribute attribute annotation in the Controller and Action is obtained through the EndPoint origin. This method is also used to intercept and obtain the AuthorizeAttribute of
After obtaining the relevant authorizeData authorization data, the following series of codes are the methods of AuthorizeAsync authorization execution through judgment. The process of authorization authentication will not be shared in detail here.
Careful students should have found that the above code has a special code:

if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
{
      await _next(context);
      return;
}

In the code, the endpoint origin is used to obtain whether the feature of AllowAnonymous is marked. If so, the next middleware will be executed directly without the following AuthorizeAsync authorization authentication method,
This is why AllowAnonymous can be marked on the Controller and Action to skip authorization authentication.

AuthorizeFilter source code

Some people will ask, what is the relationship between authorizeatirbute and AuthorizeFilter? Are they one thing?
Let's take another look at the source code of AuthorizeFilter. The code is as follows:

public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
        /// <summary>
        /// Initializes a new <see cref="AuthorizeFilter"/> instance.
        /// </summary>
        public AuthorizeFilter()
            : this(authorizeData: new[] { new AuthorizeAttribute() })
        {
        }

        /// <summary>
        /// Initialize a new <see cref="AuthorizeFilter"/> instance.
        /// </summary>
        /// <param name="policy">Authorization policy to be used.</param>
        public AuthorizeFilter(AuthorizationPolicy policy)
        {
            if (policy == null)
            {
                throw new ArgumentNullException(nameof(policy));
            }

            Policy = policy;
        }

        /// <summary>
        /// Initialize a new <see cref="AuthorizeFilter"/> instance.
        /// </summary>
        /// <param name="policyProvider">The <see cref="IAuthorizationPolicyProvider"/> to use to resolve policy names.</param>
        /// <param name="authorizeData">The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.</param>
        public AuthorizeFilter(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
            : this(authorizeData)
        {
            if (policyProvider == null)
            {
                throw new ArgumentNullException(nameof(policyProvider));
            }

            PolicyProvider = policyProvider;
        }

        /// <summary>
        /// Initializes a new instance of <see cref="AuthorizeFilter"/>.
        /// </summary>
        /// <param name="authorizeData">The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.</param>
        public AuthorizeFilter(IEnumerable<IAuthorizeData> authorizeData)
        {
            if (authorizeData == null)
            {
                throw new ArgumentNullException(nameof(authorizeData));
            }

            AuthorizeData = authorizeData;
        }

        /// <summary>
        /// Initializes a new instance of <see cref="AuthorizeFilter"/>.
        /// </summary>
        /// <param name="policy">The name of the policy to require for authorization.</param>
        public AuthorizeFilter(string policy)
            : this(new[] { new AuthorizeAttribute(policy) })
        {
        }

        /// <summary>
        /// The <see cref="IAuthorizationPolicyProvider"/> to use to resolve policy names.
        /// </summary>
        public IAuthorizationPolicyProvider PolicyProvider { get; }

        /// <summary>
        /// The <see cref="IAuthorizeData"/> to combine into an <see cref="IAuthorizeData"/>.
        /// </summary>
        public IEnumerable<IAuthorizeData> AuthorizeData { get; }

        /// <summary>
        /// Gets the authorization policy to be used.
        /// </summary>
        /// <remarks>
        /// If<c>null</c>, the policy will be constructed using
        /// <see cref="AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider, IEnumerable{IAuthorizeData})"/>.
        /// </remarks>
        public AuthorizationPolicy Policy { get; }

        bool IFilterFactory.IsReusable => true;

        // Computes the actual policy for this filter using either Policy or PolicyProvider + AuthorizeData
        private Task<AuthorizationPolicy> ComputePolicyAsync()
        {
            if (Policy != null)
            {
                return Task.FromResult(Policy);
            }

            if (PolicyProvider == null)
            {
                throw new InvalidOperationException(
                    Resources.FormatAuthorizeFilter_AuthorizationPolicyCannotBeCreated(
                        nameof(AuthorizationPolicy),
                        nameof(IAuthorizationPolicyProvider)));
            }

            return AuthorizationPolicy.CombineAsync(PolicyProvider, AuthorizeData);
        }

        internal async Task<AuthorizationPolicy> GetEffectivePolicyAsync(AuthorizationFilterContext context)
        {
            // Combine all authorize filters into single effective policy that's only run on the closest filter
            var builder = new AuthorizationPolicyBuilder(await ComputePolicyAsync());
            for (var i = 0; i < context.Filters.Count; i++)
            {
                if (ReferenceEquals(this, context.Filters[i]))
                {
                    continue;
                }

                if (context.Filters[i] is AuthorizeFilter authorizeFilter)
                {
                    // Combine using the explicit policy, or the dynamic policy provider
                    builder.Combine(await authorizeFilter.ComputePolicyAsync());
                }
            }

            var endpoint = context.HttpContext.GetEndpoint();
            if (endpoint != null)
            {
                // When doing endpoint routing, MVC does not create filters for any authorization specific metadata i.e [Authorize] does not
                // get translated into AuthorizeFilter. Consequently, there are some rough edges when an application uses a mix of AuthorizeFilter
                // explicilty configured by the user (e.g. global auth filter), and uses endpoint metadata.
                // To keep the behavior of AuthFilter identical to pre-endpoint routing, we will gather auth data from endpoint metadata
                // and produce a policy using this. This would mean we would have effectively run some auth twice, but it maintains compat.
                var policyProvider = PolicyProvider ?? context.HttpContext.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
                var endpointAuthorizeData = endpoint.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();

                var endpointPolicy = await AuthorizationPolicy.CombineAsync(policyProvider, endpointAuthorizeData);
                if (endpointPolicy != null)
                {
                    builder.Combine(endpointPolicy);
                }
            }

            return builder.Build();
        }

        /// <inheritdoc />
        public virtual async Task OnAuthorizationAsync(AuthorizationFilterContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (!context.IsEffectivePolicy(this))
            {
                return;
            }

            // IMPORTANT: Changes to authorization logic should be mirrored in security's AuthorizationMiddleware
            var effectivePolicy = await GetEffectivePolicyAsync(context);
            if (effectivePolicy == null)
            {
                return;
            }

            var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();

            var authenticateResult = await policyEvaluator.AuthenticateAsync(effectivePolicy, context.HttpContext);

            // Allow Anonymous skips all authorization
            if (HasAllowAnonymous(context))
            {
                return;
            }

            var authorizeResult = await policyEvaluator.AuthorizeAsync(effectivePolicy, authenticateResult, context.HttpContext, context);

            if (authorizeResult.Challenged)
            {
                context.Result = new ChallengeResult(effectivePolicy.AuthenticationSchemes.ToArray());
            }
            else if (authorizeResult.Forbidden)
            {
                context.Result = new ForbidResult(effectivePolicy.AuthenticationSchemes.ToArray());
            }
        }

        IFilterMetadata IFilterFactory.CreateInstance(IServiceProvider serviceProvider)
        {
            if (Policy != null || PolicyProvider != null)
            {
                // The filter is fully constructed. Use the current instance to authorize.
                return this;
            }

            Debug.Assert(AuthorizeData != null);
            var policyProvider = serviceProvider.GetRequiredService<IAuthorizationPolicyProvider>();
            return AuthorizationApplicationModelProvider.GetFilter(policyProvider, AuthorizeData);
        }

        private static bool HasAllowAnonymous(AuthorizationFilterContext context)
        {
            var filters = context.Filters;
            for (var i = 0; i < filters.Count; i++)
            {
                if (filters[i] is IAllowAnonymousFilter)
                {
                    return true;
                }
            }

            // When doing endpoint routing, MVC does not add AllowAnonymousFilters for AllowAnonymousAttributes that
            // were discovered on controllers and actions. To maintain compat with 2.x,
            // we'll check for the presence of IAllowAnonymous in endpoint metadata.
            var endpoint = context.HttpContext.GetEndpoint();
            if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
            {
                return true;
            }

            return false;
        }
    }

The code inherits two abstract interfaces, iasyncauthorizationfilter and ifilterfactory. Let's take a look at the source code of these two abstract interfaces

The source code of IAsyncAuthorizationFilter is as follows:
/// <summary>
/// A filter that asynchronously confirms request authorization.
/// </summary>
public interface IAsyncAuthorizationFilter : IFilterMetadata
{
    ///Defines the method of authorization
    Task OnAuthorizationAsync(AuthorizationFilterContext context);
}

IAsyncAuthorizationFilter code inherits IFilterMetadata interface and defines OnAuthorizationAsync abstract method. Subclasses need to implement this method. However, this method has also been implemented in AuthorizeFilter. We will explain this method in detail later. We will continue to look at IFilterFactory abstract interface. The code is as follows:

public interface IFilterFactory : IFilterMetadata
 {
       
    bool IsReusable { get; }

    //Create IFilterMetadata object method
    IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}

Let's go back to the AuthorizeFilter source code, which provides four construction initialization methods, including the AuthorizeData and Policy attributes. Let's take a look at its default construction method code

public class AuthorizeFilter : IAsyncAuthorizationFilter, IFilterFactory
{
        public IEnumerable<IAuthorizeData> AuthorizeData { get; }

        //The AuthorizeAttribute object is created by default in the default constructor
        public AuthorizeFilter()
            : this(authorizeData: new[] { new AuthorizeAttribute() })
        {
        }

        //Assign AuthorizeData
        public AuthorizeFilter(IEnumerable<IAuthorizeData> authorizeData)
        {
            if (authorizeData == null)
            {
                throw new ArgumentNullException(nameof(authorizeData));
            }

            AuthorizeData = authorizeData;
        }
}

The default constructor in the above code constructs an AuthorizeAttribute object by default and assigns it to the collection attribute of IEnumerable < iauthorizedata >;
Well, here, the AuthorizeFilter also constructs an AuthorizeAttribute object by default, that is, it constructs the IAuthorizeData information required for authorization
At the same time, in the OnAuthorizationAsync method implemented by AuthorizeFilter, the effective authorization policy is obtained through GetEffectivePolicyAsync, and the following authorization AuthenticateAsync is executed
The HasAllowAnonymous method is provided in the AuthorizeFilter code to realize whether the AllowAnonymous feature is marked on the Controller or Action to skip authorization
HasAllowAnonymous code is as follows:

private static bool HasAllowAnonymous(AuthorizationFilterContext context)
{
     var filters = context.Filters;
     for (var i = 0; i < filters.Count; i++)
     {
        if (filters[i] is IAllowAnonymousFilter)
        {
           return true;
        }
     }
     //Similarly, whether the AllowAnonymous feature is marked is obtained through the endpoint of the context
     var endpoint = context.HttpContext.GetEndpoint();
     if (endpoint?.Metadata?.GetMetadata<IAllowAnonymous>() != null)
     {
        return true;
     }

     return false;
}

Here we return to the method code of adding filters globally:

 services.AddControllers(options=>options.Filters.Add(new AuthorizeFilter()));

So far, I'm curious. How can it be added globally? I opened the source code and looked at it. The source code is as follows:

public class MvcOptions : IEnumerable<ICompatibilitySwitch>
{

        public MvcOptions()
        {
            CacheProfiles = new Dictionary<string, CacheProfile>(StringComparer.OrdinalIgnoreCase);
            Conventions = new List<IApplicationModelConvention>();
            Filters = new FilterCollection();
            FormatterMappings = new FormatterMappings();
            InputFormatters = new FormatterCollection<IInputFormatter>();
            OutputFormatters = new FormatterCollection<IOutputFormatter>();
            ModelBinderProviders = new List<IModelBinderProvider>();
            ModelBindingMessageProvider = new DefaultModelBindingMessageProvider();
            ModelMetadataDetailsProviders = new List<IMetadataDetailsProvider>();
            ModelValidatorProviders = new List<IModelValidatorProvider>();
            ValueProviderFactories = new List<IValueProviderFactory>();
        }

        //Filter set
        public FilterCollection Filters { get; }
}

The core codes related to FilterCollection are as follows:

public class FilterCollection : Collection<IFilterMetadata>
{
        
        public IFilterMetadata Add<TFilterType>() where TFilterType : IFilterMetadata
        {
            return Add(typeof(TFilterType));
        }

        //Other core codes are posted
}

The code provides an Add method that constrains objects of type IFilterMetadata, which is why the above filters inherit IFilterMetadata.
Here, the code interpretation and implementation principle have been analyzed. If there is anything wrong with the analysis, please give more advice!!!

Conclusion: the authorization middleware obtains the authorization information related to the authorization attribute object by obtaining IAuthorizeData, and constructs the authorization policy object for authorization authentication. The authorization filter will also add the authorization related data IAuthorizeData of the authorization attribute by default and implement the OnAuthorizationAsync method, At the same time, the authorization policy provider IAuthorizationPolicyProvider is used in the middleware to obtain the authorization authentication for the authorization policy

Added by crackfox on Tue, 08 Mar 2022 23:58:34 +0200