Card image cap

Implementing Policy-Based Authorization in ASP.NET Core - Getting Started

ASP.NET Core JWT Authorization  • Posted 9 months ago

In a previous article we have discussed about securing a web api in ASP.NET Core using JWT Bearer tokens. While this approach is elegant, we can have a further fine grained access to our API, 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 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. And 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 ConfigureServices() method in the Startup when the application bootstraps. 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.

Declaring a Policy - 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.

An AuthorizationRequirement 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. An AuthorizationHandler picks up this AuthorizationRequest and validates the input request of this requirement and decides whether it can be allowed or not.

An AuthorizationHandler in this case looks like this:

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

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;

        if (ReaderStore.Readers.Any(x => x.EmailAddress == emailAddress))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

The hardcoded 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>();

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;
    }

When executed, the entire flow looks like as follows:

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
    }
}

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

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.

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