0

Summary

What I want is to be able to add a custom AuthorizeAttribute to a method which is then recognised by swagger and displays the Available authorizations popup. The issue I'm having is getting the IOperationFilter to work correctly with the IAuthorizationFilter.

Code

startup.cs

        services.AddSwaggerGen(c =>
       {
           c.SwaggerDoc("v2", new OpenApiInfo { Title = "API", Version = "v2" });
           // Adds "(Auth)" to the summary so that you can see which endpoints have Authorization
           c.OperationFilter<AppendAuthorizeToSummaryOperationFilter<Filters.Authorization2>>();

           c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
           {
               Name = "Bearer",
               Description = "Standard Authorization header using the SessionKey scheme. Example: \"{token}\"",
               In = ParameterLocation.Header,
               Type = SecuritySchemeType.ApiKey
           });

           c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();
       });

controller.cs

    [Filters.Authorization2]
    [HttpGet]
    public ApiResult<List<Request>> GetRequest(int ID)

Authorization2.cs

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class Authorization2 : AuthorizeAttribute, IAuthorizationFilter
{
    public Authorization2()
    {

    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //Get the session from the cache
        Session sess = GetSession(context.HttpContext);
        if (sess.IsValid)
        {
            //set sess
        }
        else
        {
            //If it's not there then return with bad news
            context.Result = Unauthorized();
            context.HttpContext.Response.StatusCode = 401;
        }
    }
}

AuthorizeCheckOperationFilter.cs

internal class AuthorizeCheckOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        context.ApiDescription.TryGetMethodInfo(out var methodInfo);

        if (methodInfo == null)
            return;

        var hasAuthorizeAttribute = false;

        if (methodInfo.MemberType == MemberTypes.Method)
        {
            // NOTE: Check the controller itself has Authorize attribute
            hasAuthorizeAttribute = methodInfo.DeclaringType.GetCustomAttributes(true).OfType<Authorization2>().Any();

            // NOTE: Controller has Authorize attribute, so check the endpoint itself.
            //       Take into account the allow anonymous attribute
            if (hasAuthorizeAttribute)
                hasAuthorizeAttribute = !methodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any();
            else
                hasAuthorizeAttribute = methodInfo.GetCustomAttributes(true).OfType<Authorization2>().Any();
        }

        if (!hasAuthorizeAttribute)
            return;

        if (!operation.Responses.Any(r => r.Key == StatusCodes.Status401Unauthorized.ToString()))
            operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse { Description = "Unauthorized" });
        if (!operation.Responses.Any(r => r.Key == StatusCodes.Status403Forbidden.ToString()))
            operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse { Description = "Forbidden" });

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

With the code as it is, the padlock on the end point shows up and the header is set but the method throws an 500 error:

System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.

Attempt 1

I tried adding:

services.AddAuthentication(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);

but I get the same error. If I try:

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
                    options =>
                    {
                        options.LoginPath = new PathString("/auth/login");
                        options.AccessDeniedPath = new PathString("/auth/denied");
                    });

My request redirects to the loginPath, which causes a 404.

Attempt 2

If try a different tacktick and use a TypeFilterAttribute as so:

modify startup.cs

c.AddSecurityRequirement(new OpenApiSecurityRequirement()
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            },
                            Scheme = "oauth2",
                            Name = "Bearer",
                            In = ParameterLocation.Header,

                        },
                        new List<string>()
                    }
                });
//c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();

Authorization.cs

public class AuthorizationHandler : IAuthorizationFilter
{
    public AuthorizationHandler()
    {

    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //Get the session from the cache
        Session sess = GetSession(context.HttpContext);
        if (sess.IsValid)
        {
            //set sess
        }
        else
        {
            //If it's not there then return with bad news
            context.Result = Unauthorized();
            context.HttpContext.Response.StatusCode = 401;
        }
    }
}

public class AuthorizeAttribute : TypeFilterAttribute 
{
    public AuthorizeAttribute() : base(typeof(AuthorizationHandler))
    {

    }
}

and update the method to use [Filters.Authorize] the call to the method works as expected but now every method get's the lock, not just the ones with the attribute.

Question

How do I modify my code to only have locks on the methods with the attribute and process the authorization correctly?

Ben Close
  • 414
  • 1
  • 7
  • 18

1 Answers1

0

After following this response to a different question:

https://stackoverflow.com/a/61365691/2885471

I managed to get it to work. In essence I created a security definition in the SwaggerGen service configuration, put the security requirement in a IOperationFilter class and then add the the class to the OperationFilters.

Ben Close
  • 414
  • 1
  • 7
  • 18