Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Query Database for Role Authorization Before Each Action in ASP.NET Core

ASP.NET Core combined with Identity already provide a simple way to check roles once after login, but I would like to query the database for the current roles for the current user before every controller action.

I've read up on Role-based, Policy-based, and Claims-based authorization from Microsoft. (https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction) None of these solutions seem to check roles on every action. Here is my latest attempt to implement the desired outcome, in the form of some policy-based authorization:

In Startup.cs:

DatabaseContext context = new DatabaseContext();

services.AddAuthorization(options =>
{
    options.AddPolicy("IsManager",
        policy => policy.Requirements.Add(new IsManagerRequirement(context)));
    options.AddPolicy("IsAdmin",
        policy => policy.Requirements.Add(new IsAdminRequirement(context)));
});

In my requirements file:

public class IsAdminRequirement : IAuthorizationRequirement
{
    public IsAdminRequirement(DatabaseContext context)
    {
        _context = context;
    }

    public DatabaseContext _context { get; set; }
}
public class IsAdminHandler : AuthorizationHandler<IsAdminRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsAdminRequirement requirement)
    {
        // Enumerate all current users roles
        int userId = Int32.Parse(context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value);
        Roles adminRoles = requirement._context.Roles.FirstOrDefault(r => r.Name == "Administrator" && r.IsActive == true);
        bool hasRole = requirement._context.UserRoles.Any(ur => ur.UserId == userId && adminRoles.Id == ur.RoleId && ur.IsActive == true);
        // Check for the correct role
        if (hasRole)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

and in the controller:

[HttpGet]
[Authorize(Policy = "IsAdmin")]
public async Task<IActionResult> Location()
{
    // do action here
}

With this code, the requirement middleware is somehow never called, and therefore the database is never checked.

How would I correctly query the database to check for the current user's roles before carrying out each controller action?

like image 970
Carson Wiens Avatar asked Oct 17 '22 16:10

Carson Wiens


1 Answers

I solved this problem in my application (SignalR + JwtBearer) by handling the OnTokenValidated event. I just check the roles from the claims with the one in my database. If they're not valid anymore, i set the TokenValidatedContext to failed.

Here's an extract of my ASP.NET Core Startup.cs:

services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
    x.Events = new JwtBearerEvents
    {
        OnTokenValidated = async context =>
        {
            var userService = context.HttpContext.RequestServices.GetRequiredService<IUserRoleStore<User>>();
            var username = context.Principal.Identity.Name;
            var user = await userService.FindByNameAsync(username, CancellationToken.None);
            if (user == null)
            {
                // return unauthorized if user no longer exists
                context.Fail("Unauthorized");
            }
            else
            {
                // Check if the roles are still valid.
                var roles = await userService.GetRolesAsync(user, CancellationToken.None);
                foreach (var roleClaim in context.Principal.Claims.Where(p => p.Type == ClaimTypes.Role))
                {
                    if (roles.All(p => p != roleClaim.Value))
                    {
                        context.Fail("Unauthorized");
                        return;
                    }
                }
                context.Success();
            }
        },
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];

            // If the request is for our hub...
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
            {
                // Read the token out of the query string
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false
    };
});
like image 136
Alexander Avatar answered Oct 20 '22 11:10

Alexander