pbz
pbz

Reputation: 9085

Custom redirect from AuthorizationHandler (ASP.NET Core)

I'm using an authentication middleware that is making API requests to a third party service. This middleware then sets up the claims that are later handled by an AuthorizationHandler in conjunction with a IAuthorizationRequirement and a custom policy.

The middleware piece works and I'm able to build the claims:

context.User.AddIdentity(identity); // contains claims

Where I'm stuck is redirecting to a specific URL (there are custom rules for where we need to redirect) from the handler or attribute. From the handler I tried:

var mvcContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
mvcContext.Result = new RedirectToActionResult("login", "home", null);

but it's ignored; only a 401 is returned. AuthorizeAttribute no longer has OnAuthorization so I can't use that either...

Thoughts? Thanks.

Upvotes: 10

Views: 9275

Answers (4)

Andrew HB
Andrew HB

Reputation: 412

this post seems to get closest to a similar problem I am having whereby when my AuthorizationHandler fails to authorize a minimal API endpoint request I get a 302 redirect which creates havoc in my client Angular app that makes the request.

I cannot find any way that I can intercept a request that has failed authorization and based upon the route specified simply switch the response to a 401.

Upvotes: 0

Saahil Claypool
Saahil Claypool

Reputation: 11

As Brent said, calling context.Succeed(requirement) will not exit the middleware pipeline, so the controller will end up returning a body along with the redirect. This is a pretty big security issue as it can return sensitive content.

A solution my team went with is to

  • call context.Fail() which will correctly exit the middleware pipeline
    • This will set the response code to 401
  • add a middleware above authentication that checks for a 401, and changes it to a redirect.
app.Use(
    async (context, next) =>
    {
        // note: headers might be sent before middleware ends otherwise
        context.Request.EnableBuffering();
        // call the rest of the auth middleware
        await next();
        if (
            context.Response.StatusCode == 401
            && (
                context.Request.Path.StartsWithSegments("/your-route")
            )
        )
        {
            // change the response to a normal redirect.
            // could assert the response body is empty. 
            context.Response.StatusCode = 302;
            context.Response.Redirect("/LogIn");
        }
    }
);
app.UseAuthentication();
app.UseAuthorization();

Upvotes: 1

dmorawetz
dmorawetz

Reputation: 546

Your approach with the AuthorizationFilterContext in the handler was almost correct. As described in this answer you need to also say context.Succeed(requirement); for it to work.

So complete solution would be something like this:

  1. Create a custom Requirement:

    public class SomeCustomRequirement : IAuthorizationRequirement
    {}
    
  2. Create a custom Handler:

    public class SomeCustomRequirementHandler : AuthorizationHandler<SomeCustomRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserInformationCompletedRequirement requirement)
        {
            if (!fulfillsRequirement())
                if (context.Resource is AuthorizationFilterContext redirectContext)
                    redirectContext.Result = new RedirectResult("/Account/Unauthorized");
                // even though this is weird it is necessary
                context.Succeed(requirement);
            }
        }
    }
    
  3. Register the handler:

    services.AddAuthorization(options =>
    {
        options.AddPolicy(IdentityConstants.UserInformationCompletePolicy, policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.Requirements.Add(new SomeCustomRequirement());
        });
    });
    
    services.AddSingelton<IAuthorizationHandler, SomeCustomRequirementHandler>();    
    // OR transient if the handler uses the Entity Framework
    services.AddTransient<IAuthorizationHandler, SomeCustomRequirementHandler>();
    

Even though my answer is late I hope it might help future visitors.

Upvotes: 9

Christopher J.
Christopher J.

Reputation: 1283

If the only thing you want to attempt in your API's Middleware is to perform a LogIn behaviour, as your code seems to explain, these possible cases are, in my opinion, thought-provoking :

  1. If /login/home redirects to a webpage:

    • You should use HttpContext.Response.Redirect to redirect to the LogIn webpage. As the documentation says, This would send a 301 code that any web browser can interpret. The HttpContext is available in the Invoke method.
  2. If /login/home redirects to a controller that performs logic that validates user identity:

Take also a look at Nate's post, and this question.

Upvotes: 1

Related Questions