Search code examples
c#attributescustom-attributes

Attributes and custom attributes - how does authorize work?


I'm trying to emulate the [Authorize] attribute because I find it amazing that you can just stick an attribute on top of an entire class and it somehow does all this wizardy to prevent people running the methods in the class unless... something...

I looked up creating custom attributes and found a few answers that basically say it's not possible for an attribute to prevent a method being called unless we use reflection, so then I decided to dig into the Authorize attribute but can only find the "interface" methods on a metadata file so basically

What exactly is the [Authorize] attribute doing and how can I implement attributes to actually do things using reflection? E.g. the following does nothing when attributed to a class:

[System.AttributeUsage(System.AttributeTargets.Class)]
public class Authorise : System.Attribute
{
    public Authorise()
    {
        if (!SomeBoolCondition) throw new Exception ("Oh no!");
    }
}

I can't get my head round how the Authorize attribute does checks and then redirects the program to a sign in page.


Solution

  • If you want to implement your own authorization logic with a custom attribute, you'll need create and register middleware in the request pipeline as well. Your middleware will receive the entire HttpContext, and you can use that to check your endpoint for the CustomAuthorizeAttribute via it's meta data. From there you can implement the authorization logic and decide to continue processing the request in the pipeline with "await next.Invoke()", or stop processing and return an unauthorized response to the client.

    Attribute class:

    [AttributeUsage(AttributeTargets.Class)]
    public class CustomAuthorizeAttribute : Attribute
    {
        public IEnumerable<string> AllowedUserRoles { get; private set; }
    
        public CustomAuthorizeAttribute(params string[] allowedUserRoles)
        {
            this.AllowedUserRoles = allowedUserRoles.AsEnumerable();
        }
    }
    

    Controller with custom attribute:

    [ApiController]
    [Route("[controller]")]
    [CustomAuthorize("Admin", "Supervisor", "Worker")]
    public class WeatherForecastController : ControllerBase
    {
    }
    

    Startup.Configure with custom middleware:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseRouting();
    
        app.Use(async (httpContext, next) =>
        {
            var endpointMetaData = httpContext.GetEndpoint()
                .Metadata;
    
            bool hasCustomAuthorizeAttribute = endpointMetaData.Any(x => x is CustomAuthorizeAttribute);
    
            if (hasCustomAuthorizeAttribute)
            {
                // get the endpoint's instance of CustomAuthorizeAttribute
                CustomAuthorizeAttribute customAuthorieAttribute = endpointMetaData
                    .FirstOrDefault(x => x is CustomAuthorizeAttribute) as CustomAuthorizeAttribute;
    
                // here you will have access to customAuthorizeAttribute.AllowedUserRoles
                // and can execute your custom logic with it
                bool isAuthorized = true;
    
                if (isAuthorized)
                {
                    // continue processing the request
                    await next.Invoke();
                }
                else
                {
                    // stop processing request and return unauthorized response
                    httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
                    await httpContext.Response.WriteAsync("Unauthorized");
                }
            }
        });
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }