11

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() function that usually looks at the AD for roles.

However, when running the application it still seems to be using the original code and not my CustomClaimsPrincipal. I get the error "The trust relationship between the primary domain and the trusted domain failed".

In ASP.Net MVC 5 I used a Custom RoleProvider which is essentially what I am trying to replicate here.

CustomClaimsPrincipal.cs

public class CustomClaimsPrincipal : ClaimsPrincipal { private readonly ApplicationDbContext _context; public CustomClaimsPrincipal(ApplicationDbContext context) { _context = context; } public override bool IsInRole(string role) { var currentUser = ClaimsPrincipal.Current.Identity.Name; IdentityUser user = _context.Users.FirstOrDefault(u => u.UserName.Equals(currentUser, StringComparison.CurrentCultureIgnoreCase)); var roles = from ur in _context.UserRoles.Where(p => p.UserId == user.Id) from r in _context.Roles where ur.RoleId == r.Id select r.Name; if (user != null) return roles.Any(r => r.Equals(role, StringComparison.CurrentCultureIgnoreCase)); else return false; } } 

Startup.cs

 services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>(); services.AddScoped<ClaimsPrincipal,CustomClaimsPrincipal>(); 

Not sure if the above code in Startup.cs is the correct way to override the ClaimsPrincipal as I'm new to the .Net Core framework.

3
  • ClaimsPrincipal is not injected as a service so what you're doing won't work. It's set on HttpContext.User by the authentication provider. Commented Aug 17, 2018 at 5:54
  • 1
    It is not well documented, it won't throw an exception but Custom ClaimsPrincipal are not supported and may disappear further down in the pipeline: github.com/aspnet/Security/issues/323 Commented May 14, 2019 at 14:42
  • RoleProvider is not an MVC Framework class, it's an Identity Framework class. And it still exists as a Store Provider. Commented Sep 11, 2019 at 19:21

1 Answer 1

38

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<IClaimsTransformation, ClaimsTransformer>(); } } public class ClaimsTransformer : IClaimsTransformation { private readonly ApplicationDbContext _context; public ClaimsTransformer(ApplicationDbContext context) { _context = context; } public async Task<ClaimsPrincipal> 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<Claim> { 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.

Sign up to request clarification or add additional context in comments.

5 Comments

One caveat: you have currentUser instead of currentUserName on this line: IdentityUser user = await _context.Users.FirstOrDefaultAsync(u => u.UserName.Equals(currentUser, StringComparison.CurrentCultureIgnoreCase));
Thanks @RyanBattistone will make an edit straight away!
I will say though - this is an /excellent/ and painless way of quickly creating custom claims. This is a better explanation than anything I've found in the MSDN - which usually "hand waves" this stuff into a closed class that you can't really modify. (Pairs nicely with EF Core Power Tools, too, for those who want to take a data-first approach with claims that are already built in a DB.)
Thanks, this works great. Once question though: The TransformAsync function gets called multiple times for each request. Already tried to add the services as a scoped service, but that didn't help. Does anyone know why it runs multiple times, or how to call the function only once each request?
The fact that it's called multiple times isn't linked to the lifetime used for the service registration. You can't prevent it from being called multiple times, but you can prevent side-effects by returning a new ClaimsPrincipal instead of a modified one. More info in Brock Allen's post here: brockallen.com/2017/08/30/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.