ASP.NET MVC throw and handle error from controller - asp.net-mvc

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.

Related

Can I show a message on my Login page explaining that authorization had failed?

I'm using a custom authorization scheme, and when a user isn't authorized, I return an HttpUnauthorizedResult. This causes the user to be redirected to the login page. Is it somehow possible, in the login page, to detect that it is being used because of an authorization failure and tell the user this? If so, how could I do this?
It would be a bonus if I could tell the user, "You need to log in as a user with x role to perform the action you requested", or something like that.
Rather than return an HTTP 401, return a web page with the message you want, and a button to go to the login page.
Actually, you think that you are sending an Unauthorized response, but in reality ASP.NET is intercepting that HTTP 401 response and sending an HTTP 302 (Redirection) to your login page instead. So if you want a custom message, just redirect yourself to the page you want.
Cheers.
UPDATE:
If you create your own Authorize filter, you can define what happen if the user is not authorized/authenticated:
public class MyAuthorizeAttribute : AuthorizeAttribute
{
readonly String _customError;
public MyAuthorizeAttribute(String customError)
{
_customError = customError;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
FormsAuthentication.SignOut();
filterContext.Controller.TempData["Error"] = _customError;
filterContext.Result = new RedirectResult("~/Account/yourErrorView");
}
}
(Not tested)
That way you can use your attribute this way:
[MyAuthorize("You are not authorized to see this thing")]
public ActionResult MyActionMethod()
{
return View();
}
And then the user will be redirected to "~/Account/yourErrorView", and in the TempData you will find the custom error message.
Cheers.
I think it would be better pass additional parameter which will describe the cause of error, for example:
/Account/Login?error=4
and in the Login action check if error exists.
Besides you can store your error messages in different ways: session, cookie.
Use ActionFilterAttribute instead of the AuthorizeFilterAttribute to point it to your error handling page.
public class RoleAuthorize: ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = (YourController)filterContext.Controller;
try
{
if (!controller.CheckForRoleMethod())
{
throw new System.Security.SecurityException("Not Authorized!");
}
}
catch (System.Security.SecurityException secEx)
{
if (secEx != null)
{
// I use TempData for errors like these. It's just me.
TempData["ErrorMessage"] = secEx.Message;
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary { { "controller", "ErrorHandler" }, { "action", "Error" } });
}
}
}
base.OnActionExecuting(filterContext);
}
You have to make a separate method on that controller being decorated to check if the cached user is Authorized or not like so:
public class ApplicationController : Controller
{
public bool CheckForRoleMethod(){
// get formsauthentication details to retrieve credentials
// return true if user has role else false
}
}

ASP.NET MVC 3 determine session status (new or timeout)

I currently use the default forms authentication method for my ASP.NET MVC application.
above all of my actionmethods that require authentication I have this attribute
[Authorize()]
When someone tries to call the page that that action method "serves" and they haven't yet logged in, it sends them to the login page...perfect! However, if their session times out and they try to hit that page, they're also just redirected to the login page with no indication of why. I'd like to be able to determine if it's a new visit, or if it's a timeout and display a different message on the login screen accordingly.
Is that possible?
Have a look at this custom authorize attribute i have made. It was to implement some custom role based authorization, but you could make it work for you as well. There is a Session.IsNewSession property you can check to see if this request takes place on a new session.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.User.Identity.IsAuthenticated)
{
httpContext.User = new GenericPrincipal(httpContext.User.Identity, AdminUserViewModel.Current.SecurityGroups.Select(x => x.Name).ToArray());
}
return base.AuthorizeCore(httpContext);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult("/Authentication/NotAuthorized", false);
}
else
{
if (filterContext.HttpContext.Session.IsNewSession)
{
// Do Something For A New Session
}
base.HandleUnauthorizedRequest(filterContext);
}
}
}
On sign-in, you can set a cookie that's tied to the browser session. If that cookie exists, you know that the session timed out. If not, you know it's a new visit.

Asp.net MVC Authorize attribute, redirect to custom "no rights" page

Asp.net MVC2 does redirect to login page with response 302 when authenticated user has no rights.
I would like to split into two actions
If user is not authenticated then do what it does, redirect to login page.
If user is authenticated but has no required rights then return appropriate http status code and show no rights dude page.
Is there any way to do it? Or am I doing something wrong with authorize and form authentication? Only way I can think of is by writing custom authorize attribute, which I want to avoid.
You could write custom filter attribute like this:
public class CustomAuthorizeAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.User.Identity == null || !filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult(System.Web.Security.FormsAuthentication.LoginUrl + "?returnUrl=" +
filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
}
//Check user right here
if (userNotRight)
{
filterContext.HttpContext.Response.StatusCode = 302;
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
And use it in controller:
[CustomAuthorize]
public class HomeController : Controller
{
}
You could write a custom authorize attribute and in the AuthorizeCore method if the user is not authenticated return a HttpUnauthorizedResult and if he is authenticated but not in roles perform some other action you would like. Note that if you return 401 status code the FormsAuthentication framework will eventually redirect with 302 to the login page.
As suggested in Customizing authorization in ASP.NET MVC, you could subclass the AuthorizeAttribute to intercept the authenticated-but-unauthorized scenario and replace the result with a redirect.
Implement a custom AuthorizeAttribute and add the following override. The basics is to check if user is authenticated but not authorized and then redirect to you own "Access Denied" page. Hope this helps!
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Check if user is authenticated and if this action requires authorization
if (filterContext.HttpContext.User.Identity.IsAuthenticated
&& filterContext.ActionDescriptor.IsDefined(typeof(AuthorizeAttribute), true)
|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AuthorizeAttribute), true))
{
List<object> attributes = new List<object>(filterContext.ActionDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true));
attributes.AddRange(filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(AuthorizeAttribute), true));
// Check all authorzation attributes
foreach (var attribute in attributes)
{
var authAttribute = attribute as AuthorizeAttribute;
if (authAttribute != null)
{
if (!filterContext.HttpContext.User.IsInRole(authAttribute.Roles))
{
// User is not authorized so redirect to our access denied error page
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "area", "" },
{ "controller", "Error" },
{ "action", "AccessDenied" }
});
break;
}
}
}
}
}
Similar to solutions suggested by #hellangle and #Andreas, I used the following code to solve this problem:
public class CustomizedAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var userAuthInfo = GetUserAuthInfo();
if (!userAuthInfo.IsAuthenticated())
{
filterContext.Result = new RedirectResult(UrlToYourLoginPage);
return;
}
if (!userAuthInfo.IsAuthorized())
{
var result = new ViewResult {ViewName = "UnAuthorized"};
result.ViewBag.Message = "Sorry! You are not authorized to do this!";
filterContext.Result = result;
}
}
}
Of course, you need to implement the user authorization information class and related methods (GetUserAuthInfo, IsAuthenticated, IsAuthorized) according to your specific needs. Also a View named 'UnAuthorized' should be put to somewhere the MVC engine can find. Then it can be used on a controller class (pointed out in #hellangle's answer) or a action method:
[CustomizedAuthorizeAttribute]
public class TargetController : Controller
{
[CustomizedAuthorizeAttribute]
public ActionResult TargetAction()
{
// Your Code
}
}
In order to provide different access control strategy for various controller classes and action methods, implements a constructor for CustomizedAuthorizeAttribute class which accepts parameter(s) representing access control information and then Instantiate CustomizedAuthorizeAttribute class accordingly.

Setting result for IAuthorizationFilter

I am looking to set the result action from a failed IAuthorizationFilter. However I am unsure how to create an ActionResult from inside the Filter. The controller doesn't seem to be accible from inside the filter so my usual View("SomeView") isn't working. Is there a way to get the controler or else another way of creating an actionresult as it doesn't appear to be instantiable?
Doesn't work:
[AttributeUsage(AttributeTargets.Method)]
public sealed class RequiresAuthenticationAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = View("User/Login");
}
}
}
You should look at the implementation of IAuthorizationFilter that comes with the MVC framework, AuthorizeAttribute. If you are using forms authentication, there's no need for you to set the result to User/Login. You can raise a 401 HTTP status response and ASP.NET Will redirect to the login page for you.
The one issue with setting the result to user/login is that the user's address bar is not updated, so they will be on the login page, but the URL won't match. For some people, this is not an issue. But some people want their site's URL to correspond to what the user sees in their browser.
You can instantiate the appropriate ActionResult directly, then set it on the context. For example:
public void OnAuthorization(AuthorizationContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ViewResult { ViewName = "Whatever" };
}
}

401 response code for json requests with ASP.NET MVC

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.

Resources