authorization.net-5asp.net-authorizationredirecttoaction

Is there a way to Redirect in AuthorizationHandler in .Net 5?


I would like to redirect to an action in my AuthorizationFilter if it fails certain checks. The reason why I want to redirect instead of sending user to "AccessDenied" view is to prevent the user from knowing that the endpoint exists under certain conditions.

A better example is when you create a login system, you "should" never tell the user explicitly if the username/email or password was wrong. If one tells them the username/email was correct they know it is stored in the database, now they just need to figure out the password.

When redirecting inside AuthorizationHandler before .Net Core 3.0 I could use:

if (context.Resource is AuthorizationFilterContext redirectContext) 
{
    redirectContext.Result = new RedirectResult("/Account/NoPageHere");
    context.Succeed(requirement); 
}

But from .Net Core 3.0+ this was changed and is no longer supported. So my question is then, can one redirect from AuthorizationHandler or is it completely removed?

A solution which I could use is to store some data in the HttpContext.Items via the HttpContextAccessor in the AuthorizationHandler and then redirect based on that data. This would work somewhat well if my controllers inherited from a base controller which has a method for processing the data inside HttpContext.Items and deciding if we should redirect and where. The only issue is that I would need to insert this base class method in every Action which uses the AuthorizationFilter => pain. I want to keep the logic in one place and not have to copy paste code around.

If anyone has a better suggestions I would love to hear them!


Solution

  • Did not find any good solution for Redirecting inside the AuthorizationHandler so I did the second best thing I could think of and created a TypeFilterAttribute and a Service for storing redirect information after the AuthorizationHandler. And redirect right before the ActionMethod fires.

    The idea is that in the AuthorizationHandler we make all our validation checks, with the issue that we must always validate the request. This is not ideal, but without setting the request as Succeed() we are not able to redirect from my knowledge. When we come into one or more fail conditions in the AuthroizationHandler we store redirect information in the RedirectService which we will use again in the TypeFilterAttribute to redirect to the correct method. You can stop the AuthorizationHandler early by just returning, or continue like my example to add potential multiple redirect with priority Id.

    The TypeFilterAttribute is of type IActionFilter and has methods that will run before ActionMethod and right after. For my need I must check before the ActionMethod, but one could redirect at any other point if needs be.

    Added code example below based on my own code with changes due to safety reasons, but hope it gets the point across.

    AuthorizationHandler

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MembershipRequirement requirement)
    {  
        // We preemptively set authorization as successful as we need to succeed to redirect.
        context.Succeed(requirement);
        
        var id = _service.GetId();
        if (id == 0)
        {
            // Prio Id is used in case multiple redirects are set to find the most important ( 1 => most important )
            _redirectService.AddRedirect(1, MVC.Home.Error("404"));
        }
    
        var name = _service.getName();
        if (name.toLower() != "cool")
        {
            // Prio Id is used in case multiple redirects are set to find the most important ( 1 => most important )
            _redirectService.AddRedirect(2, MVC.Account.ChangeName());
        }
    }
    

    Redirect Service

    public class RedirectService : IRedirectService
    {
        public bool IsRedirecting { get; set; }
        public SortedDictionary<int, IActionResult> RedirectResults { get; set; } = new SortedDictionary<int, IActionResult>();
        
        // Ignores value if order ( key ) is already set
        public void AddRedirect(int order, IActionResult redirectResult)
        {
            if (redirectResult == null || RedirectResults.ContainsKey(order)) return;
            
            IsRedirecting = true;
            RedirectResults.Add(order, redirectResult);
        }
    }
    
    public interface IRedirectService
    {
        public bool IsRedirecting { get; set; }
        public SortedDictionary<int, IActionResult> RedirectResults { get; set; }
    
        public void AddRedirect(int order, IActionResult redirectResult);
    }
    

    Redirect TypeFilterAttribute:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class RedirectFilterAttribute : TypeFilterAttribute
    {
        public RedirectFilterAttribute() : base(typeof(RedirectFilterType)) {}
    
        private class RedirectFilterType : IActionFilter
        {
            private readonly IRedirectService _redirectService;
        
            public RedirectFilterType(IRedirectService redirectService)
            {
                _redirectService = redirectService;
            }
    
            public void OnActionExecuting(ActionExecutingContext context)
            {
                // Check if we should redirect
                if (!_redirectService.IsRedirecting) return;
            
                var callInfo = _redirectService.RedirectResults.First().Value.GetR4ActionResult();
                context.Result = new RedirectToRouteResult(null, callInfo.RouteValueDictionary, null);
            }
    
            public void OnActionExecuted(ActionExecutedContext context) {}
        }
    }
    

    Startup.cs DependencyInjection

    // Setup RedirectService
    services.AddScoped<IRedirectService, RedirectService>();
    
    // Setup AuthroiztionHandler(s)
    services.AddScoped<IAuthorizationHandler, MembershipAuthorizationHandler>();
    
    config.AddPolicy(nameof(MembershipRequirement.MembershipPolicy),
        policy => policy.Requirements.Add(new MembershipRequirement(isAdmin: false)));
    
    config.AddPolicy(nameof(MembershipRequirement.AdminMembershipPolicy),
        policy => policy.Requirements.Add(new MembershipRequirement(isAdmin: true)));
    

    PS: I am using R4MVC ( which is amazing ) so you would need to change how you redirect inside the TypeFilterAttribute.