I am struggling to find a good solution for doing custom authorization checks without having to repeat the authorization check manually over and over again.
To illustrate, suppose I have the following setup for a .net core web api, which has two endpoints, one for GET and one for POST. I would like to check (maybe against db) whether the user has the right to see the resource, or the right to create a resource.
This is what the documentation refers to as resource based authorization
and would look something like this:
[Authorize]
[ApiVersion ("1.0")]
[Route ("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ResourcesController : ControllerBase {
private readonly IAuthorizationService _authorizationService;
//..constructor DI
[HttpGet ("{resourceId}")]
public ActionResult<Resource> Get (Guid resourceId) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, resourceId, ServiceOperations.Read);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Get (resourceId));
}
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {
var authorizationCheck = await _authorizationService.AuthorizeAsync (User, null, ServiceOperations.Write);
if (!authorizationCheck.Succeeded) {
return Forbid ();
}
return Ok (ResourceRep.Create (resource));
}
}
Now imagine the ServiceOperations enum has a long list of supported operations, or there are 100 different endpoints, I will have to do the same check everywhere, or even worse, might forget to add a check where I should definitely have added a check. And there is not an easy way to pick this up in unit tests.
I thought of using attributes but as the docs state:
Attribute evaluation occurs before data binding and before execution of the page handler or action that loads the document. For these reasons, declarative authorization with an [Authorize] attribute doesn't suffice. Instead, you can invoke a custom authorization method—a style known as imperative authorization.
So it seems I cannot use an authorization policy and decorate the methods with authorization attributes (which are easy to unit test that they are there) when the check itself requires a parameter that is not available (the resourceId).
So for the question itself:
How do you use imperative (resource based) authorization generically without having to repeat yourself (which is error-prone). I would love to have an attribute like the following:
[HttpGet ("{resourceId}")]
[AuthorizeOperation(Operation = ServiceOperations.Read, Resource=resourceId)]
public ActionResult<Resource> Get (Guid resourceId) {..}
[AuthorizeOperation(Operation = ServiceOperations.Write)]
[HttpPost]
public ActionResult<Resource> Post ([FromBody] Resource resource) {..}
You can achieve it using AuthorizationHandler in a policy-based authorization and combine with an injected service specifically created to determine the Operation-Resources pairing.
To do it, first setup the policy in Startup.ConfigureServices :
services.AddAuthorization(options =>
{
options.AddPolicy("OperationResource", policy => policy.Requirements.Add( new OperationResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IOperationResourceService, OperationResourceService>();
next create the OperationResourceHandler :
public class OperationResourceHandler: AuthorizationHandler<OperationResourceRequirement>
{
readonly IOperationResourceService _operationResourceService;
public OperationResourceHandler(IOperationResourceService o)
{
_operationResourceService = o;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, OperationResourceRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext filterContext)
{
var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
if (_operationResourceService.IsAuthorize(area, controller, action, id))
{
context.Succeed(requirement);
}
}
}
}
the OperationResourceRequirement can be an empty class:
public class OperationResourceRequirement : IAuthorizationRequirement { }
The trick is, rather than specify action's Operation in attribute, we specify it elsewhere such as in database, in appsettings.json, in some config file, or hardcoded.
Here's an example getting the Operation-Resource pair from config file:
public class OperationResourceService : IOperationResourceService
{
readonly IConfiguration _config;
readonly IHttpContextAccessor _accessor;
readonly UserManager<AppUser> _userManager;
public class OpeartionResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u)
{
_config = c;
_accessor = a;
_userManager = u;
}
public bool IsAuthorize(string area, string controller, string action, string id)
{
var operationConfig = _config.GetValue<string>($"OperationSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
//all of needed data are available now, do the logic of authorization
return result;
}
}
Please note that to make IHttpContextAccessor injectable, add services.AddHttpContextAccessor() in Startup.ConfigurationServices method body.
After all is done, use the policy on an action:
[HttpGet ("{resourceId}")]
[Authorize(Policy = "OperationResource")]
public ActionResult<Resource> Get (Guid resourceId) {..}
the authorize policy can be the same for every action.
Related
I have web API with custom policies and authorization handlers.
I wanted to reuse authorization handlers but HttpContext is null when attribute is used on signalr's hub.
For example this is my controller.
[Authorize]
public sealed class ChatsController : ControllerBase
{
[HttpPost("{chatId}/messages/send")]
[Authorize(Policy = PolicyNames.ChatParticipant)]
public Task SendMessage() => Task.CompletedTask;
}
And this my my authorization handler. I can extract "chatId" from HttpContext and then use my custom logic to authorize user.
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement)
{
if(_httpContextAccessor.HttpContext != null)
{
// Logic
}
return Task.CompletedTask;
}
}
However this won't work with Azure SignalR because I don't have access to HttpContext. I know that I can provide custom IUserIdProvider but I have no idea how to access "chatId" from "Join" method in my custom authorization handler.
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
[Authorize(Policy = PolicyNames.ChatParticipant)]
public async Task Join(Guid chatId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
Is it possible to reuse my authorization handlers?
I would like to avoid copypasting my code.
One solution is to extract my authorization code to separate services but then I have to manually call those from my hubs and abandon [Authorize] way.
Your chat is a resource, and you want to use resource based authorization. In this case declarative authorization with an attribute is not enough, because chat id is known at runtime only. So you have to use imperative authorization with IAuthorizationService.
Now in your hub:
[Authorize]
public sealed class ChatHub : Hub<IChatClient>
{
private readonly IAuthorizationService authService;
public ChatHub(IAuthorizationService authService)
{
this.authService = authService;
}
public async Task Join(Guid chatId)
{
// Get claims principal from authorized hub context
var user = this.Context.User;
// Get chat from DB or wherever you store it, or optionally just pass the ID to the authorization service
var chat = myDb.GetChatById(chatId);
var validationResult = await this.authService.AuthorizeAsync(user, chat, PolicyNames.ChatParticipant);
if (validationResult.Succeeded)
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId.ToString());
}
}
}
Your authorization handler should look different, because it needs the chat resource in its signature to do this kind of evaluation:
internal sealed class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, Chat>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ChatParticipantRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, Chat chat)
{
// You have both user and chat now
var user = context.User;
if (this.IsMyUserAuthorizedToUseThisChat(user, chat))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
Edit: there is actually another option I didn't know about
You can make use of HubInvocationContext that SignalR Hub provides for authorized methods. This can be automatically injected into your AuthorizationHandler, which should look like this:
public class ChatParticipantRequirementHandler : AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ChatParticipantRequirement requirement, HubInvocationContext hubContext)
{
var chatId = Guid.Parse((string)hubContext.HubMethodArguments[0]);
}
}
Hub method will be decorated normally with [Authorize(Policy = PolicyNames.ChatParticipant)]
You still will have two authorization handlers, AuthorizationHandler<ChatParticipantRequirement> and AuthorizationHandler<ChatParticipantRequirement, HubInvocationContext>, no way around it. As for code dublication, you can however just get the Chat ID in the handler, either from HttpContext or HubInvocationContext, and than pass it to you custom written MyAuthorizer that you could inject into both handlers:
public class MyAuthorizer : IMyAuthorizer
{
public bool CanUserChat(Guid userId, Guid chatId);
}
One can authorize an action by using the [Authorize] attribute. But I want to only perform authorization on the action in specific conditions, so I cannot use this attribute. I don't think I am able to use IAuthorizationService.AuthorizeAsync as I don't have any policy names. Here's my service configuration in Startup.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(Configuration, "AzureAdB2C");
So my question is, how can I move the [Authorize] evaluation into the action code?
The AuthorizeAttribute will be converted (maybe with others) into an AuthorizeFilter and this filter will be executed with some code that is not equivalent to the simple IAuthorizationService.AuthorizeAsync. But if you want to use that anyway, we can get the default policy (which is used by [Authorize] without any policy specified) by using the IAuthorizationPolicyProvider.GetDefaultPolicyAsync. After that you can authorize the User to get an AuthorizationResult. It is succeeded if the property Succeeded is true. Otherwise, you can have the detailed failure in the property Failure (of type AuthorizationFailure). Here's the code:
public class TestController {
readonly IAuthorizationService _authorizationService;
readonly IAuthorizationPolicyProvider _authorizationPolicyProvider;
public TestController(IAuthorizationService authorizationService,
IAuthorizationPolicyProvider authorizationPolicyProvider){
_authorizationService = authorizationService;
_authorizationPolicyProvider = authorizationPolicyProvider;
}
public async Task<IActionResult> SomeAction(){
var defaultPolicy = await _authorizationPolicyProvider.GetDefaultPolicyAsync();
var authResult = await _authorizationService.AuthorizeAsync(User, defaultPolicy);
if(authResult.Succeeded){
//do something ...
}
}
}
In a WebAPI .net core project I have created a Middleware class that validates an api key. From validating it, it retrieves the permissions that the key has (user or admin) within the invoke method.
I pass it through a switch to set the principle like so
GenericIdentity identity = new GenericIdentity("API");
GenericPrincipal principle = null;
//we have a valid api key, so set the role permissions of the key
switch (keyValidatorRes.Role)
{
case Roles.User:
principle = new GenericPrincipal(identity, new[] { "User" });
context.User = principle;
break;
case Roles.Admin:
principle = new GenericPrincipal(identity, new[] { "Admin" });
context.User = principle;
break;
default:
principle = new GenericPrincipal(identity, new[] { "Other" });
context.User = principle;
break;
}
On controllers methods I have
[Authorize(Roles = "Admin")]
to validate the roles of an authenticated api key
If the user has the admin principle it goes through as expected. However, if it has a user or other principle then I get an error about
not having a DefaultForbidScheme
I googled around and added Authentication to my startup.cs with a customer scheme
services.AddAuthentication(options=> {
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<AuthSchemeHandle>("forbidScheme", "Handle Forbidden");
});
and created the AuthSchemeHandle
public class AuthSchemeHandle : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
throw new NotImplementedException();
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.FromResult(AuthenticateResult.Fail("Failed Auth"));
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
Now if the principle does not have Admin it fails without the error but the response that is returned on the API is 200 with no content. I was expecting a 4xx response with the message "Failed Auth"
I am just trying to work out why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
Is there a better way that I should be doing this?
why it is not as expected as although it seems "fixed" I do not understand how it has fixed it.
There's no dark magic when the authentication handler calls IAuthenticationHandler.ForbidAsync() method. We have to do relevant things ourself. In short, setting the StatusCode=403 as your need.
public async Task ForbidAsync(AuthenticationProperties properties)
{
properties = properties ?? new AuthenticationProperties();
_context.Response.StatusCode = 403;
// ...
return Task.CompletedTask;
}
As a side note, you don't need return a Task.FromResult() as it doesn't care about the result.
Is there a better way that I should be doing this?
The ASP.NET Core Team provides us an abstract class AuthenticationHandler to handle authentication. This abstract class has a built-in implementation for ForbidAsync(AuthenticationProperties properties) (and also for other public methods). So it's much easy to extends this abstract class as below:
public class MyAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
return AuthenticateResult.NoResult();
}
}
Finally, add a configuration for authentication service:
services
.AddAuthentication(options=>{
options.DefaultAuthenticateScheme = "forbidScheme";
options.DefaultForbidScheme = "forbidScheme";
options.AddScheme<MyAuthenticationHandler>("forbidScheme", "Handle Forbidden");
});
It should work as expected.
I am exposing an endpoint for integration with a 3rd party and their requirement is for me to authorize their requests to my endpoint based on a key passed in the body being posted. My code will then needs to validate that the passed key matches some predetermined value on my side. The incoming model will look something like this:
public class RequestBase
{
public string ApiKey { get; set; }
...
}
Exploring the options for Authorization in ASP.NET Core I don't really see a match for what I am attempting to do. I am thinking a custom AuthorizeAttribute from this question would work but I'm not having any luck and get a 401 regardless of what I do. This is what I have so far:
[AttributeUsage(AttributeTargets.Class)]
public class MyAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
private static IEnumerable<string> _apiKeys = new List<string>
{
"some key... eventually will be dynamic"
};
public void OnAuthorization(AuthorizationFilterContext context)
{
var req = context.HttpContext.Request;
req.EnableRewind();
using (var reader = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
{
var bodyStr = reader.ReadToEnd();
var isAuthorized = _apiKeys.Any(apiKey => bodyStr.Contains(apiKey));
if (!isAuthorized)
{
context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
return;
}
}
req.Body.Position = 0;
}
}
When the key is not found in the body the 403 is returned as expected. However, when the key is found the result I get back is still a 401. Almost seems as if the base.OnAuthorization is being called. I have other endpoints that use a standard AurhorizeAttribute. They work as expected when only if I pass in a JWT.
Questions:
Am I on the right path with a custom AuthorizeAttribute or is there a better way?
If a customer AuthorizeAttribute is the right path... what am I missing?
Appreciate any help!
For using your own authorize logic with IAuthorizationFilter, you should not use with AuthorizeAttribute which will check the Authentication with default authentication schema.
Try to change AuthorizeAttribute to Attribute.
[AttributeUsage(AttributeTargets.Class)]
public class KeyAuthorizeAttribute : Attribute, IAuthorizationFilter
{
I would like to implement the following in MVC6:
[Authorize(Roles = "Shopper")]
public class HomeController
{
[Authorize(Roles = "Editor"), OverrideAuthorization]
public IActionResult EditPage() {}
}
But OverrideAuthorizationAttribute no longer exists. So how do you set it so that a user only needs to be in the Editor role and not Editor and Shopper role to access EditPage in MVC6?
I found this blog post from Filip W that explains how write your own solution using the filter providers.
However the framework has changed a lot and his solution has to be updated to take into account the changes in the framework up to beta8.
First you will create a new attribute where you can specify the type of the filter that you want to override. (In your case this would be the AuthorizeFilter)
public class OverrideFilter : ActionFilterAttribute
{
public Type Type { get; set; }
}
If you want. you could create more specific filters like:
public class OverrideAuthorization : OverrideFilter
{
public OverrideAuthorization()
{
this.Type = typeof(AuthorizeFilter);
}
}
Then you need to create a new IFilterProvider.
This filter provider will be executed after the default providers in
the framework have run.
You can inspect the
FilterProviderContext.Results and search for your OverrideFilter
If found, you can then inspect the rest of the filters, and delete
any filter that is of the filtered type and a lower scope
For example create a new OverrideFriendlyFilterProvider following this idea:
public class OverrideFriendlyFilterProvider : IFilterProvider
{
//all framework providers have negative orders, so ours will come later
public int Order => 1;
public void OnProvidersExecuting(FilterProviderContext context)
{
if (context.ActionContext.ActionDescriptor.FilterDescriptors != null)
{
//Does the action have any OverrideFilter?
var overrideFilters = context.Results.Where(filterItem => filterItem.Filter is OverrideFilter).ToArray();
foreach (var overrideFilter in overrideFilters)
{
context.Results.RemoveAll(filterItem =>
//Remove any filter for the type indicated in the OverrideFilter attribute
filterItem.Descriptor.Filter.GetType() == ((OverrideFilter)overrideFilter.Filter).Type &&
//Remove filters with lower scope (ie controller) than the override filter (i.e. action method)
filterItem.Descriptor.Scope < overrideFilter.Descriptor.Scope);
}
}
}
public void OnProvidersExecuted(FilterProviderContext context)
{
}
}
You need to register it on the ConfigureServices of your startup class:
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IFilterProvider, OverrideFriendlyFilterProvider>());
With all this pieces you will be able to override the authorization filter (or any other filter).
For example in the default HomeController of a new mvc application, any logged in user will be able to access the Home action, but only the ones with the admin role will be able to access the About action:
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[Authorize(Roles = "admin"), OverrideAuthorization]
public IActionResult About()
{
return View();
}
I think it would be better to use the new policy based authorization approach instead of using roles directly.
There is not a lot of documentation yet about policy based authorization but this article is a good start