Search code examples
asp.net-mvcasp.net-coreasp.net-identity

ASP.NET Core EF Identity Roles while using JWT Bearer Scheme


I have an ASP.Net Core API project, which is working on a database that was built for a ASP.NET MVC project (on .NET 4.6) using Asp.Net Identity.

My ASP.Net Core Api is using JWT Token validation for Authorization on API calls, but is also using the UserManager to Check Username/Passwords to authenticate before giving a token. Previous question regarding this asked here

The challenge I had was that Using

builder.Services.AddIdentity<User, UserRole>()
                    .AddEntityFrameworkStores<AuthdbContext>();

in conjunction with

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer()

Resulted in my controllers not authenticating on the Token. Following the advice OF this stackoverflow post I stopped using AddIdentity and I am now instead doing this

IdentityBuilder idbuilder = builder.Services.AddIdentityCore<User>();
idbuilder = new IdentityBuilder(idbuilder.UserType, builder.Services);
idbuilder.AddRoles<Role>(); // Add this line
idbuilder.AddEntityFrameworkStores<AuthdbContext>();
idbuilder.AddSignInManager<SignInManager<User>>();
idbuilder.AddRoleManager<RoleManager<Role>>();

And now my authorization logic is working nicely. All is well in the world, but now I am trying to work with ROLES.

Just for background, I scaffolded 3 Classes from the database

 - User
 - Role
 - UserRole

I also adjusted the Classes to inherit as follows

 - User : IdentityUser<string>
 - Role : IdentityRole<string>
 - UserRole : IdentityUserRole<string>

note: both User and Role needed some extra fields added to the table and classes for Asp/Net Core Identity

USER

public string NormalizedUserName { get; set; }
public string NormalizedEmail { get; set; }
public DateTime? LockoutEnd { get; set; }
public string ConcurrencyStamp { get; set; }

ROLE

public string NormalizedName { get; set; }
public string ConcurrencyStamp { get; set; }

But anytime I tried to get a Role for a User, e.g.

var inRole = await _userManager.IsInRoleAsync(user, "APIRole");

I would get an Exception,

System.InvalidOperationException: Cannot create a DbSet for 'IdentityUserRole<string>' because this type is not included in the model for the context.

Only solution I could find was to add this into my AuthdbContext

modelBuilder.Entity<IdentityUserRole<String>>().ToTable("UserRole").HasKey(p => new { p.UserId, p.RoleId });

No no more exception, I I cannot retrieve any roles for a User, and this annotation for my controller

[Authorize(Roles = "APIRole")]

Will always give me a "401 Unauthorized"

How can get ASP.Net Core Identity working with an old Asp.Net Identity datastore with Roles

Note: I cannot migrate, because the legacy system is still operational


Solution

  • As an option, you can write a custom AuthorizationHandler that will allow you to search for the user based on the data your have in the claims after the bearer token authentication. Once Bearer token is verified, the service will update User information in the Context.

    But custom handler will change the Controller's attribute from Roles to Policy. Example:

    [Authorize(Policy = AuthRolesPolicy.Admin)]
    public IActionResult GetMySuperData()
    { ... }
    

    For that you would need AuthRolesPolicy.cs

    public static class AuthRolesPolicy
    {
        public const string Admin = "ADMIN";
        public const string View = "USER";
    
        public static readonly string[] UserLevel = { Admin, User};
        public static readonly string[] AdminLevel = { Admin};
    }
    

    RoleRequirement class that would be used for Bearer.

    public class RoleRequirement : IAuthorizationRequirement
    {
        public string[] Roles { get; }
    
        public RoleRequirement(string[] roles)
        {
            Roles = roles;
        }
    }
    

    Actual handler MyAuthorizationHandler.cs

    public class MyAuthorizationHandler : AuthorizationHandler<RoleRequirement>, IAuthorizationRequirement
    {
        private readonly IYourService _databaseService;
    
        public PortalAuthorizationHandler(IYourService databaseService)
        {
            _databaseService = databaseService;
        }
    
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
        {
            try
            {
                // Read user identities from authentication step
                var userId = UserClaimsReader.GetUserId(context.User);
                if (!string.IsNullOrEmpty(userId))
                {
                    // Get user information from DB or get all users roles here
                    var user = await _databaseService.ReadUserCreds(userId);
    
                    if (user != null)
                    {
                        // Check is user role valid based on Policy level
                        bool isRoleValid = requirement.Roles.Contains(user.Role);
    
                        if (isRoleValid)
                        {
                            context.Succeed(requirement);
                            return;
                        }
                    }
                }
            }
            catch (Exception ex)
            {                
            }
            
            context.Fail();
            return;
        }
    }
    

    And the last one is update Program.cs

    ...
    builder.Services.AddScoped<IAuthorizationHandler, MyAuthorizationHandler>();
    ...
    builder.Services.AddAuthorization(options =>
    {
        options.AddPolicy(AuthRolesPolicy.Admin, policy => policy.Requirements.Add(new RoleRequirement(AuthRolesPolicy.AdminLevel)));
        options.AddPolicy(AuthRolesPolicy.User, policy => policy.Requirements.Add(new RoleRequirement(AuthRolesPolicy.UserLevel)));
    });
    ...