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.
Related
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
}
}
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.
I'd like [Authorize] to redirect to loginUrl unless I'm also using a role, such as [Authorize (Roles="Admin")]. In that case, I want to simply display a page saying the user isn't authorized.
What should I do?
Here is the code from my modified implementation of AuthorizeAttribute; I named it SecurityAttribute. The only thing that I have changed is the OnAuthorization method, and I added an additional string property for the Url to redirect to an Unauthorized page:
// Set default Unauthorized Page Url here
private string _notifyUrl = "/Error/Unauthorized";
public string NotifyUrl {
get { return _notifyUrl; } set { _notifyUrl = value; }
}
public override 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);
}
/// This code added to support custom Unauthorized pages.
else if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (NotifyUrl != null)
filterContext.Result = new RedirectResult(NotifyUrl);
else
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
/// End of additional code
else
{
// Redirect to Login page.
HandleUnauthorizedRequest(filterContext);
}
}
You call it the same way as the original AuthorizeAttribute, except that there is an additional property to override the Unauthorized Page Url:
// Use custom Unauthorized page:
[Security (Roles="Admin, User", NotifyUrl="/UnauthorizedPage")]
// Use default Unauthorized page:
[Security (Roles="Admin, User")]
Extend the AuthorizeAttribute class and override HandleUnauthorizedRequest
public class RoleAuthorizeAttribute : AuthorizeAttribute
{
private string redirectUrl = "";
public RoleAuthorizeAttribute() : base()
{
}
public RoleAuthorizeAttribute(string redirectUrl) : base()
{
this.redirectUrl = redirectUrl;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
string authUrl = this.redirectUrl; //passed from attribute
//if null, get it from config
if (String.IsNullOrEmpty(authUrl))
authUrl = System.Web.Configuration.WebConfigurationManager.AppSettings["RolesAuthRedirectUrl"];
if (!String.IsNullOrEmpty(authUrl))
filterContext.HttpContext.Response.Redirect(authUrl);
}
//else do normal process
base.HandleUnauthorizedRequest(filterContext);
}
}
Usage
[RoleAuthorize(Roles = "Admin, Editor")]
public class AccountController : Controller
{
}
And make sure you add your AppSettings entry in the config
<appSettings>
<add key="RolesAuthRedirectUrl" value="http://mysite/myauthorizedpage" />
</appSettings>
The easiest way I've found is to extend and customize the AuthorizeAttribute so that it does something different (i.e., not set an HttpUnauthorizedResult) when the Role check fails. I've written an article about this on my blog that you might find useful. The article describes much what you are wanting, though it goes further and allows the user who "owns" the data to also have access to the action. I think it should be fairly easy to modify for your purposes -- you'd just need to remove the "or owner" part.
By default, the MVC Authorize attribute sets the HttpContext.Response.StatusCode = 401 when a user is not authorized and the section in the web.config routes to the loginUrl property.
I want to do something similar with other response codes. For example, I have an attribute called ActiveAccount which verifies the user's account is currently active and then allows them access to the controller. If they are not active I want to route them to a specific controller and view (to update their account).
I'd like to copy the Authorize attributes way of handling this and set the StatusCode to something like 410 (warning: preceding number pulled out of thin air) and have the user routed to a location defined in the web.config file.
What can I do to implement this behavior? Or is there a simpler method?
Edit: Results
I ended up avoiding the StatusCode and just performing a redirection from within the attribute as this was much simpler. Here is my code in a nutshell:
// using the IAuthorizationFilter allows us to use the base controller's
// built attribute handling. We could have used result as well, but Auth seems
// more appropriate.
public class ActiveAccountAttribute: FilterAttribute, IAuthorizationFilter
{
#region IAuthorizationFilter Members
public void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
return;
// ... code which determines if our customer account is Active
if (!user.Status.IsActive)
filterContext.Result = new RedirectToRouteResult("Default", new RouteValueDictionary(new {controller = "Account"}));
}
#endregion
}
You could inherit the RedirectToRouteResult class and add a constructor parameter for the status code.
public class StatusRedirectResult : RedirectToRouteResult
private string _status;
public StatusRedirectResult(string action, RouteValueDictionary routeValues, string statusCode)
{
_status = statusCode;
base.RedirectToRouteResult(action, routeValues);
}
public override ExecuteResult(ControllerContext context)
{
context.HttpContext.Current.Response.Status = _status;
base.ExecuteResult(context);
}
}
To use this in a controller action, just
return StatusRedirect("NewAction", new RouteValueDictionary(new { controller = "TheController" }, "410");
it should be two piece of code.
in your controller you return an error code by
Response.StatusCode = (int)HttpStatusCode.NotFound;
then you custom error code to desired route
<customErrors mode="On">
<error statusCode="404" redirect="~/Profile/Update" />
</customErrors>
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.