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.