517

I'm trying to make a custom authorization attribute in ASP.NET Core. In previous versions it was possible to override bool AuthorizeCore(HttpContextBase httpContext). But this no longer exists in AuthorizeAttribute.

What is the current approach to make a custom AuthorizeAttribute?

What I am trying to accomplish: I am receiving a session ID in the Header Authorization. From that ID I'll know whether a particular action is valid.

AnotherGuy
  • 585
  • 11
  • 18
jltrem
  • 10,475
  • 4
  • 37
  • 46
  • I'm not sure how to do it, but MVC is open source. You could pull the github repo and look for implementations of IAuthorizationFilter. If I have time today I'll look for you and post an actual answer, but no promises. github repo: https://github.com/aspnet/Mvc – bopapa_1979 Jul 16 '15 at 21:07
  • OK, out of time, but look for usages of AuthorizationPolicy in the MVC Repo, which uses AuthorizeAttribute, in the aspnet/Security repo, here: https://github.com/aspnet/Security. Alternately, look in the MVC repo for the namespace where the security stuff you care about seems to reside, which is Microsoft.AspNet.Authorization. Sorry I can't be more helpful. Good luck! – bopapa_1979 Jul 16 '15 at 21:30

14 Answers14

569

The approach recommended by the ASP.Net Core team is to use the new policy design which is fully documented here. The basic idea behind the new approach is to use the new [Authorize] attribute to designate a "policy" (e.g. [Authorize( Policy = "YouNeedToBe18ToDoThis")] where the policy is registered in the application's Startup.cs to execute some block of code (i.e. ensure the user has an age claim where the age is 18 or older).

The policy design is a great addition to the framework and the ASP.Net Security Core team should be commended for its introduction. That said, it isn't well-suited for all cases. The shortcoming of this approach is that it fails to provide a convenient solution for the most common need of simply asserting that a given controller or action requires a given claim type. In the case where an application may have hundreds of discrete permissions governing CRUD operations on individual REST resources ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder", etc.), the new approach either requires repetitive one-to-one mappings between a policy name and a claim name (e.g. options.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));), or writing some code to perform these registrations at run time (e.g. read all claim types from a database and perform the aforementioned call in a loop). The problem with this approach for the majority of cases is that it's unnecessary overhead.

While the ASP.Net Core Security team recommends never creating your own solution, in some cases this may be the most prudent option with which to start.

The following is an implementation which uses the IAuthorizationFilter to provide a simple way to express a claim requirement for a given controller or action:

public class ClaimRequirementAttribute : TypeFilterAttribute
{
    public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
    {
        Arguments = new object[] {new Claim(claimType, claimValue) };
    }
}

public class ClaimRequirementFilter : IAuthorizationFilter
{
    readonly Claim _claim;

    public ClaimRequirementFilter(Claim claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
        if (!hasClaim)
        {
            context.Result = new ForbidResult();
        }
    }
}


[Route("api/resource")]
public class MyController : Controller
{
    [ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
    [HttpGet]
    public IActionResult GetResource()
    {
        return Ok();
    }
}
Grigory Zhadko
  • 860
  • 1
  • 12
  • 22
Derek Greer
  • 13,465
  • 5
  • 38
  • 47
  • 105
    This should be marked as the CORRECT ANSWER. Here you see how the people at Microsoft considers the developers feedback. I don't understand the reason they are so "closed minded" arround this, since it's a very common situation to have a miriad of different permissions, having to code one policy for each one is a complete overkill. I was looking for this for such a long time... (I already asked this question almost two years ago, when vNext was still a bet here: http://stackoverflow.com/questions/32181400/pass-parameters-to-a-requirement-policy-in-asp-net-mvc-6 but we're still stuck there) – Vi100 Mar 10 '17 at 12:00
  • 5
    This is good stuff. We have authentication middleware on the Web API but grained security on the authorization permissions by role; so having to just throw in an attribute like: [MyAuthorize(MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] looks very fine. – Mariano Peinador Apr 21 '17 at 01:07
  • This is a great approach. I modified this for my situation to deal with Permissions (enum with flags attributes) instead. – ctv Jun 24 '17 at 00:06
  • 7
    @Derek Greer: This is the best answer. However, you are implement an ActionFilter which run after Authorize Action Filter. Is there anyway to implement and Authorize Action Filter? – Jacob Phan Sep 08 '17 at 04:03
  • 8
    @JacobPhan You're correct, this would be better implemented using the IAuthorizationFilter interface. I've updated the code to reflect the changes. – Derek Greer Sep 08 '17 at 18:22
  • 4
    so `new ForbidResult()` doesn't work (causes exception/500) because it doesn't have an associated authorization scheme. What would I use for this case? – Sinaesthetic Mar 28 '18 at 20:51
  • See also the discussion in this github issue: https://github.com/aspnet/Mvc/issues/5607, which is closed in favor of this issue: https://github.com/aspnet/Security/issues/1359 – Matty Apr 09 '18 at 10:44
  • 1
    Note that if your `OnAuthorization` implementation needs to await an async method, you should implement `IAsyncAuthorizationFilter` instead of `IAuthorizationFilter` otherwise your filter will execute synchronously and your controller action will execute regardless of the outcome of the filter. – Codemunkie Mar 23 '19 at 20:47
  • @Codemunkie It would certainly be advisable to implement the async version of the interface if the behavior being implemented requires the use of asynchronous methods, but your comment can really be distilled down to the advise of not invoking an asynchronous behavior whose results or side-effects need to be completed first before other operations. – Derek Greer Mar 25 '19 at 15:40
  • this is an awesome answer! However, it **does not** work on SignalR Hubs and Hub methods – ilkerkaran Jul 30 '19 at 20:37
  • Note this approach may now exhibit a breaking change, if your implementation previously accounted for `AllowAnonymous`. See here: https://github.com/aspnet/Announcements/issues/391 – shannon Oct 17 '19 at 19:30
  • 1
    If I'm understanding this correctly, this would be relevant for anyone using this approach to place an claim attribute at the controller level to affect all actions where they desired the ability to override a given action with [AllowAnonymous]. The code presented here doesn't account for the AllowAnonymous attribute and so it would work the same before and after the breaking change, but that certainly would be a desirable and expected behavior of this attribute. Thanks for the link to the solution for accounting for this with version 3. – Derek Greer Oct 18 '19 at 14:36
  • This is one of the best answers – StackOrder Mar 29 '20 at 17:28
  • In aspnet core 3.1, the shortcoming outlined in the post appears to have been corrected; https://docs.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-3.1 – Paul Taylor Apr 09 '20 at 15:03
  • @ilkerkaran could you comment why using this + SignalR is a problem? – Notbad Apr 13 '20 at 14:16
  • @Notbad, TypeFilterAttribute does not triggered from Hubs. You need to use IAuthorizationFilter – ilkerkaran Apr 13 '20 at 15:30
  • @ilkerkaran just to know, does the previous concept works using IAuthorizationFilter instead of TypeFilterAttribute? I mean did you get something like what Derek posted buy SignalR compatible? – Notbad Apr 13 '20 at 19:56
  • The accepted answer is not realistically maintainable or suitable because "CanReadResource" is essentially not a claim but a policy, unless you have a data source where "CanReadResource" had been mapped to a user. This approach is not OK in the way it was used at that answer because if an action method requires many different claims setups, then with that answer you would have to repeatedly write the attribute for just one action, for example. Check my answer for my suggestion (https://stackoverflow.com/a/61321866/4974715). – Olumide Apr 20 '20 at 12:41
  • There's a number of points to address with this comment for which this context doesn't provide adequate space to respond. What I can do is refer you to [this](https://github.com/aspnet/Mvc/issues/5607#issuecomment-269650196) comment to help distinguish the concepts of permissions and policies. CanReadResource is a permission in this example, not a policy. It implies a policy, but it is not itself a policy. If you need the new policy functionality to group permissions or dynamically deduce permissions, great! Use it. This approach is however most definitely maintainable. – Derek Greer Apr 23 '20 at 14:48
  • @DerekGreer I'm trying to use this in a Blazor WASM Standalone app but it looks like OnAuthorization is never called, have you ever tested this on Blazor? – Pietro Jun 10 '20 at 14:52
  • @Pietro Sorry, haven't played with blazor – Derek Greer Jun 12 '20 at 21:24
  • Seems to block the call correctly, but it still returns an Http Status Code of 200 (OK) to the caller. Would be nice if there was a way to return a status code of 403. – Vaccano Aug 19 '20 at 19:37
  • What changes required for Web API project. aka `:ControllerBase` class? – Faizan Mubasher Jan 30 '21 at 14:53
  • Anyone, maybe @DerekGreer, knows how to adapt this to gRPC service? It works great on a normal webapi controller but I'm not able to do the same for a gRPC service. – Pietro Mar 21 '21 at 13:56
  • This policy-based authorisation is a typical example of Microsoft over-engineered and therefore under-engineered solutions. – neeohw May 11 '21 at 08:16
278

I'm the asp.net security person. Firstly let me apologize that none of this is documented yet outside of the music store sample or unit tests, and it's all still being refined in terms of exposed APIs. Detailed documentation is here.

We don't want you writing custom authorize attributes. If you need to do that we've done something wrong. Instead, you should be writing authorization requirements.

Authorization acts upon Identities. Identities are created by authentication.

You say in comments you want to check a session ID in a header. Your session ID would be the basis for identity. If you wanted to use the Authorize attribute you'd write an authentication middleware to take that header and turn it into an authenticated ClaimsPrincipal. You would then check that inside an authorization requirement. Authorization requirements can be as complicated as you like, for example here's one that takes a date of birth claim on the current identity and will authorize if the user is over 18;

public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
        public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
        {
            if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
            {
                context.Fail();
                return;
            }

            var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
            int age = DateTime.Today.Year - dateOfBirth.Year;
            if (dateOfBirth > DateTime.Today.AddYears(-age))
            {
                age--;
            }

            if (age >= 18)
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
        }
    }
}

Then in your ConfigureServices() function you'd wire it up

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", 
        policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});

And finally, apply it to a controller or action method with

[Authorize(Policy = "Over18")]
Suit Boy Apps
  • 3,005
  • 8
  • 36
  • 54
blowdart
  • 52,422
  • 11
  • 102
  • 145
  • Thanks for the example of making an AuthorizationHandler; I was able to get this working. Alternatively it seems I can accomplish what I need by subclassing ActionFilterAttribute and overriding OnActionExecuting, though this would be 'hacky' since it is outside the authentication framework. – jltrem Jul 17 '15 at 13:10
  • 2
    Yea, please don't head down the route of the action filter. We're hopeful that the new policy based system should be flexible enough for 99% of scenarios, and as a bonus, can be used in 3rd party frameworks as well. If you find cases it isn't working for you, email me at MSFT. – blowdart Jul 20 '15 at 22:54
  • @blowdart Is the way to handle authorize without a value for policy "options.AddPolicy(string.Empty, policy => policy.AddRequirements(new Test()));" ? I'm in the process of trying it out but right now i get an empty page + debugging does not work – Dbl Jul 23 '15 at 19:30
  • 1
    @AndreasMüller I'd be interesting why you want a blank policy name and what you expect to happen. It's not something we've tested, or coded for. This should be raised as an issue on github though. – blowdart Jul 23 '15 at 22:10
  • @blowdart well. basically i would like to have manual control over how authorization is verified even when it's just [Authorize] without any policy. That way it would be easy to allow scenarios entirely independent of ClaimsPrincipal being set (imagine single use request tokens for example). Are you going to raise the issue? I could try to but last time i had a reason to do so i got lost in githubs weird UI and gave up – Dbl Jul 23 '15 at 23:18
  • 1
    That still doesn't explain why you want a blank policy name. In any case you'll still need a principal. That's what authorization acts upon. As I still don't understand your issue I'd rather you open it. https://github.com/aspnet/security/issues and click the new issue button – blowdart Jul 24 '15 at 02:34
  • 1
    @blowdart alright. i'll do that when i get home. thanks. – Dbl Jul 24 '15 at 08:44
  • @blowdart How would I go about accessing HttpContext/RouteData (params) in this requirement? It seems AuthorizationContext context.Resource is limited. Thanks! – daredev Aug 04 '15 at 17:57
  • @daredev you can get to them in an `AuthenticationHandler`. See http://stackoverflow.com/a/31688792/571637 – jltrem Aug 06 '15 at 02:11
  • 1
    @dareDev you can also cast context.Resouce to Microsoft.AspNet.Mvc.AuthorizationContext. Example: Microsoft.AspNet.Mvc.AuthorizationContext resource = (Microsoft.AspNet.Mvc.AuthorizationContext) context.Resource; string id = resource.RouteData.Values["id"].ToString(); – Dustin Gamester Aug 12 '15 at 16:14
  • 97
    I wonder... how would one implement a fine grained access control with that? Let's say the `ManageStore` Requirement from Music Store sample. As it's in the sample, there is only an either "allow all or nothing" way to do it. Do we then have to create a new policy for every possible permutation? i.e. "Users/Read", "Users/Create", "Users/AssignRole", "Users/Delete" if we want fine-grained claims? Sounds like pretty much setup work to get it working and abundance of policies just to manage claims rather than a `[ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]` attribute? – Tseng Aug 30 '15 at 12:31
  • 1
    We rewrote it because writing your own authorize attribute was riddled with failure and *was* broken. – blowdart Dec 07 '15 at 21:22
  • 6
    It's now, finally, documented. https://docs.asp.net/en/latest/security/authorization/index.html – blowdart Dec 08 '15 at 22:03
  • 100
    I have to comment that, all this is more complex than implementing a custom authorization method. I know how I want authorization to be done I could just go and write it in MVC 5, in MVC 6 they add a lot of "done" code that is actually more complex to understand than implementing the core "thing" itself. Gets me sitting in front of a page trying to figure something out instead of writing code right through, also a big pain for people who use RDBMS other than Microsoft's (or No-Sql). – Felype Dec 10 '15 at 17:24
  • 1
    I correct myself, all of the requests can be finely filtered and processed using `Microsoft.AspNet.Mvc.Filters` and `Middleware` classes, AspNet 5 does provide both built-in sophisticated tools and the possibility to fine tune web applications from the very root of request handling through a very nice and sophisticated pipeline system. – Felype Dec 11 '15 at 10:57
  • @blowdart Is there any naming convention for the policies? – Mariusz Jamro Dec 21 '15 at 13:40
  • Nope, they're strings and you can name them as you like. – blowdart Dec 21 '15 at 13:52
  • 19
    From my point of view, this doesnt solve all scenarios. Prior to MVC 6, I used a custom Authorize Attribute, to implement my own "Permission System". I could add the Authorize attribute to all actions, and pass one specific needed permission (as Enum-Value). The permission itself is was mapped to groups/users within the DB. So, I don't see a way to handle this with policies!? – Gerwald Jan 17 '16 at 09:22
  • You'd replace your enum with a policy and requirement, and then DI your database into the handler. – blowdart Jan 18 '16 at 14:54
  • 2
    I agree, does not seem possible with Authorization attributes as one does not have access to the permission value in the attribute. @blowdart: Don't think it will work, the policy and handlers don't have access to the permission value encoded in the attribute. – kwaclawek Jan 21 '16 at 19:42
  • 1
    This is getting out of hand now. If you have questions then create them as questions, not comments. – blowdart Jan 21 '16 at 19:51
  • 5
    @blowdart: Wasn't a question. – kwaclawek Jan 21 '16 at 22:22
  • "We don't want you writing custom authorize attributes. If you need to do that we've done something wrong." How about returning a 403 Forbidden response when a user is authenticated but not authorized? 401 when a user is authenticated but not authorized is wrong. – danludwig Feb 26 '16 at 18:53
  • @danludwig that's what it does. Did you try it? Are you seeing something else? If so log a bug. – blowdart Feb 26 '16 at 19:05
  • 10
    blowdart: How would you code this AuthorizationHandler if the "Over18" requirement was of an arbitrary age for each specific action? Would I have to code an register a Requirement for every possible age? That's the case for @Gerwal, Tseng and mine. How can I pass a parameter to the concrete "instance" of the attribute??? – Vi100 Apr 08 '16 at 09:10
  • @Vi100 you'd parameterise it. See https://github.com/blowdart/AspNetAuthorization-Samples/blob/master/src/AspNetAuthorization/Authorization/MinimumAgeRequirement.cs – blowdart Apr 08 '16 at 12:15
  • 6
    That's the point. You can parameterise the Requirement, but not the Handler, nor the attribute... You can't do: [Authorize(MinAge = 21)] Of course Age is an example, I need to pass an enum with more than one hundred possible values to the handler. This way I'm forced to code a handler for every possible value of the requirement ("Over18", "Over19", "Over20", etc.) – Vi100 Apr 08 '16 at 14:07
  • 1
    You parameterise the requirement, and you then configure it when you configure the *policy*. So you'd have multiple policies, but a single handler and a single requirement. Come RC2 you will be able to replace the policy provider, so you could stringify your requirements into, [Authorize(Policy="Over18")] then parse the policy name itself and return whatever policy you like. – blowdart Apr 08 '16 at 14:21
  • 53
    I, like many others in these comments, am very disappointed that using attributes for authorization has been so greatly neutered over what was possible in Web API 2. Sorry guys, but your "requirement" abstraction fails to cover any case where we could previously use attribute constructor parameters to inform an underlying authorization algorithm. It used to be brain-dead simple to do something like `[CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]`. I could use a single custom attribute in an infinite number of ways simply by modifying the constructor parameters. – NathanAldenSr Nov 14 '16 at 20:51
  • 77
    I am also shocked that the self-proclaimed "Lead ASP.NET security guy" is actually suggesting to use magic strings (hacking the meaning of `IAuthorizeData.Policy`) and custom policy providers to overcome this blatant oversight, rather than addressing it within the framework. I thought we weren't supposed to be creating our own implementations? You've left several of us no choice except to re-implement authorization from scratch (again), and this time without even the benefit of Web API's old `Authorize` attribute. Now we have to do it on the action filter or middleware level. – NathanAldenSr Nov 14 '16 at 20:54
  • 3
    I posted a GitHub issue at https://github.com/aspnet/Mvc/issues/5532 that summarizes not only this issue but several other bad design choices I noticed while trying to migrate from Web API 2 to MVC 6. – NathanAldenSr Nov 15 '16 at 18:37
  • @NathanAldenSr Please see my answer, it will allow you to use custom attributes on your controllers and actions, and access them in the AuthorizationHandler. http://stackoverflow.com/a/40824351/436494 – Shawn Nov 27 '16 at 02:38
  • 3
    @Shawn This is quite overengineered... I solved the same using a simple AuthorizationFilterAttribute wich receives a parameter. You don't need reflection for this, it seems even more artificious than the "official" solution (that I find quite poor). – Vi100 Nov 28 '16 at 10:21
  • 1
    @blowdart is there a way to access HTTP headers inside requirement? – SHM Dec 02 '16 at 13:45
  • @SHM that would be better as a separate question – blowdart Dec 02 '16 at 14:12
  • I've added a comment to the issue spawned from this SO question: https://github.com/aspnet/Mvc/issues/5607#issuecomment-269266125 – Derek Greer Dec 27 '16 at 03:58
  • 11
    I'm sorry that you feel you would do something wrong if we had to program something we need. I really needed this feature, and it was taken away unnecessarily. You don't need to make the program for us, we are programmers. – maksymiuk Feb 07 '17 at 00:47
  • 3
    I'd be interested in hearing more about how the attribute approach was broken and what issues can arise from their use that is being set forth as the reason you're recommending that no one should use that approach. – Derek Greer Apr 12 '17 at 13:05
  • While it is the asp.net core team's choice do to this, I think it is not their business to tell developers that they can't handle their own implementations. – Sprague Oct 24 '17 at 07:16
  • Honestly, I like .Net Core very much! but I find the built-in authentication framework (Identity) not for me, it needs a rigid application and database structure. – Mohammed Noureldin Nov 11 '17 at 11:44
  • 2
    Policies are not sufficient at all. What if you need to check a claim to see if a user is a member of a certain group or organization, but the group or organization could be different based upon the request, policies fall completely flat. This new system offers little of value. – Jeremy Holovacs Nov 11 '17 at 14:55
  • 1
    I can't let that comment go from Jeremy, especially when the request is available during policy evaluation, so policies don't fall flat here at all, it supports exactly the example given about why they don't work. – blowdart Nov 11 '17 at 16:45
  • @blowdart Are there some unit test examples for AuthorizationHandler? Specifically I am trying to test my handler behaves correctly if/when user belongs to AD Groups using context.User.IsInRole(roleName) – philreed Dec 11 '17 at 15:38
  • @blowdart Does similar documentation exist for ASP.Net, or is this stuff Core only? – Laurence Feb 01 '18 at 15:32
  • @Felype you sound really accomodated. I was also in your position, and i've read to the point that i have the same security coding on aspnet core that i had on asp.net mvc 5. Technology will always progress, and we should always stay on or even forward to it as much as we can, or we'll fall behind. Love! – Edgar Froes Feb 02 '18 at 18:23
  • @EdgarSalazar yeah, I've figured, I'm using .net core since the early betas, now I know the story of what I'm using, its based on Ruby's Rack/Sinatra with a very strong Nodejs influence, I studied both of those superficially and It really added a lot, I'm also learning a lot in SPA and Rest and been flirting with the JavaScript development pipelines too. – Felype Feb 05 '18 at 12:25
  • 3
    *I'd be interested in hearing more about how the attribute approach was broken* - @DerekGreer: I know this is an old question, but I believe you can find your answer in [Passive Attributes](http://blog.ploeh.dk/2014/06/13/passive-attributes/). It is not a good design approach to make attributes contain behavior because they have constrained constructors, which makes it impossible to use the DI pattern to inject dependencies. Breaking the behavior out into individual components that can be registered at startup (and controlled by passive attributes) is a better approach. – NightOwl888 Feb 22 '18 at 19:41
  • 1
    You can't use constructor injection with attributes, but you can do property injection. It's actually fairly easy with Autofac and I would image every other major container supports this. That aside, I highly doubt that was the aspect of the design that he's referring to when he says the design was broken. – Derek Greer Feb 24 '18 at 03:39
  • 8
    Oh what the heck, man... I am on a very tight deadline here and I don't have the time or energy to learn how to design middleware or claims or any of that... the point of a framework is to abstract this stuff out for us so we can simply focus on the core business logic (in my case, executing some code with the Firebase Admin SDK to ensure that a bearer token is valid). What should've been like 3 lines of code has turned into something a lot more. – arao6 May 12 '19 at 21:49
  • 3
    @blowdart despite all the shit you have received i like how authorization works now. Why am i saying this? It sucks when ppl only give feedback when they are frustrated instead of also when they are happy. At least i can just add one requirement and do attribute based specifics within the logic of that requirement now, which enables me to do the same stuff i used to be able to do earlier as well (apart from straight going the IAsyncAuthorizationFilter attribute way) - so yeah. Thanks. – Dbl Sep 04 '19 at 13:16
  • 1
    @blowdart - How can I return HTTP 401 when the policy doesn't match. Right now for me, it throws invalid operation exception with `InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.` – Hiren Desai Jan 02 '20 at 14:20
  • @blowdart I have asked a question (https://stackoverflow.com/q/65153325/1658737) and now I found this answer from you. Would you mind to take a look at mine and provide some feedback? What would be the way to solve a scenario when someone doesn't want to decorate every single controller and action on an API and, instead, centralize the logic by reading the permissions from somewhere else and evaluate the controller and action that user is attempting to access? This seemed easier on the (now) old approach of AuthorizeFilters... – CesarD Dec 05 '20 at 20:57
  • 2
    "we've done something wrong" Of all the things I love about .Net Core... the authorize story is definitely you've done something wrong. What a clusterF*** – WernerCD Feb 02 '21 at 19:48
  • 1
    @NathanAldenSr WernerCD I have to whole heartedly agree with you. MSFT should replace blowdart and put someone else in charge who listens to the community and industry's needs. Among others, Microsoft own mechanism does NOT align with Microsoft own Active Directory Groups. Security, tenants, UI, Groups is completely broken, it leaves Microsoft wide open, going to bite them back one day! – Transformer Feb 12 '21 at 18:18
  • This is terrible. I don't care how y'all want me to write my code... you're not my boss. I care that you gave me a way to do something and then you took it away and are now making me waste my time relearning this stuff. I could have written my own custom attribute in minutes and it would be perfectly secure and suite my needs just fine. It's wonderful that you've added this new stuff... it's just terrible that you've chosen to try to force people to use it when the old way was perfectly reasonable for a great many cases. – BVernon Feb 17 '21 at 21:24
127

It seems that with ASP.NET Core 2, you can again inherit AuthorizeAttribute, you just need to also implement IAuthorizationFilter (or IAsyncAuthorizationFilter):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _someFilterParameter;

    public CustomAuthorizeAttribute(string someFilterParameter)
    {
        _someFilterParameter = someFilterParameter;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // you can also use registered services
        var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

        var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
        if (!isAuthorized)
        {
            context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}
gius
  • 8,475
  • 3
  • 30
  • 59
  • 6
    So you can only use this to _deny_ authorization, not _grant_ it? – MEMark Aug 29 '18 at 10:44
  • 1
    @MEMark By *granting*, you mean overriding another authorization attribute? – gius Aug 30 '18 at 11:05
  • I guess you could put it like that. I mean effectively allowing access to the resource. – MEMark Aug 30 '18 at 15:06
  • 3
    AFAIK, access is allowed by default, so you need to explicitly deny it (e.g., by adding an AuthorizeAttribute). Check this question for more details: https://stackoverflow.com/questions/17272422/multiple-authorization-attributes-on-method – gius Sep 03 '18 at 10:08
  • 22
    Also note, in suggested example one doesn't have to inherit from AuthorizeAttribute. You can inherit from **Attribute** and **IAuthorizationFilter**. This way you wouldn't get the following exception if some non-standard authentication mechanism is used: **InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.** – Anatolyevich Nov 30 '18 at 08:39
  • @Anatolyevich I still receive that error if I choose to do a `context.Result = new ForbidResult();`. But I'm liking this option so far. – ryancdotnet Dec 01 '18 at 05:23
  • 16
    Note that if your `OnAuthorization` implementation needs to await an async method, you should implement `IAsyncAuthorizationFilter` instead of `IAuthorizationFilter` otherwise your filter will execute synchronously and your controller action will execute regardless of the outcome of the filter. – Codemunkie Mar 23 '19 at 20:47
  • I wanted to deny expired tokens, but accept anonymus as well, this helped me return unauthorized if the token is expired but accept no token. Thanks, this helped a lot – Lucas Venturella Apr 10 '20 at 17:35
48

Based on Derek Greer GREAT answer, i did it with enums.

Here is an example of my code:

public enum PermissionItem
{
    User,
    Product,
    Contact,
    Review,
    Client
}

public enum PermissionAction
{
    Read,
    Create,
}


public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(PermissionItem item, PermissionAction action)
    : base(typeof(AuthorizeActionFilter))
    {
        Arguments = new object[] { item, action };
    }
}

public class AuthorizeActionFilter : IAuthorizationFilter
{
    private readonly PermissionItem _item;
    private readonly PermissionAction _action;
    public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
    {
        _item = item;
        _action = action;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

public class UserController : BaseController
{
    private readonly DbContext _context;

    public UserController( DbContext context) :
        base()
    {
        _logger = logger;
    }

    [Authorize(PermissionItem.User, PermissionAction.Read)]
    public async Task<IActionResult> Index()
    {
        return View(await _context.User.ToListAsync());
    }
}
bruno.almeida
  • 2,036
  • 1
  • 18
  • 29
  • 1
    Thanks for this. I created this post with a slightly different implementation and a request for validation https://stackoverflow.com/questions/49551047/custom-bearer-token-authorization-for-asp-net-core – Anton Swanevelder Mar 29 '18 at 08:13
  • 3
    MumboJumboFunction <3 – Marek Urbanowicz Sep 07 '19 at 05:27
  • U didnt show in your answer how one would apply this to the user ie how did u store the permission bit against the current user – rogue39nin May 13 '21 at 00:11
  • @rogue39nin, you can use Claims (context.HttpContext.User.Claims) to add some extra **public** metadata to your token. You can also use database, call external services or any other methods that allow you to get that information. – bruno.almeida May 13 '21 at 15:12
38

You can create your own AuthorizationHandler that will find custom attributes on your Controllers and Actions, and pass them to the HandleRequirementAsync method.

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();

        var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
        if (action != null)
        {
            attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
            attributes.AddRange(GetAttributes(action.MethodInfo));
        }

        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
    }
}

Then you can use it for any custom attributes you need on your controllers or actions. For example to add permission requirements. Just create your custom attribute.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public string Name { get; }

    public PermissionAttribute(string name) : base("Permission")
    {
        Name = name;
    }
}

Then create a Requirement to add to your Policy

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    //Add any custom requirement properties if you have them
}

Then create the AuthorizationHandler for your custom attribute, inheriting the AttributeAuthorizationHandler that we created earlier. It will be passed an IEnumerable for all your custom attributes in the HandleRequirementsAsync method, accumulated from your Controller and Action.

public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
    {
        foreach (var permissionAttribute in attributes)
        {
            if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
            {
                return;
            }
        }

        context.Succeed(requirement);
    }

    private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
    {
        //Implement your custom user permission logic here
    }
}

And finally, in your Startup.cs ConfigureServices method, add your custom AuthorizationHandler to the services, and add your Policy.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Permission", policyBuilder =>
            {
                policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
            });
        });

Now you can simply decorate your Controllers and Actions with your custom attribute.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}
Shawn
  • 661
  • 8
  • 9
  • 9
    This is quite overengineered... I solved the same using a simple AuthorizationFilterAttribute wich receives a parameter. You don't need reflection for this, it seems even more artificious than the "official" solution (that I find quite poor). – Vi100 Nov 28 '16 at 10:20
  • 2
    @Vi100 I couldn't find much information on AuthorizationFilters in ASP.NET Core. The official documentation page says they are currently working on this topic. https://docs.microsoft.com/en-us/aspnet/core/security/authorization/authorization-filters – Shawn Nov 28 '16 at 21:13
  • 5
    @Vi100 Can you please share your solution, if there is a simpler way to achieve this I would love to know. – Shawn Nov 29 '16 at 02:55
  • 2
    I actually like this solution, it leverages the new policy system and combines Attributes to provide a pretty clean solution. I use a global Authorize attribute to ensure the user is logged in, then apply a permission policy where required. – teatime Jun 21 '17 at 12:54
  • 2
    One thing to note the use of UnderlyingSystemType above does not compile, but removing it seems to work. – teatime Jun 21 '17 at 12:54
  • I'm trying this solution out, but in `HandleRequirementAsync` the `action` property becomes null, and then no attributes get set. – David Berg Oct 12 '20 at 09:13
  • I found the solution to my problem here: https://stackoverflow.com/a/64247810/1813326 – David Berg Oct 12 '20 at 12:42
  • This solution is great. Reminds me of the IdentityServer3 ResourceAuthorization. It looks like AuthorizeAsync handles the permission checking logic. Do you have example use cases for what PermissionAuthorizationRequirement would be used for? Going to policy based requirements is new for me. – gilm0079 Dec 18 '20 at 16:50
  • @gilm0079 Requirements are, typically, declarative ways to specifying what needs to be fulfilled for a given Policy. This example is a bit different because the `PermissionAuthorizationRequirement` is not specifying the requirement data, instead it is being used to "glue" a Policy (`"Permission"`) to a Requirement Handler (`PermissionAuthorizationHandler`). In this example, the attributes are storing the declarative requirements, instead of having 100s of specific Requirement Policy mappings. – seangwright Dec 19 '20 at 22:41
27

What is the current approach to make a custom AuthorizeAttribute

For pure authorization scenarios (like restricting access to specific users only), the recommended approach is to use the new authorization block: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84-L92

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<AuthorizationOptions>(options =>
        {
            options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
        });
    }
}

public class StoreController : Controller
{
    [Authorize(Policy = "ManageStore"), HttpGet]
    public async Task<IActionResult> Manage() { ... }
}

For authentication, it's best handled at the middleware level.

What are you trying to achieve exactly?

Kévin Chalet
  • 33,128
  • 7
  • 104
  • 124
  • 1
    I am receiving a session ID in the Header Authorization. From that ID I'll know whether a particular action is valid. – jltrem Jul 16 '15 at 21:09
  • 1
    Then that's not an authorization concern. I guess your "session ID" is actually a token containing the identity of the caller: this should definitely be done at the middleware level. – Kévin Chalet Jul 16 '15 at 21:12
  • 4
    It isn't authentication (establishing who the user is) but it is authorization (determining if a user should have access to a resource). So where are you suggesting I look to solve this? – jltrem Jul 16 '15 at 21:18
  • Curious: how are you creating your session identifiers? – Kévin Chalet Jul 16 '15 at 21:19
  • 3
    @jltrem, agreed, what you are talking about is authorization, not authentication. – bopapa_1979 Jul 16 '15 at 21:21
  • @Pinpoint I'm not creating them. I'm just the guy in the middle providing access to another system. – jltrem Jul 16 '15 at 21:22
  • @EricBurcham a session identifier is definitely a way to identity (= authenticate) a user, I'm afraid. – Kévin Chalet Jul 16 '15 at 21:22
  • @jltrem but you're extracting some information from the identifier to determine who's the caller, right? – Kévin Chalet Jul 16 '15 at 21:23
  • 2
    @Pinpoint I am not. I query another system for that info. That system authenticates (determines the user) and authorizes (tells me what that user can access). Right now I have it hacked to work by calling a method in each controller action to have the other system verify the session. I'd like to have this automatically happen via an attribute. – jltrem Jul 16 '15 at 21:32
  • `I am not. I query another system for that info`... well, you're asking an external provider to authenticate your users, but that's exactly the same thing, I'm afraid. You should consider creating a new middleware for the external authentication part (it would query your remote system and retrieve user's identity before creating a new ClaimsPrincipal containing the roles or the actions the user is allowed to query) and adding a new authorization policy to ensure the caller has a role/claim allowing him/her to make the request. – Kévin Chalet Jul 16 '15 at 21:38
11

The modern way is AuthenticationHandlers

in startup.cs add

services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            User user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid User-name or Password");

            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

IUserService is a service that you make where you have user name and password. basically it returns a user class that you use to map your claims on.

var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            }; 

Then you can query these claims and her any data you mapped, ther are quite a few, have a look at ClaimTypes class

you can use this in an extension method an get any of the mappings

public int? GetUserId()
{
   if (context.User.Identity.IsAuthenticated)
    {
       var id=context.User.FindFirst(ClaimTypes.NameIdentifier);
       if (!(id is null) && int.TryParse(id.Value, out var userId))
            return userId;
     }
      return new Nullable<int>();
 }

This new way, i think is better than the old way as shown here, both work

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.Headers.Authorization != null)
        {
            var authToken = actionContext.Request.Headers.Authorization.Parameter;
            // decoding authToken we get decode value in 'Username:Password' format
            var decodeauthToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
            // spliting decodeauthToken using ':'
            var arrUserNameandPassword = decodeauthToken.Split(':');
            // at 0th postion of array we get username and at 1st we get password
            if (IsAuthorizedUser(arrUserNameandPassword[0], arrUserNameandPassword[1]))
            {
                // setting current principle
                Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(arrUserNameandPassword[0]), null);
            }
            else
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }

    public static bool IsAuthorizedUser(string Username, string Password)
    {
        // In this method we can handle our database logic here...
        return Username.Equals("test") && Password == "test";
    }
}
Walter Vehoeven
  • 2,461
  • 14
  • 28
  • This brilliant answer just works like a charm! Thank you for that and I wish you it will get upvoted, as it is the best answer I have found after like a six hours of searching through blogs, documentation and stack for Basic authentication plus Role authorization. – Piotr Śródka Mar 30 '20 at 11:04
  • @PiotrŚródka, you are welcome, please note that the answer is a little "simplified", test if you have a ':' in the text as a malicious user could try and crash your service by simply not playing nice ending in an index out of range exception. as always test what is given to you by external sources – Walter Vehoeven Mar 30 '20 at 11:11
  • This was really helpful. The other thing I needed to do was make sure app.UseAuthentication(); was before app.UseAuthorization(); – Robooto Aug 21 '20 at 21:19
6

If anyone just wants to validate a bearer token in the authorize phase using the current security practices you can,

add this to your Startup/ConfigureServices

    services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

    services.AddAuthorization(options => options.AddPolicy("Bearer",
        policy => policy.AddRequirements(new BearerRequirement())
        )
    );

and this in your codebase,

public class BearerRequirement : IAuthorizationRequirement
{
    public async Task<bool> IsTokenValid(SomeValidationContext context, string token)
    {
        // here you can check if the token received is valid 
        return true;
    }
}

public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> 
{

    public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject)
    {
       ...
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.Contains("Bearer"))
        {
            var token = authHeader.Replace("Bearer ", string.Empty);
            if (await requirement.IsTokenValid(thatYouCanInject, token))
            {
                context.Succeed(requirement);
            }
        }
    }
}

If the code doesn't reach context.Succeed(...) it will Fail anyway (401).

And then in your controllers you can use

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Gabriel P.
  • 2,552
  • 2
  • 26
  • 19
  • Why would you choose to perform your own validation of the token when the JwtBearer middleware already takes care of this? It also puts the correct content in the WWW-Authenticate response header for an auth/token validation/expiration failure. If you want access to the authentication pipeline there are specific events you can tap into on AddJwtBearer options (OnAuthenticationFailed, OnChallenge, OnMessageReceived and OnTokenValidated). – Darren Lewis Jan 21 '20 at 10:40
  • This is infinitely simpler than any other solution I've seen. Especially for simple api key use cases. One update: for 3.1 the cast to AuthorizationFilterContext is no longer valid because of the endpoint routing stuff. You need to grab the context via HttpContextAccessor. – JasonCoder May 13 '20 at 17:06
4

The accepted answer (https://stackoverflow.com/a/41348219/4974715) is not realistically maintainable or suitable because "CanReadResource" is being used as a claim (but should essentially be a policy in reality, IMO). The approach at the answer is not OK in the way it was used, because if an action method requires many different claims setups, then with that answer you would have to repeatedly write something like...

[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")] 
[ClaimRequirement(MyClaimTypes.AnotherPermision, "AnotherClaimVaue")]
//and etc. on a single action.

So, imagine how much coding that would take. Ideally, "CanReadResource" is supposed to be a policy that uses many claims to determine if a user can read a resource.

What I do is I create my policies as an enumeration and then loop through and set up the requirements like thus...

services.AddAuthorization(authorizationOptions =>
        {
            foreach (var policyString in Enum.GetNames(typeof(Enumerations.Security.Policy)))
            {
                authorizationOptions.AddPolicy(
                    policyString,
                    authorizationPolicyBuilder => authorizationPolicyBuilder.Requirements.Add(new DefaultAuthorizationRequirement((Enumerations.Security.Policy)Enum.Parse(typeof(Enumerations.Security.Policy), policyWrtString), DateTime.UtcNow)));

      /* Note that thisn does not stop you from 
          configuring policies directly against a username, claims, roles, etc. You can do the usual.
     */
            }
        }); 

The DefaultAuthorizationRequirement class looks like...

public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
    public Enumerations.Security.Policy Policy {get; set;} //This is a mere enumeration whose code is not shown.
    public DateTime DateTimeOfSetup {get; set;} //Just in case you have to know when the app started up. And you may want to log out a user if their profile was modified after this date-time, etc.
}

public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
    private IAServiceToUse _aServiceToUse;

    public DefaultAuthorizationHandler(
        IAServiceToUse aServiceToUse
        )
    {
        _aServiceToUse = aServiceToUse;
    }

    protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
    {
        /*Here, you can quickly check a data source or Web API or etc. 
           to know the latest date-time of the user's profile modification...
        */
        if (_aServiceToUse.GetDateTimeOfLatestUserProfileModication > requirement.DateTimeOfSetup)
        {
            context.Fail(); /*Because any modifications to user information, 
            e.g. if the user used another browser or if by Admin modification, 
            the claims of the user in this session cannot be guaranteed to be reliable.
            */
            return;
        }

        bool shouldSucceed = false; //This should first be false, because context.Succeed(...) has to only be called if the requirement specifically succeeds.

        bool shouldFail = false; /*This should first be false, because context.Fail() 
        doesn't have to be called if there's no security breach.
        */

        // You can do anything.
        await doAnythingAsync();

       /*You can get the user's claims... 
          ALSO, note that if you have a way to priorly map users or users with certain claims 
          to particular policies, add those policies as claims of the user for the sake of ease. 
          BUT policies that require dynamic code (e.g. checking for age range) would have to be 
          coded in the switch-case below to determine stuff.
       */

        var claims = context.User.Claims;

        // You can, of course, get the policy that was hit...
        var policy = requirement.Policy

        //You can use a switch case to determine what policy to deal with here...
        switch (policy)
        {
            case Enumerations.Security.Policy.CanReadResource:
                 /*Do stuff with the claims and change the 
                     value of shouldSucceed and/or shouldFail.
                */
                 break;
            case Enumerations.Security.Policy.AnotherPolicy:
                 /*Do stuff with the claims and change the 
                    value of shouldSucceed and/or shouldFail.
                 */
                 break;
                // Other policies too.

            default:
                 throw new NotImplementedException();
        }

        /* Note that the following conditions are 
            so because failure and success in a requirement handler 
            are not mutually exclusive. They demand certainty.
        */

        if (shouldFail)
        {
            context.Fail(); /*Check the docs on this method to 
            see its implications.
            */
        }                

        if (shouldSucceed)
        {
            context.Succeed(requirement); 
        } 
     }
}

Note that the code above can also enable pre-mapping of a user to a policy in your data store. So, when composing claims for the user, you basically retrieve the policies that had been pre-mapped to the user directly or indirectly (e.g. because the user has a certain claim value and that claim value had been identified and mapped to a policy, such that it provides automatic mapping for users who have that claim value too), and enlist the policies as claims, such that in the authorization handler, you can simply check if the user's claims contain requirement.Policy as a Value of a Claim item in their claims. That is for a static way of satisfying a policy requirement, e.g. "First name" requirement is quite static in nature. So, for the example above (which I had forgotten to give example on Authorize attribute in my earlier updates to this answer), using the policy with Authorize attribute is like as follows, where ViewRecord is an enum member:

[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

A dynamic requirement can be about checking age range, etc. and policies that use such requirements cannot be pre-mapped to users.

An example of dynamic policy claims checking (e.g. to check if a user is above 18 years old) is already at the answer given by @blowdart (https://stackoverflow.com/a/31465227/4974715).

PS: I typed this on my phone. Pardon any typos and lack of formatting.

Olumide
  • 178
  • 8
  • imho, the Policy is more a static validation procedure with custom logic and currently it cannot be parameterized as easy as it was in old `AuthorizeAttribute`. You have to generate all possible instances of `DefaultAuthorizationRequirement` during app startup to be able to use them in controllers. I would prefer to have one policy that can accept some scalar parameters (potentially infinite combination). This way I don't break Open-Closed principle. And your example does. (anyway I appreciate it) – neleus Sep 09 '20 at 20:16
  • @neleus, you have to use a requirement that accepts a resource. For example, in the original question, that resource is the SessionID. In your comment, the resource is the scalar property you're talking about. So, inside the requirement, the resource would be evaluated against the claims of the users and then determine if the authorization should succeed or fail. – Olumide Sep 10 '20 at 14:33
  • @neleus, already, the user should have been authenticated and also authorized to call the controller action, but the requirement I just described would then be used inside the controller action to determine if the user can go further based on the information contained in the resource provided to it. The resource can come from request headers, query string, data fetched from database, etc. I can write the code if you show interest in such. – Olumide Sep 10 '20 at 14:34
  • do you mean the specific authorization decisions are rather job of the controller than requirements? – neleus Sep 23 '20 at 08:58
  • @neleus Not exactly, you're only calling the code from inside the controller action. But another thing you can do is register IHttpContextAccessor as a service (by services.AddHttpContextAccessor()), and then inject it into the AuthorizationHandler for the Requirement. From IHttpContextAccessor, you can get HttpContext.GetRouteData() and then retrieve the route data you want (e.g. the Seesion ID in the discussion). Beware of nulls. – Olumide Sep 26 '20 at 21:41
  • I don't really see what this solves. I would personally avoid passing in two things here and just use params to pass in however many permissions enums are required. If you need a ton of permissions passed in then I could see policy creation via these static enums as okay. This isn't that difficult, either you need policies or you don't. There is no "right" way. – perustaja Jan 06 '21 at 22:31
2

As of this writing I believe this can be accomplished with the IClaimsTransformation interface in asp.net core 2 and above. I just implemented a proof of concept which is sharable enough to post here.

public class PrivilegesToClaimsTransformer : IClaimsTransformation
{
    private readonly IPrivilegeProvider privilegeProvider;
    public const string DidItClaim = "http://foo.bar/privileges/resolved";

    public PrivilegesToClaimsTransformer(IPrivilegeProvider privilegeProvider)
    {
        this.privilegeProvider = privilegeProvider;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity is ClaimsIdentity claimer)
        {
            if (claimer.HasClaim(DidItClaim, bool.TrueString))
            {
                return principal;
            }

            var privileges = await this.privilegeProvider.GetPrivileges( ... );
            claimer.AddClaim(new Claim(DidItClaim, bool.TrueString));

            foreach (var privilegeAsRole in privileges)
            {
                claimer.AddClaim(new Claim(ClaimTypes.Role /*"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" */, privilegeAsRole));
            }
        }

        return principal;
    }
}

To use this in your Controller just add an appropriate [Authorize(Roles="whatever")] to your methods.

[HttpGet]
[Route("poc")]
[Authorize(Roles = "plugh,blast")]
public JsonResult PocAuthorization()
{
    var result = Json(new
    {
        when = DateTime.UtcNow,
    });

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

In our case every request includes an Authorization header that is a JWT. This is the prototype and I believe we will do something super close to this in our production system next week.

Future voters, consider the date of writing when you vote. As of today, this works on my machine.™ You will probably want more error handling and logging on your implementation.

No Refunds No Returns
  • 7,077
  • 4
  • 26
  • 38
0

Just adding to the great answer from @Shawn. If you are using dotnet 5 you need to update the class to be:

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();
        
        if (context.Resource is HttpContext httpContext)
        {
            var endPoint = httpContext.GetEndpoint();

            var action = endPoint?.Metadata.GetMetadata<ControllerActionDescriptor>();

            if(action != null)
            {
                attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
                attributes.AddRange(GetAttributes(action.MethodInfo));
            }
        }
        
        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo) => memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
}

Noting the way getting the ControllerActionDescriptor has changed.

Rtype
  • 775
  • 12
  • 27
0

The below code worked for me in .Net Core 5

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AccessAuthorizationAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public string Module { get; set; } //Permission string to get from controller

    public AccessAuthorizationAttribute(string module)
    {
        Module = module;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        //Validate if any permissions are passed when using attribute at controller or action level

        if (string.IsNullOrEmpty(Module))
        {
            //Validation cannot take place without any permissions so returning unauthorized
            context.Result = new UnauthorizedResult();
            return;
        }
       
        if (hasAccess)
        {
            return;
        }

        context.Result = new UnauthorizedResult();
        return;
    }
}
Prince Prasad
  • 1,100
  • 1
  • 11
  • 18
-1

For authorization in our app. We had to call a service based on the parameters passed in authorization attribute.

For example, if we want to check if logged in doctor can view patient appointments we will pass "View_Appointment" to custom authorize attribute and check that right in DB service and based on results we will athorize. Here is the code for this scenario:

    public class PatientAuthorizeAttribute : TypeFilterAttribute
    {
    public PatientAuthorizeAttribute(params PatientAccessRights[] right) : base(typeof(AuthFilter)) //PatientAccessRights is an enum
    {
        Arguments = new object[] { right };
    }

    private class AuthFilter : IActionFilter
    {
        PatientAccessRights[] right;

        IAuthService authService;

        public AuthFilter(IAuthService authService, PatientAccessRights[] right)
        {
            this.right = right;
            this.authService = authService;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var allparameters = context.ActionArguments.Values;
            if (allparameters.Count() == 1)
            {
                var param = allparameters.First();
                if (typeof(IPatientRequest).IsAssignableFrom(param.GetType()))
                {
                    IPatientRequest patientRequestInfo = (IPatientRequest)param;
                    PatientAccessRequest userAccessRequest = new PatientAccessRequest();
                    userAccessRequest.Rights = right;
                    userAccessRequest.MemberID = patientRequestInfo.PatientID;
                    var result = authService.CheckUserPatientAccess(userAccessRequest).Result; //this calls DB service to check from DB
                    if (result.Status == ReturnType.Failure)
                    {
                        //TODO: return apirepsonse
                        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
                    }
                }
                else
                {
                    throw new AppSystemException("PatientAuthorizeAttribute not supported");
                }
            }
            else
            {
                throw new AppSystemException("PatientAuthorizeAttribute not supported");
            }
        }
    }
}

And on API action we use it like this:

    [PatientAuthorize(PatientAccessRights.PATIENT_VIEW_APPOINTMENTS)] //this is enum, we can pass multiple
    [HttpPost]
    public SomeReturnType ViewAppointments()
    {

    }
Abdullah
  • 187
  • 2
  • 5
  • 15
  • 2
    Please note that IActionFilter will be a problem when you want to use the same attribute for Hub methods in SignalR.SignalR Hubs expect IAuthorizationFilter – ilkerkaran Dec 11 '19 at 10:58
  • Thanks for the info. I am not using SignalR in my application right now so i havent tested it with it. – Abdullah Dec 11 '19 at 13:02
  • Same principle I guess as you will still have to use the header's authorisation entry, the implementation will differ – Walter Vehoeven Mar 30 '20 at 11:06
-1

I have bearer token and I can read claims. I use that attribute on controllers and actions

public class CustomAuthorizationAttribute : ActionFilterAttribute
{
    public string[] Claims;

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // check user 
        var contextUser = context?.HttpContext?.User;
        if (contextUser == null)
        {
            throw new BusinessException("Forbidden");
        }


        // check roles
        var roles = contextUser.FindAll("http://schemas.microsoft.com/ws/2008/06/identity/claims/role").Select(c => c.Value).ToList();
        if (!roles.Any(s => Claims.Contains(s)))
        {
            throw new BusinessException("Forbidden");
        }

        base.OnActionExecuting(context);
    }
}

example

[CustomAuthorization(Claims = new string[]
    {
        nameof(AuthorizationRole.HR_ADMIN),
        nameof(AuthorizationRole.HR_SETTING)
    })]
[Route("api/[controller]")]
[ApiController]
public class SomeAdminController : ControllerBase
{
    private readonly IMediator _mediator;

    public SomeAdminController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet("list/SomeList")]
    public async Task<IActionResult> SomeList()
        => Ok(await _mediator.Send(new SomeListQuery()));
}

That is Roles

public struct AuthorizationRole
{
    public static string HR_ADMIN;
    public static string HR_SETTING;
}
Ergin Çelik
  • 489
  • 5
  • 10