This article is all about policy-based authorization using .NET Core. Please refer to the link if you need to learn the basics of the authorization techniques. Let’s break down the key components:
-
Authentication Setup
- First, set up authentication in your application. This ensures that users are properly identified and authenticated.
- You’ll typically use a third-party authentication provider (like Microsoft Identity Platform) to handle user logins and issue tokens.
-
Authorization Policies
- Next, define authorization policies. These policies determine who can access specific parts of your application.
- Policies can be simple (e.g., “authenticated users only”) or more complex (based on claims, roles, or custom requirements).
-
Default Policy
- Create a default policy that applies to all endpoints unless overridden.
- For example, you might require users to be authenticated and have specific claims (like a preferred username).
-
Custom Policies
- Add custom policies for specific scenarios. These allow fine-grained control over access.
- For instance, you can create policies based on permissions (e.g., “create/edit user” or “view users”).
-
Permission Requirements
- Define permission requirements (e.g.,
PermissionAuthorizationRequirement
). These represent specific actions or features.
- For each requirement, check if the user has the necessary permissions (based on their roles or other criteria).
-
Role-Based Authorization
- Optionally, incorporate role-based authorization.
- Roles group users with similar access levels (e.g., “admin,” “user,” etc.). You can assign roles to users.
-
Authorization Handlers
- Implement custom authorization handlers (e.g.,
AuthorizationHandler
).
- These handlers evaluate whether a user meets the requirements (e.g., has the right permissions or roles).
-
Controller Actions
- In your controller actions, apply authorization rules.
- Use
[Authorize]
attributes with either policies or roles.
-
Middleware Result Handling
- Customize how authorization results are handled (e.g., 401 Unauthorized or 403 Forbidden responses).
- You can create an
AuthorizationMiddlewareResultHandler
to manage this behavior
Let's go ahead will the actual coding for your Web API:
Program.cs: User Azure AD authentication as it is. Decide your Permission Keys for Authorization.
Only one policy in the AddPolicy method
//In Program.cs file, add the below code
var builder = WebApplication.CreateBuilder(args);
// Authentication using Microsoft Identity Platform
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));
// Default policy-based authorization
services.AddAuthorizationCore(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("preferred_username")
.RequireScope("user_impersonation")
.Build();
options.AddPolicy("Permission1", policy =>
policy.Requirements.Add(new PermissionAuthorizationRequirement("Permission1")));
options.AddPolicy("Permission2", policy =>
policy.Requirements.Add(new PermissionAuthorizationRequirement("Permission2")));
});
// Authorization handler
services.AddScoped<IAuthorizationHandler, AuthorizationHandler>();
// Middleware result handler for response errors (401 or 403)
services.AddScoped<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();
// Other services and configurations...
PermissionAuthorizationRequirement .cs: Just copy and paste the requirement
//Create new PermissionAuthorizationRequirement.cs file for Custom requirement for permission-based authorization
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string allowedPermission)
{
AllowedPermission = allowedPermission;
}
public string AllowedPermission { get; }
}
AuthorizationHandler.cs: (Just copy and paste the code. Make sure to hit the DB call to get the permissions list by using App Manager)
// Custom authorization handler to check user permissions
public class AuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
// Business layer service for user-related operations
private readonly IAppManager _appManager;
public AuthorizationHandler(IAppManager appManager)
{
_appManager= appManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
// Find the preferred_username claim
var preferredUsernameClaim = context.User.Claims.FirstOrDefault(c => c.Type == "preferred_username");
if (preferredUsernameClaim is null)
{
// User is not authenticated
context.Fail(new AuthorizationFailureReason(this, "UnAuthenticated"));
return;
}
// Call the business layer method to check if the user exists
var user = await _appManager.GetUserRolesAndPermissions(preferredUsernameClaim);
if (user is null || !user.IsActive)
{
// User does not exist or is inactive
context.Fail(new AuthorizationFailureReason(this, "UnAuthenticated"));
return;
}
// Select the list of permissions that the user is assigned
// Here you will fetch the Permission1 and Permission2
var userPermissions = user.UserPermissions?.Select(k => k.PermissionKey);
// Get the current permission key from the controller's action method
string allowedPermission = requirement.AllowedPermission;
// Check if the current request carries this permission
if (userPermissions.Any(permission => permission == allowedPermission))
{
// Permission granted
context.Succeed(requirement);
return;
}
// Permission denied
context.Fail();
}
}
AuthorizationMiddlewareResultHandler.cs: Just copy and paste. No changes are required.
// AuthorizationMiddlewareResultHandler to decide response code (401 or success)
public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly ILogger<AuthorizationMiddlewareResultHandler> _logger;
private readonly Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler _defaultHandler = new();
public AuthorizationMiddlewareResultHandler(ILogger<AuthorizationMiddlewareResultHandler> logger)
{
_logger = logger;
}
public async Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
var authorizationFailureReason = authorizeResult.AuthorizationFailure?.FailureReasons.FirstOrDefault();
var message = authorizationFailureReason?.Message;
if (string.Equals(message, "UnAuthenticated", StringComparison.CurrentCultureIgnoreCase))
{
// Set response status code to 401 (Unauthorized)
context.Response.StatusCode = "401";
_logger.LogInformation("401 failed authentication");
return;
}
// If not unauthorized, continue with default handler
await _defaultHandler.HandleAsync(next, context, policy, authorizeResult);
}
}
Controller.cs: Finally, in the dashboard controller, add the attributes [Authorize] and [Policies]. Here you will define Permission1 and Permission2
// Dashboard controller
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class DashboardController : ControllerBase
{
[HttpGet]
[Route("get-dashboardDetails")]
[Authorize(Policy = "Permission1")]
public async Task GetAllDashboardDetailsAsync()
{
// Your logic for fetching all user details
return await GetAllDashboardDetailsAsync();
}
[HttpGet]
[Route("create-product")]
[Authorize(Policy = "Permission2")]
//[Authorize(Policy = nameof(Read string from ENUM))]
public async Task CreateProductAsync([FromBody] Product product)
{
// Your logic for creating a new product
}
}
Now if you want to combine with Roles based authorization in your Web API code. Follow the below steps or you can avoid moving forward. It is almost the same steps as we did with the policy-based authorization with a small difference that you will figure out in the code.
1. Register the RoleAuthorizationHandler: In your Program.cs
file, just add the following line to register the RoleAuthorizationHandler
builder.Services.AddScoped<IAuthorizationHandler, RoleAuthorizationHandler>();
2. RoleAuthorizationHandler: Below is the RoleAuthorizationHandler.cs
one that checks whether the user has the required roles. Create one more handler
public class RoleAuthorizationHandler : AuthorizationHandler<RolesAuthorizationRequirement>
{
private readonly IAppManager _appManager;
public RoleAuthorizationHandler(IAppManager appManager)
{
_appManager= appManager;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
{
// Find the preferred_username claim
Claim claim = context.User.Claims.FirstOrDefault(c => c.Type == "preferred_username");
if (claim is not null)
{
// Get user details
var userRoles = await _appManager.GetUserRolesAndPermissions(claim.Value);
// Check if the user's roles match the allowed roles
var roles = requirement.AllowedRoles;
if (userRoles .Any(x => roles.Contains(x)))
{
context.Succeed(requirement); // User has the required roles
}
else
{
context.Fail(); // User does not have the required roles
return;
}
}
await Task.CompletedTask;
}
}
3. Usage in DashboardController: In your DashboardController
, you can use both roles and policies for authorization. For example:
[HttpGet]
[Route("get-dashboardDetails")]
[Authorize(Roles = "Super_Admin, Role_Administrator")] // Can have multiple roles. You can choose either Roles or Policy or both
[Authorize(Policy = "Permission1")] // Can have only one policy per action to accept.
public async Task GetAllDashboardDetailsAsync()
{
// Your logic for fetching all user details
return await GetAllDashboardDetailsAsync();
}
That's all. Your Web API will work like magic. :-) Stay tuned for more learning.