Search code examples
c#asp.netasp.net-coreasp.net-core-mvcasp.net-authorization

ASP.Net Core 6 - Authorize Roles not working


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!


Solution

  • 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.