I am trying to figure out what is the best way to create custom authorization attribute for my asp.net core application. I have seen this post and I am aware of the 2 approaches discussed here.
How do you create a custom AuthorizeAttribute in ASP.NET Core?
1) Using IAuthorizationFilter
2) Using Policies
I saw that the official document suggests that we should be using policies and not IAuthorizationFilter but I felt that using policies for my scenario is an overkill. I personally liked IAuthorizationFilter approach more.
I have a very basic requirement. I want to create an authorize attribute for my web api and need to throw 403 if the current user is not whitelisted to use this API. I really don't care about the scopes(canRead, canWrite, can readWrite etc). If I go ahead with policy approach, I may be using the same policy for all my APIs.
What is the best way to achieve this?
Using policies for something like this isn't overkill. You need a requirement:
public class WhitelistRequirement: IAuthorizationRequirement
{
}
A handler:
public class WhitelistHandler : AuthorizationHandler<WhitelistRequirement>
{
// Implement a constructor to inject dependencies, such as your whitelist
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
WhitelistRequirement requirement)
{
if (isInWhitelist) // Your implementation here
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Register both in ConfigureServices:
services.AddAuthorization(options =>
options.AddPolicy("WhitelistPolicy",
b => b.AddRequirements(new WhitelistRequirement())));
services.AddSingleton<IAuthorizationHandler, WhitelistHandler>();
Then use your policy:
[Authorize(Policy = "WhitelistPolicy")]
You can apply the policy globally with a global filter:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new WhitelistRequirement())
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
The resulting behavior for unauthenticated or forbidden users depends on the implementation of the "challenge" and "forbid" behaviors in your app's authentication handler.
See here.
Related
I have an existing REST API running on ASP.NET Core 3.0. It uses MVC filter to perform an authorization check based on a header value and returns error in case of authorization failure so that the request is not passed to the controller.
Now, I am experimenting with gRPC and trying to port this API to a gRPC service. However, I do not see any obvious solutions that might act as an MVC filter replacement.
Is there some way to achieve similar authorization checking functionality, perhaps using metadata?
For MVC and gRpc, they are different. ActionFilter is not exist under gRpc.
If you want to apply checking request header for all actions, you could try implement your custom middleware before app.UseEndpoints and check the request header.
For another way, you could try Policy like below:
GrpcRequireemnt and GrpcHandler
public class GrpcRequireemnt : IAuthorizationRequirement
{
}
public class GrpcHandler : AuthorizationHandler<GrpcRequireemnt>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public GrpcHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, GrpcRequireemnt requirement)
{
var headers = _httpContextAccessor.HttpContext.Request.Headers;
StringValues token;
if (!headers.TryGetValue("token", out token))
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
Register required services
services.AddAuthorization(options =>
{
options.AddPolicy("TokenAuthorize", policy =>
{
policy.AddRequirements(new GrpcRequireemnt());
});
});
services.AddHttpContextAccessor();
services.AddSingleton<IAuthorizationHandler, GrpcHandler>();
UseCase
[Authorize("TokenAuthorize")]
public override Task<BuyTicketsResponse> BuyTickets(BuyTicketsRequest request, ServerCallContext context)
{
var user = context.GetHttpContext().User;
return Task.FromResult(new BuyTicketsResponse
{
Success = _ticketRepository.BuyTickets(user.Identity.Name!, request.Count)
});
}
This will have slightly different answers depending on if you're using Grpc.Core, which is a wrapper around the C GRPC library initially developed at Google, which has been available for a while and supports a variety of .Net targets (including Framework), or if you're using the new Grpc.AspNetCore which launched with .Net Core 3.0 and is built on Kestrel and ASP.NET Core internals.
Grpc.Core
For Grpc.Core you would want to pass your header value as metadata, and then create a server-side Interceptor to handle the metadata and the request. You can also consider using the AsyncAuthInterceptor, however the core Grpc implementation on the client side will not send credentials over insecure (non-TLS) connections.
Grpc.AspNetCore
Grpc.AspNetCore is built on ASP.NET and can use ASP.NET middleware, including the default ASP.NET authentication. If you can convert your filter into a middleware, you would be able to share the authentication between both implementations.
I want to improve my API's security with some sort of "self" policy to validate the call to some user actions (like DELETE user) is made by the same user the token was issued to. Is there a way to do this in a similar way to the policy based authorization?
I have a .Net Core 2.2 with MVC WebAPI running on Kestrel. I have users, roles and user-roles and I have token-based authentication with roles enabled. I can issue tokens and validate then with the "Authorize" attribute in the controllers. However, I've been looking for a way to validate that some actions to users are made only by the users itself, a "self" authentication policy to validate that, for example, user 3 is trying to delete user 3 and only user 3. I've dug up to the claims and everything and I know I can make a simple service passing the claims and the validating it but I wanted to do it in a smoother way similar to the policy-based or role-based authentication. I don't know if I can make it with some sort of middleware or something but it would be great to be able to make it as clean as possible.
[Edit]
The main purpose is to avoid users to delete resources created by other users and make them be able only to delete resources created by themselves.
[Edit2 - Solution]
Thanks to Paul Lorica's Answer I can now describe how I did it.
The first thing is to create a Requirement and a Handler similar to the examples provided by Microsoft in the docs. What we do is to add a Claim to the token generation method/service we have and add the ID as NameIdentifier. After that, we inject in the IHttpContextAccessor in the handler. And then we can validate if the ID in the request is the same than the Id in the Claim. So it was very easy.
I'm adding examples of logic to make it work.
PS: Inject IHttpContextAccessor as a singleton in the startup clas or it won't work.
Handler:
public class SelfUserHandler: AuthorizationHandler<SelfUserRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public SelfUserHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
SelfUserRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.NameIdentifier))
{
return Task.CompletedTask;
}
var nameIdentifier = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier).Value;
if (_httpContextAccessor.HttpContext.Request.Path.ToString().ToUpper().Contains(nameIdentifier.ToUpper()))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
Requirement
public class SelfUserRequirement : IAuthorizationRequirement
{
public SelfUserRequirement() { }
}
Additional info:
Nate Barbettini Answer here
Joe Audette Answer here
First off, when your code validates against the policy, the policy has no understanding, and does not need to know, what you are doing.
I suppose you can retrieve the context via URL. So say if its a DELETE user/3
then you can create a policy that would check the user's claims that it has an ID == 3.
See the docs here on creating policies and accessing the httpContext
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2
Its a bit of a naive check, I would rather just place that logic within the method of the controller.
We are trying to understand what is the expected handling of a ChallengeResult when there are multiple authentication schemes registered.
We need to handle such a scenario because we have an ASP.NET core 2.2 app exposing some action methods (we use the MVC middleware) that must be used by an angularjs SPA which relies on cookies authentication and some third parties applications which use an authentication mechanism based on the Authorization HTTP request header. Please notice that the involved action methods are the same for both the users, this means that each one of them must allow authentication using both the cookie and the custom scheme based on Authorization HTTP request header. We know that probably this is not an optimal design but we cannot modify the overall architecture.
This documentation seems to confirm that what we would like to achieve is entirely possible using ASP.NET core 2.2. Unfortunately, the cookie authentication used by the UI app and the custom authentication used by the third parties must behave differently in case of an authentication challenge and their expected behaviors are not compatible with each other: the UI app should redirect the user to a login form, while a thir party application expects a raw 401 status code response. The documentation linked above does not offer a clear explanation of the ChallengeResult handling, so we decided to experiment with a test application.
We created two fake authentication handlers:
public class FooAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Foo failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
public class BarAuthenticationHandler : IAuthenticationHandler
{
private HttpContext _context;
public Task<AuthenticateResult> AuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.Fail("Bar failed"));
}
public Task ChallengeAsync(AuthenticationProperties properties)
{
_context.Response.StatusCode = StatusCodes.Status500InternalServerError;
return Task.CompletedTask;
}
public Task ForbidAsync(AuthenticationProperties properties)
{
return Task.CompletedTask;
}
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context;
return Task.CompletedTask;
}
}
We registered the authentication schemas inside ConfigureServices method as follows:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = "Bar";
options.AddScheme<FooAuthenticationHandler>("Foo", "Foo scheme");
options.AddScheme<BarAuthenticationHandler>("Bar", "Bar scheme");
});
}
This is our middleware pipeline:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
and finally we created a controller with an action method requiring authentication:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values/5
[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Foo,Bar")]
public ActionResult<string> Get(int id)
{
return "value";
}
}
We noticed that:
both the FooAuthenticationHandler and BarAuthenticationHandler are called to handle the ChallengeResult
the order is FooAuthenticationHandler before BarAuthenticationHandler and depends on the Authorize attribute (if you swap the authentication schemes inside the Authorize attribute then BarAuthenticationHandler is called first)
the caller gets a raw 500 status code response, but this only depends on the order in which the authorization handlers are called
the call to options.DefaultChallengeScheme = "Bar"; matters if and only if inside the [Authorize] attribute the property AuthenticationSchemes is not set. If you do so, only the BarAuthenticationHandler is called and FooAuthenticationHandler never gets a chance to authenticate the request or handle an authentication challenge.
So, the question basically is: when you have such a scenario, how are you expected to handle the possible "incompatibility" of different authentication schemes regarding ChallengeResult handling since they get both called ?
In our opinion is fine that both have a chance to authenticate the request, but we would like to know if it is possible to decide which one should handle the authentication challenge.
Thanks for helping !
You should not specify the schemes on the Authorize attribute.
Instead, specify one scheme as the default, and setup a forward selector.
The implementation of the selector depends on your case, but usually you can somehow figure out which scheme was used in a request.
For example, here is an example from the setup of an OpenID Connect scheme.
o.ForwardDefaultSelector = ctx =>
{
// If the current request is for this app's API
// use JWT Bearer authentication instead
return ctx.Request.Path.StartsWithSegments("/api")
? JwtBearerDefaults.AuthenticationScheme
: null;
};
So what it does is forward challenges (and well, everything) to the JWT handler if the route starts with /api.
You can do any kind of checks there, headers etc.
So in this case OpenID Connect and Cookies are setup as defaults for everything, but if a call is received that is going to the API, use JWT authentication.
The example here forwards all the "actions" you can do with authentication (challenge, forbid etc.).
You can also setup forward selectors for just challenges etc.
I have an ASP.NET Core 2.1 site that has the following in the WebApiConfig file:
services.AddMvc(config =>
{
// Force authorization for all controllers
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
This forces all callers to authenticate before calling any of the controllers.
Unfortunately, this also forces user authentication to access the OData "$metadata" URL.
Is there a way to allow anonymous access to the metadata page?
Note: this is the same question as below (but for .NET Core)
ASP.NET allow anonymous access to OData $metadata when site has global AuthorizeAttribute
With the following implementation you can have anonymous access to OData $metadata and keep authentication for any other controllers:
(A controller with the name "metadata" will be automatically generated if you previously set up OData service correctly - this is not included here).
1.) Add following class
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
public class AuthorizeFiltersControllerConvention :
IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (!controller.ControllerName.Contains("Metadata"))
{
controller.Filters.Add(new AuthorizeFilter("defaultPolicy"));
}
}
}
2.) In the ConfigureServices method of the StartUp class remove your existing addMvc method and add the following:
services.AddAuthorization(o =>
{
o.AddPolicy("defaultPolicy", b =>
{
b.RequireAuthenticatedUser();
});
});
services.AddMvc(o =>
{
o.Conventions.Add(new AuthorizeFiltersControllerConvention());
})
I'm just getting started with ASP.NET Core Identity and have the following requirements defined:
public sealed class IsCustomerUserRequirement : IAuthorizationRequirement
public sealed class IsSuperUserRequirement : IAuthorizationRequirement
With the following basic handlers:
public class IsCustomerUserHandler : AuthorizationHandler<IsCustomerUserRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsCustomerUserRequirement requirement)
{
if (context.User.HasClaim(_ => _.Type == "customer"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class IsSuperUserHandler : AuthorizationHandler<IsSuperUserRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsSuperUserRequirement requirement)
{
if (context.User.IsInRole("super_user"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
I can then put these inside basic policies:
services
.AddAuthorization(options =>
{
options.AddPolicy("MustBeSuperUser", policy => policy.Requirements.Add(new IsSuperUserRequirement()));
options.AddPolicy("CustomersOnly", policy => policy.Requirements.Add(new IsCustomerUserRequirement()));
});
And apply it using [Authorize("CustomersOnly")], which works fine.
My requirement is to be able to allow super users, claim principals with the super_user role but without the customer claim, to also access Customers Only areas.
I have currently implemented this by changing the handler to manually check:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IsCustomerUserRequirement requirement)
{
if (context.User.HasClaim(_ => _.Type == Claims.Customer) ||
context.User.IsInRole(Roles.SuperUser))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
My issue is this feels like I'm missing the point. Is there a better way to define this so I don't have to repeat the super user check in each handler in future?
The bigger picture in all this is I use IdentityServer4 (ASP.NET Identity-backed) to Authenticate, and then intend to use some JWT-based claims (one claim, two roles) to further identify the user Authorisation falls into an application-specific roles / permissions structure and some custom middleware that has nothing to do with Identity Server. What, if any, best practices are there around this topic?
“this feels like I'm missing the point” – Yes, in a way you are missing the point. You are doing role based authorization: A user can be a customer or a super user.
But instead, the new model is claims based authorization where the user has a claim about something, and you are using that to authorize them. So ideally, the super user would get the same claim the customer gets, and is allowed access to the resource that way. Such a claim also wouldn’t be called customer then, but be rather something that is a property of the user.
You can still use a role-based authorization model with claims but you should probably avoid mixing them. As you noticed yourself, this gets a bit weird eventually.
That being said, there are multiple ways to succeed a policy using different requirements. If you were using roles only (instead of that customer claim), you could simply use the built-in way:
options.AddPolicy("MustBeSuperUser", policy => policy.RequireRole("super_user"));
options.AddPolicy("CustomersOnly", policy => policy.RequireRole("customer", "super_user"));
That way, the CustomersOnly policy would be fulfilled by both customer and super_user roles.
Since you aren’t using a role for your customers, you will have to follow your requirements implementation here. The way authorization requirements work though is that you can have multiple handlers for the same requirement type and only one of them needs to succeed (as long as none fails) for the requirement to be successful.
So you could have your IsSuperUserHandler handle multiple requirements. YOu can follow the AuthorizationHandler<T> implementation to make this work:
public class IsSuperUserHandler : IAuthorizationHandler
{
public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var req in context.Requirements)
{
if (req is IsSuperUserRequirement || req is IsCustomerUserRequirement)
{
if (context.User.IsInRole("super_user"))
context.Succeed(req);
}
}
}
}
So your IsSuperUserHandler is now an authorization handler for both the IsSuperUserRequirement and the IsCustomerUserRequirement. So the CustomersOnly policy that requires the IsCustomerUserRequirement will also be fulfilled for super users.