I am trying to add some custom role based authorisation, but I am unable to
get the Startup configured to call my AuthorizationHandler.
I found some related information on GitHub: here.
Is this a bug or not ?
I am using ASP.NET Core 3.1 and my initializaion is as follows:
1: This retrieves the url/roles from the database using Dapper ORM:
private List<UrlRole> GetRolesRoutes()
{
var urlRole = DapperORM.ReturnList<UrlRole>("user_url_role_all");
return urlRole.Result.ToList();
}
2: In my Startup, I get the url/roles and store the result in a global variable:
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
this.environment = env;
UrlRoles = GetRolesRoutes();
}
3: My Configuration is: Note the UrlRoles which is passed along
public void ConfigureServices(IServiceCollection services)
{
// .. snip
services.AddAuthorization(o =>
o.AddPolicy(_RequireAuthenticatedUserPolicy,
builder => builder.RequireAuthenticatedUser()));
services.AddAuthorization(options =>
{
options.AddPolicy("Roles", policy =>
policy.Requirements.Add(new UrlRolesRequirement(UrlRoles)));
});
services.AddSingleton<AuthorizationHandler<UrlRolesRequirement>, PermissionHandler>();
}
5: My Handler: which is not being called
public class PermissionHandler : AuthorizationHandler<UrlRolesRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UrlRolesRequirement urlRolesRequirement)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
}
return Task.CompletedTask;
}
}
6: My Requirement class:
public class UrlRolesRequirement : IAuthorizationRequirement
{
private List<UrlRole> UrlRoles { get; }
public UrlRolesRequirement(List<UrlRole> urlRoles)
{
UrlRoles = urlRoles;
}
}
When I debug the ASP.NET Core AuthorizationHandler, I never see that my custom Requirement as being a requirement, which I configured in the Startup. I expected to see the requirement, and if the requirement is present then the "callback" will happen I assume. But for some reason my configuration fails to add the requirement.
public virtual async Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.Resource is TResource)
{
foreach (var req in context.Requirements.OfType<TRequirement>())
{
await HandleRequirementAsync(context, req, (TResource)context.Resource);
}
}
}
Anybody in 2022 having this problem with asp.net core on dotnet 6+ ?
The problem I had with my code was that, I registered my handlers without specifying their interface type of "IAuthorizationHandler".
For example:
Instead of doing this: builder.Services.AddScoped<MustHaveAdminManagementScopeHandler>();
Do this:
builder.Services.AddScoped<IAuthorizationHandler, MustHaveAdminManagementScopeHandler>();
Also, do not register multiple handlers as Singletons. I see the Microsoft docs recommending this, but for some reason, registering multiple IAuthorizationHandler as singletons cause an exception.
Without telling ASP.NET Core to do so, it will not use your configured policy to authorize anything. Authorization policies are so that you can predefine complex authorization conditions so that you can reuse this behavior when you need it. It however does not apply by default, and it couldn’t considering that you already configure two policies: Which of those should apply? All of them? Then why configure separate policies?
So instead, no policy is being used to authorize a user unless you explicitly tell the framework that. One common method is to use the [Authorize] attribute with the policy name. You can put that on controller actions but also on controllers themselves to make all its actions authorize with this policy:
[Authorize("Roles")] // ← this is the policy name
public class ExampleController : Controller
{
// …
}
If you have a policy that you want to use most of the times to authorize users, then you can configure this policy as the default:
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
}
This for example will define a policy that requires an authenticated users as the default. So whenever you use the [Authorize] attribute without specificing an explicit policy, then it will use that default policy.
This all will still require you to mark your routes somehow that you require authorization. Besides using the [Authorize] attribute, you can also do this in a more central location: The app.UseEndpoints() call in your Startup class.
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.RequireAuthorization("Roles");
This is the default route template for controllers but with a call to RequireAuthorization which will basically require the Roles authorization policy on all routes that match this route template.
You can use also use this place to configure different default authorization policies for your different routes: By splitting up your route template, you can have multiple calls to MapControllerRoute with different route templates that all specify their own authorization policy.
I was thinking that instead of decorating each and every controller or action, I rather wanted to have some pre-configuration map in a DB, and then in the pipeline verify the users role or roles which are allocated when the user authenticates. When the user then tries to access a url, the users role gets verified and access is granted or rejected.
You could move the logic how exactly a user is authorized into the authorization handler that verifies your requirement. You would still enable the policy that has this requirement for all the routes you want to test though.
However, I would generally advise against this: Authorization requirements are meant to be simple, and you usually, you want to be able to verify them without hitting a database or something other external resource. You want to use the user’s claims directly to make a quick decision whether or not the user is authorized to access something. After all, these checks run on every request, so you want to make this fast. One major benefit of claims based authorization is that you do not need to hit the database on every request, so you should keep that benefit by making sure everything you need to authorize a user is available in their claims.
Here is a tested solution, which enables runtime configuration changes.
Also relieving the burden of decorating each class or action.
In the Startup Add the Role Authorization Requirement, and also register the
RoleService which will be responsible for ensuring a particular role is authorised to access a particular URL.
Here is the Startup.cs where we configure the requirement and also the role service:
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new UrlRolesRequirement())
.Build();
});
services.AddSingleton<IUserService, UserService>(); // authenticate
services.AddSingleton<IUserRoleService, UserRoleService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IAuthorizationHandler, PermissionHandler>(); // authorise
The role IUserRoleService: - the UserRoleService implementation
validates a users claimed role (a JWT claim) against a configuration map consisting of url/role entries that are allowed, by looking in either in cached map or retrieving the data from the database.
A typical url(path) to role map has the following format, retrieve from a database, then cached (if lookups fail, the data is retrieved from the database):
/path/to/resource ROLE
public interface IUserRoleService
{
public bool UserHasAccess(ClaimsPrincipal user, string path);
}
The Permission handler:
public class PermissionHandler : IAuthorizationHandler
{
private readonly IUserRoleService userRoleService;
private readonly IHttpContextAccessor contextAccessor;
public PermissionHandler(IUserRoleService userRoleService, IHttpContextAccessor contextAccessor)
{
this.userRoleService = userRoleService;
this.contextAccessor = contextAccessor;
}
public Task HandleAsync(AuthorizationHandlerContext context)
{
var pendingRequirements = context.PendingRequirements.ToList();
foreach (var requirement in pendingRequirements)
{
if (!(requirement is UrlRolesRequirement)) continue;
var httpContext = contextAccessor.HttpContext;
var path = httpContext.Request.Path;
if (userRoleService.UserHasAccess(context.User, path))
{
context.Succeed(requirement);
break;
}
}
return Task.CompletedTask;
}
}
The RolesRequirement - just a POCO
public class UrlRolesRequirement : IAuthorizationRequirement
{
}
Here is a partial implementation of the UserRoleService which validates the JWT role claimed.
private bool ValidateUser(ClaimsPrincipal user, string path)
{
foreach (var userClaim in user.Claims)
{
if (!userClaim.Type.Contains("claims/role")) continue;
var role = userClaim.Value;
var key = role + SEPARATOR + path;
if (urlRoles.ContainsKey(key))
{
var entry = urlRoles[key];
if (entry.Url.Equals(path) && entry.Role.Equals(role))
{
return true;
}
}
}
Console.WriteLine("Access denied: " + path);
return false;
}
I had an immediate 403 response, my custom authorization handler code was never reached. Turns out I forgot to inject them (Scoped is fine). That solved the issue for me.
Related
The remote user uses the API for which authorization is needed, and passes it successfully.
Created an Asp Core Web API project, I use Identity to control access, deleted a user through UserManager.DeleteAsync:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Info { Title = "API", Version = "v1" });
});
services.AddDbContext<CommonDBContext>(options => options.UseNpgsql(Configuration.GetConnectionString("PostgreSQL")));
services.AddScoped<IFamilyRepository, FamilyRepository>()
.AddScoped<IProfileRepository, ProfileRepository>();
services.AddIdentity<UserEntity, RoleEntity>(options =>
{
options.SignIn.RequireConfirmedEmail = false;
options.User.RequireUniqueEmail = true;
})
.AddUserManager<UserManagerExtensions>()
.AddEntityFrameworkStores<CommonDBContext>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API");
});
app.UseMiddleware<TestMiddelwwre>();
app.UseAuthentication();
app.UseMvc();
}
}
Expected Result: A user who has been deleted cannot use the action with [Authorize] attributes.
Actual result: the user successfully passes Authentication and Authorization
The default authorization middleware validates the access token supplied - be it a cookie or bearer token - and simply decodes the information encoded therein, which can be out of sync with changes to the user that occurred after the token was issued. It does not check the current state of the user back in the database. If you want this functionality, you will have to add it to the authorization middleware. Please note that this means an additional trip to the database, so use it wisely - might want to invoke this only on certain critical endpoints and leave the default policy everywhere else, but thats up to you.
When you configure your authorization, you can add an authorization policy that requires a user that "is not deleted" (in my example i added a policy called "ActiveUserPolicy"), or edit the default policy to require this.
One way to achieve this is via authorization requirements & requirement handler. The main advantage of this is that it allows dependency injection into the requirement handlers, so you can do whatever you want with registered services. Here is a short example.
You will need an authorization requirement class, lets say ActiveUserRequirement. It simple needs to extend the IAuthorizationRequirement interface
public class ActiveUserRequirement: IAuthorizationRequirement {}
Then you will need the handler
public class ActiveUserRequirementHandler : AuthorizationHandler<ActiveUserRequirement>
{
private readonly UserManager<IdentityUser> _userManager;
// UserManager<TUser> available via dependency injection. U can inject anything you want
public ActiveUserRequirementHandler(UserManager<IdentityUser> userManager)
{
this._userManager = userManager;
}
/// <inheritdoc />
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ActiveUserRequirement requirement)
{
var userFromDb = await _userManager.GetUserAsync(context.User);
// Check whatever qualifies as a deleted user in your usecase.
if (userFromDb != null && userFromDb.EmailConfirmed) // user exists in db, & we confirmed their email
{
context.Succeed(requirement);
}
else
{
context.Fail();
// calling .Fail() is not recommended, the framework assumes a requirement has failed automatically if no handler explicitly says it passed.
// this allows multiple requirement handlers to have their own way of determining whether a requirement has passed
}
}
}
Now all you need is to register this with your authorization middleware.
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
....
services.AddAuthorization(options =>
{
options.AddPolicy("ActiveUserPolicy", policyBuilder =>
{
policyBuilder.RequireAuthenticatedUser();
policyBuilder.AddRequirements(new ActiveUserRequirement());
});
});
services.AddSingleton<IAuthorizationHandler, ActiveUserRequirementHandler>();
}
With this setup, you can add a [Authorize(Policy = "ActiveUserPolicy")] attribute to your controllers, or actions.
If you with this to work simply with [Authorize], then you might need to change the default authorization policy. yo can simply set this up as follows:
services.AddAuthorization(options =>
{
options.DefaultPolicy =
new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new ActiveUserRequirement())
.Build();
}
Make sure you call app.UseAuthorization() right after app.AddAuthentication() to ensure the authorization middleware is applied.
Also read Policy based authorization from the official docs to learn more on this.
I am developing an intranet asp.net core web api application. The requirements for authentications are:
REQ1 - when user which is trying to access the website is not in Active Directory's special group (let's name it "commonUsers") it is simply not authorized
REQ2 - when user which is trying to access the website is in Active Directory's group "commonUsers" is is authorized and a web resource is returned
REQ3 - when user which is trying to access the website is in Active Directory's group "superUser", it need to be prompted for his domain password once again (because it tries to access some very restricted resources)
Now, what I have so far:
My service is hosted using http.sys server in order to support windows authentication.
I am using claims transformer middlewere in order to check the user's Active Directory group, let's say something like this:
public class ClaimsTransformer : IClaimsTransformation {
private readonly IAuthorizationService _authorizationService;
public ClaimsTransformer(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
_authorizationService.Authorize(principal as IHmiClaimsPrincipal);
return Task.FromResult(principal);
}}
I have specified a special policies also in my service configuration, for instance something like that:
services.AddAuthorization(options =>
{
options.AddPolicy("TestPolicy", policy =>
policy.RequireClaim(ClaimTypes.Role, "TestUser"));
options.AddPolicy("TestPolicy2", policy =>
policy.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
I am using [Authorize] attribute with specific policy in order to restrict access to specific resources based on policies
Now the question is, how should I satisfy REQ3?
I think I would try to use MVC Filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#authorization-filters
Filters run after all Middleware, but before the Action. This will allow you to control the redirect to credentials page just for specific actions or controllers. Whilst normally this is not the recommended method for authorization, I think it fits your requirements for a hybrid secondary authentication.
public class SuperUserFilter : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.Request.Cookies.TryGetValue("SuperUserCookie", out string cookieVal))
{
if (!IsValidCookie(cookieVal))
context.Result = LoginPage(context);
}
else
{
context.Result = LoginPage(context);
}
}
private bool IsValidCookie(string cookieVal)
{
//validate cookie value somehow
// crytpographic hash, store value in session, whatever
return true;
}
private ActionResult LoginPage(AuthorizationFilterContext context)
{
return new RedirectToActionResult("SuperUser", "Login",
new {redirectUrl = context.HttpContext.Request.GetEncodedUrl()});
}
}
Then you create a Login Controller
public class LoginController : Controller
{
[HttpGet]
public IActionResult SuperUser(string redirectUrl)
{
// return a page to enter credentials
// Include redirectUrl as field
}
[HttpPost]
public IActionResult SuperUser(LoginData loginData)
{
// Validate User & Password
Response.Cookies.Append("SuperUserCookie", "SomeValue");
return Redirect(loginData.RedirectUrl);
}
}
Then you can decorate specific actions (or controllers) as required:
public class MyController : Controller
{
[HttpGet]
[SuperUserFilter]
public IActionResult MySensitiveAction()
{
// Do something sensitive
}
}
I'm guessing you are try to implement two step authentication for some of your resource.
To do that you must use multiple authentication scheme and Authorize policies,
but it's difficult because windows authentication is not controllable. we need to use some trick to know this is your second login.
authentication
The Default Authenticaiton Scheme : Windows, it's the basic scheme for authenticate a windows user.
Second Cookies base Authentication scheme : SuperUserTwoStep. we need this to goto our custom login logic.
Authorize
the Authorize policies for specified scheme.
a login page for login to SuperUserTwoStep scheme.
//startup
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme)
.AddCookie("SuperUserTwoStep",op=>op.LoginPath = "/account/superuser2steplogin");
services.AddAuthorization(op =>
{
op.AddPolicy("SuperUser", b => b.AddAuthenticationSchemes("SuperUserTwoStep")
.RequireAuthenticatedUser()
.RequireClaim(ClaimTypes.Role, "SuperUser"));
});
// login
public static IDictionary<string, string> States { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
[Route("/account/superuser2steplogin")]
public async Task<IActionResult> LoginTwoStepConfirm(string returnUrl, [FromServices]IAuthorizationService authorizationService,
[FromServices]IAuthorizationPolicyProvider policyProvider)
{
var winresult = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
if (winresult.Succeeded)
{
if (States.TryGetValue(winresult.Principal.Identity.Name, out _))
{
States.Remove(winresult.Principal.Identity.Name);
var principal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity(winresult.Principal.Claims,"twostepcookie"));
await HttpContext.SignInAsync("SuperUserTwoStep", principal);
return Redirect(returnUrl);
}
else
{
States[winresult.Principal.Identity.Name] = "1";
return Challenge(IISDefaults.AuthenticationScheme);
}
}
else
{
return Challenge(IISDefaults.AuthenticationScheme);
}
}
[Authorize("SuperUser")]
public IActionResult YourSecurePage()
{
return Content("hello world");
}
the most difficult thing is to track that this is the second time to login, I try to use cookie , but it doen't work, so I crate a static IDitionary<string,string> to track ,maybe use distributed cache is better
I think in my opinion you should consider using: Policy-based authorization with Requirements, basically you have different authorization requirements that you want to treat them on and AND basis
REQ1 and REQ2 and REQ3
Here you have the link to the documentation: Requirements
But you need to understand that identity != permissions, the guys that introduce this concept of policies to Microsoft created a project named: PolicyServer and it is opensource: PolicyServer Git and they created a pattern there of how you should use your policies. Basically, you have external and internal users that are authenticated against your AD, all internal users should have permissions assigned to a role. And you only decorate your controller action with the permission rule you created for that policy
[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
// omitted
}
To understand the code and how they evaluate a policy, I think you should see the video they have online on the website: Policy Server
Hope this helps
Long story short, Is it possible to place an environment based authorization attribute on my API so that the authorization restriction would be turned off in development and turned back on in Production?
I have a separate Angular 2 project that I wish to call a .NET Core API with. We created a separate project so we could open the Angular 2 project in vscode and debug the typescript. When we are finished, we will build the project and place it inside the .NET Core project for security reasons.
Our problem is that during the debugging stages, we are unable to connect to the API because they are two separate projects and our Angular 2 project does not have Active Directory. The .NET Core project currently has Authentication Attributes and wont allow access (401) to the API. It would be nice if we could turn that off during development and back on during production.
I'm also open to any other suggestions on how we can best solve this problem.
[Authorize: (Only in Production)] <-- // something like this???
[Route("api/[controller]")]
public class TestController : Controller
{
...
ASP.NET Core authorization is based on policies. As you may have seen, the AuthorizeAttribute can take a policy name so it knows which criteria need to be satisfied for the request to be authorized. I suggest that you have a read of the great documentation on that subject.
Back to your problem, it looks like you don't use a specific policy, so it uses the default one, which requires the user to be authenticated by default.
You can change that behaviour in Startup.cs. If you're in development mode, you can redefine the default policy so that it doesn't have any requirements:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(x =>
{
// _env is of type IHostingEnvironment, which you can inject in
// the ctor of Startup
if (_env.IsDevelopment())
{
x.DefaultPolicy = new AuthorizationPolicyBuilder().Build();
}
});
}
Update
im1dermike mentioned in a comment that an AuthorizationPolicy needs at least one requirement, as we can see here. That code wasn't introduced recently, so it means the solution above was broken the whole time.
To work around this, we can still leverage the RequireAssertion method of AuthorizationPolicyBuilder and add a dummy requirement. This would look like:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(x =>
{
// _env is of type IHostingEnvironment, which you can inject in
// the ctor of Startup
if (_env.IsDevelopment())
{
x.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAssertion(_ => true)
.Build();
}
});
}
This ensures we have at least one requirement in the authorization policy, and we know that it will always pass.
I end up with this, might help :
public class OnlyDebugModeAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
}
public override void OnActionExecuting(ActionExecutingContext context)
{
#if DEBUG
//Ok
#else
context.Result = new ForbidResult();
return;
#endif
}
}
and then apply it on controler
[OnlyDebugMode]
[Route("api/[controller]")]
[ApiController]
public class DebugController : ControllerBase
{
Here's my solution:
New attribute for your controllers:
[AzureADAuthorize]
AzureADAuthorize.cs:
public class AzureADAuthorize : AuthorizeAttribute
{
public AzureADAuthorize() : base(AzureADPolicies.Name)
{
}
}
AzureADPolicies.cs:
public static class AzureADPolicies
{
public static string Name => "AzureADAuthorizationRequired";
public static void Build(AuthorizationPolicyBuilder builder)
{
if (StaticRepo.Configuration.GetValue<bool>("EnableAuthorization") == true)
{
var section = StaticRepo.Configuration.GetSection($"AzureAd:AuthorizedAdGroups");
var groups = section.Get<string[]>();
builder.RequireClaim("groups", groups);
}
else if (StaticRepo.Configuration.GetValue<bool>("EnableAuthentication") == true)
{
builder.RequireAuthenticatedUser();
}else
{
builder
.RequireAssertion(_ => true)
.Build();
}
}
}
Startup.cs:
//Authentication & Authorization
#region AUTHENTICATION / AUTHORICATION
StaticRepo.Configuration = Configuration;
services.AddAuthorization(options =>
{
options.AddPolicy(
AzureADPolicies.Name, AzureADPolicies.Build);
});
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
#endregion
Appsettings.json:
"EnableAuditLogging": false,
"EnableAuthentication": true,
"EnableAuthorization": false,
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "https://MyDomain.onmicrosoft.com/",
"TenantId": "b6909603-e5a8-497d-8fdb-7f10240fdd10",
"ClientId": "6d09a1bf-4678-4aee-b67c-2d6df68d5324",
"CallbackPath": "/signin-oidc",
//Your Azure AD Security Group Object IDs that users needs to be member of to gain access
"AuthorizedAdGroups": [
"568bd325-283f-4909-9fcc-a493d19f98e8",
"eee6d366-0f4d-4fca-9965-b2bc0770506d"
]
}
(These are random guids)
Now you can conditional control if you want to have anonymous access, azure ad authentication, authentication + group authorization. There are still some stuff you need to setup in your azure ad app manifest file to get it to work, but I think it's outside the scope here.
This question is essentially the same as the one here, but, for asp.net core while using the asp.net core cookie middleware.
Is accessing query string/request body data possible on validation, and if it is, would you encourage the idea? It seems that according to this that it is very much possible, however, are the same rules in play from big boy asp.net (such as you are only to read the request data once in a given requests lifetime)?
Example: I'm creating an app where people have one account, but, are members of different teams. They can perform many different actions in the app, and, they can perform that action while in the "context" of one team or another that they are a member of. So, I have a teamId integer being passed in requests made to the server. I'd like to pull claims off the ClaimsPrincipal verifying that they really are a member of that team in the authorization portion of the pipeline.
As you said it is possible to access request's data on OnValidatePrincipal event. So, you can write something like this:
OnValidatePrincipal = async (context) =>
{
if (context.Request.Path.Value.StartsWith("/teams/"))
{
var teamId = // get team id from Path;
if (user is not team member)
{
context.Response.StatusCode = 403;
}
}
}
However, i think your requirement is related Authorization rather than Authentication. I would use Policy-Based Authorization to handle the requirement. Example policy should be like this:
Requirement and Handler:
public class TeamMemberHandler: AuthorizationHandler<TeamMemberRequirement>
{
private readonly IActionContextAccessor _accessor; // for getting teamId from RouteData
public TeamMemberHandler(IActionContextAccessor accessor)
{
_accessor = accessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TeamMemberRequirement requirement)
{
var teamId = // get teamId with using _accessor
if (user is not member of team(by teamId))
{
context.Fail();
}
return Task.FromResult(0);
}
}
public class TeamMemberRequirement : IAuthorizationRequirement
{
}
Configure Services:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddAuthorization(options =>
{
options.AddPolicy("TeamMember",
policy => policy.Requirements.Add(new TeamMemberRequirement()));
});
services.AddSingleton<IAuthorizationHandler, TeamMemberHandler>();
}
Finally use it on top of controller(or if you want, you can add filter globally)
Authorize[(Policy = "TeamMember")]
public class TeamHomeController : Controller
{
// Authorize[(Policy = "AnotherPolicy")]
public IActionResult Index(){}
}
Following the docs here I tried to implement a policy-based auth scheme. http://docs.asp.net/en/latest/security/authorization/policies.html#security-authorization-handler-example
I ran into the issue that my Handle method was not being called on my custom AuthorizationHandler. (It does not throw here). It also does inject the dependency currently in the constructor.
Here it the AuthorizationHandler Code.
using WebAPIApplication.Services;
using Microsoft.AspNet.Authorization;
namespace WebAPIApplication.Auth
{
public class TokenAuthHandler : AuthorizationHandler<TokenRequirement>, IAuthorizationRequirement
{
private IAuthService _authService;
public TokenAuthHandler(IAuthService authService)
{
_authService = authService;
}
protected override void Handle(AuthorizationContext context, TokenRequirement requirement)
{
throw new Exception("Handle Reached");
}
}
public class TokenRequirement : IAuthorizationRequirement
{
public TokenRequirement()
{
}
}
}
In Start Up I have
// Authorization
services.AddSingleton<IAuthorizationHandler, TokenAuthHandler>()
.AddAuthorization(options =>
{
options.AddPolicy("ValidToken",
policy => policy.Requirements.Add(new TokenRequirement()));
});
The controller method is
// GET: api/values
[HttpGet, Authorize(Policy="ValidToken")]
public string Get()
{
return "test";
}
Hitting this endpoint returns nothing and there is a warning in the console of
warn: Microsoft.AspNet.Mvc.Controllers.ControllerActionInvoker[0]
Authorization failed for the request at filter 'Microsoft.AspNet.Mvc.Filters.AuthorizeFilter'.
I am able to hit other endpoints that don't have the attribute successfully.
SOS,
Jack
I'm putting this here for reference because I spent way too long figuring this out...
I had implemented a custom requirement and handler (empty for testing's sake):
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
public class TestHandler : AuthorizationHandler<TestRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TestRequirement requirement)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
}
public class TestRequirement : IAuthorizationRequirement
{
}
Registered it in my Startup.cs ConfigureServices() section:
services.AddAuthorization(options =>
{
options.AddPolicy("Test", policy => policy.Requirements.Add(new TestRequirement()));
// Other policies here
}
Added it to my controller method:
[HttpGet]
[Authorize(Policy = "Test")]
public IActionResult Index()
{
Return View();
}
But was getting a 403 error (not 401) with every request to the controller method!
Turns out, I was not registering TestHandler with the ConfigureServices() (Dependency Injection) section of Startup.cs.
services.AddSingleton<IAuthorizationHandler, TestHandler>();
Hope this saves someone from banging their head on their desk. :|
The answer to this question is alluded to in a comment to adem caglin, so props to him.
The issue is that the AuthorizeFilter is rejecting the request before the AuthorizationHandler is being called. This is because for every use of the Authorize tag MVC adds AuthorizeFilter ahead of the AuthorizationHandler in the pipeline. This AuthorizeFilter checks to see if any of the current users identities are authorized. In my case there were no authorized identities associated with any user so this would always fail.
A solution (which IMO is somewhat hackish) is to insert a peice of middleware that will get executed before any MVC code. This middleware will add a generic authenticated identity to a User (if the user does not already have one).
Consequently the AuthorizeFilter check will pass and the Handle method on the AuthenticationHandler method will be executed and our problem will be solved. The middleware code (which needs to be added to Configure before app.UseMvc(); is called) is as follows
app.Use(async (context, next) =>
{
if (!context.User.Identities.Any(i => i.IsAuthenticated))
{
context.User = new ClaimsPrincipal(new GenericIdentity("Unknown"));
}
await next.Invoke();
});
An alternative way to override the AuthorizeFilter is outline here (Override global authorize filter in ASP.NET Core MVC 1.0)
Citing the response from here (Asp.Net Core policy based authorization ends with 401 Unauthorized)
Take a look at Asp.net Core Authorize Redirection Not Happening i think adding options.AutomaticChallenge = true; solves your problem.