RequireAuthorization and Swashbuckle IOperationFilter

Viewed 766

I am looking for a way to determine if endpoint requires authorization (.Net Core 3.1) using IOperationFilter.

If Authorization is setup via filter or explicitly as attribute, it can be found in OperationFilterContext context.ApiDescription.ActionDescriptor.FilterDescriptors.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter) and context.ApiDescription.CustomAttributes().OfType<AuthorizeAttribute>().

But if authorization is set as endpoints.MapControllers().RequireAuthorization();, which should add AuthorizationAttribute to all endpoints, it is not appeared neither in filters nor in attributes. Any thoughts on how to catch if auth is applied to endpoints in this case?

3 Answers

I was able to beat this today like so (swashbuckle 5.63):

  1. Make a new class like this
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace YourNameSpace
{
   public class SwaggerGlobalAuthFilter : IOperationFilter
   {
      public void Apply( OpenApiOperation operation, OperationFilterContext context )
      {
         context.ApiDescription.TryGetMethodInfo( out MethodInfo methodInfo );

         if ( methodInfo == null )
         {
            return;
         }

         var hasAllowAnonymousAttribute = false;

         if ( methodInfo.MemberType == MemberTypes.Method )
         {
            // NOTE: Check the controller or the method itself has AllowAnonymousAttribute attribute
            hasAllowAnonymousAttribute = 
               methodInfo.DeclaringType.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any() ||
               methodInfo.GetCustomAttributes( true ).OfType<AllowAnonymousAttribute>().Any();
         }

         if ( hasAllowAnonymousAttribute )
         {
            return;
         }

         // NOTE: This adds the "Padlock" icon to the endpoint in swagger, 
         //       we can also pass through the names of the policies in the List<string>()
         //       which will indicate which permission you require.
         operation.Security = new List<OpenApiSecurityRequirement>
         {
            new OpenApiSecurityRequirement()
            {
               {
                  new OpenApiSecurityScheme
                  {
                     Reference = new OpenApiReference
                     {
                        Type = ReferenceType.SecurityScheme,
                        Id = "oauth2"  // note this 'Id' matches the name 'oauth2' defined in the swagger extensions config section below
                     },
                     Scheme = "oauth2",
                     Name = "Bearer",
                     In = ParameterLocation.Header,
                  },
                  new List<string>()
               }
            }
         };
      }
   }
}

In swagger config extensions

            options.AddSecurityDefinition( "oauth2", new OpenApiSecurityScheme
            {
               Type = SecuritySchemeType.OAuth2,
               Flows = new OpenApiOAuthFlows
               {
                  Implicit = new OpenApiOAuthFlow
                  {
                    //_swaggerSettings is a custom settings object of our own
                     AuthorizationUrl = new Uri( _swaggerSettings.AuthorizationUrl ),
                     Scopes = _swaggerSettings.Scopes
                  }
               }
            } );
            options.OperationFilter<SwaggerGlobalAuthFilter>();

Put together from docs, other SO and decompiled code of built-in SecurityRequirementsOperationFilter

AFAIK, it is defining a global auth setup for all your routed endpoints except those that explicitly have AllowAnonymousAttribute on controller or endpoint. since, as your original question hints at, using the extension RequireAuthorization() when setting up routing implicitly puts that attribute on all endpoints and the built-in SecurityRequirementsOperationFilter which detect the Authorize attribute fails to pick it up. Since your routing setup effectively is putting Authorize on every controller/route it seems setting up a default global filter like this that excludes AllowAnonymous would be in line with what you are configuring in the pipeline.

I suspect there may be a more 'built-in' way of doing this, but I could not find it.

Apparently, this is an open issue on the NSwag repo as well (for people like me that drive by with the same issue, but with NSwag instead of Swashbuckle):

https://github.com/RicoSuter/NSwag/issues/2817

Where there's also another example of solving the issue (not only securityrequirement, but also its scopes).

I know it's been a long time since this question was asked.

But I was facing a similar issue, and following the advice from an issue in GitHub here, managed to resolve it using this implementation of IOperationFilter (and now works like a charm):

    public class AuthorizeCheckOperationFilter : IOperationFilter
    {
        private readonly EndpointDataSource _endpointDataSource;

        public AuthorizeCheckOperationFilter(EndpointDataSource endpointDataSource)
        {
            _endpointDataSource = endpointDataSource;
        }


        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var descriptor = _endpointDataSource.Endpoints.FirstOrDefault(x =>
                x.Metadata.GetMetadata<ControllerActionDescriptor>() == context.ApiDescription.ActionDescriptor);
            
            var hasAuthorize = descriptor.Metadata.GetMetadata<AuthorizeAttribute>()!=null;

            var allowAnon = descriptor.Metadata.GetMetadata<AllowAnonymousAttribute>() != null;

            if (!hasAuthorize || allowAnon) return;

            operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
            operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });

            operation.Security = new List<OpenApiSecurityRequirement>
            {
                new()
                {
                    [
                        new OpenApiSecurityScheme {Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "oauth2"}
                        }
                    ] = new[] {"api1"}
                }
            };
        }
    }

The issue stated this:

ControllerActionDescriptor.EndpointMetadata only reflects the metadata discovered on the controller action. Any metadata configured via the endpoint APIs do not show up here. It was primarily the reason we documented it as being infrastructure-only since it's a bit confusing to use.

There's a couple of options you could use

a) You could decorate your controllers using [Authorize]. That should allow the metadata to show up in the property.

b) You could look up the metadata by reading from EndpointDataSource.

Related