ASP.NET Core Identity 2: User.IsInRole always returns false - asp.net-core

The question: I call RoleManager.CreateAsync() and RoleManager.AddClaimAsync() to create roles and associated role claims. Then I call UserManager.AddToRoleAsync() to add users to those roles. But when the user logs in, neither the roles nor the associated claims show up in the ClaimsPrincipal (i.e. the Controller's User object). The upshot of this is that User.IsInRole() always returns false, and the collection of Claims returned by User.Claims doesn't contain the role claims, and the [Authorize(policy: xxx)] annotations don't work.
I should also add that one solution is to revert from using the new services.AddDefaultIdentity() (which is provided by the templated code) back to calling services.AddIdentity().AddSomething().AddSomethingElse(). I don't want to go there, because I've seen too many conflicting stories online about what I need to do to configure AddIdentity for various use cases. AddDefaultIdentity seems to do most things correctly without a lot of added fluent configuration.
BTW, I'm asking this question with the intention of answering it... unless someone else gives me a better answer than the one I'm prepared to post. I'm also asking this question because after several weeks of searching I have yet to find a good end-to-end example of creating and using Roles and Claims in ASP.NET Core Identity 2. Hopefully, the code example in this question might help someone else who stumbles upon it...
The setup:
I created a new ASP.NET Core Web Application, select Web Application (Model-View-Controller), and change the Authentication to Individual User Accounts. In the resultant project, I do the following:
In Package Manager Console, update the database to match the scaffolded migration:
update-database
Add an ApplicationUser class that extends IdentityUser. This involves adding the class, adding a line of code to the ApplicationDbContext and replacing every instance of <IdentityUser> with <ApplicationUser> everywhere in the project.
The new ApplicationUser class:
public class ApplicationUser : IdentityUser
{
public string FullName { get; set; }
}
The updated ApplicationDbContext class:
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{ }
// Add this line of code
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
}
In Package Manager Console, create a new migration and update the database to incorporate the ApplicationUsers entity.
add-migration m_001
update-database
Add the following line of code in Startup.cs to enable RoleManager
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
Add some code to seed roles, claims, and users. The basic concept for this sample code is that I have two claims: can_report allows the holder to create reports, and can_test allows the holder to run tests. I have two Roles, Admin and Tester. The Tester role can run tests, but can't create reports. The Admin role can do both. So, I add the claims to the roles, and create one Admin test user and one Tester test user.
First, I add a class whose sole purpose in life is to contain constants used elsewhere in this example:
// Contains constant strings used throughout this example
public class MyApp
{
// Claims
public const string CanTestClaim = "can_test";
public const string CanReportClaim = "can_report";
// Role names
public const string AdminRole = "admin";
public const string TesterRole = "tester";
// Authorization policy names
public const string CanTestPolicy = "can_test";
public const string CanReportPolicy = "can_report";
}
Next, I seed my roles, claims, and users. I put this code in the main landing page controller just for expedience; it really belongs in the "startup" Configure method, but that's an extra half-dozen lines of code...
public class HomeController : Controller
{
const string Password = "QwertyA1?";
const string AdminEmail = "admin#example.com";
const string TesterEmail = "tester#example.com";
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
// Constructor (DI claptrap)
public HomeController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IActionResult> Index()
{
// Initialize roles
if (!await _roleManager.RoleExistsAsync(MyApp.AdminRole)) {
var role = new IdentityRole(MyApp.AdminRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanReportClaim, ""));
}
if (!await _roleManager.RoleExistsAsync(MyApp.TesterRole)) {
var role = new IdentityRole(MyApp.TesterRole);
await _roleManager.CreateAsync(role);
await _roleManager.AddClaimAsync(role, new Claim(MyApp.CanTestClaim, ""));
}
// Initialize users
var qry = _userManager.Users;
IdentityResult result;
if (await qry.Where(x => x.UserName == AdminEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = AdminEmail,
Email = AdminEmail,
FullName = "Administrator"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.AdminRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
if (await qry.Where(x => x.UserName == TesterEmail).FirstOrDefaultAsync() == null) {
var user = new ApplicationUser {
UserName = TesterEmail,
Email = TesterEmail,
FullName = "Tester"
};
result = await _userManager.CreateAsync(user, Password);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
result = await _userManager.AddToRoleAsync(user, MyApp.TesterRole);
if (!result.Succeeded) throw new InvalidOperationException(string.Join(" | ", result.Errors.Select(x => x.Description)));
}
// Roles and Claims are in a cookie. Don't expect to see them in
// the same request that creates them (i.e., the request that
// executes the above code to create them). You need to refresh
// the page to create a round-trip that includes the cookie.
var admin = User.IsInRole(MyApp.AdminRole);
var claims = User.Claims.ToList();
return View();
}
[Authorize(policy: MyApp.CanTestPolicy)]
public IActionResult Test()
{
return View();
}
[Authorize(policy: MyApp.CanReportPolicy)]
public IActionResult Report()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
and I register my authentication policies in the "Startup" ConfigureServices routine, just after the call to services.AddMvc
// Register authorization policies
services.AddAuthorization(options => {
options.AddPolicy(MyApp.CanTestPolicy, policy => policy.RequireClaim(MyApp.CanTestClaim));
options.AddPolicy(MyApp.CanReportPolicy, policy => policy.RequireClaim(MyApp.CanReportClaim));
});
Whew. Now, (assuming I've noted all of the applicable code I've added to the project, above), when I run the app, I notice that neither of my "built-in" test users can access either the /home/Test or /home/Report page. Moreover, if I set a breakpoint in the Index method, I see that my roles and claims do not exist in the User object. But I can look at the database and see all of the roles and claims are there.

So, to recap, the question asks why the code provided by the ASP.NET Core Web Application template doesn't load roles or role claims into the cookie when a user logs in.
After much Googling and experimenting, there appear to be two modifications that must be made to the templated code in order to get Roles and Role Claims to work:
First, you must add the following line of code in Startup.cs to enable RoleManager. (This bit of magic was mentioned in the OP.)
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>() // <-- Add this line
.AddEntityFrameworkStores<ApplicationDbContext>();
But wait, there's more! According to this discussion on GitHub, getting the roles and claims to show up in the cookie involves either reverting to the service.AddIdentity initialization code, or sticking with service.AddDefaultIdentity and adding this line of code to ConfigureServices:
// Add Role claims to the User object
// See: https://github.com/aspnet/Identity/issues/1813#issuecomment-420066501
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
If you read the discussion referenced above, you'll see that Roles and Role Claims are apparently kind-of-deprecated, or at least not eagerly supported. Personally, I find it really useful to assign claims to roles, assign roles to users, and then make authorization decisions based on the claims (which are granted to the users based on their roles). This gives me an easy, declarative way to allow, for example, one function to be accessed by multiple roles (i.e. all of the roles that contain the claim used to enable that function).
But you DO want to pay attention to the amount of role and claim data being carried in the auth cookie. More data means more bytes sent to the server with each request, and I have no clue what happens when you bump up against some sort of limit to the cookie size.

Ahh, there are some changes from ASP.NET Core version 2.0 to 2.1. AddDefaultIdentity is the one.
I don't know where to start from your code, so, I will provide an example to create and get user role(s).
Let's create UserRoles first:
public enum UserRoles
{
[Display(Name = "Quản trị viên")]
Administrator = 0,
[Display(Name = "Kiểm soát viên")]
Moderator = 1,
[Display(Name = "Thành viên")]
Member = 2
}
Note: You can remove the attribute Display.
Then, we create RolesExtensions class:
public static class RolesExtensions
{
public static async Task InitializeAsync(RoleManager<IdentityRole> roleManager)
{
foreach (string roleName in Enum.GetNames(typeof(UserRoles)))
{
if (!await roleManager.RoleExistsAsync(roleName))
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
}
}
Next, in the Startup.cs class, we run it:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
RoleManager<IdentityRole> roleManager)
{
// other settings...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
var task = RolesExtensions.InitializeAsync(roleManager);
task.Wait();
}
Note: Configure requires a returned type void, so we need to create a task to initialize the user roles and we call Wait method.
Do not change the returned type like this:
public async void Configure(...)
{
await RolesExtensions.InitializeAsync(roleManager);
}
Source: Async/Await - Best Practices in Asynchronous Programming
In the ConfigureServices method, these configurations would NOT work (we cannot use User.IsInRole correctly):
services.AddDefaultIdentity<ApplicationUser>()
//.AddRoles<IdentityRole>()
//.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
I don't know why but AddRoles and AddRoleManager don't support to check role for a user (User.IsInRole).
In this case, we need to register service like this:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
By using this way, we create 3 user roles in the databse:
When register new user, we just need to call:
await _userManager.AddToRoleAsync(user, nameof(UserRoles.Administrator));
Finally, we can use [Authorize(Roles = "Administrator")] and:
if (User.IsInRole("Administrator"))
{
// authorized
}
// or
if (User.IsInRole(nameof(UserRoles.Administrator)))
{
// authorized
}
// but
if (User.IsInRole("ADMINISTRATOR"))
{
// authorized
}
P/S: There are a lot things which need to be implement to achieve this goal. So maybe I missed something in this example.

Also you can try to fix Authentication like this
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
});

If I use “Roles” instead of ClaimTypes.Role in .net6 blazor wasm , #attribute [Authorize(Roles = "admin")] not work and get this error in browser console :
RolesAuthorizationRequirement:User.IsInRole must be true for one of the following roles: (admin)”
By using of ClaimTypes.Role the problem resolved :
private async Task<List<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>()
{
new Claim("UserName", user.Email),
new Claim("FullName", user.FirstName+" "+user.LastName),
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
claims.Add(new Claim(ClaimTypes.Role, role)); // this line
return claims;
}
https://github.com/mammadkoma/Attendance/blob/master/Attendance/Server/Controllers/AccountsController.cs

Related

Windows Authentication using Active Directory Groups as Authorization Roles in ASP.NET Core 2.2?

I have a dot net core 2.2 application that needs to have Windows Authentication with an Active Directory Group lookup to get a list of assigned groups for the current principal. These assigned groups will be the 'roles' that will be used in the Authorize attribute of certain methods. At least, in theory, that's what I'm hoping to accomplish.
I have completed the AD lookup and retrieval of the groups. At this point I'm not sure how to configure the Startup to persist this info within an auth token/cookie of some type or any UserManager/RoleManager setup kinda stuff.
Here are a couple of previous, somewhat similar questions, among others I've looked at. This previous post from .net 4.5 appears to be a similar issue, but it's the wrong version of .NET : windows-authentication-with-active-directory-groups. Can these AD groups be added as roles? Here's a potentially helpful post with this where they create a role for a user: how-to-create-roles-in-asp-net-core-2-2-and-assign-them-to-users. Confused about how this works. I've always found Identity, claims, tokens, etc. confusing so hopefully someone can assist with this in Core 2.2.
What do I need to do to get this to work? I've included most of my current code (AD code, some middleware parts, etc.), but then what? I'm sure there are others that would benefit from this too! Thank you!
I get the current Windows user and their AD record here:
return Task.Run(() =>
{
try
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
//var identityName = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
var identityName = identity.Name; // when windows authentication is checked
principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, identity.Name);
}
return AdUser.CastToAdUser(principal);
}
catch (Exception ex)
{
//TODO LOGGING
throw new Exception("Error retrieving AD User", ex);
}
});
The extension method CastToAdUser to create a more useful model is here:
public static AdUser CastToAdUser(UserPrincipal user)
{
return new AdUser
{
AccountExpirationDate = user.AccountExpirationDate,
AccountLockoutTime = user.AccountLockoutTime,
BadLogonCount = user.BadLogonCount,
Description = user.Description,
DisplayName = user.DisplayName,
DistinguishedName = user.DistinguishedName,
EmailAddress = user.EmailAddress,
EmployeeId = user.EmployeeId,
Enabled = user.Enabled,
GivenName = user.GivenName,
Guid = user.Guid,
HomeDirectory = user.HomeDirectory,
HomeDrive = user.HomeDrive,
LastBadPasswordAttempt = user.LastBadPasswordAttempt,
LastLogon = user.LastLogon,
LastPasswordSet = user.LastPasswordSet,
MiddleName = user.MiddleName,
Name = user.Name,
PasswordNeverExpires = user.PasswordNeverExpires,
PasswordNotRequired = user.PasswordNotRequired,
SamAccountName = user.SamAccountName,
ScriptPath = user.ScriptPath,
Sid = user.Sid,
Surname = user.Surname,
UserCannotChangePassword = user.UserCannotChangePassword,
UserPrincipalName = user.UserPrincipalName,
VoiceTelephoneNumber = user.VoiceTelephoneNumber,
Token = string.Empty,
};
}
return Task.Run(() =>
{
PrincipalSearchResult<Principal> groups = UserPrincipal.Current.GetGroups();
IEnumerable<SecurityGroup> securityGroups = groups.Select(x => x.ToAdUserSecurityGroups());
return securityGroups;
});
With the extension method to create a useful model, ToAdUserSecurityGroups here:
public static SecurityGroup ToAdUserSecurityGroups (this Principal result)
{
var securityGroup = new SecurityGroup
{
Sid = result.Sid.Value,
Name = result.SamAccountName,
Guid = result.Guid.Value,
};
return securityGroup;
}
So now I have the AD user, and the security groups that will hopefully be used for Authorization. I wire in my AD lookup stuff using some custom middleware, called UseAdMiddleWare. In my Startup class, I have an extension in the Configure method to fire off all the above 'stuff':
app.UseAdMiddleware();
And in my ConfigureServices I have the AddAuthentication stuff, which is needed, but might not be configured correctly for what I'm trying to do:
services.AddAuthentication();
In separate classes I have the code that allows this. The IAdUserProvider is my own class that does the AD lookup, with an entry point called Create:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseAdMiddleware(this IApplicationBuilder builder) =>
builder.UseMiddleware<AdUserMiddleware>();
}
public class AdUserMiddleware
{
private readonly RequestDelegate next;
public AdUserMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context, IAdUserProvider userProvider, IConfiguration config)
{
if (!(userProvider.Initialized))
{
await userProvider.Create(context, config);
}
await next(context);
}
}
So I think I'm well on my way to getting this wired up, but how/where do I add the security group specifics into claims or whatever? Thank you very much!
I (mostly) found a solution to this using Nan's recommendation to use the IClaimsTransformer. The concrete implementation of this class fires every Authorize request, and I'm not sure if there's a possible way to persist these claims?
Here's my Startup.ConfigureServices, where I have some IIS options to automatically log in using my Windows auth, and there's the line to create the singleton of my IClaimsTransformation:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.Configure<IISServerOptions>(options =>
{
options.AutomaticAuthentication = true;
});
services.Configure<IISOptions>(options =>
{
options.AutomaticAuthentication = true;
options.ForwardClientCertificate = true;
});
services.AddSingleton<IClaimsTransformation, CustomClaimsTransformation>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
}
In Startup.Configure I have this: Do I need the cookiepolicy?
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Add whatever you typically need here...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
My CustomClaimsTransformation is here, and this fires at each Authorize. Is this normal? I'm adding the Security Groups as ROLES so I can use these to Authorize the users based on the groups they are assigned. I had hoped that this would be handled once, and the claims would be permanent for the duration. Thoughts on this?
public class CustomClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
//add new claim
// Check and see if Groups are already part of the principal, and add them as claims.
// var groups = userClaimsId.Claims.Where(x => x.Type.Equals("groups")).ToList();
var ci = (ClaimsIdentity)principal.Identity;
var c = new Claim(ci.RoleClaimType, "Super_Special_User");
ci.AddClaim(c);
return Task.FromResult(principal);
}
}
Within the Controller I add the Authorize attribute (seems to be case sensitive). It might be a good ideas to create a static class of role string constants to hold all these values. Keeps you free of the magic strings all over the place.
[Authorize(Roles = "Super_Special_User")]
Please let me know if I can improve this! Thanks for your time!

Authorization .net core 2.0

I have setup cookie authentication in my asp.net core application. I have a login page where the credentials matchup against the active directory. All this works fine.
Now next I want to implement authorization in my application. I have a table of users together with permission against them. For example permission like Reading & Write. When the user is successfully authenticated I want to check for these permissions and show them certain functionality while restricting others. For example, show certain dropdowns for write permission while hiding for reading permission.
What is the best approach to handle this in the .NET Core.
I have read about adding policy like:
services.AddAuthorization(options => {
options.AddPolicy("Read", policy =>
policy.RequireClaim("Read", "MyCLaim"));
});
Then in my controller:
[Authorize(Policy = "Read")]
public class HomeController : Controller
{
}
Where do I get the permissions for logged in user from my database and how to verify if the user has those permissions or not.
Would appreciate inputs.
Where do I get the permissions for logged in user from my database and
how to verify if the user has those permissons or not.
Right after a user is authenticated, you collect user's claims, and store them in Authentication Cookie.
For example, SignInAsync method.
public async Task SignInAsync(User user, IList<string> roleNames)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.GivenName, user.FirstName),
new Claim(ClaimTypes.Surname, user.LastName)
};
foreach (string roleName in roleNames)
{
claims.Add(new Claim(ClaimTypes.Role, roleName));
}
var identity = new ClaimsIdentity(claims, "local", "name", "role");
var principal = new ClaimsPrincipal(identity);
await _httpContextAccessor.HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme, principal);
}
FYI: It happens to be that I store them as Role claims. You do not have to follow that route, if you don't want.
You can then verify the policy inside Startup.cs.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
// Set up policies from claims
// https://leastprivilege.com/2016/08/21/why-does-my-authorize-attribute-not-work/
services.AddAuthorization(options =>
{
options.AddPolicy(Constants.RoleNames.Administrator, policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser()
.RequireAssertion(context => context.User.HasClaim(
ClaimTypes.Role, Constants.RoleNames.Administrator))
.Build();
});
});
...
}
}
Usage is same as what you have described.
[Authorize(Policy = "Read")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}

How can I add new roles in ASP.Net Core?

I am new to asp.net core and want to know how can I add my custom roles in ASP.Net Core. For example seller, buyer, etc. so I can restrict them to some actions.
I am using default ASP.Net Core template with individual user authentication for now.
I've answered this question multiple times here, and because of the occurrence, I decided to write an article about it here. However, I'll answer it once again.
Here's how you go about it Wajahat
You could do this easily by creating a CreateRoles method in your startup class. This helps check if the roles are created, and creates the roles if they aren't; on application startup. Like so.
private async Task CreateRoles(IServiceProvider serviceProvider)
{
//initializing custom roles
var RoleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
var UserManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
string[] roleNames = { "Admin", "Store-Manager", "Member" };
IdentityResult roleResult;
foreach (var roleName in roleNames)
{
var roleExist = await RoleManager.RoleExistsAsync(roleName);
// ensure that the role does not exist
if (!roleExist)
{
//create the roles and seed them to the database:
roleResult = await RoleManager.CreateAsync(new IdentityRole(roleName));
}
}
// find the user with the admin email
var _user = await UserManager.FindByEmailAsync("admin#email.com");
// check if the user exists
if(_user == null)
{
//Here you could create the super admin who will maintain the web app
var poweruser = new ApplicationUser
{
UserName = "Admin",
Email = "admin#email.com",
};
string adminPassword = "p#$$w0rd";
var createPowerUser = await UserManager.CreateAsync(poweruser, adminPassword);
if (createPowerUser.Succeeded)
{
//here we tie the new user to the role
await UserManager.AddToRoleAsync(poweruser, "Admin");
}
}
}
and then you could call the await CreateRoles(serviceProvider); method from the Configure method in the Startup class.
ensure you have IServiceProvider as a parameter in the Configure class.
To restrict them to some actions. You can easily define what roles have access to certain controllers or controller actions, like so.
[Authorize(Roles="Admin")]
public class ManageController : Controller
{
//....
}
You can also use role-based authorization in the action method like so. Assign multiple roles, if you will
[Authorize(Roles="Admin")]
public IActionResult Index()
{
/*
.....
*/
}
While this works fine, for a much better practice, you might want to read about using policy-based authorization or role checks. You can find it on the ASP.NET core documentation here, or this article I wrote about it here

ASP.NET Core Windows Authentication and Application Roles

I'm trying to create a fairly simple intranet application that will use Active Directory for authentication, and will use the AspNetRoles table to check if the user is in a certain application role. This app is just an in-house lottery where some users can create events/contests that other users can then submit an entry to the contest. I'm thinking of starting out with 2 basic roles:
Administrator - Can perform CRUD operations on "Event" or
"Contest" entities
Contestant - Can perform GET operations on
"Contest" entities, and can create new "Entry" entities.
Here's where I'm stuck: I've got Windows Authentication working in the sense that from a controller, I can do a User.Identity.Name and see my domain login name. Furthermore, I can verify that an account belongs to a domain group by doing User.IsInRole("Domain Users"). If I want to avoid creating new AD groups for each role in my application (let's say design changes down the road require additional roles), how can I use Authorization on controllers to check against Application Roles?
Here's an example controller I want to use:
[Route("api/[controller]")]
[Authorize(Roles = "Contestant")]
public class EventTypesController : Controller
{
private IRaffleRepository _repository;
private ILogger<EventTypesController> _logger;
public EventTypesController(IRaffleRepository repository, ILogger<EventTypesController> logger)
{
_repository = repository;
_logger = logger;
}
[HttpGet("")]
public IActionResult Get()
{
try
{
var results = _repository.GetAllEventTypes();
return Ok(Mapper.Map<IEnumerable<EventTypeViewModel>>(results));
}
catch (Exception ex)
{
_logger.LogError($"Failed to get all event types: {ex}");
return BadRequest("Error occurred");
}
}
}
In my Startup.cs, in ConfigureServices, I'm wiring up Identity as follows:
services.AddIdentity<RaffleUser, ApplicationRole>()
.AddEntityFrameworkStores<RaffleContext>();
My RaffleUser class is really just the default implementation of IdentityUser:
public class RaffleUser : IdentityUser
{
}
My ApplicationRole class is also just the default implementation of IdentityRole. I also tried seeding some data in a seed class:
if (!await _roleManager.RoleExistsAsync("Administrator"))
{
var adminRole = new ApplicationRole()
{
Name = "Administrator"
};
await _roleManager.CreateAsync(adminRole);
await _context.SaveChangesAsync();
}
if (await _userManager.FindByNameAsync("jmoor") == null)
{
using (var context = new PrincipalContext(ContextType.Domain))
{
var principal = UserPrincipal.FindByIdentity(context, "DOMAIN\\jmoor");
if (principal != null)
{
var user = new RaffleUser()
{
Email = principal.EmailAddress,
UserName = principal.SamAccountName
};
await _userManager.CreateAsync(user);
await _context.SaveChangesAsync();
var adminRole = await _roleManager.FindByNameAsync("Administrator");
if (adminRole != null)
{
await _userManager.AddToRoleAsync(user, adminRole.Name);
await _context.SaveChangesAsync();
}
}
}
}
The data makes it to the tables, but it just seems like at the controller level, I need to convert the authenticated user to an IdentityUser. Do I need some middleware class to do this for me? Would that be the best way to make authorization reusable on all controllers?
First, I ended up creating a custom ClaimsTransformer that returns a ClaimsPrincipal populated with UserClaims and RoleClaims (after refactoring my app, I decided to go with policy-based authorization, and the access claim can be added at either the role or user level):
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var identity = (ClaimsIdentity)context.Principal.Identity;
var userName = identity.Name;
if (userName != null)
{
var user = await _userManager.FindByLoginAsync("ActiveDirectory", userName);
if (user != null)
{
identity.AddClaims(await _userManager.GetClaimsAsync(user));
var roles = await _userManager.GetRolesAsync(user);
identity.AddClaims(await GetRoleClaims(roles));
}
}
return context.Principal;
}
private async Task<List<Claim>> GetRoleClaims(IList<string> roles)
{
List<Claim> allRoleClaims = new List<Claim>();
foreach (var role in roles)
{
var rmRole = await _roleManager.FindByNameAsync(role);
var claimsToAdd = await _roleManager.GetClaimsAsync(rmRole);
allRoleClaims.AddRange(claimsToAdd);
}
return allRoleClaims;
}
I wired that up in the Startup.cs:
services.AddScoped<IClaimsTransformer, Services.ClaimsTransformer>();
I also went with Policy-based authorization:
services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", policy => policy.RequireClaim("AccessLevel", "Administrator"));
options.AddPolicy("Project Manager", policy => policy.RequireClaim("AccessLevel", "Project Manager"));
});
So, users or roles can have a claim set with a name of "AccessLevel" and a value specified. To finish everything off, I also created a custom UserManager that just populates the User object with additional details from ActiveDirectory during a CreateAsync.
You need to add a DefaultChallangeScheme to use Windows authentication. This is how i do, but if someone has a better solution i am all ears :)
I use the following setup in my current application.
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<SecurityDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
});
Then i put in my application claims in a transformer.
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
I hope this will get you in the right direction.

Recommended best practice for role claims as permissions

The app I am working on is a SPA and we are using JWT Bearer authentication and OpenIdConnect/OAuth2 when communicating with our backend API which uses .NETCore and ASP.NET Identity. Our API endpoints are secured using Custom Policy based authentication as shown here:
Custom Policy Based Authentication
We decided to use the out of the box AspNetRoleClaims table to store claims for our users as permissions. Each user is assigned 1 primary role although the potential is there to have multiple roles. Each role will have many claims - which are stored in the AspNetRoleClaims table.
Role claims would look like this:
ClaimType: Permission
ClaimValue(s):
MyModule1.Create
MyModule1.Read
MyModule1.Edit
MyModule1.Delete
MyModule1.SomeOtherPermission
MyModule2.Read
MyModule3.Read
MyModule3.Edit
etc.
The more permissions or role claims that a user has, the larger the access_token will be, thereby increasing the HTTP header size. Also the ASP.NET Identity Authorization cookie - as there are more and more role claims it gets chunked out into multiple cookies.
I have experimented with adding in a lot of role claims and eventually the request fails because the header gets too big.
I am looking for some advice on what is considered "best practice" when it comes to bearer authentication with role claims. Microsoft gives you AspNetRoleClaims out of the box that work for my scenario and from what I understand the advantage of storing these role claims in the access_token is that we don't have to hit the database on each API endpoint that is secured with the custom policy.
The way I see it, I can try to make the claim values smaller, and in the case of where a user has multiple roles that may share common role claims that are duplicated, I can try to intercept when these get written into the cookie and remove the duplicates.
However, since the app is still in development, I can foresee more and more roles claims being added and there is always the possibility that the HTTP header will become too large with the cookies and the access_token. Not sure if this is the best approach.
The only alternative I see is to hit the database each time we hit our protected API. I could inject a DbContext in each custom claim policy requirement handler and talk to the AspNetRoleClaims table on each request.
I haven't seen too many examples out there of how people accomplish a more finely grained permissions scheme with ASP.NET Identity and .NET Core API. This must be a fairly common requirement I would think...
Anyways, just looking for some feedback and advice on recommended best practice for a scenario like this.
****UPDATE - See answer below ****
I never did find a recommended "best practice" on how to accomplish this but thanks to some helpful blog posts I was able to architect a nice solution for the project I was working on. I decided to exclude the identity claims from the id token and the Identity cookie and do the work of checking the users permissions (role claims) server side with each request.
I ended up using the architecture are described above, using the built in AspNetRoleClaims table and populating it with permissions for a given role.
For example:
ClaimType: Permission
ClaimValue(s):
MyModule1.Create
MyModule1.Read
MyModule1.Edit
MyModule1.Delete
I use Custom policy based authentication as described in the Microsoft article in the link above.
Then I lock down each of my API endpoints with the Role based policy.
I also have an enum class that has all the permissions stored as enums. This enum just lets me refer to the permission in code without having to use magic strings.
public enum Permission
{
[Description("MyModule1.Create")]
MyModule1Create,
[Description("MyModule1.Read")]
MyModule1Read,
[Description("MyModule1.Update")]
MyModule1Update,
[Description("MyModule1.Delete")]
MyModule1Delete
}
I register the permissions in Startup.cs like so:
services.AddAuthorization(options =>
{
options.AddPolicy("MyModule1Create",
p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Create)));
options.AddPolicy("MyModule1Read",
p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Read)));
options.AddPolicy("MyModule1Update",
p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Update)));
options.AddPolicy("MyModule1Delete",
p => p.Requirements.Add(new PermissionRequirement(Permission.MyModule1Delete)));
}
So there is a matching Permission and a PermissionRequirement like so:
public class PermissionRequirement : IAuthorizationRequirement
{
public PermissionRequirement(Permission permission)
{
Permission = permission;
}
public Permission Permission { get; set; }
}
public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>,
IAuthorizationRequirement
{
private readonly UserManager<User> _userManager;
private readonly IPermissionsBuilder _permissionsBuilder;
public PermissionRequirementHandler(UserManager<User> userManager,
IPermissionsBuilder permissionsBuilder)
{
_userManager = userManager;
_permissionsBuilder = permissionsBuilder;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
if (context.User == null)
{
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
{
return;
}
var roleClaims = await _permissionsBuilder.BuildRoleClaims(user);
if (roleClaims.FirstOrDefault(c => c.Value == requirement.Permission.GetEnumDescription()) != null)
{
context.Succeed(requirement);
}
}
}
The extension method on the permission GetEnumDescription just takes the enum that I have in the code for each permission and translates it to the same string name as it is stored in the database.
public static string GetEnumDescription(this Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null &&
attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
My PermissionHandler has a PermissionsBuilder object. This is a class I wrote that will hit the database and check if the logged in user has a particular role claim.
public class PermissionsBuilder : IPermissionsBuilder
{
private readonly RoleManager<Role> _roleManager;
public PermissionsBuilder(UserManager<User> userManager, RoleManager<Role> roleManager)
{
UserManager = userManager;
_roleManager = roleManager;
}
public UserManager<User> UserManager { get; }
public async Task<List<Claim>> BuildRoleClaims(User user)
{
var roleClaims = new List<Claim>();
if (UserManager.SupportsUserRole)
{
var roles = await UserManager.GetRolesAsync(user);
foreach (var roleName in roles)
{
if (_roleManager.SupportsRoleClaims)
{
var role = await _roleManager.FindByNameAsync(roleName);
if (role != null)
{
var rc = await _roleManager.GetClaimsAsync(role);
roleClaims.AddRange(rc.ToList());
}
}
roleClaims = roleClaims.Distinct(new ClaimsComparer()).ToList();
}
}
return roleClaims;
}
}
I build up a list of distinct role claims for a user - I use a ClaimsComparer class to help do this.
public class ClaimsComparer : IEqualityComparer<Claim>
{
public bool Equals(Claim x, Claim y)
{
return x.Value == y.Value;
}
public int GetHashCode(Claim claim)
{
var claimValue = claim.Value?.GetHashCode() ?? 0;
return claimValue;
}
}
The controllers are locked down with the role based custom policy:
[HttpGet("{id}")]
[Authorize(Policy = "MyModule1Read", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Get(int id){
Now here is the important part - you need to override the UserClaimsPrincipalFactory in order to prevent the role claims from being populated into the Identity cookie. This solves the problem of the cookie and the headers being too big. Thanks to Ben Foster for his helpful posts (see links below)
Here is my custom AppClaimsPrincipalFactory:
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<User, Role>
{
public AppClaimsPrincipalFactory(UserManager<User> userManager, RoleManager<Role> roleManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity("Identity.Application",
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
// code removed that adds the role claims
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
return new ClaimsPrincipal(id);
}
}
Register this class in Startup.cs
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// override UserClaimsPrincipalFactory (to remove role claims from cookie )
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, AppClaimsPrincipalFactory>();
Here are the links to Ben Foster's helpful blog posts:
AspNet Identity Role Claims
Customizing claims transformation in AspNet Core Identity
This solution has worked well for the project I was working on - hope it helps someone else out.
I haven't seen too many examples out there of how people accomplish a more finely grained permissions scheme with ASP.NET Identity and .NET Core API. This must be a fairly common requirement I would think...
Your current design is RBAC (Role Based Access Control). Since you are experiencing a "role explosion", you need ReBAC (Relationship Based Access Control), which allows for fine-grained permissions. See my other answer here for more details on current offerings.