ASP.NET Core 3.0 Policy Redirect

Viewed 4363

We have this portion of code who controls some Service Premium Pages. On method Deny redirects to a upgrade page, for the people who don't have premium membership.

The code runs perfect on asp.NET core 2, but fails on asp.NET core 3. context.Resource is no longer of type AuthorizationFilterContext, but Endpoint not provide a Result member.

¿How can I make the page redirect with the new Enpoint provided on asp.Net core 3?

public Task Deny(AuthorizationHandlerContext context, SubscriptionRequirement requirement)
    {
        var mvcContext = context.Resource as AuthorizationFilterContext;
        if (mvcContext == null)
            return Task.CompletedTask;

        mvcContext.Result = new RedirectToActionResult("Upgrade", "Subscription", new { ReturnUrl = _contextAccessor.HttpContext.Request.Path });
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
2 Answers

According to the SOC principal, it seems that mixing up Authorization with Response Redirection is not a good practice.

Instead, you can wrap your authorization logic into a policy and then invoke the IAuthorizationService and redirect anywhere/anytime you need.

Let's say you've defined a "premium membership" policy. And then you can redirect a request with a Middleware/Resource Filter/Action Filter or even an Action Method without any effort. For example, I create MembershipResourceFilter as below:

public class MembershipResourceFilter : IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var HttpContext = context.HttpContext;
        var authZ = HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
        var routeData= context.RouteData;
        var result = await authZ.AuthorizeAsync(HttpContext.User, routeData,"premium membership");
        if(!result.Succeeded)
        {
            context.Result = new RedirectToActionResult("Upgrade", "Subscription", new { ReturnUrl = HttpContext.Request.Path });
        }
        await next();
    }
}

I test the above code with the following policy, it works fine for me.

services.AddAuthorization(o =>{
    o.AddPolicy("premium membership", pb => pb
        .RequireAuthenticatedUser()
        .RequireAssertion((context)=>{
            // check current context.User has premium membership
            var user = context.User;
            var routeData = context.Resource as RouteData;
            if(routeData != null){
                try{
                    var controller = routeData.Values["controller"]?.ToString();
                    var action = routeData.Values["action"]?.ToString();
                    // now you get the route value
                    if(controller == "Home" && action == "Action"){
                        // ...
                        return true;
                    }
                }catch{
                    return false;
                }
            }
            return false;
        })
    );
});

[Edit]

If You don't want to change the [Authorize("Premium")], you can create a simple middleware instead of a Resource Filter:

    ...
    app.UseAuthentication();
    app.UseRouting();


    app.Use(async(ctx,next)=>{
        var ep= ctx.Features.Get<IEndpointFeature>()?.Endpoint;
        var authAttr = ep?.Metadata?.GetMetadata<AuthorizeAttribute>()
        if(authAttr!=null && authAttr.Policy == "premium membership"){
            var authService = ctx.RequestServices.GetRequiredService<IAuthorizationService>();
            var result = await authService.AuthorizeAsync(ctx.User, ctx.GetRouteData(),authAttr.Policy);
            if(!result.Succeeded)
            {
                var path = $"/Subscription/Upgrade?ReturnUrl={ctx.Request.Path}";
                ctx.Response.Redirect(path) ;
                return;
            }
        }
        await next();
    });

    app.UseAuthorization();
    app.UseEndpoints(endpoints =>{ ... });

The middleware and the Resource Filter basically does the same thing : invoke the authorization service and redirect when need.

I resolved this by injecting IHttpContextAccessor into the AuthorizationHandler constructor then doing this:

public Task Deny(AuthorizationHandlerContext context, SubscriptionRequirement requirement)
{
    //your logic here
    context.Succeed(requirement);
    return _contextAccessor.HttpContext.ExecuteResultAsync(
      new RedirectToActionResult("Upgrade", "Subscription", new { 
        ReturnUrl = _contextAccessor.HttpContext.Request.Path 
    }));
}

HttpContext.ExecuteResultAsync is an extension method:

 public static class HttpContextExtensions  
{
    private static readonly RouteData EmptyRouteData = new RouteData();

    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public static Task ExecuteResultAsync<TResult>(this HttpContext context, TResult result)
        where TResult : IActionResult
    {
        if (context == null) throw new ArgumentNullException(nameof(context));
        if (result == null) throw new ArgumentNullException(nameof(result));

        var executor = context.RequestServices.GetRequiredService<IActionResultExecutor<TResult>>();

        var routeData = context.GetRouteData() ?? EmptyRouteData;
        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

        return executor.ExecuteAsync(actionContext, result);
    }
}

I'm using core 3.1 in case it makes a difference.

Related