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 extendsIdentityUser
. This involves adding the class, adding a line of code to theApplicationDbContext
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-databaseAdd the following line of code in
Startup.cs
to enableRoleManager
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, andcan_test
allows the holder to run tests. I have two Roles,Admin
andTester
. TheTester
role can run tests, but can't create reports. TheAdmin
role can do both. So, I add the claims to the roles, and create oneAdmin
test user and oneTester
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 toservices.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.
Also you can try to fix Authentication like this
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:Note: You can remove the attribute
Display
.Then, we create
RolesExtensions
class:Next, in the
Startup.cs
class, we run it:Note:
Configure
requires a returned typevoid
, so we need to create a task to initialize the user roles and we callWait
method.Do not change the returned type like this:
Source: Async/Await - Best Practices in Asynchronous Programming
In the
ConfigureServices
method, these configurations would NOT work (we cannot useUser.IsInRole
correctly):I don't know why but
AddRoles
andAddRoleManager
don't support to check role for a user (User.IsInRole
).In this case, we need to register service like this:
By using this way, we create 3 user roles in the databse:
When register new user, we just need to call:
Finally, we can use
[Authorize(Roles = "Administrator")]
and:P/S: There are a lot things which need to be implement to achieve this goal. So maybe I missed something in this example.
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.)
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 withservice.AddDefaultIdentity
and adding this line of code toConfigureServices
: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.