2

I used policy-based authorization in asp.net core mvc 3.1, I have an authorization requirement for read, named ReadAuthorizationRequirement, also I have a global IPAllowedAuthorizationRequirement that checks a user's IP and if this IP is allowed succeed the context.

the problem is when I add global authorization filter read requirement invoked two times in first invoke its resource is Microsoft.AspNetCore.Routing.RouteEndpoint in second invoke its resource is Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.AuthorizationFilterContextSealed and this prevent context to authorize successfully.

My Startup.cs is as below:

           services.AddAuthorization(options =>
            {
                options.FallbackPolicy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
            });

            services.AddControllersWithViews(options =>
            {                
                options.Filters.Add(new AuthorizeFilter(policy: Constants.Authorization.IpAllowed));
                options.Filters.Add(typeof(Filters.RequestLoggingAttribute));
            });

ReadAuthorizationHandler.cs

 public class ReadAuthorizationHandler : AuthorizationHandler<ReadAuthorizationRequirement>
    {
        private readonly IApplicationRouteService _applicationRouteService;

        public ReadAuthorizationHandler(IApplicationRouteService applicationRouteService)
        {
            _applicationRouteService = applicationRouteService;
        }

        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ReadAuthorizationRequirement requirement)
        {
            var roles = AuthorizationHanderHelper.FindRole(_applicationRouteService, context, requirement);

            if (roles.Any(role => role.Read == true))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }
    }

IpAllowedAuthorizationHandler.cs

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IpAllowedAuthorizationRequirement requirement)
        {
            if (context.Resource is Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext mvcContext)
            {
                var userIps = ((ClaimsIdentity)context.User.Identity).Claims
                    .Where(c => c.Type == Constants.Authorization.ClaimTypes.IpAddress)
                    .Select(c => c.Value).ToList();

                var currentIp = mvcContext.HttpContext.Connection.RemoteIpAddress;
                
                var bytes = currentIp.GetAddressBytes();
                var badIp = true;
                foreach (var address in userIps)
                {                    
                    if (IPAddress.TryParse(address, out IPAddress testIp))
                    {
                        if (testIp.GetAddressBytes().SequenceEqual(bytes))
                        {
                            badIp = false;
                            break;
                        }
                    }
                }

                if (!badIp)
                {
                    context.Succeed(requirement);
                }
                else
                {
                    var routeData = mvcContext.RouteData;
                    var action = routeData.Values["action"] as string;
                    var controller = routeData.Values["controller"] as string;
                    var area = routeData.DataTokens["area"] as string;

                    if (string.IsNullOrEmpty(area) && controller == "Account" && action == "AccessDenied")
                    {
                        mvcContext.ModelState.TryAddModelError("", $"IP izni verilmemiştir! (IP: {currentIp})");
                        context.Succeed(requirement);
                    }
                    else
                    {
                        context.Fail();
                    }
                }
            }

            return Task.CompletedTask;
        }

This is pictures of calls to read on IpAllowed handlers: 1- first call is happened in Read authorization handler and context is endpoint (this call is succeed): enter image description here

2- second call is happened in Read authorization handler and context is Mvc's AuthorizationFilterContext and this time it fails. in second call IpAllowed is in pending requirements.

enter image description here

mesut
  • 2,099
  • 3
  • 23
  • 35
  • I think this policy `Constants.Authorization.IpAllowed` also contains the requirement `ReadAuthorizationRequirement`. So when you have both the requirement handler & the `AuthorizeFilter` built from the same policy, the handler will run twice of course. You clearly duplicate the handler. – King King Mar 02 '21 at 07:20
  • @KingKing, no I have a requirement and an handler for each, the reason is that I use endpoint routing but used not preferred old style global filter. I solved the error, and going to answer below. thanks – mesut Mar 02 '21 at 09:13
  • I'm not so sure if you understand correctly my first comment, but if possible please post the code building your policy of `Constants.Authorization.IpAllowed`. I've tried reproducing it sucessfully by the same condition. – King King Mar 02 '21 at 09:58

1 Answers1

1

As mentioned here Setting Global Authorization policies using defaultpolicy and the fallback policy in aspnet core 3

and here

I used old style global authorization filter for asp.net mvc core, that was ok with asp.net core 2.2, but in 3.0+ its not working as expected when using endpoint routing.

so I changed my code as below: Start.cs:

removed fallback policy from configure services, because I added IpAllowed policy as global requirement as below. removed global authorization filter from options in configure services method and added Require authorization inside configure method inside app.useEndpoints as below:

 app.UseEndpoints(endpoints =>
{
    endpoints.MapDefaultControllerRoute().RequireAuthorization(Constants.Authorization.IpAllowed);
});

IpAllowedAuthorizationHandler.cs

updated handler as below:

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IpAllowedAuthorizationRequirement requirement)
        {
            if (context.Resource is Microsoft.AspNetCore.Routing.RouteEndpoint endpoint)
            {
                var userIps = ((ClaimsIdentity)context.User.Identity).Claims
                    .Where(c => c.Type == Constants.Authorization.ClaimTypes.IpAddress)
                    .Select(c => c.Value).ToList();
                
                var currentIp = _contextAccessor?.HttpContext.Connection.RemoteIpAddress;
                
                if(currentIp == null)
                {
                    context.Fail();
                    return Task.CompletedTask;
                }

                var bytes = currentIp.GetAddressBytes();
                var badIp = true;
                foreach (var address in userIps)
                {                    
                    if (IPAddress.TryParse(address, out IPAddress testIp))
                    {
                        if (testIp.GetAddressBytes().SequenceEqual(bytes))
                        {
                            badIp = false;
                            break;
                        }
                    }
                }

                if (!badIp)
                {
                    context.Succeed(requirement);
                }
                else
                {
                    context.Fail();
                }
            }

            return Task.CompletedTask;
        }
mesut
  • 2,099
  • 3
  • 23
  • 35
  • 2
    at least, please explain why that handler is invoked twice. Don't just stop at a work-around like this for yourself. It has no meaning at all for the community. – King King Mar 02 '21 at 09:52
  • Handler invoked first by Endpoint authorization filter context, second by MVC Authorization filter context. this is some kind of conflict in filters. I think – mesut Mar 02 '21 at 12:09
  • I don't think the framework has such a flaw, that's kind of completely unacceptable. It affects the logic we used in the handler which is assumed to run once (at least when authorizing). – King King Mar 02 '21 at 12:18
  • I cant send you images here, but I will update the question with snapshots of what is seen in two invokes, maybe you can found the real reason for two invoke. – mesut Mar 03 '21 at 05:25
  • @mesut I am facing similar issue, were u able to solve this and ensure that handler gets called only once? – Rashmi Pandit Jun 16 '23 at 04:54
  • @RashmiPandit This thread is answer to how I solved: removed fallback policy from configure services, because I added IpAllowed policy as global requirement as below. removed global authorization filter from options in configure services method and added Require authorization inside configure method inside app.useEndpoints as below:... – mesut Jun 23 '23 at 11:07