AJAX and FormsAuthentication, how prevent FormsAuthentication overrides HTTP 401? - asp.net

In one application configured with FormsAuthentication, when a user access without the auth cookie or with an outdated one to a protected page, ASP.NET issue a HTTP 401 Unauthorized, then the FormsAuthentication module intercepts this response before the request end, and change it for a HTTP 302 Found, setting a HTTP header "Location: /path/loginurl" in order to redirect the user agent to the login page, then the browser goes to that page and retrieves the login page, that is not protected, getting an HTTP 200 OK.
That was a very good idea indeed, when AJAX was not being considered.
Now I have a url in my application that returns JSON data and it needs the user to be authenticated. Everything works well, the problems is that if the auth cookie expires, when my client side code call the server it will get a HTTP 200 OK with the html of the login page, instead a HTTP 401 Unauthorized (because the explained previously). Then my client side is trying to parse the login page html as json, and failing.
The question then is : How to cope with an expired authentication from client side? What is the most elegant solution to cope with this situation? I need to know when the call has been successful or not, and I would like to do it using the HTTP semantic.
Is it possible to read custom HTTP Headers from client side in a safe cross browser way?
Is there a way to tell the FormsAuthenticationModule to not perform redirections if the request is an AJAX request?
Is there a way to override the HTTP status using a HTTP header in the same way you can override the HTTP request method?
I need the Forms authentication, and I would like to avoid rewrite that module or write my own form authentication module.
Regards.

I had the same problem, and had to use custom attribute in MVC. You can easy adapt this to work in web forms, you could override authorization of your pages in base page if all your pages inherit from some base page (global attribute in MVC allows the same thing - to override OnAuthorization method for all controllers/actions in application)
This is how attribute looks like:
public class AjaxAuthorizationAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest()
&& !filterContext.HttpContext.User.Identity.IsAuthenticated
&& (filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0
|| filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true).Count() > 0))
{
filterContext.HttpContext.SkipAuthorization = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
filterContext.Result = new HttpUnauthorizedResult("Unauthorized");
filterContext.Result.ExecuteResult(filterContext.Controller.ControllerContext);
filterContext.HttpContext.Response.End();
}
}
}
Note that you need to call HttpContext.Response.End(); or your request will be redirected to login (I lost some of my hair because of this).
On client side, I used jQuery ajaxError method:
var lastAjaxCall = { settings: null, jqXHR: null };
var loginUrl = "yourloginurl";
//...
//...
$(document).ready(function(){
$(document).ajaxError(function (event, jqxhr, settings) {
if (jqxhr.status == 401) {
if (loginUrl) {
$("body").prepend("<div class='loginoverlay'><div class='full'></div><div class='iframe'><iframe id='login' src='" + loginUrl + "'></iframe></div></div>");
$("div.loginoverlay").show();
lastAjaxCall.jqXHR = jqxhr;
lastAjaxCall.settings = settings;
}
}
}
}
This showed login in iframe over current page (looking like user was redirected but you can make it different), and when login was success, this popup was closed, and original ajax request resent:
if (lastAjaxCall.settings) {
$.ajax(lastAjaxCall.settings);
lastAjaxCall.settings = null;
}
This allows your users to login when session expires without losing any of their work or data typed in last shown form.

I'm stealing this answer heavily from other posts, but an idea might be to implement an HttpModule to intercept the redirect to the login page (instructions at that link).
You could also modify that example HttpModule to only intercept the redirect if the request was made via AJAX if the default behavior is correct when the request is not made via AJAX:
Detect ajax call, ASP.net
So something along the lines of:
class AuthRedirectHandler : IHttpModule
{
#region IHttpModule Members
public void Dispose()
{
}
public void Init(HttpApplication context)
{
context.EndRequest+= new EventHandler(context_EndRequest);
}
void context_EndRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (app.Response.StatusCode == 302
&& app.Request.Headers["X-Requested-With"] == "XMLHttpRequest"
&& context.Response.RedirectLocation.ToUpper().Contains("LOGIN.ASPX"))
{
app.Response.ClearHeaders();
app.Response.ClearContent();
app.Response.StatusCode = 401;
}
}
#endregion
}
You could also ensure the redirect is to your actual login page if there are other legit 302 redirects in your app.
Then you would just add to your web.config:
<httpModules>
<add name="AuthRedirectHandler" type="SomeNameSpace.AuthRedirectHandler, SomeNameSpace" />
</httpModules>
Anyhow. Again, actual original thought went into this answer, I'm just pulling various bits together from SO and other parts of the web.

I was having issues implementing the accepted answer. Chiefly, my error logs were getting filled with Server cannot set status after HTTP headers have been sent errors.
I tried implementing the accepted answer to question Server cannot set status after HTTP headers have been sent IIS7.5, again no success.
Googling a bit I stumbled upon the SuppressFormsAuthenticationRedirect property
If your .Net version is >= 4.5, then you can add the following code to the HandleUnauthorizedRequest method of your custom AuthorizeAttribute class.
public sealed class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
base.HandleUnauthorizedRequest(filterContext);
return;
}
base.HandleUnauthorizedRequest(filterContext);
return;
}
}
The important part is the if block. This is the simplest thing to do if you are on .Net 4.5 & already have custom authorization in place.

Related

why the duplicate returnUrl redirect logic in asp.net web applications with owin / identity?

When you create a new web application (either webforms or mvc) in visual studio, there is post-authentication logic that checks for a ReturnUrl param in the query string, and then redirects the user there if it exists:
In weforms - Login.aspx.cs you have this:
protected void LogIn(object sender, EventArgs e)
{
...
switch (result)
{
case SignInStatus.Success:
IdentityHelper.RedirectToReturnUrl(Request.QueryString["ReturnUrl"], Response);
break;
...
}
}
}
In MVC - AccountController.cs you have this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
...
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
...
}
}
Now in addition, the Owin configuration is setup to use cookie authentication, which uses the Microsoft.Owin.Security.Cookies.CookieAuthenticationHandler class, which itself checks for a ReturnUrl parameter, and applies a redirect if it exists:
protected override async Task ApplyResponseGrantAsync()
{
AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType);
bool shouldSignin = signin != null;
AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
bool shouldSignout = signout != null;
if (shouldSignin || shouldSignout || _shouldRenew)
{
...
if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200)
{
IReadableStringCollection query = Request.Query;
string redirectUri = query.Get(Options.ReturnUrlParameter);
if (!string.IsNullOrWhiteSpace(redirectUri)
&& IsHostRelative(redirectUri))
{
var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri);
Options.Provider.ApplyRedirect(redirectContext);
}
}
}
}
Both redirects appear to execute during the login/authentication request. One is applied on the HttpContext.Response, and the other applied on the Owin redirect context. In my experience, it looks like the latter redirect call wins, which is a problem if you have special redirect logic applied post-login in your website code because it gets overridden by the built-in Owin redirect.
Is there a good reason for this duplicate logic? Is this just poor design? So if I'm using asp.net Owin CookieAuthentication, should I have post-login redirect code logic on account controller or aspx code behind? And if so, should the redirect be applied on the HttpContext.Response or through Owin somehow?
As you said, these three ways to redirect all belong to different parts: WebForms, MVC, OWIN. Each one of them can be used independently from the other (OWIN in self-hosting situation), so there is a need to do the same thing in each and every one of them.
However I'm not entirely sure why latest MVC template needs RedirectToLocal. I'd go with the backward compatibility - this method been there for ages.
Also the OWIN redirect does not win in MVC - in one of my apps, I always redirect user depending on their role, even if there is a parameter with local URL to go to, my users always end up on the page specified in MVC controller.
However, having looked therough OWIN source code and the redirecting logic, it seems strange that MVC wins. Probably need to step-through all the way down and see what happens in MVC scenario.

ASP.NET Web API how to authenticate user

I'm trying to create a simple user authentication function but I just can't get it to work.
Here is the code I'm working on:
public class LoginController : ApiController
{
private void SetPrincipal(IPrincipal principal)
{
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
{
HttpContext.Current.User = principal;
}
}
public bool Login(string token)
{
//Check token
if (.....)
{
//Authenticate user
var identity = new GenericIdentity("Test user");
SetPrincipal(new GenericPrincipal(identity, new string[]{"Test role"}));
}
}
[Authorize]
public string TestFun()
{
return "Hello " + User.Identity.Name;
}
}
So, if I try to call method TestFun() first, it returns error code 401 like it should.
However when I call method Login() it should somehow save user credentials, but this is where I get lost, I just can't get it to work.
TestFun() always returns error code 401 even if I call Login() first.
If I try to put return "Hello " + User.Identity.Name; in the Login() function it returns correct username, but in the TestFun() the user is not available.
I've even tried using Sessions and FormsAuthentication but I just can't get it to work, even on this really simple example.
Can someone please tell me what am I missing?
Thanks!
The Login method sets the principal for current request only. Just after the request completes, the principal context is wiped out so that the server can handle other requests for other users. When a new request comes, eons later from the server perspective, the principal context no longer exists and if nothing restores it, the request is unauthenticated.
To fix this you have to return something from your login method to the client. Not only bool but rather - an authentication token. Something the client could use to authenticate further requests.
It could be anything. Forms cookie would be fine as long as the client remembers to append it to further requests. Another common practice is to have a custom authentication token returned to the client and then appended by the client in a custom authentication header. And as forms cookies are handled by the Forms Authentication module, custom headers would need a custom mvc authentication filter or custom asp.net authentication module so that the token is readed, the identity is extracted and restored just before the request is about to execute.
If you don't like to bake your own token infrastructure, I would also recommend OAuth2 tokens. There is a great book that contains easy to follow examples on this and other possible authentication methods:
http://www.amazon.com/Pro-ASP-NET-Web-API-Security/dp/1430257822/ref=sr_1_1?ie=UTF8&sr=8-1&keywords=web+api+security
I just got the same issue, yes, I agreed we need to save that principal into somewhere (cookie, session) for other action to use, so, in SetPrincipal function I added
HttpContext.Current.Session["user"] = HttpContext.Current.User;
Now, the issue is how to get it back for other action, the idea popups in my mind is to extend AuthorizeAttribute and override IsAuthrized function, it will read the session first and if it found the session, it will return true, otherwise it will return false.
namespace BinZ
{
public class MyAuthorizeAttribute:AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext) {
HttpContext.Current.User = HttpContext.Current.Session["user"] as IPrincipal;
return HttpContext.Current.User != null;
}
}
}
Please remember to replace [Authorize] to [MyAuthorizeAttribute] in WebApi controller.
It works for me very well.
Cheers

How to set http cookies (headers) in HTTP request

I'm trying to set up a WCF service hosted in IIS in ASP.Net compatibility mode that is protected via Forms Authentication and accessed via a .Net User Control in IE. (see Secure IIS hosted WCF service for access via IE hosted user control).
The User Control in IE is needed because it uses a specific third-party control for which there doesn't exist anything comparable in Silverlight or AJAX.
So I need the UserControl to set the authentication and session id cookies in the http request headers before it accesses the WCF service. My approach is to set up a Message inspector that does this.
So I've defined the Message Inspector:
public class CookieInspector : IClientMessageInspector {
public void AfterReceiveReply(ref Message reply, object correlationState) {
}
public object BeforeSendRequest(
ref Message request,
IClientChannel channel) {
HttpRequestMessageProperty messageProperty;
if (request.Properties.ContainsKey(HttpRequestMessageProperty.Name)) {
messageProperty = (HttpRequestMessageProperty) request.Properties[
HttpRequestMessageProperty.Name
];
}
else {
messageProperty = new HttpRequestMessageProperty();
request.Properties.Add(
HttpRequestMessageProperty.Name,
messageProperty
);
}
// Set test headers for now...
messageProperty.Headers.Add(HttpRequestHeader.Cookie, "Bob=Great");
messageProperty.Headers.Add("x-chris", "Beard");
return null;
}
}
and an Endpoint behaviour:
public class CookieBehavior : IEndpointBehavior {
public void AddBindingParameters(
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters) {
}
public void ApplyClientBehavior(
ServiceEndpoint endpoint,
ClientRuntime clientRuntime) {
clientRuntime.MessageInspectors.Add(new CookieInspector());
}
public void ApplyDispatchBehavior(
ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher) {
}
public void Validate(ServiceEndpoint endpoint) {
}
}
and I configure and create my channel and WCF client in code:
var ea = new EndpointAddress("http://.../MyService.svc");
// EDIT: Http cookies can't be set with WSHttpBinding :-(
// var binding = WSHttpBinding();
var binding = new BasicHttpBinding();
// Disable automatically managed cookies (which enables user cookies)
binding.AllowCookies = false;
binding.MaxReceivedMessageSize = 5000000;
binding.ReaderQuotas.MaxStringContentLength = 5000000;
var cf = new ChannelFactory<ITranslationServices>(binding, ea);
cf.Endpoint.Behaviors.Add(new CookieBehavior());
ITranslationServices service = cf.CreateChannel();
However when I look at my request with Fiddler, the http header and cookie aren't set, and I have no clue why. I've read various articles on the Net, Stackoverflow etc that basically say that it should work, but it doesn't. Either I'm missing something obvious, or there's a bug in WCF or something else?
Well I figured it out, if I use a basicHttpBinding instead of a WSHttpBinding it works. No idea why though...
WSHttpBinding may be composed of more than one physical message to one logical message. So when successive physical calls are made, they may not be carrying the cookie appropriately

Forms authentication: disable redirect to the login page

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.
I'm not sure if this will work for everyone, but in IIS7 you can call Response.End() after you've set the status code and description. This way, that #&$^##*! FormsAuthenticationModule won't do a redirect.
public void ProcessRequest(HttpContext context) {
Response.StatusCode = 401;
Response.StatusDescription = "Authentication required";
Response.End();
}
To build on zacharydl's answer slightly, I used this to solve my woes. On every request, at the beginning, if it's AJAX, immediately suppress the behavior.
protected void Application_BeginRequest()
{
HttpRequestBase request = new HttpRequestWrapper(Context.Request);
if (request.IsAjaxRequest())
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
}
I don't know how that Response.End() worked for you. I tried it with no joy, then looked at MSDN for Response.End(): 'stops execution of the page, and raises the EndRequest event'.
For what it's worth my hack was:
_response.StatusCode = 401;
_context.Items["401Override"] = true;
_response.End();
Then in Global.cs add an EndRequest handler (which will get called after Authentication HTTPModule):
protected void Application_EndRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Items["401Override"] != null)
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.StatusCode = 401;
}
}
what you've found out is correct about the forms auth intercepting the 401 and doing a redirect but we also can do that to reverse that.
Basically what you need is an http module to intercept the 302 redirect to the login page and reverse it to a 401.
Steps on doing that is explained in here
The given link is about a WCF service but it is the same in all the forms auth scenarios.
As explained in the above link you need to clear the http headers as well but remember to put the cookie header back to the response if the original response (i.e. before intercepting) contained any cookies.
I know there is already an answer with tick but while trying to solve similar problem I found this (http://blog.inedo.com/2010/10/12/http-418-im-a-teapot-finally-a-%e2%80%9clegitimate%e2%80%9d-use/) as an alternative.
Basically you return your own HTTP status code (e.g. 418) in your code. In my case a WCF data service.
throw new DataServiceException(418, "401 Unauthorized");
Then use a HTTP module to handle it at the EndRequest event to rewrite the code back to 401.
HttpApplication app = (HttpApplication)sender;
if (app.Context.Response.StatusCode == 418)
{
app.Context.Response.StatusCode = 401;
}
The browser / client will receive the correct content and status code, it works great for me :)
If you are interested to learn more about HTTP status code 418 see this question & answer.
That's a known issue, and there's a NuGet Package for that and/or the source code available.
Funny hack if you use.NET Framework >= v4.0 but < v4.5. It uses reflection to set value of inaccessible SuppressFormsAuthenticationRedirect property:
// Set property to "true" using reflection
Response
.GetType()
.GetProperty("SuppressFormsAuthenticationRedirect")
.SetValue(Response, true, null);
You do not set the WWW-Authenticate header in the code you show, so the client cannot do HTTP authentication instead of forms authentication. If this is the case, you should use 403 instead of 401, which will not be intercepted by the FormsAuthenticaitonModule.
I had the problem that I wanted to avoid not only the redirect but also the forms authentication itself in order to make a web api work. Entries in web.config with a location tag for the api didn't help.
Thus I used SuppressFormAuthenticationRedirect and HttpContext.Current.SkipAuthorization to suppress the authentication in general.
In order to identify the sender I used e.g. the UserAgent in the Header but it is of course recommendable to implement further authentification steps, e.g. check against the sending IP or send another key with the request.
Below is inserted in the Global.asax.cs.
protected void Application_BeginRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Request.UserAgent == "SECRET-AGENT")
{
AppLog.Log("Redirect suppressed");
HttpApplication context = (HttpApplication)sender;
context.Response.SuppressFormsAuthenticationRedirect = true;
HttpContext.Current.SkipAuthorization = true;
}
}
In order to redirect the user to Unauthorize Page rather than to the login page, the Hack is to implement Application_EndRequest in Global and check for Response Status Code 302, which is a temporary redirect from the current called to action.
protected void Application_EndRequest(object sender, EventArgs e)
{
if(HttpContext.Current.Response.StatusCode == 302 && User.Identity.IsAuthenticated)
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.Redirect("/UnauthorizedPageUrl");
}
}
Look inside your Web.config file in configuration\authentication.
If there is a forms subelement there with a loginUrl attribute, remove it and try again.

How do you handle ajax requests when user is not authenticated?

How do you handle ajax requests when user is not authenticated?
Someone enters the page, leaves room for an hour, returns, adds comment on the page that goes throuh ajax using jQuery ($.post). Since he is not authenticated, method return RedirectToRoute result (redirects to login page). What do you do with it? How do you handle it on client side and how do you handle it in controller?
EDIT:
I wrote above answer a long time ago and now I believe that sending 403 is not proper way to go. 403 has slightly different meaning and it just shouldn't be used. This is corrected attribute using 401. It differs only with additional context.HttpContext.Response.End() in Http401Result and different HTTP code:
public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
private class Http401Result : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 401.
context.HttpContext.Response.StatusCode = 401;
context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
context.HttpContext.Response.End();
}
}
private readonly bool _authorize;
public OptionalAuthorizeAttribute()
{
_authorize = true;
}
//OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller.
//That is why parameter is introduced.
public OptionalAuthorizeAttribute(bool authorize)
{
_authorize = authorize;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//When authorize parameter is set to false, not authorization should be performed.
if (!_authorize)
return true;
var result = base.AuthorizeCore(httpContext);
return result;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 401 error.
filterContext.Result = new Http401Result();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
OLD ANSWER:
While I like the ideas posted in other answers (which I had an idea about earlier), I needed code samples. Here they are:
Modified Authorize attribute:
public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
private class Http403Result : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 403.
context.HttpContext.Response.StatusCode = 403;
context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
}
}
private readonly bool _authorize;
public OptionalAuthorizeAttribute()
{
_authorize = true;
}
//OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller.
//That is why parameter is introduced.
public OptionalAuthorizeAttribute(bool authorize)
{
_authorize = authorize;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//When authorize parameter is set to false, not authorization should be performed.
if (!_authorize)
return true;
var result = base.AuthorizeCore(httpContext);
return result;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 403 error.
filterContext.Result = new Http403Result();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
HandleUnauthorizedRequest is overridden, so it returns Http403Result when using Ajax. Http403Result changes StatusCode to 403 and returns message to the user in response. There is some additional logic in attribute (authorize parameter), because I turn on [Authorize] in the base controller and disable it in some pages.
The other important part is global handling of this response on client side. This is what I placed in Site.Master:
<script type="text/javascript">
$(document).ready(
function() {
$("body").ajaxError(
function(e,request) {
if (request.status == 403) {
alert(request.responseText);
window.location = '/Logout';
}
}
);
}
);
</script>
I place a GLOBAL ajax error handler and when ever $.post fails with a 403 error, the response message is alerted and the user is redirected to logout page. Now I don't have to handle the error in every $.post request, because it is handled globally.
Why 403, and not 401? 401 is handled internally by MVC framework (that is why redirection to login page is done after failed authorization).
What do you think about it?
The idea I came up with when a coworker asked about how to handle it was this - make an AuthorizeAjax attribute. It can interrogate and verify that Request.IsAjaxRequest() and, if the request isn't authenticated, return a specific JSON error object. It's possible you could simply override the default AuthorizeAttribute and have it call the base unless it's an unauthorized AJAX request so you don't have to worry about whether to tag controller actions with [Authorize] or [AuthorizeAjax].
On the client-side, all your pages would have to be equipped to deal with the returned error, but that logic can likely be shared.
I would propose creating your own AuthorizeAttribute and if the request is an Ajax request, throw an HttpException(401/403). And also switch to use jQuery's Ajax Method instead.
Assuming you've implemented error pages and they return the correct status code, the error callback will be executed instead of the success callback. This will be happen because of the response code.
The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.
Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:
$(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
// if request returns non MVC page reload because this means the user
// session has expired
var mvcHeaderName = "X-AspNetMvc-Version";
var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);
if (!mvcHeaderValue) {
location.reload();
}
});
The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.
If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.
Here's a solution I use. It is dead simple, if a bit brute-force. I like it because I'm lazy and I don't want to think about special attributes on action methods and I don't want to write ajax error handlers if I don't have to (although there's no reason client script couldn't detect the 403 status code and do something user friendly).
Putting this in Global.axax detects any unauthenticated ajax request and simply returns 403, with no content. This prevents unauthenticated ajax calls getting redirected to the login form when forms authentication is in use.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
// Prevent Ajax requests from being returned the login form when not authenticated
// (eg. after authentication timeout).
if ((Request.Headers["X-Requested-With"] != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest")
||
(Request["X-Requested-With"] != null && Request["X-Requested-With"] == "XMLHttpRequest"))
{
if (!Request.IsAuthenticated)
{
Response.Clear();
Response.StatusCode = 403;
Response.Flush();
Response.End();
}
}
}
You can detect ajax request and send 401, and on client side you can even show an ajax dialog with login prompt, after which you can "continue" your failed ajax request and make your application work and user feel like session timeout never happened. See this answer for details.

Resources