Introduction – Why do you need Claims Transformation?
Imagine you are developing a web application for users and have provided various options for User to login to the system (Email, Social Logins etc). Once the user has logged into the system, you need to identify the user who has currently logged into the system and find which roles the User currently has.
You might also need to fetch the user authorization information beforehand, so that you can authorize user later on.
Because the Login mechanism is externalized (social logins and other OpenID systems), you can’t really update the roles information of your users which is internal to your application over there. This information should exist within your premises, while still being able to identify user from an external login system.
Similar is the case with WebAPI solutions, where you need to fetch further information about the User from the HttpContext set by the id_token passed in the request. The Authorization middleware validates the token and creates a ClaimsIdentity based on the Token claims. Any further transformation is not really easy over there.
To achieve this, you can add an implementation of IClaimsTransformation where you can fetch the user information from the backend database and add this information to the User Claims, which can be later used in Authorize filters or in any other place.
Claims Transformation happens before MVC middleware routes the request to respective paths, so you are assured that the claims are set before the endpoints are called.
In this detailed article, let’s see how we can use IClaimsTransformation to add additional information to logged in user identity with an illustrated example in ASP.NET Core.
What is IClaimsTransformation?
IClaimsTransformation is an interface provided in the microsoft.aspnetcore.authentication namespace. It can be used to add extra claims or modify existing claims in the ClaimsPrincipal class.
The IClaimsTransformation interface provides a single method TransformAsync. We will use this method while implementing the IClaimsTransformation interface. It has a single parameter of ClaimsPrincipal and it also returns the same.
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// you can add or update Claims of the currently logged in user here
}
This method might get called multiple times during the login flow. A ClaimsIdentity is created to add the new claims and this can be added to the ClaimsPrincipal. One must keep in mind to only add a new claim if it does not already exist in the ClaimsPrincipal. To handle this, we always add a Claim within a conditional block.
The implementation of IClaimsTransformation interface is registered as a Transient service in ASP.NET Core Dependency Injection. This means we can inject any scoped or singleton services inside our Transformation class.
Let’s get Hands-On: How to add Claims to Identity using IClaimsTransformation
To better understand, let’s take the example of SocialNinja – my boilerplate solution which contains solved implementation of Social Logins using Google and Facebook (more Login providers coming soon).
In this solution, we have two login options to the user – Google and Facebook.
When user logins using any of these providers, the web application takes the user context set after redirection and saves the login information to an Sqlite database inside the application.
Currently, we store the user information into the database after login redirect happens. Post that we don’t really know which user has logged into the system.
Now let’s assume when user logins to the system we want to first find which user has logged into the system. Within this step, we will also add some roles to the logged in user based on who has logged in. These Roles are later used for Authorizing logged in user.
To achieve this, I’ll implement IClaimsTransformation interface. My complete implementation looks like below –
using Microsoft.AspNetCore.Authentication;
using SocialNinja.Contracts.Data;
using SocialNinja.Contracts.Data.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace SocialNinja.Web.Services
{
public class MyClaimsTransformation : IClaimsTransformation
{
private readonly IUnitOfWork _uow;
public MyClaimsTransformation(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var claims = principal.Claims;
var emailAddress = claims.Where(x => x.Type == ClaimTypes.Email).FirstOrDefault();
var oid = claims.Where(x => x.Type == ClaimTypes.NameIdentifier).FirstOrDefault();
var authProvider = principal.Identity.AuthenticationType;
var loggedInUser = _uow.Users.GetQueryable().Where(
x => x.EmailAddress == (emailAddress != null ? emailAddress.Value : "") &&
x.OId == (oid != null ? oid.Value : "") &&
x.OIdProvider == authProvider);
if (loggedInUser.Any())
{
var user = loggedInUser.FirstOrDefault();
AddLoggedInUserIdentity(principal, claims, user);
}
else
{
var id = principal.FindFirst(ClaimTypes.NameIdentifier);
var email = principal.FindFirst(ClaimTypes.Email);
var name = principal.FindFirst(ClaimTypes.Name);
var provider = principal.FindFirst("LoginProvider");
var userProfile = new UserProfile
{
EmailAddress = email != null ? email.Value : "",
OIdProvider = provider != null ? provider.Value : principal.Identity.AuthenticationType,
OId = id != null ? id.Value : ""
};
var databaseId = await _uow.Users.GetOrCreateExternalUserAsync(userProfile);
userProfile.Id = databaseId;
AddLoggedInUserIdentity(principal, claims, userProfile);
}
return principal;
}
private void AddLoggedInUserIdentity(ClaimsPrincipal principal, IEnumerable<Claim> claims, UserProfile user)
{
var identity = new ClaimsIdentity();
if (!principal.HasClaim(x => x.Type == "DatabaseId"))
{
identity.AddClaim(new Claim("DatabaseId", user.Id.ToString()));
}
if (!principal.HasClaim(x => x.Type == "LoginProvider"))
{
identity.AddClaim(new Claim("LoginProvider", user.OIdProvider.ToString()));
}
// add remaining claims from input
foreach (var claim in claims)
{
if (!principal.HasClaim(x => x.Type == claim.Type))
{
identity.AddClaim(claim);
}
}
principal.AddIdentity(identity);
}
}
}
Like I mentioned before, any implementation of IClaimsTransformation needs to provide TransformAsync() method. Since IClaimsTransformation is registered as a service in DI framework, I can inject any service I wish to use for my logic.
Here I’m injecting a UnitOfWork object which encapsulates my DbContext to access the underlying Database – via Entity Framework Core.
namespace SocialNinja.Web.Services
{
public class MyClaimsTransformation : IClaimsTransformation
{
private readonly IUnitOfWork _uow;
public MyClaimsTransformation(IUnitOfWork uow)
{
_uow = uow;
}
...
}
}
Next, inside my TransformAsync() method, I’m first fetching the query parameters I need to query for the logged in user. There can be two scenarios in which this method is called by the system –
- User is already registered and is available in the Database
- User has never registered before and the entry isn’t available in the Database
In both the cases, I need to obtain 3 attributes from the User Claims –
- User Identifier in the respective Social Login Provider, which is of the type NameIdentifier
- The Email address associated with the user, which is of the type Email
- The Social Login Provider through which the user has logged in – the AuthenticationType. In our case it can be either Google or Facebook.
The below code snippet from the implementation above fetches these three values –
namespace SocialNinja.Web.Services
{
public class MyClaimsTransformation : IClaimsTransformation
{
private readonly IUnitOfWork _uow;
public MyClaimsTransformation(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var claims = principal.Claims;
var emailAddress = claims.Where(x => x.Type == ClaimTypes.Email).FirstOrDefault();
var oid = claims.Where(x => x.Type == ClaimTypes.NameIdentifier).FirstOrDefault();
var authProvider = principal.Identity.AuthenticationType;
....
}
}
}
Once these values are obtained, I can simply query from the database and see if the user is already available or not. If available, get the data or else insert it.
var loggedInUser = _uow.Users.GetQueryable().Where(
x => x.EmailAddress == (emailAddress != null ? emailAddress.Value : "") &&
x.OId == (oid != null ? oid.Value : "") &&
x.OIdProvider == authProvider);
if (loggedInUser.Any())
{
var user = loggedInUser.FirstOrDefault();
AddLoggedInUserIdentity(principal, claims, user);
}
else
{
UserProfile userProfile = await AddNewUserToTheDatabase(principal);
AddLoggedInUserIdentity(principal, claims, userProfile);
}
In both the cases, add the user along with the new claims obtained from the database – namely LoginProvider and DatabaseId to the claims. This eliminates any further need to query for the database user in any further endpoint operations or business logic. You will have the database user primary key ready to be used for your queries.
At this point, you can also add any Roles available to the user, or even transform existing Roles that are issued by the Identity Provider.
To demonstrate this, I’ll add the below lines of code in the method AddLoggedInUserIdentity() to show how we can add some custom Roles to user based on some condition. This we can later on use to Authorize access to endpoints or routes. In realworld we can obtain these values from a backing database or any other backend system internal to our application.
private void AddLoggedInUserIdentity(ClaimsPrincipal principal, IEnumerable<Claim> claims, UserProfile user)
{
var identity = new ClaimsIdentity();
if (!principal.HasClaim(x => x.Type == "DatabaseId"))
{
identity.AddClaim(
new Claim("DatabaseId", user.Id.ToString()));
}
if (!principal.HasClaim(x => x.Type == "LoginProvider"))
{
identity.AddClaim(
new Claim("LoginProvider", user.OIdProvider.ToString()));
}
// Just to demonstrate
// Add a special Role to all the Users
// who have an EVEN database Identitifier
if (user.Id % 2 == 0)
{
if (!principal.HasClaim(
x => x.Type == "LoginProvider" && x.Value == "EvenUser"))
{
identity.AddClaim(new Claim(ClaimTypes.Role, "EvenUser"));
}
}
else
{
if (!principal.HasClaim(
x => x.Type == "LoginProvider" && x.Value == "OddUser"))
{
identity.AddClaim(new Claim(ClaimTypes.Role, "OddUser"));
}
}
// add remaining claims from input
foreach (var claim in claims)
{
if (!principal.HasClaim(x => x.Type == claim.Type))
{
identity.AddClaim(claim);
}
}
principal.AddIdentity(identity);
}
Back in UserController, I add two new routes as below, where I can authorize user access based on custom Roles added during Claims Transformation.
namespace SocialNinja.Web.Controllers
{
[ResponseCache(
NoStore = true,
Location = ResponseCacheLocation.None)]
public class UsersController : Controller
{
private readonly IUnitOfWork _repo;
private readonly IUserManager _manager;
public UsersController(
IUnitOfWork repo,
IUserManager manager
)
{
_repo = repo;
_manager = manager;
}
[HttpGet, Route("[controller]/Login")]
public IActionResult Login()
{
return View();
}
[Authorize]
[HttpGet, Route("[controller]/Profile")]
public IActionResult Profile()
{
var claims = User.Claims.Select(
x => new KeyValuePair<string, string>(
x.Type, x.Value));
return View(claims);
}
[HttpGet, Route("[controller]/Logout")]
public IActionResult Logout()
{
_manager.SignOut();
return LocalRedirect("~/");
}
[Authorize(Roles = "OddUser")]
[HttpGet, Route("[controller]/OddUser")]
public IActionResult OddUsersOnly()
{
return Ok("Hello there! You're an Odd User");
}
[Authorize(Roles = "EvenUser")]
[HttpGet, Route("[controller]/EvenUser")]
public IActionResult EvenUsersOnly()
{
return Ok("Hello there! You're an Even User");
}
}
}
When I hit any of the endpoints /users/evenuser or /users/odduser, I may get a success string or get an access denied based on the Roles I added from within the Claims Transformation.
Conclusion
IClaimsTransformation is a useful interface provided by the framework to further customize and add any custom logic while creating Identity to logged in users. Since the implementation can be registered as any other service, we also have the provision to use the dependency injection features in a clean and decoupled fashion.
I find this implementation pretty useful in real-world scenarios where the Authentication is externalised but the Authorization needs to be done in integration with internal systems and data.
All the code snippets used in this article are a part of SocialNinja boilerplate where you can find working code for Social Logins implemented in an ASP.NET Core (.NET 6) MVC solution. If you find the solution useful, please do leave a Star in GitHub – it motivates me to find and build new opensource code for the community.
Please do share this article, if you find it useful.
No proper answer.
Can you elaborate.