Why you should not use Authorize attributes to protect your API endpoints
Protecting ASP.NET Core WebAPI without using Authorize attribute
Authentication is an essential component of pretty much any REST API. Most of the resources you expose through REST API services, unless they are protected inside intranet, need to be protected by some kind of authentication.
This is pretty much clear and it is easy achieved by just simply decorating your controller actions or even whole controllers with Authorize or AllowAnonymous attributes.
[Route("api/[controller]")] [ApiController] [Authorize] public class SampleController : ControllerBase { [HttpPost] [Authorize(Policy = "writepolicy")] public async Task<IActionResult> PostAsync([FromBody]SampleModel model) { await Task.CompletedTask; return Ok(); } [HttpGet("{id}")] [AllowAnonymous] public async Task<ActionResult<SampleModel>> GetAsyncPublic(Guid id) { await Task.CompletedTask; return Ok(); } [HttpGet] [Authorize(Policy = "readpolicy")] public async Task<ActionResult<IEnumerable<SampleModel>>> GetAsyncProtected() { await Task.CompletedTask; return Ok(); } }
This tells the MVC framework that you want some actions to be exposed publically and for some you need to authenticate prior you make the call and request the resource. This is pretty much straight forward. If you rely on OAuth, you can easuly add policies and nail down the authorization by simply registering policies based on claims in the Startup class.
public void ConfigureServices(IServiceCollection services) { services.AddAuthorization(options => { options.AddPolicy("readpolicy", policy => policy.RequireClaim("scope", "read")); options.AddPolicy("writepolicy", policy => policy.RequireClaim("scope", "write")); }); }
This all looks pretty fine and most important above all is that it works. The question is how maintainable it is. Here are few scenarios you might get in trouble by deciding to use Authorize attributes for protecting the REST API endpoints.
Authorization is scattered in your code
Since your authorization is declared on the controller or action level, each controller has to be updated in order to change the authorization. This might not look as a big deal if you have just few controllers, by as your application grows, you might end up with authentication which configuration is hard to maintain and change.
One of the simple reasons you may not want to use the authorize attributes is your development. You simply do not need to create and overhead and spend additional time to do localhost development for action where user authenticated context is not required.
Switching off authentication for localhost
Although authentication is realy important, you do not rely need it on your localhost to test the functionality before you push your changes to production or test environment. In most of the cases endpoints do not need authenticated user to perform an action so acquiring the token just to quickly test the endpoint response may be annoying while developing on your development machine.
The easiest way to strip down the authentication from the action or whole controller is to just comment out the Authorize attribute.
Although this looks quite easy and straight forward, you can easily miss to strip comment slashes from the authorize attribute and eventually this controller or action ends op in test or even worse production environment without any security restriction and you may end up with data leaks or even worse problems.
The way to fix this is to simply configure your filter attributes in the Startup class.
public void ConfigureServices(IServiceCollection services) { services.AddAuthorization(options => { options.AddPolicy("readpolicy", policy => policy.RequireClaim("scope", "read")); options.AddPolicy("writepolicy", policy => policy.RequireRole("scope", "write")); }); services.AddMvc(options => { if (!this.HostingEnvironment.IsDevelopment() && !this.HostingEnvironment.IsEnvironment("localhost")) { var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); options.Filters.Add(new AuthorizeFilter(policy)); } }); }
Although this will work there are some additional things you have to take into consideration and that is flexibility of putting some actions to be accessible publicly, while keeping others secured as well as requiring different policies for different actions.
All this can be achieved by implementing your own logic for IControllerModelConvention and IActionModelConvention interfaces.
Applying different conventions for the Controllers and Controller Actions
OK, we kinda have a picture why we should not use authorize attributes for securing out REST API endpoints, but how do we deal with variety of different rules of what should be authenticated and what not.
For this purpose we use conventions for our controllers and controller actions. First thing we need to do is to write our implementation of these two interface where we declare how do we want to apply the filter attributes to our controllers and actions. Let's try to represent the authentication configuration for the controller from the first snippet, but this time we do it from the Startup class by adding our convention implementations.
public class AthorizationControllerConvention : IControllerModelConvention { public void Apply(ControllerModel controller) { controller.Filters.Add(new AuthorizeFilter()); } } public class AthorizationActionConvention : IActionModelConvention { public void Apply(ActionModel action) { //Require specific claims for mutable actions if (action.Attributes.Any(a => a is HttpPostAttribute || a is HttpPutAttribute || a is HttpDeleteAttribute)) { action.Filters.Add(new AuthorizeFilter(policy:"writepolicy")); } else if (action.Attributes.Any(a => a is HttpGetAttribute)) { //Expose publicaly GET with the parameter if (action.Parameters.Any()) { action.Filters.Add(new AllowAnonymousFilter()); } //GET actions with no paremeters are protected else { action.Filters.Add(new AuthorizeFilter(policy:"readpolicy")); } } } }
For all the controllers we apply the AuthorizeFilter, but for controller actions we selectively add attributes with different policies so that we achieve same functionality as we are doing with attributes in the sample controller.
Once we have our conventions implemented all that is left is to add them to MVC options when registering services in the startup.
public void ConfigureServices(IServiceCollection services) { services.AddAuthorization(options => { options.AddPolicy("readpolicy", policy => policy.RequireClaim("scope", "read")); options.AddPolicy("writepolicy", policy => policy.RequireRole("scope", "write")); }); services.AddMvc(options => { if (!this.HostingEnvironment.IsDevelopment() && !this.HostingEnvironment.IsEnvironment("localhost")) { options.Conventions.Add(new AthorizationControllerConvention()); options.Conventions.Add(new AthorizationActionConvention()); } }); }
Now we are ready do debug our controller actions without any need for authentication on the localhost without any worry of deploying the wrong code and at the same time we can control our authentication configuration for all controllers and actions from a single place.
References
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article