I am creating a Windows Authentication app but the roles sit within the custom database and not on the AD so I created a custom ClaimsPrincipal to override the User.IsInRole() f
I think I would tackle that problem differently: instead of trying to have the instance of ClaimsPrincipal talk to the database to figure out if they belong to a specific role, I would modify the ClaimsPrincipal and add the roles they belong to in the ClaimsPrincipal instance.
To do so, I would use a feature that is unfortunately not well documented. The authentication pipeline exposes an extensibility point where once the authentication is done, you can transform the ClaimsPrincipal instance that was created. This can be done through the IClaimsTransformation interface.
The code could look something like:
public class Startup
{
public void ConfigureServices(ServiceCollection services)
{
// Here you'd have your registrations
services.AddTransient();
}
}
public class ClaimsTransformer : IClaimsTransformation
{
private readonly ApplicationDbContext _context;
public ClaimsTransformer(ApplicationDbContext context)
{
_context = context;
}
public async Task TransformAsync(ClaimsPrincipal principal)
{
var existingClaimsIdentity = (ClaimsIdentity)principal.Identity;
var currentUserName = existingClaimsIdentity.Name;
// Initialize a new list of claims for the new identity
var claims = new List
{
new Claim(ClaimTypes.Name, currentUserName),
// Potentially add more from the existing claims here
};
// Find the user in the DB
// Add as many role claims as they have roles in the DB
IdentityUser user = await _context.Users.FirstOrDefaultAsync(u => u.UserName.Equals(currentUserName, StringComparison.CurrentCultureIgnoreCase));
if (user != null)
{
var rolesNames = from ur in _context.UserRoles.Where(p => p.UserId == user.Id)
from r in _context.Roles
where ur.RoleId == r.Id
select r.Name;
claims.AddRange(rolesNames.Select(x => new Claim(ClaimTypes.Role, x)));
}
// Build and return the new principal
var newClaimsIdentity = new ClaimsIdentity(claims, existingClaimsIdentity.AuthenticationType);
return new ClaimsPrincipal(newClaimsIdentity);
}
}
For full disclosure, the TransformAsync method will run every time the authentication process takes place, so most likely on every request, also meaning it will query the database on every request to fetch the roles of the logged-in user.
The advantage of using this solution over modifying the implementation of ClaimsPrincipal is that the ClaimsPrincipal is now dumb and not tied to your database. Only the authentication pipeline knows about it, which makes things like testing easier as you could, for example, new-up a ClaimsPrincipal with specific roles to make sure they do or don't have access to specific actions, without being tied to the database.