We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Role-based and Claims-based Authorization in ASP.NET Core using Policies - Hands on

ASP.NET Core JWT Authorization  • Posted 28 days ago

So far we have seen why Token based Authentication using JWT is an easy and elegant way of securing API endpoints against unauthorized or unwanted access when exposed to the Internet and how Authentication and Authorization differ from each other.

We have also looked at how we can implement a simple authentication mechanism using JWT Bearer tokens in ASP.NET Core followed by creating user specific Policies to customize the Authorization process.

In this article let's talk about two important scenarios in which an authenticated user be authorized for API access:

  1. Based on a Role defined by the system and
  2. Based on a Claim value inside the user token

While these two may seem to be of different in their functionalities and usage cases, they both branch out from the Policy based authorization that the ASP.NET Core provides us with.

Role-based Authorizing ASP.NET Core APIs using Policies:

A Role can be assumed as a designation or an earmarking assigned to a specific user which serves a responsibility. Typical examples for Roles include:

  • an admin
  • an author
  • an editor or
  • a librarian

These roles provide certain levels of access restriction and abstraction with them, such as:

  • "not all users are granted access to book keeping system except the librarian"
  • "not all users can edit posts except the editors" and
  • "not all editors can delete the post schema except the admin".

These are differentiated in real-world at the authentication levels, by providing with separate logins or separate portals. We can also restrict access levels for such endpoints by passing in extra attributes for the earmarked users. In the world of token based authentication and authorization systems, it is made further simple by making use of the ClaimType attributes provided within the ClaimsIdentity libraries. We can then configure the Authorization middleware to look for Role attributes available in the token to allow access to the decorated endpoints.

For example, let's consider the previous example of a Policy based Authorization, where we have restricted access permissions to the GET AllReaders endpoint for only those users who are also Readers. While this indirectly implements a Role-based authorization, if we closely observe, there's another simpler approach to this: the Role attribute which is present for every User.

We can leverage the role-based access by means of this User property by passing it along with the token claims. We can then check for the Role attribute and verify whether it belongs to only that particular Role it is meant for - the Reader in this case. We can then permit access to requests which satisfy this requirement while all other requests are Forbidden (403) from access.

Note: Starting from ASP.NET Core 3.x, we are required to add the below NuGet packages to access the JwtBearerDefaults and Token generation libraries. We begin by adding the below packages to the project where the auth token is generated and where the Authentication middleware is created.

> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
> dotnet add package System.IdentityModel.Tokens.Jwt

Example:

// The Roles expects a comma separated list of Role values
[Authorize(Roles = "Admin,Editor")]
[Route("all")]
[HttpGet]
public List<Reader> Get()
{
    return ReaderStore.Readers;
}

While generating an auth Token for the validated user, we add an additional Claim called "Role" which indicates the Role for the validated user as below:

public class TokenManager : ITokenManager
{
    public AuthToken Generate(User user)
    {
        List<Claim> claims = new List<Claim>() {
            new Claim (JwtRegisteredClaimNames.Jti, 
                Guid.NewGuid().ToString()),
            
            new Claim (JwtRegisteredClaimNames.Email, 
                user.EmailAddress),
            
            new Claim (JwtRegisteredClaimNames.Sub, 
                user.Id.ToString()),
		    
            // Add the ClaimType Role which carries the Role of the user
            new Claim (ClaimTypes.Role, user.Role)
        };

        JwtSecurityToken token = new TokenBuilder()
            .AddAudience(TokenConstants.Audience)
            .AddIssuer(TokenConstants.Issuer)
            .AddExpiry(TokenConstants.ExpiryInMinutes)
            .AddKey(TokenConstants.key)
            .AddClaims(claims)
            .Build();

        string accessToken = new JwtSecurityTokenHandler()
            .WriteToken(token);

        return new AuthToken() 
        {
            AccessToken = accessToken,
            ExpiresIn = TokenConstants.ExpiryInMinutes
        };
    }
}

Alternative approach - Creating Policy for Custom Roles:

This can also be implemented by creating a policy and adding an AuthorizationHandler which inspects the token for Role attribute and checks whether the attribute satisfies a given role. Inside the Startup.cs we can register a new Policy for the same as below:

services.AddAuthorization(config =>
{
    config.AddPolicy("ShouldBeAReader", options =>
    {
        options.RequireAuthenticatedUser();
        options.AuthenticationSchemes.Add(
                JwtBearerDefaults.AuthenticationScheme);
        options.Requirements.Add(new ShouldBeAReaderRequirement());
    });
		
	// Add a new Policy with requirement to check for Admin
    config.AddPolicy("ShouldBeAnAdmin", options =>
    {
        options.RequireAuthenticatedUser();
        options.AuthenticationSchemes.Add(
                JwtBearerDefaults.AuthenticationScheme);
        options.Requirements.Add(new ShouldBeAnAdminRequirement());
    });
});

Where the new Policy requirements ShouldBeAnAdminRequirement() and ShouldBeAReaderRequirement() are defined and handled with a custom AuthorizationHandler as below:

public class ShouldBeAnAdminRequirement
: IAuthorizationRequirement
{
}

public class ShouldBeAnAdminRequirementHandler
: AuthorizationHandler<ShouldBeAnAdminRequirement>
{
    
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ShouldBeAnAdminRequirement requirement)
    {
        // check if Role claim exists - Else Return
        // (sort of Claim-based requirement)
        if (!context.User.HasClaim(x => x.Type == ClaimTypes.Role))
            return Task.CompletedTask;

        // claim exists - retrieve the value
        var claim = context.User
            .Claims.FirstOrDefault(x => x.Type == ClaimTypes.Role);
        var role = claim.Value;

        // check if the claim equals to either Admin or Editor
        // if satisfied, set the requirement as success
        if (role == Roles.Admin || role == Roles.Editor)
            context.Succeed(requirement);

        return Task.CompletedTask;
    }
}

This new Policy is invoked whenever any Endpoint decorated with the Policy requirement name as:

[Authorize("ShouldBeAnAdmin")]
[Route("all")]
[HttpGet]
public List<Reader> Get()
{
    return ReaderStore.Readers;
}

Claims-based Authorizing ASP.NET Core APIs using Policies:

As we know that a typical JWT token contains three sections - Header, Payload and the Signature, the Payload section of the JWT Token contains a list of user attributes added when the token is generated. When the token is validated in the Authorization middleware, this payload section is parsed and transformed into a list of KeyValuePairs defined by the IdentityFramework - called as Claims.

A Claim is a KeyValue pair containing an arbitary user data, where the Key is a string and the Value is an object. There are a list of predefined Key names provided by the Identity Framework which are frequently used during token exchanges and are specified in the authentication schemes (such as OAuth or OpenId).

There can be scenarios where we would like to allow access to only those users who possess certain information with them and forbid others from access if that information isn't passed. For example, a librarian with a certain IdCardFlag only needs to be allowed access to the top-secret section of the library system (just saying).

In such scenarios, we configure the Authorization middleware to allow access to only those requests which contain such claims in the passed tokens. This mechanism is called as Claims-based Authorization.

This can also be implemented simply by defining a new Policy for inspecting tokens, or a shorter way is to use a method call during the Policy definition.

It can be simply added to the middleware as shown below:

services.AddAuthorization(config => {
	config.AddPolicy("ShouldContainRole", 
        options => options.RequireClaim(ClaimTypes.Role));
});

And is invoked in the same way as a Policy based solution as below:

[Authorize("ShouldContainRole")]
[Route("all")]
[HttpGet]
public List<Reader> Get()
{
    return ReaderStore.Readers;
}

In this way we can implement both Role-based and Claims-based Authorization where both the schemes internally use a policy based approach. The sample application used in the above illustration is available at: https://github.com/referbruv/role-policy-authorization-sample

asp.net core role claims claims based authorization role claims asp.net core claims-based authorization claims-based authorization in asp.net core role claims role-based authorization vs claims-based authorization claims based authorization sample asp.net claims based authentication asp.net identity roles vs claims