Question: Using ASP.NET Core 6 (MVC, Razor), I am struggling to correctly and efficiently implement Authorization when it comes to a user invoking certain Actions/Controllers - with CookieAuthentication.
To phrase it with more detail: I am trying to tell the .NET Core request pipeline how to distinguish between invocations on Actions/Controllers from:
An authenticated and unauthenticated user (this works)
A user having or lacking the correct privileges (being a manager or not)
A user having or lacking the correct subscription tier/edition for the action they are trying to perform. (e.g. only premium subscription users may invoke this action).
The tricky bit is - these users must all be routed to a different page depending on what is lacking
Let's assume all users concerned are authenticated (because I've got that working just fine. Unauthenticated users are supposed to be redirected to the login screen).
Next, I've used Claims (to populate a Claims Identity, which populates the ClaimsPrincipal) during the HttpContext.SignInAsync() process to determine the user's role (e.g. manager or user etc.), and decorated the appropriate Controllers/Actions with (and in combination)
[Authorize(Roles = "User")] //A user must have "user level" privileges
[Authorize(Policy = "BasicEdition")] //User must have the "basicEdition" subscription tier to access this method
Problem is I cannot get the request pipeline to redirect the user to different paths/URLs/actions depending on which one of these Authorize attributes fail.
For instance: If you're an authenticated user, and do not have "manager-level" privileges, you get redirected to accounts/ask-your-manager. However, if you're an authenticated user, and do not have "AdvancedEdition" subscription, you get redirected to subscriptions/upgrade-to-access.
I really hope I could explain this simply - Now for my code:
Startup.cs file:
services.AddAuthentication(opts =>
{ //I have even tried not using this.
opts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opts.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
opts.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(c =>
{
c.Cookie.Name = "AuthCookie";
c.LoginPath = "/accounts/login";
c.Cookie.IsEssential = true;
c.AccessDeniedPath = "/accounts/accessdenied";
c.SlidingExpiration = true;
c.ExpireTimeSpan = new TimeSpan(0, 2, 0);
});
services.AddAuthorization(config =>
{
//This should be the Tiers/Editions
config.AddPolicy("BasicEdition", policyBuilder =>
{
policyBuilder.UserRequireCustomClaim("BasicEdition");
});
config.AddPolicy("AdvancedEdition", policyBuilder =>
{
policyBuilder.UserRequireCustomClaim("AdvancedEdition");
});
config.AddPolicy("PremiumEdition", policyBuilder =>
{
policyBuilder.UserRequireCustomClaim("PremiumEdition");
});
});
services.AddScoped<IAuthorizationHandler, PoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, RolesAuthorizationHandler>();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(12);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => context.Request.PathBase.Equals("/NeedsConsent");
});
Then the Configure() method in the same class:
app.UseCookiePolicy();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();
app.UseSession();
And then simply placing the [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] attribute on the Controllers in general. That works, and will send my user directly to the login screen - this is correct and as expected.
Currently, I use this these bits to hook into the IAuthorizationHandler (according to this) to check the Role, and similarly, the Policy to interpret the subscription. However, I have discovered that unlike previous versions of the context, I cannot assign a new RedirectToAction (or anything like it) to it. I can merely tell the context whether or not the the request has succeeded or failed. How can I change this? Do I need to implement Filters or different types?
public class PoliciesAuthorizationHandler : AuthorizationHandler<CustomUserRequireClaim>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
CustomUserRequireClaim requirement)
{
if (context.User == null || !context.User.Identity.IsAuthenticated)
{
context.Fail();
return Task.CompletedTask;
}
var hasClaim = context.User.Claims.Any(c => c.Value == requirement.ClaimType);
if (hasClaim)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
context.Fail();
return Task.CompletedTask;
}
}
public class CustomUserRequireClaim : IAuthorizationRequirement
{
public string ClaimType { get; }
public CustomUserRequireClaim(string claimType)
{
ClaimType = claimType;
}
}
public class RolesAuthorizationHandler : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationHandler
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
RolesAuthorizationRequirement requirement)
{
if (context.User == null || !context.User.Identity.IsAuthenticated)
{
context.Fail();
return Task.CompletedTask;
}
var validRole = false;
if (requirement.AllowedRoles == null ||
requirement.AllowedRoles.Any() == false)
{
validRole = true;
}
else
{
var claims = context.User.Claims;
//var userName = claims.FirstOrDefault(c => c.Type == "UserId").Value;
var roles = requirement.AllowedRoles;
validRole = context.User.Claims.Any(c => c.Type == ClaimsHelper.Claim_UserRole
&& roles.Contains(c.Value));
//validRole = new Users().GetUsers().Where(p => roles.Contains(p.Role) && p.UserName == userName).Any();
}
if (validRole)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
I have followed MANY different articles on S.O as well as other forums to try and achieve my goal. Including this wonderful article that sent me down this road.
These articles (and credit to the respective OPs and those that answered them) helped me point in the right direction, however it is not exactly what I need.
How do you create a custom AuthorizeAttribute in ASP.NET Core?
(Yes I know this is a dated version of .NET Core)
How to add multiple policies in action using Authorize attribute using identity 2.0?
I have even considered combining them (kinda like multiplexing) the policies/roles but that doesn't do the exact thing I need either - since they will both send me down the same page.
How to include multiple policies
Your assistance, or even just pointing me in the right direction is truly appreciated. I'd like to gain a good understanding of what I'm building here to ensure I can optimize it later, and even expand on it.
UPDATE:
In conjunction with the accepted answer, what I also discovered was that my startup.cs file configuration was incorrect to allow the solution to work.
Inside the Startup.Configure() method, I used:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Instead of:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
You should create your own custom IAuthorizationMiddlewareResultHandler. Take a look at the documentation here: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-6.0
Example for your scenario:
public class SampleAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();
public async Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
if (authorizeResult.Forbidden)
{
if (authorizeResult.AuthorizationFailure!.FailedRequirements
.OfType<CustomUserRequireClaim>().Any())
{
context.Response.Redirect("/subscriptions/upgrade-to-access");
return;
}
if (authorizeResult.AuthorizationFailure!.FailedRequirements
.OfType<RolesAuthorizationRequirement>().Any())
{
context.Response.Redirect("/accounts/ask-your-manager");
return;
}
}
await defaultHandler.HandleAsync(next, context, policy, authorizeResult);
}
}
Program.cs:
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, SampleAuthorizationMiddlewareResultHandler>();
The handler is triggered automatically by the AuthorizationMiddleware (app.UseAuthorization()) for every controller/action. The middleware evaluates the authorization policies you have defined for the controller/action (e.g.: [Authorize(Policy = "BasicEdition")] or [Authorize(Roles = "User")]) then passes the authorization result to this handler. Inside the handler we can customize the response based on the authorization policy and the authorization result.
Related
I have an ASP.NET Core web service running on Azure AppService. With every REST call, I check the auth0 token against my profile table to double-check they are a valid user.
Is there a way of refactoring this code out of every REST call, so it always run for every ASP.NET Core call coming in?
Here is a typical example of a REST call... (there are about 30 in all)
[Authorize]
[HttpGet("accounts/{accountId}")]
public async Task<ActionResult<ArchiveJob>> AccountGet(int accountId)
{
//--- common code repeated in every function
var profile = await RetrieveProfile();
if (profile is null)
{
return NotFound("Profile not found for this user")
}
// ... rest of function
return Ok();
}
The right way, is to use AddAuthorization to define your authorisation policies. Perhaps changing the default policy;
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireAssertion(context => {
// note; https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-6.0#access-mvc-request-context-in-handlers
if (context.Resource is HttpContext c)
{
var something = c.RequestServices.GetRequiredService < ...> ();
var profile = await something.RetrieveProfile();
if (profile != null)
return true;
}
return false;
})
.Build();
});
You can create a IAuthorizationFilter to create a custom filter in your core application so it always runs for every ASP.NET Core call coming in.
public class MyAuthAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
//check access
if (CheckPermissions())
{
//all good, add some code if you want. Or don't
}
else
{
//DENIED!
//return "ChallengeResult" to redirect to login page (for example)
context.Result = new ChallengeResult(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}
you can also find detail here
I can't find a simple way to do something that should be quite common.
I have Razor Pages and I want to add some authorization logic to ensure that a request
https://localhost:5001/admin/subscriptions/123`
where the 123 is any Guid is the {subscriptionId}.
I'm using a cookie based authentication, so I should have a claim Name or similar with a specific subscriptionId such as 124 for the current request.
I want to handle these subscription, so that it forbids access when the subscriptionId in the URL doesn't match a specific claim.
It's embarrassing but I don't know how to do it with AspNet Core 3.1 and Razor pages.
It simply doesn't seem to be design for these kind of scenarios. The examples I find consider that we know beforehand what the claims should be. For example in Microsoft official documentation it shows how an "age" claim can be compared to a minimum age of hardcoded magic int 21.
What If both values are request-dependant?
When I add
services
.AddAuthorization(
options =>
{
options.AddPolicy(
"SameSubscriptionIdPolicy",
policy =>
policy.RequireAssertion(context =>
{
var claims = context.User.Claims;
var subscriptionId = context.User.FindFirst(x => x.Type == ClaimTypes.Name);
// how can I access here the Request and do the comparison?
}));
});
If I access the context, I see the URL parametrized, not the real value with the request.
I see admin/subscriptions/{subscriptionId} instead a specific value such as admin/subscriptions/124
How can I access the URL query? so that I can compare it with a claim value?
UPDATE 1:
I read somewhere something here about casting like this:
context.Resource is AuthorizationFilterContext mvcContext
That does not work. Resource is a AuthorizationHandlerContext
Also I've tried the following
services
.AddAuthorization(
options =>
{
options.AddPolicy(
"SameSubscriptionIdPolicy",
policy =>
policy.RequireAssertion(context =>
{
var claims = context.User.Claims;
var subscriptionId = context.User.FindFirst(x => x.Type == ClaimTypes.Name);
var accessor = new HttpContextAccessor();
var url = accessor.HttpContext.GetRouteData();
return false;
}));
});
but HttpContext is null. Maybe by the time the policy starts, there is no request yet?
It took a while but I managed to get it working for DotNetCore 3.1+.
It turns out I needed to inject the IHttpContextAccessor service, so for that I decided not to use the inline policy func implementation and add all the pieces.
First an IAuthorizationRequirement
public class SubscriptionRequirement
: IAuthorizationRequirement
{
public string ClaimName { get; }
public SubscriptionRequirement(string claimName)
{
ClaimName = claimName;
}
}
Not to leave an empty class I decided to pass it the claim name I'm looking for, so that it's a bit more generic.
then the AuthorizationHandler capable of accessing Claims (from cookie or whatever) and the http request.
public class SubscriptionRequirementHandler
: AuthorizationHandler<SubscriptionRequirement>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public SubscriptionRequirementHandler(
IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
SubscriptionRequirement subscriptionRequirement)
{
var claims = context.User.Claims;
var subscriptionId =
context
.User
.FindFirst(x => x.Type == subscriptionRequirement.ClaimName)
.Value;
var httpContext = _httpContextAccessor.HttpContext;
var subscriptionIdInUrl = httpContext.Request.RouteValues["subscriptionId"];
if (subscriptionIdInUrl != null)
{
var isAuthorized = subscriptionId == subscriptionIdInUrl.ToString();
if (isAuthorized)
{
context.Succeed(subscriptionRequirement);
return Task.CompletedTask;
}
}
context.Fail();
return Task.CompletedTask;
}
}
To add this policy:
services
.AddAuthorization(
options =>
{
options.AddPolicy(
"SameSubscriptionIdPolicy",
policy =>
policy.Requirements.Add(new SubscriptionRequirement(ClaimTypes.Name)));
});
I can also (optionally for Razor Pages) specify that I want to apply this policy to all pages within a specific folder
services
.AddRazorPages(
options =>
{
options.Conventions.AuthorizeFolder("/admin", "SameSubscriptionIdPolicy");
options.Conventions.Add(new PageRouteTransformerConvention(new LowerCaseRoutes()));
});
and finally, very importantly, register the handler and the http
services.AddSingleton<IAuthorizationHandler, SubscriptionRequirementHandler>();
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
UPDATE 2020/06/18:
As per Kirk's comment, a nicer way to register the HttpContextAccessor is to do so as a singleton using the already available extension instead.
services.AddHttpContextAccessor();
I'm trying to add a new policy based authorization for a certain user who needs to execute certain actions of a controller that requires rights. I'm new with Policy based authorization but I followed all the instructions on this post, and seems pretty simple.
In my Startup.cs and after the AddMvc() method I have:
services.AddAuthorization(options =>
{
options.AddPolicy("AgentsActivityReport ", policy => policy.RequireUserName("AnaR"));
});
Then, In my controller action I have:
[Authorize(Policy = "AgentsActivityReport")]
public ActionResult AgentsActivity()
{
//some code
}
However, when I launch the application, I receive the following error:
InvalidOperationException: The AuthorizationPolicy named:
'AgentsActivityReport' was not found.
I have also readed a few other threads/posts such as:
Claim Based And Policy-Based Authorization With ASP.NET Core 2.1
reported issue
And everything seems pretty much correct. Any thougts?
Based on this post, I was able to declare and allow my user to invoke an action of a controller by using a requirement. Since my condition has to be, "allow certain roles OR a certain user named AnaR", I had to put that logic into the AuthorizationHandler.cs
Startup.cs
services.AddAuthorization(options => {
options.AddPolicy("ReportActivityPolicy", policy =>
{
policy.AddRequirements(new UserNameRequirement("AnaR"));
});
});
services.AddSingleton<IAuthorizationHandler, AgentsActivityAuthorizationHandler>();
And then, in a separate file:
public class AgentsActivityAuthorizationHandler : AuthorizationHandler<UserNameRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserNameRequirement requirement)
{
if (context.User.IsInRole("Administrator") || context.User.IsInRole("Manager") || context.User.Identity.Name == requirement.UserName)
{
context.Succeed(requirement);
}
return Task.FromResult(0);
}
}
public class UserNameRequirement : IAuthorizationRequirement
{
public UserNameRequirement(string username)
{
this.UserName = username;
}
public string UserName { get; set; }
}
Then, in my controller, the following:
[Authorize(Policy = "ReportActivityPolicy")]
public ActionResult AgentsActivity()
{
//code of your controller.
}
Hope it helps!
It seems that you register a policy named "AgentsActivityReport " with a white space at the end in the middleware but annotate the controller without the whitespace "AgentsActivityReport".
I have setup a standard authentication system up in an ASP.NET Core application.
Users, Roles, RoleClaims(acting as permissions)
In Startup.cs I create a policy for each Role and each Permission. Assuming this would give me full flexibility in my Views to be able to say I want this button to show if user is part of a role that has claim DeleteCustomer or if User belongs to role Superuser.
How can I do an OR condition using the Authorize attribute. For example all throughout my site I want SuperuserRole Policy to have full permission to everything.
Over an action method let's say I have the following:
[Authorize(Policy = "EditCustomer")]
This will require that the logged in user is assigned to a role that has the claim: Edit.Customer since I am creating a policy for the claim Edit.Customer. This is all working fine, but how do I say I would like any User with the Role Superuser to be able to access EditCustomer action method. Superuser is in the database as a Role and additionally added as a policy called RequireSuperUser.
You can add OR condition in Startup.cs:
services.AddAuthorization(options => {
options.AddPolicy("EditCustomer", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => (c.Type == "DeleteCustomer" || c.Type == "Superuser"))));
});
I was facing similar issue where I wanted only "John Doe", "Jane Doe" users to view "Ending Contracts" screen OR anyone only from "MIS" department also to be able to access the same screen. The below worked for me, where I have claim types "department" and "UserName":
services.AddAuthorization(options => {
options.AddPolicy("EndingContracts", policy =>
policy.RequireAssertion(context => context.User.HasClaim(c => (c.Type == "department" && c.Value == "MIS" ||
c.Type == "UserName" && "John Doe, Jane Doe".Contains(c.Value)))));
});
You can use a two different handlers of AuthorizationHandler with the same requirement of IAuthorizationRequirement:
public class FooOrBooRequirement : IAuthorizationRequirement { }
public class FooHandler : AuthorizationHandler<FooOrBooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, FooOrBooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Foo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
public class BooHandler : AuthorizationHandler<FooOrBooRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationContext context, FooOrBooRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == "Boo" && c.Value == true))
{
context.Succeed(requirement);
return Task.FromResult(0);
}
}
}
So in the AddPolicy you will have only one requirement that can be fulfilled of by the FooHandler or by the BooHandler:
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
"MustBeFooOrBoo",
policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new FooOrBooRequirement());
});
});
Please note that the AuthorizationHandler provides a more powerful custom policy so it most relevant only for special cases that require a more complex authentication, for example if you want to determine if the user is a resource owner.
For an only simple claims-based policy I'd advice to use the #richard-mneyan's answer.
We can inject DBContext (if using EF, or user repository if not) or HttpContextAccessor to the AuthorizationHandler to achieve this.
By injecting EF Context (or user repository), we could query database to figure out if the current user is a superuser.
Or by injecting HttpContextAccessor, we can determine if the user is a superuser from that. This approach is faster than the first one but depend on whether User Context have the information needed or not.
To achieve this, assuming what you have are EditCustomerRequirement and EditCustomerAuthorizationHandler:
public class EditCustomerAuthorizationHandler: AuthorizationHandler<EditCustomerRequirement>
{
readonly AppDbContext _context;
readonly IHttpContextAccessor _contextAccessor;
public EditCustomerAuthorizationHandler(DbContext c, IHttpContextAccessor ca)
{
_context = c;
_contextAccessor = ca;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, EditCustomerRequirement requirement)
{
//here we have the access to DB and HttpContext
//we can get the information if the user is superuser from either db or httpContext
var isSuperUser = await IsSuperUser(_context);
//or
var isSuperUser = IsSuperUser(_contextAccessor);
if(isSuperUser)
{
context.Succeed(requirement);
}
else
{
//the current implementation of EditCustomer Policy
}
}
}
IsSuperUser is some kind of method that we could make to get the information from either db context or http context.
I have an almost working implementation of OAuth2 in asp.net 5 (RC1). My solution is based upon the code given by Mark Hughes in Token Based Authentication in ASP.NET 5 (vNext), which is brilliant.
My problem is that my setup is using CORS requests and almost every request is preceded by an OPTIONS request. Even though I only apply the Authorize attribute to the GetAll controller action/method, as shown below, the preceding OPTIONS request is authorized as well.
[Route("api/[controller]")]
public class TextController : Controller
{
[HttpGet]
[Authorize("Bearer", Roles = "admin")]
public IEnumerable<string> GetAll()
{
return _repository.GetAll;
}
...
}
The authorization service setup in startup.cs looks like this:
services.AddAuthorization(auth =>
{
auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser().Build());
});
Is ther any way I can change the behavior of the authorization middleware to skip authorization of OPTIONS requests?
Note:
I have tried creating my own authorization attribute, but for some reason IsAuthenticated always evaluates to false, as if the authorization has not happened yet when reaching this code:
public class BearerAuthorizationAttribute : Attribute, IAuthorizationFilter
{
private readonly string Role;
public BearerAuthorizationAttribute(string Role = null)
{
this.Role = Role;
}
[Authorize("Bearer")]
public void OnAuthorization(Microsoft.AspNet.Mvc.Filters.AuthorizationContext context)
{
string meth = context.HttpContext.Request.Method;
if (meth != "OPTIONS")
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ContentResult() { Content = "Unauthorized", StatusCode = 401 };
return;
}
if (Role != null && !context.HttpContext.User.IsInRole(Role))
{
context.Result = new ContentResult() { Content = "Unauthorized, role level insufficient", StatusCode = 401 };
return;
}
}
}
}
I finally figured out how to fix my problem. In my Startup.cs I was using services.AddCors, like so:
// Create CORS policies
services.AddCors(options =>
{
// Define one or more CORS policies
options.AddPolicy("AllowSpecificOrigin",
builder =>
{
builder.WithOrigins(Configuration.Get<string[]>("AppSettings:AllowedOrigins")) // TODO: revisit and check if this can be more strict and still allow preflight OPTION requests
.AllowAnyMethod()
.AllowAnyHeader();
}
);
});
// Apply CORS policy globally
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new CorsAuthorizationFilterFactory("AllowSpecificOrigin"));
});
It turns out that this only worked partially.
The solution for me was to do app.UseCors in stead, i.e., delete the code above and doing this:
app.UseCors(builder =>
{
builder.WithOrigins(Configuration.Get<string[]>("AppSettings:AllowedOrigins")) // TODO: revisit and check if this can be more strict and still allow preflight OPTION requests
.AllowAnyMethod()
.AllowAnyHeader();
});
When using app.UseCors I get fully working CORS handling, which bounces the OPTIONS reqests before they are being authorized.
The solution is inspired by CORS is not working in web api with OWIN authentication.