Web sites are unfortunately prone to security risks. And so are any networks to which web servers are connected. Setting aside risks created by employee use or misuse of network resources, your web server and the site it hosts present your most serious sources of security risk.
ASP.NET is an open-source server-sideweb application framework designed for web development to produce dynamic web pages. It was developed by Microsoft to allow programmers to build dynamic web sites, web applications and web services.
CheapASPNETHostingreview.com | Cheap and reliable ASP.NET hosting. To make things worse, ASP.NET and MVC traditionally had not much more built-in to offer than boring role checks. This lead to either unmaintainable code (hard coded role names and Authorize attributes) or complete custom implementations – or both.
In ASP.NET 5, a brand new authorization API is supposed to improve that situation – and IMHO – oh yes it does. Let’s have a look.
Overview
ASP.NET 5 supports two styles of authorization out of the box – policy-based and resource-based. Both styles are a substantial improvement over the current ASP.NET authorization features and reduce the need to write your own authorization attribute/filter/infrastructure – though this is still totally possible.
The new Authorize Attribute
My main gripe with the old attribute is that it pushes developers towards hard-coding roles (or even worse – names) into their controller code. It violates separation of concerns and leads to hard to maintain code with roles names sprinkled all over your code base.
Also – let’s face it – declarative, role-based security might be nice for demos but is nowhere near flexible enough to write anything but trivial applications.
The new Authorize attribute can still do role checks like this :
1 2 3 | [Authorize(Roles = "Sales")] public IActionResult DoSalesyStuff() { /* .. */ } |
But this is mainly for backwards compatibility (the ability to check for names is gone). The more recommended pattern is to use so called authorization policies instead:
1 2 3 | [Authorize("SalesOnly")] public IActionResult DoSalesyStuff() { /* .. */ } |
Policies & Requirements
Policies are a way to create re-usable authorization logic. Policies consist of one or more so called requirements. If all requirements of a policy are met, the authorization check is successful – otherwise it fails.
Policies are created using a policy builder, and the following snippet creates a very simple policy (aka “require authenticated users”) and sets that globally in MVC :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class Startup { public void ConfigureServices(IServiceCollection services) { // only allow authenticated users var defaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); services.AddMvc(setup => { setup.Filters.Add(new AuthorizeFilter(defaultPolicy)); }); } } |
There are more extension methods similar to RequireAuthenticatedUser – e.g. RequireClaim or RequireRole.
Another common use case are named policies which can be referenced like the above SalesOnly example (again in Startup.ConfigureServices):
1 2 3 4 5 6 7 8 9 10 11 12 13 | services.AddAuthorization(options => { // inline policies options.AddPolicy("SalesOnly", policy => { policy.RequireClaim("department", "sales"); }); options.AddPolicy("SalesSenior", policy => { policy.RequireClaim("department", "sales"); policy.RequireClaim("status", "senior"); }); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class StatusRequirement : AuthorizationHandler<StatusRequirement>, IAuthorizationRequirement { private readonly string _status; private readonly string _department; public StatusRequirement(string department, string status, bool isSupervisorAllowed = true) { _department = department; _status = status; } protected override void Handle(AuthorizationContext context, StatusRequirement requirement) { if (context.User.IsInRole("supervisor")) { context.Succeed(requirement); return; } if (context.User.HasClaim("department", _department) && context.User.HasClaim("status", _status)) { context.Succeed(requirement); } } } |
1 2 3 4 5 6 7 | options.AddPolicy("DevInterns", policy => { policy.AddRequirements(new StatusRequirement("development", "intern")); // ..or using an extension method //policy.RequireStatus("development", "intern"); }); |
Under the covers, the AddAuthorization extension method also puts an IAuthorizationService (or more specifically the DefaultAuthorizationService) into the DI container. This class can be used to programmatically evaluate policies (amongst other things – more on that later).
To make the authorization service available – simply add it to e.g. a controller constructor:
1 2 3 4 5 6 7 8 9 | public class TestController : Controller { private readonly IAuthorizationService _authz; public TestController(IAuthorizationService authz) { _authz = authz; } } |
1 2 3 4 5 6 7 8 | public async Task<IActionResult> SalesSeniorImperative() { if (await _authz.AuthorizeAsync(User, "SalesSenior")) { return View("success"); } return new ChallengeResult(); } |
Remark ChallengeResult can be used to trigger an “access denied” condition in MVC. The cookie middleware e.g. will translate that either into a redirect to a login page for anonymous users, or a redirect to an access denied page for authenticated users.
Remark 2 Since views in MVC 6 also support DI, you can inject the authorization service there as well. Some people like this approach to conditionally render UI elements.
1 2 3 4 5 6 7 8 9 10 11 12 13 | @{ ViewData["Title"] = "Home Page"; @using Microsoft.AspNet.Authorization @inject IAuthorizationService _authz } @if (await _authz.AuthorizeAsync(User, "SalesOnly")) { <div> <a href="test/salesOnly">Sales only</a> </div> } |
This is a nice way to centralize authorization policies and re-use them throughout the application.
The only thing I don’t like about this approach is, that it pushes you towards using the claims collection as the sole data source for authorization decisions. As we all know, claims describe the identity of the user, and are not a general purpose dumping ground for all sorts of data – e.g. permissions.
It would be nice if one could use the DI system of ASP.NET to make further data source accessible in custom requirement. I’ve opened an issue for that – we’ll see what happens.
Resource-based Authorization
This is a new approach for ASP.NET and is inspired by the resource/action based approach that we had in WIF before (which was ultimately inspired by XACML). We also like that approach a lot, but the problem with the WIF implementation (and also ours) was always that due to the lack of strong typing, the implementation became messy quickly (or at least you needed a certain amount of discipline to keep it clean over multiple iterations).
The idea is simple – you identify resources that your application is dealing with – e.g. customers, orders, products (yawn). Then you write a so called handler for each of these resources where you express the authorization requirements in code.
You use requirements to express whatever action is supposed to be applied to the resource, and conclude with a success/failure, e.g.:
1 2 3 4 5 6 7 8 9 10 | public class CustomerAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Customer> { protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, Customer resource) { // implement authorization policy for customer resource } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public static class CustomerOperations { public static OperationAuthorizationRequirement Manage = new OperationAuthorizationRequirement { Name = "Manage" }; public static OperationAuthorizationRequirement SendMail = new OperationAuthorizationRequirement { Name = "SendMail" }; public static OperationAuthorizationRequirement GiveDiscount(int amount) { return new DiscountOperationAuthorizationRequirement { Name = "GiveDiscount", Amount = amount }; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public async Task<IActionResult> Discount(int amount) { var customer = new Customer { Name = "Acme Corp", Region = "south", Fortune500 = true }; if (await _authz.AuthorizeAsync(User, customer, CustomerOperations.GiveDiscount(amount))) { return View("success"); } return new ChallengeResult(); } |
What I like about this approach is that the authorization policy has full strong typed access to the domain object it implements authorization for, as well as the principal. This is a huge improvement over the WIF API. It also makes it easy to unit test your controller without the authorization code – and even more importantly (at least for security guy) – it allows unit testing the authorization policy itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | protected override void Handle(AuthorizationContext context, OperationAuthorizationRequirement requirement, Customer resource) { // user must be in sales if (!context.User.HasClaim("department", "sales")) { context.Fail(); return; } // ...and responsible for customer region if (!context.User.HasClaim("region", resource.Region)) { context.Fail(); return; } // if customer is fortune 500 - sales rep must be senior if (resource.Fortune500) { if (!context.User.HasClaim("status", "senior")) { context.Fail(); return; } } if (requirement.Name == "GiveDiscount") { HandleDiscountOperation(context, requirement, resource); return; } context.Succeed(requirement); } |
Imagine a permission service that queries your authorization using some backing store (the _permissions variable):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | private void HandleDiscountOperation( AuthorizationContext context, OperationAuthorizationRequirement requirement, Customer resource) { var discountOperation = requirement as DiscountOperationAuthorizationRequirement; var salesRep = context.User.FindFirst("sub").Value; var result = _permissions.IsDiscountAllowed( salesRep, resource.Id, discountOperation.Amount); if (result) { context.Succeed(requirement); } else { context.Fail(); } } |
All in all – I am very pleased with this new API and it will be interesting to see how it performs in a real application. Happy coding 🙂