We've been talking about separation of concerns of authentication and authorization quite a bit in the past (see the blog post that started all and the video that showed off our first prototype). As a result, we have developed a commercial product called PolicyServer.
This repository contains a free, open source, and simplified version of the PolicyServer product, but has all the necessary code to implement the authorization pattern we are recommending.
This open source library does not have the advanced features of the PolicyServer product like hierarchical policies, client/server separation, management APIs and UI, caching, auditing etc., but is syntax-compatible with its "big brother". This allows an upgrade path with minimal code changes.
The authorization policy is defined as a JSON document (typically in appsettings.json
). In the policy you can define two things
- application roles (and the users that are members of these roles)
- application permissions (and which application roles have these permissions)
Role membership can be defined based on the IDs (aka subject IDs) of the users, e.g.:
{
"Policy": {
"roles": [
{
"name": "doctor",
"subjects": [ "1", "2" ]
}
]
}
}
The value of the user's sub
claim is used to determine role membership at runtime.
Additionally identity roles coming from the authentication system can be mapped to application roles, e.g.:
{
"Policy": {
"roles": [
{
"name": "patient",
"identityRoles": [ "customer" ]
}
]
}
}
The user's role
claims are used to map identity roles to application roles.
In the permissions element you can define permissions, and which roles they are mapped to:
{
"Policy": {
"roles": [
{
"name": "doctor",
"subjects": [ "1", "2" ],
"identityRoles": [ "surgeon" ]
},
{
"name": "nurse",
"subjects": [ "11", "12" ],
"identityRoles": [ "RN" ]
},
{
"name": "patient",
"identityRoles": [ "customer" ]
}
],
"permissions": [
{
"name": "SeePatients",
"roles": [ "doctor", "nurse" ]
},
{
"name": "PerformSurgery",
"roles": [ "doctor" ]
},
{
"name": "PrescribeMedication",
"roles": [ "doctor", "nurse" ]
},
{
"name": "RequestPainMedication",
"roles": [ "patient" ]
}
]
}
}
Fist, you need to register the PolicyServer client with the DI system. This is where you specify the configuration section that holds your policy.
services.AddPolicyServerClient(Configuration.GetSection("Policy"));
After that you can inject the IPolicyServerClient
anywhere into your application code, e.g.:
public class HomeController : Controller
{
private readonly IPolicyServerClient _client;
public HomeController(IPolicyServerClient client)
{
_client = client;
}
}
PolicyServerClient
has three methods:
EvaluateAsync
- returns application roles and permissions for a given user.IsInRoleAsync
- queries for a specific application roleHasPermissionAsync
- queries for a specific permission
public async Task<IActionResult> Secure()
{
// get roles and permission for current user
var result = await _client.EvaluateAsync(User);
var roles = result.Roles;
var permissions = result.Permissions;
// check for doctor application role
var isDoctor = await _client.IsInRoleAsync(User, "doctor");
// check for PrescribeMedication permission
var canPrescribeMedication = await _client.HasPermissionAsync(User, "PrescribeMedication");
// rest omitted
}
You can now use this simple client library directly, or build higher level abstractions for your applications.
Instead of using the PolicyServerClient
class directly, you might prefer a programming model where the current user's claims are automatically populated with the policy's application roles and permissions. This is mainly useful if you want to use the standard ClaimsPrincipal
-based APIs or the [Authorize(Roles = "...")]
attribute.
A middleware (registered with UsePolicyServerClaimsTransformation
) is provided for this purpose, and runs on every request to map the user's authorization data into claims:
public void Configure(IApplicationBuilder app)
{
app.UseAuthentication();
app.UsePolicyServerClaimsTransformation();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
Then in the application code you could query application roles or permissions like this:
// get all roles
var roles = User.FindAll("role");
// or
var isDoctor = User.HasClaim("role", "doctor");
Another option is to automatically map permissions of a user to ASP.NET Core authorization policies.
This way you can use the standard ASP.NET Core authorization APIs or the [Authorize("permissionName")]
syntax.
To enable that, you need to register a custom authorization policy provider when adding the PolicyServer client library:
services.AddPolicyServerClient(Configuration.GetSection("Policy"))
.AddAuthorizationPermissionPolicies();
This will allow you to decorate controllers/actions like this:
[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
// omitted
}