Claims-based authorization mechanisms are central to modern authorization in ASP.NET Core. However, the access tokens issued by your Identity Provider (IDP) might not always perfectly align with your application's internal authorization needs.
External IDPs like Microsoft Entra ID (previously Azure AD) orAuth0 might have their own schema for claims or might not directly issue all the claims your application needs for its authorization logic.
The solution? Claims transformation.
Claims transformation allows you to modify the claims before the application uses them for authorization.
In today's issue, we will:
- Explore the concept of claims transformation in ASP.NET Core
- Explore the
IClaimsTransformation
interface with practical examples - Address considerations for security and RBAC (Role-Based Access Control)
How Does Claims Transformation Work?
They say a picture is worth a thousand words. In software engineering, we have something called UML diagrams that we can use to paint a picture.
Here's a sequence diagram showing the claims transformation flow:
- The user authenticates with the Identity Provider
- The user calls the backend API and provides an access token
- The backend API performs claims transformation and authorization
- If the user is correctly authorized, the backend API returns a response
Let's see how to implement this in ASP.NET Core.
Simple Claims Transformation
Claims can be created from any user or identity data issued by a trusted identity provider. A claim is a name-value pair that represents the subject's identity, not what the subject can do.
The core of claims transformationin ASP.NET Core is the IClaimsTransformation
interface.
It exposes a single method to transform claims:
public interface IClaimsTransformation
{
Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal);
}
Here's a simple example of using IClaimsTransformation
to add a custom claim:
internal static class CustomClaims
{
internal const string CardType = "card_type";
}
internal sealed class CustomClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.HasClaim(claim => claim.Type == CustomClaims.CardType))
{
return Task.FromResult(principal);
}
ClaimsIdentity claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(new Claim(CustomClaims.CardType, "platinum"));
principal.AddIdentity(claimsIdentity);
return Task.FromResult(principal);
}
}
The CustomClaimsTransformation
class should be registered as a service:
builder.Services
.AddTransient<IClaimsTransformation, CustomClaimsTransformation>();
Finally, you can define a custom authorization policy that uses this claim:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"HasPlatinumCard",
builder => builder
.RequireAuthenticatedUser()
.RequireClaim(CustomClaims.CardType, "platinum"));
});
There are a few caveats with using IClaimsTransformation
you should be aware of:
-
Might execute multiple times : The
TransformAsync
method might get called multiple times. Claims transformation should be idempotent to avoid adding the same claim multiple times to theClaimsPrincipal
. - Potential performance impact : Since it's executed on authentication requests, be mindful of your transformation logic's performance, especially if it involves external calls (database, APIs). Consider caching where appropriate.
Implementing RBAC With Claims Transformation
Role-Based Access Control (RBAC) is an authorization model where permissions are assigned to roles, and users are granted roles. Claims transformation helps implement RBAC smoothly. By adding role claims and potentially permission claims, authorization logic throughout your application can be simplified. Another benefit is that you can keep the access token smaller and free of any role or permission claims.
Let's consider a scenario where your application manages resources at a granular level, but your identity provider only provides coarse-grained roles like Registered
or Member
. You could use claims transformation to map the Member
role to specific fine-grained permissions like SubmitOrder
and PurchaseTicket
.
Here's a more complex CustomClaimsTransformation
implementation. We send a database query using GetUserPermissionsQuery
and get the PermissionsResponse
back. The PermissionsResponse
contains the user's permissions, which are added as custom claims.
internal sealed class CustomClaimsTransformation(
IServiceProvider serviceProvider)
: IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(
ClaimsPrincipal principal)
{
if (principal.HasClaim(c => c.Type == CustomClaims.Sub ||
c.Type == CustomClaims.Permission))
{
return principal;
}
using IServiceScope scope = serviceProvider.CreateScope();
ISender sender = scope.ServiceProvider.GetRequiredService<ISender>();
string identityId = principal.GetIdentityId();
Result<PermissionsResponse> result = await sender.Send(
new GetUserPermissionsQuery(identityId));
if (result.IsFailure)
{
throw new ClaimsAuthorizationException(
nameof(GetUserPermissionsQuery), result.Error);
}
var claimsIdentity = new ClaimsIdentity();
claimsIdentity.AddClaim(
new Claim(CustomClaims.Sub, result.Value.UserId.ToString()));
foreach (string permission in result.Value.Permissions)
{
claimsIdentity.AddClaim(
new Claim(CustomClaims.Permission, permission));
}
principal.AddIdentity(claimsIdentity);
return principal;
}
}
Now that the ClaimsPrincipal
contains the permissions as custom claims, you can do some interesting things. For example, you can implement a permission-based AuthorizationHandler
:
internal sealed class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
HashSet<string> permissions = context.User.GetPermissions();
if (permissions.Contains(requirement.Permission))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Takeaway
Claims transformation is an elegant way to bridge the gap between claims provided by identity providers and the needs of your ASP.NET Core application. The IClaimsTransformation
interface enables you to customize the claims of the current ClaimsPrincipal
. Whether you need to add roles, map external groups to internal permissions, or extract additional information from a user profile, claims transformation offers the flexibility to do so.
However, it's important to use claims transformation with a few key considerations in mind:
- Claims transformations are executed on each request.
- The
IClaimsTransformation
should be idempotent. It should not add existing claims to theClaimsPrincipal
if executed multiple times. - Design your transformations efficiently, and consider caching the results if you're fetching external data to enrich your claims.
If you want to see a complete implementation of RBAC in ASP.NET Core, check out this Authentication & Authorization playlist.
Hope this was helpful.
See you next week.
P.S. Whenever you're ready, there are 3 ways I can help you:
Modular Monolith Architecture: This in-depth course will transform the way you build monolith systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. Join the waitlist here.
Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 2,600+ students here.
Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 1,050+ engineers here.