ASP.NET Core Identity 2: User.IsInRole always returns false

Bob.at.Indigo.Health

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 = "[email protected]";
        const string TesterEmail = "[email protected]";
    
        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.

Bob.at.Indigo.Health

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.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

ASP.NET MVC 5 Identity 2 PasswordSignInAsync method always returns false

ASP.NET Core 2.0 Identity: SignInManager.IsSignedIn(User) returns false after signing in

User.IsInRole always returns false with Token Authentication

User.IsInRole() always returns false only in controller

User.Identity.IsAuthenticated always false in .net core custom authentication

User.IsInRole returns nothing in ASP.NET Core (Repository Pattern implemented)

Test a controller in .Net Core always returns false?

Proper way to assess Role in Authorization as User.IsInRole() always returns false

User.IsInRole always returns false in View or code using Policy based Authorization

How to custom/override User.IsInRole in ASP.NET Core

User.Identity.IsAuthenticated always return false .NET CORE C#

ASP.NET Core Identity 3.1: UserManager.RemoveFromRoleAsync always returns UserNotInRole

User.Identity.Name returns null in ASP.NET Core MVC application

ASP.NET Core Identity user groups

Asp.NET Identity Core IsInRole InvalidOperationException: Sequence contains more than one element

NLog .NET Core aspnet-user-identity always empty

ASP.NET CheckBox returns always false value

MVC 4: User.IsInRole() returns false after logout

Entity framework Core with Identity and ASP.NET Core RC2 not creating user in database

ASP.NET Core returns InternalServerError while using Identity server

ASP.NET Core 2 MVC CheckBoxFor always returning false in model

ASP.NET Core Custom Role Based Authorization (Custom User.IsInRole)?

Refresh user cookie ticket in ASP.Net Core Identity

asp.net core 2.0 identity entity framework user not saved

InvalidOperationException when registering a new user with ASP .NET Core Identity and EntityFrameworkCore

How to sign out other user in ASP.NET Core Identity

Adding name to the user model in Asp.Net Core Identity

Store User Settings in ASP.NET Core Identity AspNetUsers Table or Not

ASP.NET Core Identity - get current user