Hey y’all 👋, So let us do some learning on Policies in Asp.Net. The release of .NET 8 Preview 7 comes with a new bearer token authentication handler. This eases setting up authentication for minimal APIs using JWT.
In this post, I will be using the new bearer token authentication handler to handle authentication for the minimal API endpoint.
Policies in ASP.NET enable you to easily configure authorization by setting up rules and checks in the authorization pipeline. These checks determine if access will be granted to the request to an endpoint.
There are two important things to note when defining policies:
- Requirements
- Requirements Handler
Requirement
Requirements are used to define the conditions that a user must meet to access a resource or perform an operation.
Requirements are usually created by implementing the IAuthorizationRequirement and setting properties that hold the required values to be checked by the handlers.
// A simple class to define a location requirement
public class LocationRequirement : IAuthorizationRequirement
{
public string Location { get; set; }
public LocationRequirement(string location)
{
Location = location;
}
}
In the code above, the LocationRequirement
class requires that the location property be set during class instantiation. Mostly done during Policy definition.
Requirements Handler
The requirements handler is used to check the user’s claims or details against the value set in the Requirement
class. If the check is valid, access is granted to the route; otherwise, access is restricted.
Access is granted when the Succeed
method on the AuthorizationHandlerContext
is called with the requirement passed into the Succeed
method.
Authorization Handlers can be defined in different ways based on the interface or generic type that they inherit. The basic means of defining a handler is by implementing the IAuthorizationHandler
.
public class LocationHandler : IAuthorizationHandler{
public Task HandleAsync(AuthorizationHandlerContext context){
var locationRequirement = context.Requirements.OfType<LocationRequirement>().FirstOrDefault();
if(LocationRequirement != null){
if(context.User.FindFirst(ClaimTypes.Locality).Value == locationRequirement.Location){
context.Succeed(LocationRequirement);
}
}
return Task.CompletedTask;
}
}
The problem with handlers implementing the IAuthorizatonHandler
is that they get processed regardless of whether the policy for a specific endpoint defines it. But they can come in handy when you want to add global handlers to the authorization pipeline.
Another way the handler can be defined is by implementing the AuthorizationHandler<TRequirement>
. This makes the handler to be processed only when the endpoint/route that is requested from the client has a policy that adds the <TRequirement>
.
The example below basically does the same thing as the one above but doesn’t run globally and is also shorter to write.
public class LocationHandler2 : AuthorizationHandler<LocationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocationRequirement requirement)
{
if (context.User.FindFirstValue(ClaimTypes.Locality) == requirement.Location)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
And finally, one important step to not forget is registering your handler in the Service Collection.
service.AddSingleton<IAuthorizationHandler, LocationHandler>();
Securing your API with Policy-Base Authorization
This application configures an endpoint with a policy that only allows users with email addresses containing the word "sam"
to access the resource. You’ll need to have at least .NET 8 Preview 7 to run this code.
Steps Taken:
- Run
"dotnet new web -o miniAPIAuthorizationPolicy"
to create a minimal API project in a folder called"miniAPIAuthorizationPolicy"
- Install Some Packages by executing the command below:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
- To enable preview features like the primary constructors in C# 12 Add this flag to your
.csproj
file inside thePropertyGroup
tag.
<LangVersion>preview</LangVersion>
- Replace the code in the
Program.cs
file with the one below.
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
// Add Bearer Token Authentication Handler
builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme);
// Add Authorization Policy and add a requirement to the policy
// Decided to add the policy name to the requirement class
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(EmailContainsRequirement.Policy, x =>
{
x.AddRequirements(new EmailContainsRequirement("sam"));
});
});
// Add the requirement handler to the service collection
builder.Services.AddSingleton<IAuthorizationHandler, EmailContainsHandler>();
builder.Services.AddDbContext<ApplicationDbContext>(x => x.UseSqlite("DataSource=datastore.db"));
builder.Services.AddIdentityCore<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddApiEndpoints();
var app = builder.Build();
// registers the Identity API endpoints
app.MapIdentityApi<IdentityUser>();
// Add a route that requires the EmailContainsRequirement.Policy Policy
// The policy is added to the route using the RequireAuthorization method
app.MapGet("/contains-sam", (ClaimsPrincipal principal) =>
{
var email = principal.FindFirstValue(ClaimTypes.Email);
return $"Your Email is : {email}";
}).RequireAuthorization(EmailContainsRequirement.Policy);
app.Run();
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
}
// The EmailContainsRequirement class sets the substring to search for in the email
// of the authenticated user.
public class EmailContainsRequirement(string substring) : IAuthorizationRequirement
{
public string SubString = substring;
public const string Policy = "EmailContainsSubStringPolicy";
}
// The EmailContainsHandler class checks if the authenticated user's email contains
// the substring specified in the requirement.
public class EmailContainsHandler : AuthorizationHandler<EmailContainsRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EmailContainsRequirement requirement)
{
if (context.User.FindFirstValue(ClaimTypes.Email).Contains(requirement.SubString))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
- The code above makes use of SQLite and won’t work until you make migrations of the IdentityUser types.
To do this, execute the command below:
dotnet ef migrations add <any-migraton-name-of-your-choice>
- Next, create the tables in your SQL database based on the created migration by executing the command below:
dotnet ef database update
- Start the application by running
"dotnet run"
.
- On my end, my API starts at
http://localhost:5111
. Ensure to change the port to yours. - From your terminal execute the command below to register a user having “testuser@mail.com” as an email address.
curl --location -v 'http://localhost:5111/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "testuser",
"password": "TestPassword1$",
"email": "testuser@mail.com"
}'
- To log in and obtain an access token, execute the code below
curl --location -v 'http://localhost:5111/login' \
--header 'Content-Type: application/json' \
--data '{
"username": "testuser",
"password": "TestPassword1$"
}'
Copy the access token from the response and use it to query the "contains-sam"
end-point
- Execute the code below to query the
"contains-sam"
endpoint.
curl --location -v 'http://localhost:5111/contains-sam' \
--header 'Authorization: Bearer <Your access token>'
You should receive a 403 Forbidden status in the response header. This is because the user’s email (“testuser@mail.com”) doesn’t contain the substring “sam”.
But if a user registers with any email address that contains “sam” eg (tesame@mail.com, casamik@mail.com). The user should be able to access that route and receive a response.
For instance, if a user with the email: samike@mail.com registers and logs in, the token retrieved during login should permit the user to access the “/contains-sam” endpoint.
Some things to Note when using Policy-Based Authorization
- If none of the handlers for policy requirement calls the
"Succeed"
method of the AuthorizationHandlerContext, the user would be restricted from accessing the endpoint. - If only one of the handlers for a policy requirement calls
"Succeed"
method of the AuthorizationHandlerContext, the user would be allowed access to the endpoint as long as no handler calls the"Fail"
method of the AuthorizationHandlerContext.
In ASP.NET, Policy-Based Authorization facilitates the modular organization of your authorization logic throughout your application.
The code used here can be found on my GitHub. To learn more about Policy-based Authorization in ASP.NET check here.
Thanks for reading through and don’t fail to share it if you find it useful 😄.
Reminder: Process βOR49. RECEIVE => https://telegra.ph/Go-to-your-personal-cabinet-08-25?hs=3503180e199611fbf3b2426c3975a850&
3yslr5