I've been using CookieAuthenticationHandler
and am failing authorization by accessing a view handled by a controller method with the Authorize
attribute.
CookieAuthenticationHandler
then redirects me to a configurable path in either HandleForbiddenAsync
or HandleChallengeAsync
(depending on whether it's authentication or authorization). However, upon redirecting I notice that the HTTP statuscode gets lost.
I've added a breakpoint to the controller action that gets redirected to, where the statuscode is 200.
I was expecting a different statuscode (401 or 403).
This is what happens in HandleForbiddenAsync
(from github):
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
var returnUrl = properties.RedirectUri;
if (string.IsNullOrEmpty(returnUrl))
{
returnUrl = OriginalPathBase + Request.Path + Request.QueryString;
}
var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
await Events.RedirectToAccessDenied(redirectContext);
}
So I implemented my own HandleForbiddenAsync
(with a custom AuthenticationHandler
that extends CookieAuthenticationHandler
), and tried to set the statuscode directly by adding this line:
redirectContext.Response.StatusCode = StatusCodes.Status403Forbidden;
But when I get to the breakpoint I still see statuscode 200.
I'm most likely going about things the wrong way. What I'm trying to accomplish is to get a different statuscode in the controller of my Index view.
Any ideas?
I've delved through the source code and figured out what's going on.
The statuscode gets changed in Events.RedirectToAccessDenied(redirectContext);
It starts in the CookieAuthenticationEvents (github link)
public virtual Task RedirectToAccessDenied(RedirectContext<CookieAuthenticationOptions> context)
=> OnRedirectToAccessDenied(context);
into
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToAccessDenied { get; set; }
= context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers["Location"] = context.RedirectUri;
context.Response.StatusCode = 403;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
This then calls the Redirect
method of the abstract HttpResponse
class (github link here) which in this case uses the DefaultHttpResponse
implementation, which is
public override void Redirect(string location, bool permanent)
{
if (permanent)
{
HttpResponseFeature.StatusCode = 301;
}
else
{
HttpResponseFeature.StatusCode = 302;
}
Headers[HeaderNames.Location] = location;
}
So the statuscode is changed here, and the redirect statuscodes get picked up by MVC and turned into 200.
If all this is cut out by simply doing the following in the HandleChallengeAsync
or HandleForbiddenAsync
methods...
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri("whatever"));
redirectContext.Response.Headers["Location"] = redirectContext.RedirectUri;
redirectContext.Response.StatusCode = 403;
await Task.CompletedTask;
...then the correct statuscode will finally be sent. However, it will now get picked up by the browser instead of MVC, so even though the statuscode is now finally there, the browser will give a default browser error page and that's that.
Since what I wanted to do was to get to my redirect page and display a message, I decided to just add the statuscode in the querystring. This is my final HandleChallengeAsync
:
var unauthorizedUri = Options.LoginPath + QueryString.Create("error", "401");
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(unauthorizedUri));
await Events.RedirectToLogin(redirectContext);
Then in the controller or view I can simply check the querystring.