How to disable standard ASP.NET handling of 401 response code (redirecting to login page) for AJAX/JSON requests?
For web-pages it's okay, but for AJAX I need to get right 401 error code instead of good looking 302/200 for login page.
Update:
There are several solutions from Phil Haack, PM of ASP.NET MVC - http://haacked.com/archive/2011/10/04/prevent-forms-authentication-login-page-redirect-when-you-donrsquot-want.aspx
In classic ASP.NET you get a 401 http response code when calling a WebMethod with Ajax. I hope they'll change it in future versions of ASP.NET MVC. Right now I'm using this hack:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == 302 && Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Context.Response.Clear();
Context.Response.StatusCode = 401;
}
}
The ASP.NET runtime is developed so that it always will redirect the user if the HttpResponse.StatusCode is set to 401, but only if the <authentication /> section of the Web.config is found.
Removing the authentication section will require you to implement the redirection to the login page in your attribute, but this shouldn't be a big deal.
I wanted both Forms authentication and to return a 401 for Ajax requests that were not authenticated.
In the end, I created a custom AuthorizeAttribute and decorated the controller methods. (This is on .Net 4.5)
//web.config
<authentication mode="Forms">
</authentication>
//controller
[Authorize(Roles = "Administrator,User"), Response302to401]
[AcceptVerbs("Get")]
public async Task<JsonResult> GetDocuments()
{
string requestUri = User.Identity.Name.ToLower() + "/document";
RequestKeyHttpClient<IEnumerable<DocumentModel>, string> client =
new RequestKeyHttpClient<IEnumerable<DocumentModel>, string>(requestUri);
var documents = await client.GetManyAsync<IEnumerable<DocumentModel>>();
return Json(documents, JsonRequestBehavior.AllowGet);
}
//authorizeAttribute
public class Response302to401 : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = new { Message = "Your session has died a terrible and gruesome death" },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.StatusDescription = "Humans and robots must authenticate";
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
}
//base.HandleUnauthorizedRequest(filterContext);
}
}
You could also use the Global.asax to interrupt this process with something like this:
protected void Application_PreSendRequestHeaders(object sender, EventArgs e) {
if (Response.StatusCode == 401) {
Response.Clear();
Response.Redirect(Response.ApplyAppPathModifier("~/Login.aspx"));
return;
}
}
I don't see what we have to modify the authentication mode or the authentication tag like the current answer says.
Following the idea of #TimothyLeeRussell (thanks by the way), I created a customized Authorize attribute (the problem with the one of #TimothyLeeRussell is that an exception is throw because he tries to change the filterContext.Result an that generates a HttpException, and removing that part, besides the filterContext.HttpContext.Response.StatusCode = 401, the response code was always 200 OK). So I finally resolved the problem by ending the response after the changes.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class BetterAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
//Set the response status code to 500
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.HttpContext.Response.StatusDescription = "Humans and robots must authenticate";
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
filterContext.HttpContext.Response.End();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
You can call this method inside your action,
HttpContext.Response.End();
Example
public async Task<JsonResult> Return401()
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Response.End();
return Json("Unauthorized", JsonRequestBehavior.AllowGet);
}
From MSDN: The End method causes the Web server to stop processing the script and return the current result. The remaining contents of the file are not processed.
You could choose to create a custom FilterAttribute implementing the IAuthorizationFilter interface.
In this attribute you add logic to determine if the request are supposed to return JSON. If so, you can return an empty JSON result (or do whatever you like) given the user isn't signed in. For other responses you would just redirect the user as always.
Even better, you could just override the OnAuthorization of the AuthorizeAttribute class so you don't have to reinvent the wheel. Add the logic I mentioned above and intercept if the filterContext.Cancel is true (the filterContext.Result will be set to an instance of the HttpUnauthorizedResult class.
Read more about "Filters in ASP.NET MVC CodePlex Preview 4" on Phil Haacks blog. It also applies to the latest preview.
Related
I am using ASPNet Identity 2.0 (Full framework, not the core framework) and MVC.
I would like to execute C# code once the user successfully login to the site.
i know that i can write some code right after the SignInManager.PasswordSignInAsync command and it will work for new login but not will not work for users who used "remember me" feature and returned to the site later (Cookie authentication).
I am looking for an option to catch the event of all the users who signed in to the site either by entering the password and by using the "remember me" cookie.
One way you can do it is by handling the application event in your global.asax file.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Request.IsAuthenticated)
{
Response.Write("HELLO");
}
}
There are many ways to do it. You can create and use custom attribute.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
readonly IAuthentication _authentication;
public CustomAuthorizeAttribute(IAuthentication authentication)
{
_authentication = authentication;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!_authentication.Authorize(filterContext.HttpContext))
filterContext.Result = new HttpUnauthorizedResult();
//your code .....
}
}
And now rather than using [Authorize] use your new [CustomAuthorizeAttribute] attribute
[CustomAuthorizeAttribute]
public ActionResult Index()
{
ViewBag.Title = "Welcome";
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
What you're after is FormsAuthentication_OnAuthenticate (I appreciate Forms-based authentication was not mentioned in the question, but it's an example of Cookie-based remember me authentication, adapt at will)
Unfortunately, there is no trigger for OnCookieBasedFormsAuthenticate_SessionCreate :)
So what you can do is check the Forms-based authentication every so often, because it (and Application_AuthenticateRequest) is fired for every request, CSS pages, images etc, going off to the database to check multiple times per request is an overly resource hungry idea. Luckily the forms cookie ticket has an issued on date and we can use that to check:
public void FormsAuthentication_OnAuthenticate(object sender, FormsAuthenticationEventArgs args)
{
if (FormsAuthentication.CookiesSupported)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(
Request.Cookies[FormsAuthentication.FormsCookieName].Value);
if ((DateTime.Now - ticket.IssueDate).TotalMinutes > 10)
{
if(/*Insert logic to check username here with ticket.Name*/)
{
//recreate cookie with new issuedate
FormsAuthentication.SetAuthCookie(ticket.Name, ticket.IsPersistent);
}
else
{
FormsAuthentication.SignOut();
}
}
Debug.WriteLine($"{ticket.Name} {ticket.IssueDate.ToUniversalTime()}");
}
catch (Exception e)
{
//Elmah
ErrorSignal.FromCurrentContext().Raise(e);
//Cannot decrypt cookie, make the user sign in again
FormsAuthentication.SignOut();
}
}
}
else
{
throw new HttpException("Cookieless Forms Authentication is not supported for this application.");
}
}
This is not the same as cookie expiration, because the user will still retain login status; the user should never see any login request unless there account is no longer valid.
Ok. So I have an issue where I need to do some authorization checks inside the controller action.
There are authorization roles, but it can exist that someone has TypeOnePayment, but not TypeTwo
[Authorize(Roles = "TypeOnePayment;TypeTwoPayment")]
public ActionResult EnterRevenue(PaymentType payment)
{
payment = "TypeOne"; // This exists for show only.
var permission = string.Concat(payment,"Permission");
if (!SecurityUtility.HasPermission(permission))
{
return View("Unauthorized", "Error");
}
return this.PartialView("_EnterRevenue");
}
But since this is returning the partial view, the "Error" screen only appears in the partial view portion of the page. Is there a way to redirect to an entirely new page?
EDIT: EnterRevenue is being retrieved through an ajax call. So just the html is being returned and it's being placed in the view it was called from.
You can redirect to some other action :
public ActionResult EnterRevenue
{
if (!SecurityUtility.HasPermission(permission))
{
return View("Unauthorized", "Error");
}
return RedirectToAction("NotAuthorized","Error");
}
Assume we have ErrorController with action NotAuthorized which returns normal View which displays you are not authorized to view this page.
If you need this check on every action, then you need to implement custom action filter attribute in which you will have to check if it is normal request redirect else return staus as json and redirect from client side. See asp.net mvc check if user is authorized before accessing page
Here is a chunk of code:
public class AuthorizationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
string actionName = filterContext.ActionDescriptor.ActionName;
string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
if (filterContext != null)
{
HttpSessionStateBase objHttpSessionStateBase = filterContext.HttpContext.Session;
var userSession = objHttpSessionStateBase["userId"];
if (((userSession == null) && (!objHttpSessionStateBase.IsNewSession)) || (objHttpSessionStateBase.IsNewSession))
{
objHttpSessionStateBase.RemoveAll();
objHttpSessionStateBase.Clear();
objHttpSessionStateBase.Abandon();
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.Result = new JsonResult { Data = "LogOut" };
}
else
{
filterContext.Result = new RedirectResult("~/Home/Index");
}
}
else
{
if (!CheckAccessRight(actionName, controllerName))
{
string redirectUrl = string.Format("?returnUrl={0}", filterContext.HttpContext.Request.Url.PathAndQuery);
filterContext.HttpContext.Response.Redirect(FormsAuthentication.LoginUrl + redirectUrl, true);
}
else
{
base.OnActionExecuting(filterContext);
}
}
}
}
}
and use it on action like this:
[Authorization]
public ActionResult EnterRevenue
{
return this.PartialView("_EnterRevenue");
}
Or just use a standard redirect call. This should work everywhere (just don't do it inside of a using statement or it will throw an exception in the background):
Response.Redirect("/Account/Login?reason=NotAuthorised", true);
I think what you need can be boiled down to a way for the ajax call to behave differently based on what you are returning to it. The best I have found for doing this can be summarized as follows:
When you detect that you have no permission, add a model state error to your model.
Override the OnActionExecuted (Hopefully all your controllers inherit from a base one so you can do it in one place, if not it might be a good idea to implement that now). In the override, check if the request is ajax and model state is not valid (if you want you can check for the specific error you added in the action method), change the request status to a 4xx status.
In the OnFailure of your ajax call, you can redirect to the error page using javascript code.
In my controller, for lets say edit user. In my controller, I check if the user has rights to edit then I'd like to throw some kind of authentication or prohibited error which would lead to an error page.
Is there some way to do this rather than creating a controller and action just for error? What is the correct way to do this?
Here's an example of a custom authorize attribute you could use:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// if the user is not authenticated render the AccessDenied view
filterContext.Result = new ViewResult { ViewName = "AccessDenied" };
}
}
}
and then decorate your controller action with this attribute:
[CustomAuthorizeAttribute]
public ActionResult SomeAction()
{
...
}
There's one caveat with this approach you should be aware of. If the user is not authorized the server sends 200 status code which is not very SEO friendly. It would be better to send 401 status code. The problem is that if you are using Forms Authentication there's a custom module that gets appended to the ASP.NET execution pipeline and whenever the server sends 401 status code it is intercepted and automatically redirected to the login page. This functionality is by design and is not bug in ASP.NET MVC. It has always been like this.
And as a matter of fact there is a way to workaround this unpleasant situation:
You could modify the custom authorization filter like so:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
// if the user is not authenticated render the AccessDenied view
filterContext.HttpContext.Items["unauthorized"] = true;
}
}
}
and in Global.asax:
protected void Application_EndRequest()
{
if (Context.Items.Contains("unauthorized"))
{
Context.Response.Clear();
Context.Response.StatusCode = 401;
Context.Server.Transfer("~/401.htm");
}
}
Now that's better. You get 401 status code with a custom error page. Nice.
Since your authorization is based per user (I suppose the correct process is each user can only edit their own data) you can't use provided Authorize filter.
Write a custom authorization filter instead. You can provide whatever functionality you'd like. The usual is to return a 401 HTTP status code.
All,
I'm learning MVC and using it for a business app (MVC 1.0).
I'm really struggling to get my head around exception handling. I've spent a lot of time on the web but not found anything along the lines of what I'm after.
We currently use a filter attribute that implements IExceptionFilter. We decorate a base controller class with this so all server side exceptions are nicely routed to an exception page that displays the error and performs logging.
I've started to use AJAX calls that return JSON data but when the server side implementation throws an error, the filter is fired but the page does not redirect to the Error page - it just stays on the page that called the AJAX method.
Is there any way to force the redirect on the server (e.g. a ASP.NET Server.Transfer or redirect?)
I've read that I must return a JSON object (wrapping the .NET Exception) and then redirect on the client, but then I can't guarantee the client will redirect... but then (although I'm probably doing something wrong) the server attempts to redirect but then gets an unauthorised exception (the base controller is secured but the Exception controller is not as it does not inherit from this)
Has anybody please got a simple example (.NET and jQuery code). I feel like I'm randomly trying things in the hope it will work
Exception Filter so far...
public class HandleExceptionAttribute : FilterAttribute, IExceptionFilter
{
#region IExceptionFilter Members
public void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
{
return;
}
filterContext.Controller.TempData[CommonLookup.ExceptionObject] = filterContext.Exception;
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = AjaxException(filterContext.Exception.Message, filterContext);
}
else
{
//Redirect to global handler
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = AvailableControllers.Exception, action = AvailableActions.HandleException }));
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
}
}
#endregion
private JsonResult AjaxException(string message, ExceptionContext filterContext)
{
if (string.IsNullOrEmpty(message))
{
message = "Server error"; //TODO: Replace with better message
}
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; //Needed for IIS7.0
return new JsonResult
{
Data = new { ErrorMessage = message },
ContentEncoding = Encoding.UTF8,
};
}
}
I use the OnFailure hanlder in Ajax.Beginform(). The client-side failure handler can redirect by setting window.location (among a number of other options.) This will work in 99% of modern browsers- if the browser supports AJAX it should support this.
Essentially I want to show a friendly message when someone is not part of a role listed in my attribute. Currently my application just spits the user back to the log in screen. I've read a few posts that talk about creating a custom attribute that just extends [AuthorizeAttribute], but I'm thinking there's got to be something out of the box to do this?
can someone please point me in the right direction of where I need to look to not have it send the user to the log in form, but rather just shoot them a "not authorized" message?
I might be a little late in adding my $0.02, but when you create your CustomAuthorizationAttribue, you can use the AuthorizationContext.Result property to dictate where the AuthorizeAttribute.HandleUnauthorizedRequest method directs the user.
Here is a very simple example that allows you to specify the URL where a user should be sent after a failed authorization:
public class Authorize2Attribute : AuthorizeAttribute
{
// Properties
public String RedirectResultUrl { get; set; }
// Constructors
public Authorize2Attribute()
: base()
{
}
// Overrides
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (String.IsNullOrEmpty(RedirectResultUrl))
base.HandleUnauthorizedRequest(filterContext);
else
filterContext.Result = new RedirectResult(RedirectResultUrl);
}
}
And if I wanted to redirect the user to /Error/Unauthorized as suggested in a previous post:
[Authorize2(Roles = "AuthorizedUsers", RedirectResultUrl = "/Error/Unauthorized")]
public ActionResult RestrictedAction()
{
// TODO: ...
}
I ran into this issue a few days ago and the solution is a bit detailed but here are the important bits. In AuthorizeAttribute the OnAuthorization method returns a HttpUnauthorizedResult when authorization fails which makes returning a custom result a bit difficult.
What I ended up doing was to create a CustomAuthorizeAttribute class and override the OnAuthorization method to throw an exception instead. I can then catch that exception with a custom error handler and display a customized error page instead of returning a 401 (Unauthorized).
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public virtual void OnAuthorization(AuthorizationContext filterContext) {
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}
if (AuthorizeCore(filterContext.HttpContext)) {
HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
cachePolicy.SetProxyMaxAge(new TimeSpan(0));
cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
}
else {
// auth failed, redirect to login page
// filterContext.Result = new HttpUnauthorizedResult();
throw new HttpException ((int)HttpStatusCode.Unauthorized, "Unauthorized");
}
}
}
then in your web.config you can set custom handlers for specific errors:
<customErrors mode="On" defaultRedirect="~/Error">
<error statusCode="401" redirect="~/Error/Unauthorized" />
<error statusCode="404" redirect="~/Error/NotFound" />
</customErrors>
and then implement your own ErrorController to serve up custom pages.
On IIS7 you need to look into setting Response.TrySkipIisCustomErrors = true; to enable your custom errors.
If simplicity or total control of the logic is what you want you can call this in your action method:
User.IsInRole("NameOfRole");
It returns a bool and you can do the rest of your logic depending on that result.
Another one that I've used in some cases is:
System.Web.Security.Roles.GetRolesForUser();
I think that returns a string[] but don't quote me on that.
EDIT:
An example always helps...
public ActionResult AddUser()
{
if(User.IsInRoles("SuperUser")
{
return View("AddUser");
}
else
{
return View("SorryWrongRole");
}
}
As long as your return type is "ActionResult" you could return any of the accepted return types (ViewResult, PartialViewResult, RedirectResult, JsonResult...)
Very similar to crazyarabian, but I only redirect to my string if the user is actually authenticated. This allows the attribute to redirect to the standard logon page if they are not currently logged in, but to another page if they don't have permissions to access the url.
public class EnhancedAuthorizeAttribute : AuthorizeAttribute
{
public string UnauthorizedUrl { get; set; }
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
var redirectUrl = UnauthorizedUrl;
if (filterContext.HttpContext.User.Identity.IsAuthenticated && !string.IsNullOrWhiteSpace(redirectUrl))
{
filterContext.Result = new RedirectResult(redirectUrl);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
The out-of-the-box behavior is that the [Authorize] attribute returns an HTTP 401. The FormsAuthenticationModule (which is loaded by default) intercepts this 401 and redirects the user to the login page. Take a look at System.Web.Security.FormsAuthenticationModule::OnLeave in Reflector to see what I mean.
If you want the AuthorizeAttribute to do something other than return HTTP 401, you'll have to override the AuthorizeAttribute::HandleUnauthorizedRequest method and perform your custom logic in there. Alternatively, just change this part of ~\Web.config:
<forms loginUrl="~/Account/LogOn" timeout="2880" />
And make it point to a different URL, like ~/AccessDenied.