Ogglas
Ogglas

Reputation: 69958

C# ASP.NET MVC 5 -> ActionFilterAttribute -> Stop redirecting HTTP 401 automatically

I have the System.Web.Mvc.ActionFilterAttribute below registered as a global filter. If the response is 400 BadRequest the application works as expected but If I return 401 Unauthorized what I get is a 302 Found redirect.

Since the Login?ReturnUrl= will keep appending values eventually the request will throw a query string is too long exception.

HTTP Error 404.15 - Not Found

The request filtering module is configured to deny a request where the query string is too long.

https://localhost:44311/Login?ReturnUrl=%2FLogin%3FReturnUrl%3D%252FLogin%253FReturnUrl%253D%25252FLogin%25253FReturnUrl%25253D%2525252FLogin%....

enter image description here

In Web.config I have disabled defaultRedirect and set customErrors to <customErrors mode="Off" />

public class ClientCertificateActionFilter : System.Web.Mvc.ActionFilterAttribute
{
  public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext actionContext)
  {
    if (actionContext.HttpContext.Request.Url.Scheme != Uri.UriSchemeHttps)
    {
      actionContext.Result = new HttpStatusCodeResult(HttpStatusCode.BadRequest, "HTTPS Required");
      return;

    }
    else
    {
      X509Certificate2 cert = new X509Certificate2(actionContext.HttpContext.Request.ClientCertificate.Certificate);

      if (cert == null)
      {
        actionContext.Result = new HttpStatusCodeResult(HttpStatusCode.Unauthorized, "Client Certificate Required");
        return;
      }
      ...
    }
  }
}

How can I fix this?

Upvotes: 4

Views: 3316

Answers (3)

JuanR
JuanR

Reputation: 7783

You are returning a HttpStatusCode.Unauthorized which causes a redirection to the login page. Because the filter is executing each time, it keeps redirecting to itself after it gets there.

What you should return instead is a 403 (HttpStatusCode.Forbidden).

Upvotes: 2

Ogglas
Ogglas

Reputation: 69958

Update:

I first edited my filter and used AuthorizeAttribute with AuthorizeCore. My reason for editing was when I tried something that gave an uncaught exception. In this case the user would be authenticated unless I caught the exception and returned HttpStatusCode.Unauthorized or something similar. However I had to revert my changed code. The reason for this was that Controllers or Methods declared with [AllowAnonymous] did not hit the code. The application requires a valid client certificate even to view the login page and therefore I wen't back to ActionFilterAttribute.

Original:

Looking at the response headers I could see that the response contained the header www-authenticate: Bearer.

I then wen't to the file App_Start -> Startup.Auth.cs to look over the authentication. In OAuthAuthorizationServerOptions and OAuthBearerAuthenticationOptions I did not see anything strange. However the application also has app.UseCookieAuthentication enabled. In here the CookieAuthenticationOptions was set to LoginPath = new PathString("/Login"),. This is what was causing the redirects.

Description for LoginPath:

The LoginPath property informs the middleware that it should change an outgoing 401 Unauthorized status code into a 302 redirection onto the given login path. The current url which generated the 401 is added to the LoginPath as a query string parameter named by the ReturnUrlParameter. Once a request to the LoginPath grants a new SignIn identity, the ReturnUrlParameter value is used to redirect the browser back to the url which caused the original unauthorized status code. If the LoginPath is null or empty, the middleware will not look for 401 Unauthorized status codes, and it will not redirect automatically when a login occurs.

After removing this parameter everything worked.

enter image description here

Upvotes: 1

NightOwl888
NightOwl888

Reputation: 56849

The issue is that you are extending the wrong part of MVC. MVC has an AuthorizeAttribute, which is an IAuthorizationFilter that you can extend to put custom functionality such as this. MVC uses HTTP 401 as a special status that indicates to redirect to the login page, which is what is returned from the default AuthorizeAttribute (and as far as I know there is no way to change this to use a different status code).

The "solution" you found here only disables the redirect (not just for your special case, but also for AuthorizeAttribute) so when someone tries to access a page that is not allowed, they will get an HTTP 401 response instead of being redirected to the login page (as most users would expect).

Instead of using an action filter, what you should do is add this extra check to a subclass of AuthorizeAttribute and then use your custom authorize attribute in place of AuthorizeAttribute. Then you can keep the login page in place and it will redirect as expected.

public class CustomAuthorize : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        X509Certificate2 cert = new X509Certificate2(httpContext.Request.ClientCertificate.Certificate);

        if (cert == null)
        {
            return false;
        }

        return base.AuthorizeCore(httpContext);
    }
}

If you insist on making this case different than the normal "redirect if not authorized" behavior, you should use a different status code than HTTP 401 for the case of invalid certificate so ASP.NET doesn't hijack the response and turn it into a 302.

Upvotes: 1

Related Questions