Post Cover

Implementing Policy based Authorization in ASP.NET Core (.NET 5)

ASP.NET Core JWT Authorization Posted Nov 08, 2021

In a previous article we discussed about securing ASP.NET Core Web APIs using JWT Bearer tokens. In this approach, the API verifies the incoming requests for any bearer token which is generated from a trusted and configured token source and allows access to only such requests. This approach secures the API from unwanted access.

While this approach is efficient, this approach also allows anyone with a token to access any API resource, which in some cases can cause authorization issues. For example, we can't design a system where a normal user can access an admin resource just because the user possesses a valid token.

Hence we can further improve this by authorizing the users who try to access by means of access tokens. In this article, we shall look at a policy-based approach in which all the authenticated users need to further comply to a defined policy in order to access the web api.

We shall see how we can create our own policy and set up rules which further gaurd the API for access. And we'll see how we can apply those policies over our existing API handler.

What is an Authorization?

Authorization is a concept of validating user previliges before allowing access, checking if the user is allowed to access the resource. While Authentication verifies a user's identity against a system, Authorization further enhances the security.

What is a Policy?

An authorization policy is a set of requirements and handlers. These requirements define what a request user need to satisfy inorder to proceed further. The respective handlers define how these requirements are processed when a request is made and what action needs to be presented if a rule is satisfied or failed. These requirements and handlers are registered in the Startup when the application bootstraps.

A Policy constitutes:

  1. A Requirement that defines some criterion for Authorization
  2. An AuthorizationHandler that validates the Requirement

Once a policy is defined and registered, the runtime applies these policies for validation at the endpoints where the policies are decorated with. When we have these policies in force, we can ensure that the APIs are further secured on top of Authentication and only the set of Authorized users who satisfy these policies are allowed access, else are forbidden (403) from access.

Creating a Requirement:

An Authorization Requirement is a class that implements the IAuthorizationRequest interface. Now this interface is empty and so we can have our own reasoning of how an AuthorizationRequest should behave, by means of any attributes within or a passed value.

public class ShouldBeAReaderRequirement 
    : IAuthorizationRequirement
{
    public ShouldBeAReaderRequirement()
    {
    }
}

Creating a Handler that Validates the Requirement:

An AuthorizationHandler picks up this AuthorizationRequest and validates the input request of this requirement and decides whether it can be allowed or not. It can look like this:

public class ShouldBeAReaderAuthorizationHandler 
    : AuthorizationHandler<ShouldBeAReaderRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, 
        ShouldBeAReaderRequirement requirement)
    {
        if (!context.User.HasClaim(x => x.Type == ClaimTypes.Email))
            return Task.CompletedTask;

        var emailAddress = context.User.Claims.FirstOrDefault(
                x => x.Type == ClaimTypes.Email).Value;

        // check if the datastore contains the emailAddress
        // of the incoming user context (from the token)
        if (ReaderStore.Readers.Any(
                x => x.EmailAddress == emailAddress))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

To get this setup working, let's just have a simple local data store where the Users and Readers are stored. This shall be the single point of source for the validation handler to check if the user satisfies the requirement set.

Reader store looks like below:

public class ReaderStore
{
    public static List<User> Users = new List<User>
    {
        new User {
            Id = 1,
            UserName = "Admin",
            EmailAddress = "reader1001@me.com",
            Role = Roles.Admin
        },
        new User {
            Id = 2,
            UserName = "Reader",
            EmailAddress = "reader1002@me.com",
            Role = Roles.Reader
        },
        new User {
            Id = 1,
            UserName = "Editor",
            EmailAddress = "reader1003@me.com",
            Role = Roles.Editor
        }
    };

    public static List<Reader> Readers => new List<Reader>() {
        new Reader {
            Id = 1003,
            EmailAddress = "reader1003@me.com",
            UserName = "reader1003"
        },
        new Reader {
            Id = 1002,
            EmailAddress = "reader1002@me.com",
            UserName = "reader1002"
        }
    };
}

Any custom AuthorizationHandler extends the AuthorizationHandler class with a passed type of AuthorizationRequirement which the handler is expected to validate off the request. The class needs to provide an implementation of the abstract method HandleRequirementAsync which takes the AuthorizationHandlerContext and the expected AuthorizationRequirement as parameters.

When the Policy is invoked over an invoked Endpoint this method is executed and it contains the decision of whether the requirement is satisfied or not. In the above case, the incoming request claims are read for a field Email and the email is then checked for availability in readers. If the condition is satisfied we announce the validation by calling in the context.Succeed() method which takes the AuthorizationRequirement that is satisfied as a parameter.

We register this handler in place of IAuthorizationHandler interface as a singleton in Startup.cs as

services.AddSingleton<IAuthorizationHandler, ShouldBeAReaderAuthorizationHandler>();

Registering Policy in the Startup:

The methods to define and implement Policies are available under the namespace Microsoft.AspNetCore.Authorization. Let's take the scenario of authorizing only a set of users who are readers to access the API to fetch reader information. (More about the example here).

A simple Policy declaration can look like this:

services.AddAuthorization(options =>
{
    options.AddPolicy("ShouldBeAReader", policy =>
    {
        policy.AuthenticationSchemes.Add(
                JwtBearerDefaults.AuthenticationScheme);
        
        policy.RequireAuthenticatedUser();
        
        policy.Requirements.Add(
            new ShouldBeAReaderRequirement());
    });
});

Here we register the AddAuthorization() middleware to the pipleline, and declare a policy within the Authorization. The AddPolicy() takes a name argument, which we use to specify while decorating on an endpoint to be secured, and the other is a function which passes a PolicyBuilder. Here in we define how the policy is intended to behave and what requirements it must posses.

Specifying the Requirement on the Controller

Finally, we decorate this policy on top of the required Endpoint. In this case it shall be the Get API endpoint.

[Authorize("ShouldBeAReader")]
[Route("all")]
[HttpGet]
public List<User> Get()
{
    return ReaderStore.Users;
}

Output:

When we execute this setup, the flow looks like as follows:

1. Fetch Token for Authentication:

POST /api/reader/token HTTP/1.1
Host: localhost:5000
Content-Type: application/json

{
	"emailAddress": "reader1001@me.com"
}

Response:

{
    "isSuccess": true,
    "token": {
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0Mzk3Y2I4My1hZTQxLTQxYTEtOTNjOS1mM2RmNWI2MGQ4YjIiLCJlbWFpbCI6InJlYWRlcjEwMDFAbWUuY29tIiwic3ViIjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNTczMDM4ODAzLCJpc3MiOiJ0aGlzaXNtZXlvdWtub3ciLCJhdWQiOiJ0aGlzaXNtZXlvdWtub3cifQ.9UCz63VvBzSXzcOcZ0UV4RNZxDjoDu3uBu8NjCBojRo",
        "expiresIn": 10
    }
}

2a. Fetch Readers when the Input Token User isn't a Reader:

GET /api/reader/all HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI0Mzk3Y2I4My1hZTQxLTQxYTEtOTNjOS1mM2RmNWI2MGQ4YjIiLCJlbWFpbCI6InJlYWRlcjEwMDFAbWUuY29tIiwic3ViIjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNTczMDM4ODAzLCJpc3MiOiJ0aGlzaXNtZXlvdWtub3ciLCJhdWQiOiJ0aGlzaXNtZXlvdWtub3cifQ.9UCz63VvBzSXzcOcZ0UV4RNZxDjoDu3uBu8NjCBojRo

Response:

403 Forbidden

2b. Fetch Readers when the Input Token User is a Reader (say reader1003@me.com):

GET /api/reader/all HTTP/1.1
Host: localhost:5000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2MmY0NWIwMC1mZDVkLTRjOTAtOTgyOS1iN2E4M2E3OTRkYjUiLCJlbWFpbCI6InJlYWRlcjEwMDNAbWUuY29tIiwic3ViIjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IkVkaXRvciIsImV4cCI6MTU3MzAzODk5OSwiaXNzIjoidGhpc2lzbWV5b3Vrbm93IiwiYXVkIjoidGhpc2lzbWV5b3Vrbm93In0.F-olncw8_EDdjIF_E-xV1qPXy14M445l5LyUIj-xs_M

Response:

[
    {
        "role": "Admin",
        "id": 1,
        "emailAddress": "reader1001@me.com",
        "userName": "Admin"
    },
    {
        "role": "Reader",
        "id": 2,
        "emailAddress": "reader1002@me.com",
        "userName": "Reader"
    },
    {
        "role": "Editor",
        "id": 1,
        "emailAddress": "reader1003@me.com",
        "userName": "Editor"
    }
]

In this way, we can implement Authorization based on a policy for an ASP.NET Core API using JWT Bearer. The code snippets used in this article are a part of the Web API solution, you can access the public GitHub repository here. You can also find many interesting articles on JWT Bearer Authentication and ASP.NET Core, do check them out.

If you find the article useful, please do consider showing your support.

Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

You can now show your support. 😊

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