How do you create a custom AuthorizeAttribute in ASP.NET Core?

前端 未结 11 1222
春和景丽
春和景丽 2020-11-21 17:19

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)

相关标签:
11条回答
  • 2020-11-21 17:50

    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
        }
    }
    
    0 讨论(0)
  • 2020-11-21 17:50

    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)]
    
    0 讨论(0)
  • 2020-11-21 17:53

    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()
        {
    
        }
    
    0 讨论(0)
  • 2020-11-21 17:54

    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;
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-21 17:58

    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.

    0 讨论(0)
  • 2020-11-21 18:02

    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();
        }
    }
    
    0 讨论(0)
提交回复
热议问题