I have an application that uses ASP.NET Forms Authentication. For the most part, it's working great, but I'm trying to add support for a simple API via an .ashx file. I want the ashx file to have optional authentication (i.e. if you don't supply an Authentication header, then it just works anonymously). But, depending on what you do, I want to require authentication under certain conditions.
I thought it would be a simple matter of responding with status code 401 if the required authentication was not supplied, but it seems like the Forms Authentcation module is intercepting that and responding with a redirect to the login page instead. What I mean is, if my ProcessRequest
method looks like this:
public void ProcessRequest(HttpContext context) { Response.StatusCode = 401; Response.StatusDescription = "Authentication required"; }
Then instead of getting a 401 error code on the client, like I expect, I'm actually getting a 302 redirect to the login page.
For nornal HTTP traffic, I can see how that would be useful, but for my API page, I want the 401 to go through unmodified so that the client-side caller can respond to it programmatically instead.
Is there any way to do that?
ASP.NET 4.5 added the Boolean HttpResponse.SuppressFormsAuthenticationRedirect
property.
public void ProcessRequest(HttpContext context) { Response.StatusCode = 401; Response.StatusDescription = "Authentication required"; Response.SuppressFormsAuthenticationRedirect = true; }
After a bit of investigation, it looks like the FormsAuthenticationModule adds a handler for the HttpApplicationContext.EndRequest
event. In it's handler, it checks for a 401 status code and basically does a Response.Redirect(loginUrl)
instead. As far as I can tell, there's no way to override this behaviour if want to use FormsAuthenticationModule
.
The way I ended up getting around it was by disabling the FormsAuthenticationModule
in the web.config like so:
<authentication mode="None" />
And then implementing the Application_AuthenticateEvent
myself:
void Application_AuthenticateRequest(object sender, EventArgs e) { if (Context.User == null) { var oldTicket = ExtractTicketFromCookie(Context, FormsAuthentication.FormsCookieName); if (oldTicket != null && !oldTicket.Expired) { var ticket = oldTicket; if (FormsAuthentication.SlidingExpiration) { ticket = FormsAuthentication.RenewTicketIfOld(oldTicket); if (ticket == null) return; } Context.User = new GenericPrincipal(new FormsIdentity(ticket), new string[0]); if (ticket != oldTicket) { // update the cookie since we've refreshed the ticket string cookieValue = FormsAuthentication.Encrypt(ticket); var cookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName] ?? new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue) { Path = ticket.CookiePath }; if (ticket.IsPersistent) cookie.Expires = ticket.Expiration; cookie.Value = cookieValue; cookie.Secure = FormsAuthentication.RequireSSL; cookie.HttpOnly = true; if (FormsAuthentication.CookieDomain != null) cookie.Domain = FormsAuthentication.CookieDomain; Context.Response.Cookies.Remove(cookie.Name); Context.Response.Cookies.Add(cookie); } } } } private static FormsAuthenticationTicket ExtractTicketFromCookie(HttpContext context, string name) { FormsAuthenticationTicket ticket = null; string encryptedTicket = null; var cookie = context.Request.Cookies[name]; if (cookie != null) { encryptedTicket = cookie.Value; } if (!string.IsNullOrEmpty(encryptedTicket)) { try { ticket = FormsAuthentication.Decrypt(encryptedTicket); } catch { context.Request.Cookies.Remove(name); } if (ticket != null && !ticket.Expired) { return ticket; } // if the ticket is expired then remove it context.Request.Cookies.Remove(name); return null; } }
It's actually slightly more complicated than that, but I basically got the code by looking at the implementation of FormsAuthenticationModule
in reflector. My implementation is different to the built-in FormsAuthenticationModule
in that it doesn't do anything if you respond with a 401 - no redirecting to the login page at all. I guess if that ever becomes a requirement, I can put an item in the context to disable the auto-redirect or something.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With