skip to Main Content

I have read through countless tutorials, articles and questions.

I cannot figure out why [Authorize(Roles = "SuperAdmin")] or any other role is not working. Everyone gets 403 regardless of the role specified:

//[Authorize(Roles = "SuperAdmin")] - Commented out to debug roles
public async Task<IActionResult> Index()
{
    var userID = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var user = await _userManager.FindByIdAsync(userID);
    var roles = await _userManager.GetRolesAsync(user);

    return View();
}

When I debug the user, I can see that they have the role:
enter image description here

But they still get a 403 on any controller that authorizes roles.

Here are the relevant bits of my program.cs:

builder.Services.AddIdentity<TMSUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddClaimsPrincipalFactory<MyUserClaimsPrincipalFactory>()
    .AddRoleManager<RoleManager<IdentityRole>>()
    .AddTokenProvider<DataProtectorTokenProvider<TMSUser>>(TokenOptions.DefaultProvider);

builder.Services.ConfigureApplicationCookie(options =>
{
    options.AccessDeniedPath = new PathString("/Home/HandleError/401");
    options.LoginPath = new PathString("/Home/Portal");
});

builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllersWithViews();

builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddControllers().AddNewtonsoftJson(options =>
{
    options.SerializerSettings.ContractResolver = new DefaultContractResolver();
});

builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<ITools, Tools>();

builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

builder.Services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
});

var app = builder.Build();

app.UseStatusCodePagesWithReExecute("/Home/HandleError/{0}");
app.UseHsts();

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<TMSContext>();
    context.Database.EnsureCreated();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();

app.Run();

I have changed my code countless times over the last few months trying to get this to work, but now I’m just begging for help. This is very frustrating! What have I missed? Could something else in my code be preventing this from working properly?

Thank you!

4

Answers


  1. Chosen as BEST ANSWER

    I finally came up with a solution for this. It should not have been this complicated! I've used roles in other versions .net without this problem.

    I created a custom Authorize Filter and I'm just hitting the db (which I assume all the helpers do as well so not really a big deal):

    Part of this I got from this link


    First, I created a model with my roles:

    public enum Role
    {
        User, Admin, SuperAdmin
    }
    

    Then a custom Authorize Filter:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthorizeAttribute : Attribute, IAuthorizationFilter
    {
        private readonly IList<Role> _roles;
    
        public AuthorizeAttribute(params Role[] roles)
        {
            _roles = roles ?? new Role[] { };
        }
    
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            // Skip authorization if action is decorated with [AllowAnonymous] attribute
            var allowAnonymous = context.ActionDescriptor.EndpointMetadata
                .OfType<AllowAnonymousAttribute>().Any();
    
            if (allowAnonymous)
                return;
    
            // Authorization
            var user = context.HttpContext.User;
    
            if (user != null && _roles.Any())
            {
                using (var db = context.HttpContext.RequestServices
                    .GetService<ApplicationDbContext>())
                {
                    var userRoles = (from ur in db.UserRoles
                                     join r in db.Roles on ur.RoleId equals r.Id
                                     join u in db.Users on ur.UserId equals u.Id
                                     where u.UserName == user.Identity.Name
                                     select new
                                     {
                                         r.Name
                                     }).ToList();
    
                    var isAuthorized = false;
                    foreach (var role in userRoles)
                    {
                        Role claimRole;
                        Enum.TryParse(role.Name, out claimRole);
                        if (_roles.Contains(claimRole))
                            isAuthorized = true;
                    }
    
                    if (isAuthorized)
                        return;
                }
            }
    
            context.Result = new UnauthorizedResult();
        }
    }
    

    And I use it like this:

    [Authorize(Role.SuperAdmin)]
    public async Task<IActionResult> Index()
    {
        return View();
    }
    

    I do like this approach. Its going to allow me to do some other things I wanted to do on a per controller/user level - I just can't believe it was this complicated!

    Hope this helps somebody. If there's any glaring issues with this, please let me know. I'm officially balled.


  2. check what you are getting with User.FindAll(ClaimTypes.Role).ToList()

    after that check "SuperAdmin" coming in list or not?, if your issue not resolved than post your GetRolesAsync function.

    Login or Signup to reply.
  3. its seems like you have to add the services.AddDefaultIdentity<TMSUser>()

    so the complete code would look like

    services.AddDefaultIdentity<ApplicationUser>()
                    .AddRoles<IdentityRole>()
                    .AddEntityFrameworkStores<ICMSDbContext>()
                    .AddClaimsPrincipalFactory<CustomClaimsPrincipalFactory>();
    
    Login or Signup to reply.
  4. The solution is much simpler than described. If you add both roles by .AddRoles() and custom claims by .AddClaimsPrincipalFactory(); you have to inherit from UserClaimsPrincipalFactory<TUser, TRole> not UserClaimsPrincipalFactory<TUser> which is a default one.

    So your code should look:

    public class CustomUserClaimsPrincipalFactory: UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
      {
        public CustomUserClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> options) : base(userManager, roleManager, options)
         {
         }
      //and below should be part with adding custom claim(s)
      }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search