The authorization model in ASP.NET Core got a big overhaul with the introduction of policy-based authorization. Authorization now uses requirements and handlers, which are decoupled from your controllers and loosely coupled to your data models. The result is a more modular, more testable authorization framework that fits into the modern ASP.NET Core approach nicely.
It’s still possible to do the role-based authorization most ASP.NET devs are familiar with, but that’s only the tip of the iceberg! In this post, I’ll walk you through some of the awesome new features, and how you can combine them with Stormpath for powerful, scalable authorization!
Just getting started with authentication in ASP.NET Core? Check out our tutorial 10 Minutes to Authentication in ASP.NET Core!
Role-Based Authorization in ASP.NET Core
If you’re familiar with roles in ASP.NET 4.x, you’ll find that the new features start from a familiar place. Specifically, a user can have several roles and you define what roles are required to perform a certain action, or access to specific sections or resources, within your application. You can specify what roles are authorized to access to a specific resource by using the
[Authorize]
attribute. It can be declared in such a way that the authorization could be evaluated at controller level, action level, or even at a global level.
Let’s take Slack as an example. (Slack is a real-time communication platform that was built to reinvent corporate communication. Our team is obsessed!) With Slack, users can chat, call share and send files, and also create and join both public and private channels.
Imagine you’re building a Slack clone for your company. You could have a
ChannelAdministrationController
to manage channels, which is restricted to users that have either a SuperAdministrator
or ChannelAdministrator
role. Any other user that attempts to invoke any action of the controller will be unauthorized and the action will be not invoked.
This familiar syntax still works in ASP.NET Core, as I mentioned above. It was maintained by the ASP.NET Core team for backward compatibility, but the real improvement comes with the new policy-based model. If you’re ready to try the new hotness, it’s pretty easy to refactor your code and express your role requirement using the new model!
Policy-Based Authorization in ASP.NET Core
The policy-based model consists of three main concepts: policies, requirements and handlers.
- A policy is composed by one or more requirements
- A requirement is a collection of data parameters used by the policy to evaluate the user Identity
- A handler is the responsible of evaluating the properties of the requirements to determine if the user is authorized to access to a specific resource
Let’s talk about the policy-based model here for a moment. If you were to express the previous example in a policy-based format, you would follow these steps.
First, you have to register your policy in the
ConfigureServices
method of the Startup
class, as part of the authorization service configuration.
Then, instead of specifying roles in the
[Authorize]
attribute, specify the policy you want to assert:
And that’s all!
As you can see, the name of the policy is
RequireElevatedRights
and any user with either “SuperAdministrator” or “ChannelAdministrator” role will be authorized to invoke any action of the ChannelAdministrationController
. This accomplishes the same thing (requiring a particular role to access the controller’s actions), but now the configuration is decoupled from the controller itself.
You didn’t have to write any requirements or handlers, but the
RequireRole
method uses them under the hood. If you’re curious, check out the ASP.NET Core Security module source code to see how this is implemented.Claims-Based Authorization via Policies
Role-based authorization in ASP.NET Core is simple, but limited. Imagine you want to validate a user based on other characteristics such as date of birth or employee number? Of course, creating a role for each of these possibilities is clearly not a solution.
ASP.NET Core bases the identity of a user on claims. A claim is a key-value pair which represents characteristics of a subject, such as, name, age, passport number, nationality, company, etc; and a subject can have multiple of these. A claim is issued by a trusted party and it tells you about who the subject is and not what a subject can do.
Referring back to the Slack example, let’s say there is a channel called Employees. You’d want this channel to be only accessible for those users that have an employee ID, and not accessible to guests or freelancers.
To do this, you’d have to register a new policy in the
ConfigureServices
method of the Startup
class, as part of the authorization service configuration:
In this case, the
EmployeesOnly
policy checks if the subject has an employee ID claim. You can restrict access to a controller by requiring this policy:
If you decide to refactor your code or infrastructure and need to update how the policy works “under the hood”, you can simply edit the policy definition instead of modifying each controller that uses the policy.
Complex Authorization with Custom Policies
Now, you are ready to solve for even more complicated scenarios! If your authorization needs don’t fit into a simple role- or claims-based approach, you can build your own authorization requirements and handlers that work with the policy model.
Let’s suppose you have a Happy Hour channel for employees to discuss their favorite beers. You might want to require employees to be over 21 to access the channel due to the drinking laws in some countries.
Now, suppose you have a date of birth claim, you can use this info to define an “Over21Only” policy. To do this, you have to create a “MinimumAgeRequirement” and the handler with the logic to validate if the employee is meeting the minimum age requirement.
Any requirement must implement the empty marker
In the case of this requirement, the age has to be injected in the constructor.
IAuthorizationRequirement
interface.In the case of this requirement, the age has to be injected in the constructor.
The
MinimumAgeRequirement
class acts as a “model” for the requirement, but it does not actually contain the authorization logic. As I mention above, you’ll need a handler:
Any requirement must implement the empty marker
For this requirement, the age has to be injected in the constructor.
IAuthorizationRequirement
interface.For this requirement, the age has to be injected in the constructor.
The requirement class acts as a “model” for the requirement, but it does not actually contain the authorization logic. For that, you’ll to create a handler and implement the
HandleRequirementAsync
method.
The logic here is easy to read, the only way to succeed and authorize an employee is by evaluating that they have an
But you may ask, why didn’t it fail when it didn’t find the DateOfBirth claim? You may end up having multiple handlers for a requirement, and you’d want the requirement to succeed if any of the handlers succeeded.
For this reason, the typical pattern is to return from the handler without explicitly failing, unless you want to guarantee a failure regardless of any other handlers. Of course, if the handler succeeds, it should indicate success!
DateOfBirth
claim and that it meets the minimum age required.But you may ask, why didn’t it fail when it didn’t find the DateOfBirth claim? You may end up having multiple handlers for a requirement, and you’d want the requirement to succeed if any of the handlers succeeded.
For this reason, the typical pattern is to return from the handler without explicitly failing, unless you want to guarantee a failure regardless of any other handlers. Of course, if the handler succeeds, it should indicate success!
The next step is to register your policy in the Authorization service configuration in the
Also, you have to register the handler to be injected later on by the framework:
ConfigureServices
method of the Startup
class.Also, you have to register the handler to be injected later on by the framework:
Finally, you can add this policy in any action or resource that needs to be restricted by this requirement:
This approach is better than the role-based approach because the security code is self-documented and you can rapidly check what a policy implies.
It’s also more flexible, as you can change easily what is the minimum age required because the logic is encapsulated in a single place. You can go further, and make a separated library with your company requirements and reuse it in all the applications of the company. Also, you can write your unit tests for your different handlers. Isn’t it awesome?!
Note: Check out the unit tests written by the ASP.NET Core Team.
Authorization in ASP.NET Core with Stormpath
Now, let’s look at how easy is to use Stormpath with the policy-based approach. The Stormpath ASP.NET Core library provides two ways to easily enforce authorization in your application: group- (or role-) based access control, and permissions-based access control.
To easily configure Stormpath in your ASP.NET Core project check out the quickstart documentation.
Use Stormpath Groups to Model Authorization Roles
If you need to organize your users by Role or Group, Stormpath has role-based access control built in. User accounts can belong to one or many groups, each of which can carry its own set of permissions.
With Stormpath, you can create nested or hierarchical groups, model organizational functions, or implement best-practice resource-based access control.
Stormpath Setup
The Stormpath ASP.NET Core Quickstart shows how to create an API key; here’s the abridged version:
From the Home tab of the Admin Console select Manage API Keys under the Developer Tools heading. Click the Create API Key button to trigger a download of a apiKey-{API_KEY}.properties file. Open the file in Notepad.
Using the Command Prompt or Powershell, run these commands:
Let’s re-write the RequireElevatedRights policy using Stormpath.
Log in to the Admin Console and click on the “Groups” tab. Then, click on “Create Group” and complete the form to create both SuperAdministrator and ChannelAdministrator groups.
Once you click on “Create” the group is ready to use. Any account assigned to these groups will have their specific permissions.
Now that you have all set, declare the RequireElevatedRights policy in the
ConfigureServices
method in the Startup
class as shown above.
Finally, add the
Authorize
attribute in the ChannelAdministrationController
:
As you can see, creating groups, assigning accounts to groups, and authorizing resources based on these is super easy!
Use Stormpath Custom Data to Model Fine-Grained Authorization
Stormpath allows you to add your own custom attributes to your accounts without the need of having a separate database. With Stormpath Custom Data you have a lot of flexibility as you are able to store more than just basic user credentials. Also, thanks to the Stormpath .NET API you can do many operations easily, such as searching accounts by custom data or creating/updating custom attributes.
If you’re interested in learning more about this awesome feature check out the Stormpath Custom Data documentation.
Custom data is not only useful to store additional info to the user accounts of your application; it also can be used to combine user claims with policies to implement fine-grained authorization.
Let’s rewrite the
EmployeesOnly
example with Stormpath! You could have a “Company” property in the user’s custom data, and validate if it matches with your company name.
To do this, add the “EmployeesOnly” policy by using the
StormpathCustomDataRequirement
in the ConfigureServices
method:
And that’s all! As you can see, using custom data combined with policy-based authorization is super easy thanks to the Stormpath ASP.NET Core library. With a few line of code you have added a new policy that handles authorization based on the user custom data.
Learn More About User Management, Including Authentication and Authorization, in ASP.NET Core
With ASP.NET Core and Stormpath you can model your security with a considerable number of benefits. Policy-Based Authorization allows you to write more flexible, reusable, self-documented, unit-testable, and encapsulated code. Stormpath is ready to work with this approach in a super clean and elegant way.
To learn more about Stormpath and ASP.NET Core check out these resources:
No comments:
Post a Comment