Skip to main content
SEMastery
ASP.NETintermediate

Master Claims Transformation for Flexible ASP.NET Core Authorization

Learn claims transformation in ASP.NET Core to enrich the user identity and build flexible, policy-based authorization with IClaimsTransformation.

11 min readUpdated November 20, 2025

Master Claims Transformation for Flexible ASP.NET Core Authorization

Imagine you go to a wedding hall. At the gate, a guard checks your invitation card. The card proves who you are. But the card does not say which areas you can enter. The dinner hall? The stage? The family-only room?

So a helper at the entrance looks at your name, checks a small list, and puts a coloured sticker on your card. Green sticker means you can eat. Gold sticker means you can go on stage. Now, every guard inside the hall just looks at your sticker. They do not need to ask who you are again.

In ASP.NET Core, that coloured sticker is a claim. The helper who adds the sticker is claims transformation. This article will teach you how to use it to build clean, flexible authorization.

What is a claim, really?

A claim is a small piece of information about a user. It is just a name and a value. For example:

When a user logs in, ASP.NET Core builds a ClaimsPrincipal. Think of it as the user's ID card with many claims printed on it. Your authorization rules then read these claims to decide: can this person do this action or not?

The problem is that the claims you get at login are often not the claims you need. A login token from Google or Microsoft Entra ID might give you an email and a user id, but nothing about roles or permissions inside your app. That gap is exactly what claims transformation fills.

Where claims transformation sits in the request pipeline

The big idea: enrich the identity

Authentication answers one question: who are you? Authorization answers a different question: what are you allowed to do?

Between these two steps, there is a perfect spot to add extra information. The user is already known, but no access decision has been made yet. This is where claims transformation lives. You take the basic identity and you enrich it. You add the claims your app actually needs.

Here is a simple way to picture the journey of a user's identity.

The identity journey

Login
Token Claims
Transform
Rich Identity
Decision

Steps

1

Login

User signs in

2

Token Claims

Basic email and id

3

Transform

Add roles and permissions

4

Rich Identity

Full claim set

5

Decision

Allow or deny

From login token to a rich identity your rules can use

Meet IClaimsTransformation

ASP.NET Core gives you one small interface for this job: IClaimsTransformation. It has a single method.

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
 
public class MyClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // We get the user. We can add claims. We return the user.
        return Task.FromResult(principal);
    }
}

The framework calls TransformAsync and hands you the current ClaimsPrincipal. Your job is to return a principal, usually the same one, but with extra claims added. After this runs, the authorization step sees your new claims.

To turn it on, you register it in Program.cs.

builder.Services.AddTransient<IClaimsTransformation, MyClaimsTransformation>();

That is all the wiring you need. From now on, your transformation runs on each request after authentication.

A first real example: adding a role

Let us say your login token gives you an email, but your app decides who is an admin based on a list. We can add a role claim for admins.

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
 
public class AdminClaimsTransformation : IClaimsTransformation
{
    private static readonly string[] Admins =
    {
        "[email protected]",
        "[email protected]"
    };
 
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var email = principal.FindFirstValue(ClaimTypes.Email);
 
        // Only add the role if it is missing. This keeps us idempotent.
        bool alreadyAdmin = principal.IsInRole("Admin");
 
        if (email is not null && Admins.Contains(email) && !alreadyAdmin)
        {
            var identity = (ClaimsIdentity)principal.Identity!;
            identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
        }
 
        return Task.FromResult(principal);
    }
}

Notice the check !alreadyAdmin. We only add the role if it is not there yet. This is the single most important rule in claims transformation, and we will explain why next.

The golden rule: be idempotent

"Idempotent" is a big word for a simple idea. It means: running it many times gives the same result as running it once.

Why does this matter? Because TransformAsync can be called more than once during a single request. If your code blindly adds a role claim every time it runs, the user could end up with the same role two or three times. That can cause strange bugs and bloat the identity.

So the safe pattern is always: check first, then add.

The check-then-add pattern that keeps transformation idempotent

Here is a tiny helper that makes the check easy to reuse.

private static void AddIfMissing(ClaimsIdentity identity, string type, string value)
{
    bool exists = identity.HasClaim(c => c.Type == type && c.Value == value);
    if (!exists)
    {
        identity.AddClaim(new Claim(type, value));
    }
}

With this helper, your transformation stays clean and you never create duplicates, even if the method runs several times.

A richer example: permissions from a database

Real apps rarely store permissions in a hard-coded list. They keep them in a database. So a common job for claims transformation is: read the user's permissions from the database and add them as claims.

using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
 
public class PermissionClaimsTransformation : IClaimsTransformation
{
    private readonly IPermissionService _permissions;
 
    public PermissionClaimsTransformation(IPermissionService permissions)
    {
        _permissions = permissions;
    }
 
    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var identity = (ClaimsIdentity)principal.Identity!;
 
        // If we have already added permissions, do not do it again.
        if (identity.HasClaim(c => c.Type == "permission"))
        {
            return principal;
        }
 
        var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
        if (userId is null)
        {
            return principal;
        }
 
        var userPermissions = await _permissions.GetForUserAsync(userId);
 
        foreach (var permission in userPermissions)
        {
            identity.AddClaim(new Claim("permission", permission));
        }
 
        return principal;
    }
}

This works, but it has a hidden cost. It hits the database on every request. We will fix that with caching below. First, let us see how these claims get used.

Using the claims in authorization policies

Once the claims are on the identity, authorization is the easy part. You define a policy that requires a claim, and you attach it to your endpoints.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanReadOrders", policy =>
        policy.RequireClaim("permission", "orders.read"));
 
    options.AddPolicy("CanDeleteOrders", policy =>
        policy.RequireClaim("permission", "orders.delete"));
});

Now protect an endpoint with the policy name.

app.MapGet("/orders", () => "Here are the orders")
   .RequireAuthorization("CanReadOrders");
 
app.MapDelete("/orders/{id}", (int id) => $"Deleted order {id}")
   .RequireAuthorization("CanDeleteOrders");

Note how the route /orders/{id} uses curly braces inside a code block, which is safe. In normal text we would write it as DELETE /orders/{id} inside backticks so the page does not break.

The beauty here is separation. Your endpoint does not care how a user got the orders.read permission. It only checks that the claim is present. The transformation owns the "how", and the policy owns the "what is required". This keeps both parts simple.

How a policy reads the claims that transformation added

Performance: do not punish every request

Because transformation runs on each request, a database call inside it can slow your whole app. Imagine a busy site getting one thousand requests a second. That would be one thousand extra database trips, just to load the same permissions again and again.

The fix is caching. We store the user's permissions for a short time. The first request loads from the database. The next requests, for a few minutes, read from memory.

public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
    var identity = (ClaimsIdentity)principal.Identity!;
 
    if (identity.HasClaim(c => c.Type == "permission"))
    {
        return principal;
    }
 
    var userId = principal.FindFirstValue(ClaimTypes.NameIdentifier);
    if (userId is null)
    {
        return principal;
    }
 
    // Cache the result for 5 minutes so we do not hit the DB each time.
    var permissions = await _cache.GetOrCreateAsync(
        $"perm-{userId}",
        async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            return await _permissions.GetForUserAsync(userId);
        });
 
    foreach (var permission in permissions!)
    {
        identity.AddClaim(new Claim("permission", permission));
    }
 
    return principal;
}

There is a trade-off with caching. If you remove a user's permission, they might keep it until the cache expires. A short cache time, like five minutes, is a good balance for most apps. For very sensitive actions, you may want a shorter time or no cache at all.

Common approaches compared

There is more than one way to enrich an identity. Here is how the main options compare.

ApproachWhen it runsGood forWatch out for
Claims in the tokenAt login onlyStable data like user idToken gets large; stale until re-login
IClaimsTransformationEach requestApp-specific roles and permissionsRuns often; cache slow lookups
Lookup inside the endpointWhen calledOne-off special casesLogic spread across many places

And here is a quick guide to the small pieces you will touch most often.

MemberWhat it doesExample
principal.IdentityThe user's identity objectCast to ClaimsIdentity to add claims
FindFirstValue(type)Read one claim valueFindFirstValue(ClaimTypes.Email)
HasClaim(predicate)Check if a claim existsUsed for the idempotent guard
identity.AddClaim(claim)Add a new claimnew Claim("permission", "orders.read")
IsInRole(name)Check a role claimprincipal.IsInRole("Admin")

Mapping external groups to internal roles

A very common real task: your company uses Microsoft Entra ID. Users belong to groups there, like HR-Team or Finance-Team. But your app talks in roles like HrManager. Claims transformation is the perfect bridge to map one to the other.

Group to role mapping

Entra Group
Mapping Table
App Role
Policy

Steps

1

Entra Group

HR-Team

2

Mapping Table

Lookup rule

3

App Role

HrManager

4

Policy

RequireRole

External identity groups become internal app roles

The mapping itself is just a small dictionary, and again we check before we add.

private static readonly Dictionary<string, string> GroupToRole = new()
{
    ["HR-Team"] = "HrManager",
    ["Finance-Team"] = "Accountant"
};
 
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
    var identity = (ClaimsIdentity)principal.Identity!;
 
    foreach (var groupClaim in principal.FindAll("groups"))
    {
        if (GroupToRole.TryGetValue(groupClaim.Value, out var role)
            && !principal.IsInRole(role))
        {
            identity.AddClaim(new Claim(ClaimTypes.Role, role));
        }
    }
 
    return Task.FromResult(principal);
}

Now your policies can simply say RequireRole("HrManager"). If the company renames a group tomorrow, you change one line in the mapping, not a hundred endpoints.

A few gentle warnings

Claims transformation is powerful, but a few habits keep you safe.

  • Do not throw on missing data. If a claim you expect is not there, return the principal as-is. Crashing the request is worse than missing one claim.
  • Do not store secrets in claims. Claims can travel with the request. Keep passwords and keys out of them.
  • Keep it small. Add only the claims your authorization rules truly need. A huge identity is slow to carry around.
  • Always guard against duplicates. Repeat after me: check first, then add.

Putting it all together

Here is the full mental model. Authentication says who you are. Claims transformation runs next and gives you the right stickers. Authorization reads those stickers and lets you in or keeps you out. Each part has one clear job, and that is what makes the design flexible.

When you need a new permission tomorrow, you usually touch only two places: the transformation that adds the claim, and the policy that requires it. Your endpoints stay clean and unchanged.

Quick recap

  • A claim is a small name-value fact about a user, like a coloured sticker on an ID card.
  • Claims transformation runs after authentication and before authorization, the perfect spot to enrich the identity.
  • Implement IClaimsTransformation and its single TransformAsync method, then register it with AddTransient.
  • The golden rule is to be idempotent: always check if a claim exists before adding it, because the method can run more than once.
  • Use the new claims in authorization policies with RequireClaim or RequireRole.
  • Transformation runs on every request, so cache slow lookups like database calls.
  • It is great for mapping external groups to internal roles and keeping your endpoints simple.

References and further reading

Related Posts