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?
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
};
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With