The interplay of authentication and authorization in ASP.NET Core

While Microsoft continues to improve its authentication and authorization documentation, I believe they still fall short in building a clear mental model of how everything works together. Here I briefly explain how the [Authorize] attribute and authorization policies interact with each other and with the authentication. Treat it as an extension of the article Authorize with a specific scheme in ASP.NET Core and as a description of my discoveries from testing and diving into the framework's source code, so it’s packed with GitHub links for reference. Readers are assumed to be familiar with configuring authentication and defining authorization policies. The process starts with the authentication. ASP.NET introduces the concept of the authentication schemes. Authentication scheme An authentication scheme can appear in multiple places: In the AddAuthentication method or in AuthenticationOptions.DefaultAuthenticateScheme property. In AuthorizationPolicyBuilder constructor or AuthorizationPolicyBuilder.AddAuthenticationSchemes method. In IAuthorizeData.AuthenticationSchemes property. The first thing to note is the inconsistency in naming: AuthenticateScheme versus AuthenticationScheme. The second issue is that the documentation doesn’t clarify how these parameters — or more precisely, their combination — affect authentication and authorization. Actually, this was the main motivation for the experimentation. For authentication there can be one default scheme. When it is defined, its handler is always invoked, even if [AllowAnonymous] is applied to the endpoint. Default authentication is triggered by the Authentication middleware. If an authentication scheme is not configured as the default, its handler is called only when the scheme is included in the list of authentication schemes of the effective policy. Policies ASP.NET defines Fallback and Default authorization policies. The default policy is used when evaluating IAuthorizeData with no policy name specified. It cannot be null. If not explicitly redefined, default policy just requires the user to be authenticated. The fallback policy is used when no IAuthorizeData have been provided. As a result, the AuthorizationMiddleware uses the fallback policy if there are no IAuthorizeData instances for a resource. If a resource has any IAuthorizeData then they are evaluated instead of the fallback policy. By default the fallback policy is null, and usually will have no effect unless you have the AuthorizationMiddleware in your pipeline. It is not used in any way by the default IAuthorizationService. (As AuthorizaAttribute implements IAuthorizaData, below I only use the former one, though it's not 100% correct.) Thus, any assumptions about authorization behavior when the [Authorize] attribute is applied to an endpoint are precarious unless you know the exact configuration of the default policy. Same holds true for the missing [Authorize] attribute and the fallback policy. If I had to derive a recommendation from the facts above then they would be: If you configure the fallback policy, prefer to make it always failing or the most restrictive one. If you change the default policy, try to add requirements. Relaxing the default policy is risky. What's is an effective policy? The effective policy is a combination 1, 2 of the following (N.B. [Authorize] attribute can be applied multiple times): Authorization policies explicitly specified in [Authorize.Policy] property. The Default policy from AuthorizationOptions.DefaultPolicy when [Authorize.Policy] is not set. The Fallback policy from AuthorizationOptions.FallbackPolicy when [Authorize] is not applied. A policy derived from other [Authorize] attribute properties (Roles, and AuthenticationSchemes) Requirements from IAuthorizationRequirementData. PolicyEvaluator class The PolicyEvaluator class is responsible for evaluating the effective authorization policy, including authentication. When the effective policy specifies authentication schemes, the policy evaluator uses them for authentication. If no schemes are defined in the policy, the evaluator just returns IAuthenticateResultFeature.AuthenticateResult (In most cases is will be the result of authentication using the default scheme), or the default result. As a sidenote: When an authorization policy requires authenticated users, what matters is the authentication result. DenyAnonymousAuthorizationRequirement does not care which schemes were used for authenticating user. If you need to ensure that all schemes authenticate successfully, you must implement a custom authorization requirement and its corresponding handler. Now, assuming that the Authentication and Authorization middleware have been added to the application pipeline, let's consider various scenarios. Scenarios Condition: No default authenticate scheme. No fallback policy . No IAuthorizeData metadata on the endpoint

Apr 9, 2025 - 22:37
 0
The interplay of authentication and authorization in ASP.NET Core

Cover image

While Microsoft continues to improve its authentication and authorization documentation, I believe they still fall short in building a clear mental model of how everything works together.

Here I briefly explain how the [Authorize] attribute and authorization policies interact with each other and with the authentication. Treat it as an extension of the article Authorize with a specific scheme in ASP.NET Core and as a description of my discoveries from testing and diving into the framework's source code, so it’s packed with GitHub links for reference.

Readers are assumed to be familiar with configuring authentication and defining authorization policies.

The process starts with the authentication. ASP.NET introduces the concept of the authentication schemes.

Authentication scheme

An authentication scheme can appear in multiple places:

  1. In the AddAuthentication method or in AuthenticationOptions.DefaultAuthenticateScheme property.
  2. In AuthorizationPolicyBuilder constructor or AuthorizationPolicyBuilder.AddAuthenticationSchemes method.
  3. In IAuthorizeData.AuthenticationSchemes property.

The first thing to note is the inconsistency in naming: AuthenticateScheme versus AuthenticationScheme.

The second issue is that the documentation doesn’t clarify how these parameters — or more precisely, their combination — affect authentication and authorization. Actually, this was the main motivation for the experimentation.

For authentication there can be one default scheme. When it is defined, its handler is always invoked, even if [AllowAnonymous] is applied to the endpoint. Default authentication is triggered by the Authentication middleware.

If an authentication scheme is not configured as the default,
its handler is called only when the scheme is included in the list of authentication schemes of the effective policy.

Policies

ASP.NET defines Fallback and Default authorization policies.

The default policy is used when evaluating IAuthorizeData with no policy name specified. It cannot be null.
If not explicitly redefined, default policy just requires the user to be authenticated.

The fallback policy is used when no IAuthorizeData have been provided.
As a result, the AuthorizationMiddleware uses the fallback policy if there are no IAuthorizeData instances for a resource. If a resource has any IAuthorizeData then they are evaluated instead of the fallback policy. By default the fallback policy is null, and usually will have no effect unless you have the AuthorizationMiddleware in your pipeline. It is not used in any way by the default IAuthorizationService.

(As AuthorizaAttribute implements IAuthorizaData, below I only use the former one, though it's not 100% correct.)

Thus, any assumptions about authorization behavior when the [Authorize] attribute is applied to an endpoint are precarious unless you know the exact configuration of the default policy. Same holds true for the missing [Authorize] attribute and the fallback policy.

If I had to derive a recommendation from the facts above then they would be:

  • If you configure the fallback policy, prefer to make it always failing or the most restrictive one.
  • If you change the default policy, try to add requirements. Relaxing the default policy is risky.

What's is an effective policy?

The effective policy is a combination 1, 2 of the following (N.B. [Authorize] attribute can be applied multiple times):

  • Authorization policies explicitly specified in [Authorize.Policy] property.
  • The Default policy from AuthorizationOptions.DefaultPolicy when [Authorize.Policy] is not set.
  • The Fallback policy from AuthorizationOptions.FallbackPolicy when [Authorize] is not applied.
  • A policy derived from other [Authorize] attribute properties (Roles, and AuthenticationSchemes)
  • Requirements from IAuthorizationRequirementData.

PolicyEvaluator class

The PolicyEvaluator class is responsible for evaluating the effective authorization policy, including authentication.

When the effective policy specifies authentication schemes, the policy evaluator uses them for authentication. If no schemes are defined in the policy, the evaluator just returns IAuthenticateResultFeature.AuthenticateResult (In most cases is will be the result of authentication using the default scheme), or the default result.

As a sidenote: When an authorization policy requires authenticated users, what matters is the authentication result. DenyAnonymousAuthorizationRequirement does not care which schemes were used for authenticating user. If you need to ensure that all schemes authenticate successfully, you must implement a custom authorization requirement and its corresponding handler.

Now, assuming that the Authentication and Authorization middleware have been added to the application pipeline, let's consider various scenarios.

Scenarios

Condition:

  • No default authenticate scheme.
  • No fallback policy .
  • No IAuthorizeData metadata on the endpoint.

Result:
No authentication and no authorization.

Condition:

  • Default authenticate scheme.
  • No fallback policy.
  • No IAuthorizeData metadata on the endpoint.

Result:
Authentication using the default scheme, no authorization.

Condition:

  • Default authenticate scheme.
  • Configured fallback policy without schemes.
  • No IAuthorizeData metadata on the endpoint.

Result:
Authentication using the default scheme, authorization using the fallback policy.

Condition:

  • Default authenticate scheme.
  • Configured fallback policy with schemes.
  • No IAuthorizeData metadata on the endpoint.

Result:
Authentication handlers are invoked both for the default scheme and the schemes from the fallback policy, but only the authentication result from the policy's schemes is used when policy is evaluated.

Condition:

  • No default authenticate scheme.
  • Configured fallback policy without schemes
  • No IAuthorizeData metadata on the endpoint.

Result:

System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions).
at Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, String scheme, AuthenticationProperties properties)
at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.<>c_DisplayClass0_0._Handle|0>d.MoveNext()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)

Condition:

  • No default authenticate scheme.
  • Configured fallback policy with schemes
  • No IAuthorizeData metadata on the endpoint.

Result:
Authentication handlers of the schemes from the policy are called,
authentication result is used when the policy is evaluated.

Condition:

  • [Authorize(Policy = "")] is applied to the endpoint.

Result:
Same as the previous scenarios, but the effective policy is strictly the one with the name .

Condition:

  • [Authorize] (without parameters) is applied to the endpoint.

Result:
Same as the previous scenarios, but the effective policy is the Default policy.

Condition:

  • [Authorize] is applied to the endpoint.
  • AuthorizationOptions.DefaultPolicy property is explicitly set to null (property's type is not nullable AuthorizationPolicy)

Result:

System.ArgumentNullException: Value cannot be null. (Parameter 'policy')
at System.ArgumentNullException.Throw(String paramName)
at System.ArgumentNullException.ThrowIfNull(Object argument, String paramName)
at Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder.Combine(AuthorizationPolicy policy)
at Microsoft.AspNetCore.Authorization.AuthorizationPolicy.CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable1 authorizeData, IEnumerable1 policies)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)

Condition:

  • [Authorize(AuthenticationSchemes="...")] is applied to the endpoint.

Result:
The effective policy defines the list of applied authentication schemes as a union of the schemes from the Default policy and the schemes from the attribute.

Condition:

  • [Authorize(AuthenticationSchemes="...", Policy="")] is applied to the endpoint.

Result:
The effective policy defines the list of applied authentication schemes as a union of the schemes from the policy and the schemes from the attribute.

Perhaps the most important thing to learn from this is that when you are setting the schemes in Authorize.AuthenticationSchemes, you are extending, not overriding the schemes of the policy (especially the default one).