
Authorization with Claims and Policies in ASP.NET Web API
Author - Abdul Rahman (Bhai)
Web API
29 Articles
Table of Contents
What we gonna do?
Authentication answers who are you? Authorization answers what are you allowed to do? Knowing that a token is valid does not mean the user should have access to every endpoint. Claims-based authorization — also known as ABAC (Attribute-Based Access Control), CBAC (Claims-Based Access Control), or PBAC (Policy-Based Access Control) — lets you express fine-grained access rules using the information carried inside the JWT token.
In this article, we'll walk through reading user claims inside an endpoint, building reusable authorization policies, and applying those policies to protect the ILoveDotNet API so that only users from the right channel can manage articles.
Why we gonna do?
Consider what happens when you protect an endpoint with just .RequireAuthorization(). Any authenticated user — regardless of their role or claims — can call that endpoint. For a public blog API that's fine. For an API where only certain users can publish or manage content, it is a problem:
// ❌ Any authenticated user can delete articles — that is wrong
app.MapDelete("/api/articles/{id:int}", (int id) =>
{
ArticleStore.Remove(id);
return Results.NoContent();
})
.RequireAuthorization(); // only checks "is a valid token present?"
The alternative — writing if (User.Claims...) inside every endpoint — is
tedious and error-prone. A missed check is a security hole. Authorization
policies centralise these rules so they are defined once and applied
declaratively:
// ✅ Only users with channel=WebAPI claim can delete articles
app.MapDelete("/api/articles/{id:int}", ...)
.RequireAuthorization("MustBeWebAPIChannel");
ABAC is considered the preferred modern approach over the older RBAC (Role-Based Access Control). RBAC assigns broad roles (admin, editor). ABAC evaluates any combination of user attributes at request time, which allows far more expressive rules without managing role hierarchies.
How we gonna do?
Step 1: Reading User Claims in an Endpoint
The JWT Bearer middleware automatically deserialises the token and converts it into a ClaimsPrincipal available via HttpContext.User. In minimal API handlers you can receive it directly as a parameter:
app.MapGet("/api/articles", (ClaimsPrincipal user, ILogger<Program> logger) =>
{
// Access any claim by the name you used when creating the token
var userName = user.FindFirstValue("sub");
var channel = user.FindFirstValue("channel"); // custom claim
logger.LogInformation(
"Articles requested by {User} (channel: {Channel})", userName, channel);
return Results.Ok(ArticleStore.All);
})
.RequireAuthorization();
Use the same claim name string that was set when the token was created. Custom claims (like channel) are never remapped by the middleware and are always accessible by their original name.
You could check claims directly inside every endpoint handler this way, but that is tedious and error-prone — a missed check is a security hole. The better approach is to define rules once as authorization policies and apply them declaratively.
Step 2: Building Authorization Policies
Define all policies in one place using AddAuthorizationBuilder() in Program.cs. A policy is a set of requirements — all must pass for the policy to be satisfied:
builder.Services
.AddAuthorizationBuilder()
// Policy: user must be authenticated AND have channel = "WebAPI"
.AddPolicy("MustBeWebAPIChannel", policy =>
policy
.RequireAuthenticatedUser()
.RequireClaim("channel", "WebAPI"));
Step 3: Applying Policies to Endpoints
Pass the policy name to RequireAuthorization(). You can apply different policies to different endpoints, or group endpoints and apply a policy to the whole group:
// Read: any authenticated user
app.MapGet("/api/articles", () => Results.Ok(ArticleStore.All))
.RequireAuthorization();
// Create, Update, Delete: only WebAPI channel users
app.MapPost("/api/articles", (Article article) =>
{
ArticleStore.Add(article);
return Results.Created($"/api/articles/{article.Id}", article);
})
.RequireAuthorization("MustBeWebAPIChannel");
app.MapDelete("/api/articles/{id:int}", (int id) =>
{
ArticleStore.Remove(id);
return Results.NoContent();
})
.RequireAuthorization("MustBeWebAPIChannel");
Test the policy with dotnet user-jwts — generate tokens with
different channel claim values and observe the responses:
# Token with channel=Blazor → DELETE /api/articles → 403 Forbidden
dotnet user-jwts create \
--issuer "https://localhost:7001" \
--audience "ilovedotnetapi" \
--claim channel=Blazor
# Token with channel=WebAPI → DELETE /api/articles → 204 No Content
dotnet user-jwts create \
--issuer "https://localhost:7001" \
--audience "ilovedotnetapi" \
--claim channel=WebAPI
A 403 Forbidden means the user is authenticated but does
not satisfy the policy — exactly the right response. A 401 would mean the token itself
was invalid. Swap in the channel=WebAPI token and the same endpoint returns
204 No Content, confirming the policy is working correctly.
Step 4: Grouping Endpoints with a Shared Policy
Apply a policy to every endpoint in a route group using MapGroup():
// All management endpoints require the WebAPI channel policy
var adminGroup = app.MapGroup("/api/articles")
.RequireAuthorization("MustBeWebAPIChannel");
adminGroup.MapPost("/", (Article article) => { ... });
adminGroup.MapPut("/{id:int}", (int id, Article article) => { ... });
adminGroup.MapDelete("/{id:int}", (int id) => { ... });
Beyond Basic Policies: A Note on OAuth2 and OpenID Connect
The approach above — creating and validating tokens within your own API — is sufficient for getting started and for simple internal APIs. For enterprise-grade applications or consumer-facing APIs, this rudimentary approach has limitations:
- OAuth2 is a protocol that allows client applications to obtain access tokens from a dedicated authorisation server (e.g. Microsoft Entra ID, OKTA, IdentityServer). The API only validates the token — it doesn't issue them.
- OpenID Connect extends OAuth2 with an identity token that web applications can use for login flows.
Adopting a proven identity provider dramatically reduces the attack surface — private signing keys never leave the provider, and features like token rotation, revocation, and MFA are handled out of the box.
Summary
- Authentication (who are you?) and authorization (what are you allowed to do?) are separate concerns — authentication must come first.
- The JWT Bearer middleware populates HttpContext.User (a ClaimsPrincipal) automatically — no manual parsing required.
- Use AddAuthorizationBuilder().AddPolicy() to define reusable policies that evaluate one or more claim requirements.
- Apply policies declaratively with .RequireAuthorization("PolicyName") — never scatter claim checks across individual endpoint handlers.
- A 403 Forbidden = authenticated but not authorised. A 401 Unauthorized = not authenticated (missing or invalid token).
- For production systems, delegate token issuance to a proven identity provider (Entra ID, OKTA, Keycloak) and adopt OAuth2 / OpenID Connect.