I'm having trouble specifying two separate Authorization attributes on a class method: the user is to be allowed access if either of the two attributes are true.
The Athorization class looks like this:
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public class AuthAttribute : AuthorizeAttribute {
. . .
and the action:
[Auth(Roles = AuthRole.SuperAdministrator)]
[Auth(Roles = AuthRole.Administrator, Module = ModuleID.SomeModule)]
public ActionResult Index() {
return View(GetIndexViewModel());
}
Is there a way to solve this or do I need to rethink my approach?
This is to be run in MVC2.
There is a better way to do this in later versions of asp.net you can do both OR and AND on roles. This is done through convention, listing multiple roles in a single Authorize will perform an OR where adding Multiple Authorize Attributes will perform AND.
OR example
[Authorize(Roles = "PowerUser,ControlPanelUser")]
AND Example
[Authorize(Roles = "PowerUser")]
[Authorize(Roles = "ControlPanelUser")]
You can find more info on this at the following link
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles
Multiple AuthorizeAttribute instances are processed by MVC as if they were joined with AND. If you want an OR behaviour you will need to implement your own logic for checks. Preferably implement AuthAttribute to take multiple roles and perform an own check with OR logic.
Another solution is to use standard AuthorizeAttribute and implement custom IPrincipal that will implement bool IsInRole(string role) method to provide 'OR' behaviour.
An example is here:
https://stackoverflow.com/a/10754108/449906
I've been using this solution in production environment for awhile now, using .NET Core 3.0. I wanted the OR behavior between a custom attribute and the native AuthorizeAttribute. To do so, I implemented the IAuthorizationEvaluator interface, which gets called as soon as all authorizers evaluate theirs results.
/// <summary>
/// Responsible for evaluating if authorization was successful or not, after execution of
/// authorization handler pipelines.
/// This class was implemented because MVC default behavior is to apply an AND behavior
/// with the result of each authorization handler. But to allow our API to have multiple
/// authorization handlers, in which the final authorization result is if ANY handlers return
/// true, the class <cref name="IAuthorizationEvaluator" /> had to be extended to add this
/// OR behavior.
/// </summary>
public class CustomAuthorizationEvaluator : IAuthorizationEvaluator
{
/// <summary>
/// Evaluates the results of all authorization handlers called in the pipeline.
/// Will fail if: at least ONE authorization handler calls context.Fail() OR none of
/// authorization handlers call context.Success().
/// Will succeed if: at least one authorization handler calls context.Success().
/// </summary>
/// <param name="context">Shared context among handlers.</param>
/// <returns>Authorization result.</returns>
public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
{
// If context.Fail() got called in ANY of the authorization handlers:
if (context.HasFailed == true)
{
return AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
// If none handler called context.Fail(), some of them could have called
// context.Success(). MVC treats the context.HasSucceeded with an AND behavior,
// meaning that if one of the custom authorization handlers have called
// context.Success() and others didn't, the property context.HasSucceeded will be
// false. Thus, this class is responsible for applying the OR behavior instead of
// the default AND.
bool success =
context.PendingRequirements.Count() < context.Requirements.Count();
return success == true
? AuthorizationResult.Success()
: AuthorizationResult.Failed(AuthorizationFailure.ExplicitFail());
}
}
This evaluator will only be called if added to .NET service collection (in your startup class) as follows:
services.AddSingleton<IAuthorizationEvaluator, CustomAuthorizationEvaluator>();
In the controller class, decorate each method with both attributes. In my case [Authorize] and [CustomAuthorize].
I'm not sure how others feel about this but I wanted an OR behavior too. In my AuthorizationHandlers I just called Succeed if any of them passed. Note this did NOT work with the built-in Authorize attribute that has no parameters.
public class LoggedInHandler : AuthorizationHandler<LoggedInAuthReq>
{
private readonly IHttpContextAccessor httpContextAccessor;
public LoggedInHandler(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LoggedInAuthReq requirement)
{
var httpContext = httpContextAccessor.HttpContext;
if (httpContext != null && requirement.IsLoggedIn())
{
context.Succeed(requirement);
foreach (var req in context.Requirements)
{
context.Succeed(req);
}
}
return Task.CompletedTask;
}
}
Supply your own LoggedInAuthReq. In startup inject these in services with
services.AddAuthorization(o => {
o.AddPolicy("AadLoggedIn", policy => policy.AddRequirements(new LoggedInAuthReq()));
... more here
});
services.AddSingleton<IAuthorizationHandler, LoggedInHandler>();
... more here
And in your controller method
[Authorize("FacebookLoggedIn")]
[Authorize("MsaLoggedIn")]
[Authorize("AadLoggedIn")]
[HttpGet("anyuser")]
public JsonResult AnyUser()
{
return new JsonResult(new { I = "did it with Any User!" })
{
StatusCode = (int)HttpStatusCode.OK,
};
}
This could probably also be accomplished with a single attribute and a bunch of if statements. It works for me in this scenario. asp.net core 2.2 as of this writing.
Related
I'm exploring Minimal APIs in .Net 6, and trying to apply a custom Authorization Filter to the endpoint (via Attributes or Extensions).
But it seems to me, I am doing something wrong, or it's simply not designed to work in that way (and it's sad if so).
Couldn't find anything in the docs besides the default usage of [Authorize] attribute in Minimal APIs.
Here is the Filter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
{
//Checking tokens
}
And if I try to apply it at Controller level, it works fine
[CustomAuthorize]
public class CustomController : ControllerBase
{
//Necessary routing
}
But if I switch to Minimap APIs notation and try to use attributes
app.MapGet("/customEndpoint",
[CustomAuthorize] async ([FromServices] ICustomService customService, Guid id) =>
await customService.GetCustomStuff(id));
or even an extension method
app.MapGet("/customEndpoint",
async ([FromServices] ICustomService customService, Guid id) =>
await customService.GetCustomStuff(id)).WithMetadata(new CustomAuthorizeAttribute());
It just doesn't work. The filter doesn't even being constructed.
What did I miss or did wrong?
Thx in advance
You can write a custom authorization filter for Minimal API in .NET 6.0
Here is how I tend to approach it - by using Policy-based authorization in ASP.NET Core
Step 1: Create a Requirement
A requirement implements IAuthorizationRequirement
public class AdminRoleRequirement : IAuthorizationRequirement
{
public AdminRoleRequirement(string role) => Role = role;
public string Role { get; set; }
}
Note: A requirement doesn't need to have data or properties.
Step 2: Create a Requirement Handler
A requirement handler implements AuthorizationHandler<T>
public class AdminRoleRequirementHandler : AuthorizationHandler<AdminRoleRequirement>
{
public AdminRoleRequirementHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, RoleRequirement requirement)
{
if (context.User.HasClaim(c => c.Value == requirement.Role))
{
context.Succeed(requirement);
}
else
{
_httpContextAccessor.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
_httpContextAccessor.HttpContext.Response.ContentType = "application/json";
await _httpContextAccessor.HttpContext.Response.WriteAsJsonAsync(new { StatusCode = StatusCodes.Status401Unauthorized, Message = "Unauthorized. Required admin role." });
await _httpContextAccessor.HttpContext.Response.CompleteAsync();
context.Fail();
}
}
private readonly IHttpContextAccessor _httpContextAccessor;
}
Note: HandleRequirementAsync method returns no value. The status of either success or failure is indicated by calling context.Succeed(IAuthorizationRequirement requirement) and passing the requirement that has been successfully validated or by calling context.Fail() to indicate AuthorizationHandlerContext.HasSucceeded will never return true, even if all requirements are met.
Step 3: Configure Your Policy in the Authorization Service
builder.Services.AddAuthorization(o =>
{
o.AddPolicy("AMIN", p => p.AddRequirements(new AdminRoleRequirement("AMIN")));
});
Step 4: Add Your Requirement Handler to DI
builder.Services.AddSingleton<IAuthorizationHandler, AdminRoleRequirementHandler>();
Step 5: Apply Policy to Endpoints
app.MapGet("/helloworld", () => "Hello World!").RequireAuthorization("AMIN");
I think you won't be able to inject action filter in minimal api, you can use 3 alternative approches.
Create a custom middleware and inject it in startup class, it would check every request and do the intended work as you filter is doing. You can put a check for the request path there if you only need to validate a specific controller/endpoint.
The second approach is you can inject httpcontext in minimal api like this, from that extract jwt token and validate that, if found not ok reject that request.
app.MapGet("/customEndpoint", async (HttpContext context, ICustomService service) =>
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.isNullOrEmpty(token) || <not a valid token>) return Results.Unauthorized();
// do some work
return Results.Ok(result);
});
as #Dai suggested, you can extract token in this way also
AuthenticationHeaderValue.TryParse(context.Request.Headers["Authorization"], out var parsed ) && parsed.Scheme == "BearerOrWhatever" ? parsed.Parameter : null
You can register the filter globally from startup.cs.
I have defined 2 policies, ADD and SUB as shown below.
options.AddPolicy("ADD", policy =>
policy.RequireClaim("Addition", "add"));
options.AddPolicy("SUB", policy =>
policy.RequireClaim("Substraction", "subs"));
All what I want to do isto include 2 policies on a controller method. How can I perform this operation.
[Authorize(Policy = "ADD, SUB")]
[HttpPost]
public IActionResult PerformCalculation()
{
}
However, this gives me an error:
InvalidOperationException: The AuthorizationPolicy named: 'ADD, SUB' was not found
The first thing to realize is that Authorize attribute Policy setting is singular unlike Roles which can be plural and that multiple policies are treated on an AND basis, unlike a list of roles which is treated on an OR basis.
In your example code “ADD, SUB” is considered a single policy name. If you want to attribute you method with both policies, your code should be as follows.
[Authorize(Policy = "ADD")]
[Authorize(Policy = "SUB")]
[HttpPost]
public IActionResult PerformCalculation()
{
}
However, this will not give you the effect you want of either or, since policies are AND together, hence both policies must pass to be authorized. Nor will the suggestions of writing a single policy or a requirements handler to handle the multiple requirements give you the result of treating policies on a OR basis.
Instead, the solution is to create a TypeFilterAttribute that accepts a list of policies and is tied to a IAsyncAuthorizationFilter that test for either or. The following outlines the two classes you will need to define and how to attribute your action method.
The following code defines the new attribute AuthorizeAnyPolicy class.
/// <summary>
/// Specifies that the class or method that this attribute is applied to requires
/// authorization based on user passing any one policy in the provided list of policies.
/// </summary>
public class AuthorizeAnyPolicyAttribute : TypeFilterAttribute
{
/// <summary>
/// Initializes a new instance of the AuthorizeAnyPolicyAttribute class.
/// </summary>
/// <param name="policies">A comma delimited list of policies that are allowed to access the resource.</param>
public AuthorizeAnyPolicyAttribute(string policies) : base(typeof(AuthorizeAnyPolicyFilter))
{
Arguments = new object[] { policies };
}
}
The following code defines the authorization filter class which loops through and executes each policy in the list. Should all the policies fail the result of the authorization context is set to forbid.
public class AuthorizeAnyPolicyFilter : IAsyncAuthorizationFilter
{
private readonly IAuthorizationService authorization;
public string Policies { get; private set; }
/// <summary>
/// Initializes a new instance of the AuthorizeAnyPolicyFilter class.
/// </summary>
/// <param name="policies">A comma delimited list of policies that are allowed to access the resource.</param>
/// <param name="authorization">The AuthorizationFilterContext.</param>
public AuthorizeAnyPolicyFilter(string policies, IAuthorizationService authorization)
{
Policies = policies;
this.authorization = authorization;
}
/// <summary>
/// Called early in the filter pipeline to confirm request is authorized.
/// </summary>
/// <param name="context">A context for authorization filters i.e. IAuthorizationFilter and IAsyncAuthorizationFilter implementations.</param>
/// <returns>Sets the context.Result to ForbidResult() if the user fails all of the policies listed.</returns>
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var policies = Policies.Split(",").ToList();
// Loop through policies. User need only belong to one policy to be authorized.
foreach (var policy in policies)
{
var authorized = await authorization.AuthorizeAsync(context.HttpContext.User, policy);
if (authorized.Succeeded)
{
return;
}
}
context.Result = new ForbidResult();
return;
}
}
With the policies defined as shown in the question you would attribute the method as follows.
[AuthorizeAnyPolicy("ADD,SUB")]
[HttpPost]
public IActionResult PerformCalculation()
{
}
It’s that simple and you will find similar solutions in the following Stack Overflow questions.
Authorize against a list of policies
How to add multiple policies in action using Authorize attribute using identity 2.0?
I have a web api 2 controller: TestController.cs and an action filter: TestAuthorizeAttribute.cs
I am using StructureMap.WebApi2 nuget package for Web API 2 project for setting the dependency injection.
I am trying to create instance of TestService object in both TestController.cs and TestAuthorizeAttribute.cs.
Is this the correct approach to create instance of TestService.
Is it possible that the multiple threads seem to refer to Web API handling multiple simultaneous requests that are somehow handled by the same DataContext
Please help me to know are there any issues with the below mentioned code.
[RoutePrefix("api/test")]
public class TestController : ApiController
{
public TestController(ITestService testService)
{
_testService = testService;
}
/// <summary>
/// Get details of individual test
/// </summary>
/// <param name="Id"> Id</param>
/// <param name="selectedSection">Selected Section</param>
/// <returns>Details of the Test</returns>
[Route("{Id:int}/{selectedSection?}", Name = "TestDetails")]
[HttpGet]
[TestAuthorize]
public HttpResponseMessage Get(int Id, string selectedSection = "")
{
var testDetails = _testService.GetTestResults(Id);
if (scan != null)
{
var progress = _testService.GetProgress(scan, user);
return Request.CreateResponse(HttpStatusCode.OK, scanDetails);
}
else
{
return Request.CreateResponse(HttpStatusCode.NotFound, new { error = GlobalConstants.ERROR_REVIEW_NOTFOUND });
}
}
}
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class TestAuthorizeAttribute : ActionFilterAttribute
{
ITestService testService;
public ScanAuthorizeAttribute()
{
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
_testService = actionContext.Request.GetDependencyScope().GetService(typeof(ITestService)) as ITestService;
var Id = Convert.ToInt32(actionContext.ActionArguments["Id"]);
var testDetails = _testService.GetTestResults(Id);
}
What you're doing looks pretty much spot on. This is pretty much exactly what you want to do.
A few things to note:
Assuming the ITestService is TransientScoped (the default) your Filter and your Controller will actually use the same instance of the ITestService.
If your DataContext is TransientScoped it will be unique per request (since the NestedContainer is stored on the request via the DependencyScope) so you should not see a race condition you were worried about.
There are a few caveats to this that I know of. One of which is ModelBinders and ModelBinderProviders are instantiated with this method from System.Web.Http.ModelBinding.ModelBinderAttribute:
private static object GetOrInstantiate(HttpConfiguration configuration, Type type)
{
return configuration.DependencyResolver.GetService(type) ?? Activator.CreateInstance(type);
}
This method does NOT use a DependencyScope so any DBContext used here will be completely unique (TransientScoped object from non-Nested container.)
I have seen really weird race conditions when dealing with IEnumerable dependencies in webapi. The default IEnumerables used by StructureMap do not appear to be thread safe.
Be careful with data access in Filters, Delegating Handlers, Model Binders, etc. Introducing a N+1 or otherwise costly query in these layers can really hurt since they get called for every request they handle.
Any other questions about this stuff please ask, I've done a ton of stuff in this area lately so I have a pretty decent understanding of all of it.
This question is very similar to what I want to know. I've got a web api service on an azure cloud service with Application Insights configured. On the request information portal, that is generated automatically, I want to add a custom http header that's a part of the request into the information that is being logged with each request. The question is how do I do this?
I've tried using a telemetry initializer like below, but this fails (as in I don't see the information on the portal). I also added this in the global.asax
TelemetryConfiguration.Active.TelemetryInitializers.Add(propertyTelemetryInitializer);
public class PropertyTelemetryInitializer : ITelemetryInitializer
{
private readonly HttpContext httpContext;
public PropertyTelemetryInitializer(HttpContext httpContext)
{
this.httpContext = httpContext;
}
public void Initialize(ITelemetry telemetry)
{
this.AddTelemetryContextPropertFromContextHeader(telemetry, "xyz");
this.AddTelemetryContextPropertFromContextHeader(telemetry, "abc");
this.AddTelemetryContextPropertFromContextHeader(telemetry, "123");
}
private void AddTelemetryContextPropertFromContextHeader(ITelemetry telemetry, string headerKey)
{
var requestTelemetry = telemetry as RequestTelemetry;
telemetry.Context.Properties[headerKey] = this.httpContext.Request.Headers[headerKey] ?? string.Empty;
telemetry.Context.Properties[headerKey] = this.httpContext.Request.Headers[headerKey] ?? string.Empty;
}
}
Also is there a way to do this from the controller method itself? Something similar to the below (note: the below does not work)?
[Route("api/Something")]
[HttpGet]
[ResponseType(typeof(Something))]
public async Task<Something> GetSomething()
{
var requestTelemetry = new RequestTelemetry();
this.AddCustomHeadersToRequestTelemetry(requestTelemetry);
var result = await this.Service.GetSomethingAsync();
requestTelemetry.Properties["result"] = result.ToString();
return TypeMapper.Map<Model.Something, Something>(result);
}
/// <summary>
/// Adds the custom headers to request telemetry.
/// </summary>
/// <param name="controller">The controller.</param>
/// <param name="requestTelemetry">The request telemetry.</param>
public static void AddCustomHeadersToRequestTelemetry(this ApiController controller, RequestTelemetry requestTelemetry)
{
if (controller == null)
{
throw new ArgumentNullException("controller");
}
if (requestTelemetry == null)
{
throw new ArgumentNullException("requestTelemetry");
}
requestTelemetry.Context.Properties["abc"] = controller.Request.GetABCFromHeader();
requestTelemetry.Context.Properties["xyz"] = controller.Request.GetXYZFromHeader();
requestTelemetry.Context.Properties["123"] = controller.Request.Get123FromHeader();
}
Using TelemetryInitializers is the right solution. Some comments:
var requestTelemetry = telemetry as RequestTelemetry;: you do not use requestTelemetry after that. I guess you wanted to check for null.
Adding telemetry initializer in the Active configuration should be fine. You can also consider moving it to the applicationinsights.config
Custom properties do not show up in the portal immediately. Have you tried to reopen IE after some time and check your request again?
Can you debug? Do you see that you get in your tememetry initializer? Do you see any AI specific traces in search?
Regarding your second question. Right now telemetry initializers are the only (official) way to get to the autogenerated RequestTelemetry (which is actually in the HttpContext). There are plans to make most of the classes in web public and eventually open source it. But there is no ETA yet. If you create and track request yourself you can add custom properties as you mentioned.
UPDATE: Starting from 2.0.0-beta3 autogenerated request telemetry is accessible though HttpContext extension method: System.Web.HttpContextExtension.GetRequestTelemetry
I am having one action method.
Which is having 2 attribute
[Authorization]
[OutputCache]
ActionResult LoadImage()
I am calling LoadImage action from two method
say 1: Index 2: Create
When i call LoadImage action from Index, I want both attribute of LoadImage to execute.
When i call LoadImage action from Create, I want only Authorization attribute to be execute.
I don't want to use VaryByParam.
Please see my earlier answer and see if that satisfy your requirement. If you really have to achieve what you stated in your question, here is how...
Define a custom Authorization attribute. Check a value coming in Request.Params to make a decision about whether to apply the attribute or skip the authorization similar to what you achieve through AllowAnonymous attribute.
Example code (will require some changes as per your need):
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class ProspectProfileAuthorizationAttribute : AuthorizeAttribute
{
/// <summary>
/// Special authorization check based on whether request contain valid data or not.
/// </summary>
/// <param name="filterContext"></param>
public override void OnAuthorization(AuthorizationContext filterContext)
{
Guard.ArgumentNotNull(filterContext, "filterContext");
Guard.ArgumentNotNull(filterContext.Controller, "filterContext.Controller");
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(
typeof(CustomAllowAnonymous), inherit: true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(
typeof(CustomAllowAnonymous), inherit: true);
if (skipAuthorization)
{
return;
}
var request = filterContext.RequestContext.HttpContext.Request;
NameValueCollection parameterCollection = ReadQueryStringData(filterContext, request);
if (parameterCollection.Count < 3)
{
throw new InvalidOperationException("Request with invalid number of parameter");
}
// Check 1: Is request authenticated i.e. coming from browser by a logged in user
// No further check required.
if (request.IsAuthenticated)
{
return;
}
// Check 2: Request is coming from an external source, is it valid i.e. does it contains
// valid download code.
if (string.IsNullOrEmpty(downloadCode))
{
throw new InvalidOperationException(Constants.Invalid_Download_Code);
}
if (!userType.Equals(Constants.SystemIntegrationUserName))
{
var exportReportService = DependencyResolver.Current.GetService<IExportReportService>();
if (exportReportService != null)
{
if (!exportReportService.VerifyDownloadCode(downloadCode))
{
// Invalid partner key
throw new InvalidOperationException(Constants.Invalid_Download_Code);
}
}
}
}
private static NameValueCollection ReadQueryStringData(AuthorizationContext filterContext, HttpRequestBase request)
{
// Obtain query string parameter from request
//original
//var encryptedData = request.Params["data"];
// Applying the replace for space with + symb
var encryptedData = request.Params["data"].Replace(" ","+");
var decryptedData = EncryptionHelper.DecryptString(encryptedData);
// Validate the parameter
var dict = HttpUtility.ParseQueryString(decryptedData);
return dict;
}
}
As pointed by Peter Duniho, in this situation you should have two action methods with different attribute applied to each action method as applicable.
As far as redundancy is concerned you can have common logic in a private method. This private method can be called from public action method.
I am not offering a direct solution to your problem here however I thought its important to clarify that sometimes you have to make decision to choose one principle over other. In this case I think KISS Vs DRY.
The suggestion here is to keep it simple and have two methods. It does not directly violate DRY anyway.