47

In my ASP.NET MVC app, I have most controllers decorated with

[Authorize(Roles="SomeGroup")]

When a user is not authorized to access something, they are sent to "~/Login" which is the Login action on my Account controller.

How can I determine that a user has reached the login page because of not being authorized so that I can show an appropriate error?

Ronnie Overby
  • 45,287
  • 73
  • 267
  • 346

7 Answers7

81

UPDATE (Jun 2015): @daniel-lidström has correctly pointed out that you should not use Response.Redirect in an ASP.NET MVC application. For more information about why, please see this link: Response.Redirect and ASP.NET MVC – Do Not Mix.

UPDATE (Sep 2014): I'm not sure when HandleUnauthorizedRequest was added to the AuthorizeAttribute, but either way I've been able to refine the AuthorizeRedirect code into something smaller and simpler.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeRedirect : AuthorizeAttribute
{
    public string RedirectUrl = "~/Error/Unauthorized";

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        base.HandleUnauthorizedRequest(filterContext);

        if (filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
        {
            filterContext.Result = new RedirectResult(RedirectUrl);
        }
    }
}

Original Answer Below (still fully functional)

I've left this answer here as it still gives you an insight as to how the Authorization pipeline works.

For anyone still landing here, I've edited Ben Scheirman's answer to automatically redirect to an unauthorized page when the user is logged in but not authorized. You can change the redirect path using the name parameter RedirectUrl.

EDIT: I've made the solution thread-safe thanks to the advice of Tarynn and MSDN

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeRedirect : AuthorizeAttribute
{
    private const string IS_AUTHORIZED = "isAuthorized";

    public string RedirectUrl = "~/error/unauthorized";

    protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
    {
        bool isAuthorized = base.AuthorizeCore(httpContext);

        httpContext.Items.Add(IS_AUTHORIZED, isAuthorized);

        return isAuthorized;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        var isAuthorized = filterContext.HttpContext.Items[IS_AUTHORIZED] != null 
            ? Convert.ToBoolean(filterContext.HttpContext.Items[IS_AUTHORIZED]) 
            : false;

        if (!isAuthorized && filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
        {
            filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl);
        }
    }
}
Community
  • 1
  • 1
Ben Cull
  • 9,434
  • 7
  • 43
  • 38
  • 5
    From MSDN: Deriving from AuthorizeAttribute If you derive from the AuthorizeAttribute class, the derived type must be thread safe. Therefore, do not store state in an instance of the type itself (for example, in an instance field) unless that state is meant to apply to all requests. Instead, store state per request in the Items property, which is accessible through the context objects passed to AuthorizeAttribute. isn't the _isAuthorized an instance field? – Tarynn Dec 17 '12 at 20:59
  • Does this method work with FormsAuth and the default redirect? – marvc1 May 22 '13 at 14:42
  • Yep, the default non-authenticated redirect will be unaffected. – Ben Cull May 24 '13 at 05:50
  • You can simplify the inline if statement to: `filterContext.HttpContext.Items[IsAuthorized] != null && Convert.ToBoolean(filterContext.HttpContext.Items[IsAuthorized]);` Note: cahnged `ISAUTHORIZED` to `IsAuthorized` – Nate-Wilkins Aug 02 '13 at 13:39
  • Would not work with multiple controller actions rendered on one page. `Additional information: Item has already been added. Key in dictionary: 'isAuthorized' Key being added: 'isAuthorized'` – Maksim Vi. May 22 '14 at 00:37
  • Hmm good point, although if you're calling child actions, marking them with `ChildActionOnly` and putting the `AuthoriseRedirect` on the parent action should work. – Ben Cull May 27 '14 at 00:51
  • I would say that this answer should be accepted as best answer to this question. – Serhiy Prysyazhnyy Jul 28 '14 at 10:53
  • 1
    it work perfect but i want to mention some note here. it take me more than 2 hours because some mistake when implement. - Don't add to filter like filters.Add(new AuthorizeRedirect()); - In controller implement it like: [AuthorizeRedirect(Roles = "Admins")] public class RoleController : Controller .Not implement like [Authorize(Roles = "Admins")] public class RoleController : Controller – Wolf Sep 25 '14 at 04:41
  • 2
    Instead of having the redirect dependency in the class, I replaced it with `filterContext.RequestContext.HttpContext.AddError(new HttpException(403, "You have no power here..."));` Which lets me use `customError` from the web.config. I still wish there was a way to actually return a `403` instead of the `302` though. – MisterIsaak Oct 09 '14 at 19:09
  • If I implement both HandleUnauthorizedRequest and OnAuthorization, why does HandleUnauthorizedRequest never be run? – Willy Oct 21 '14 at 15:01
  • 1
    @Dabbas Nope definitely just if(). It's saying if you're authenticaed (logged in) but not authorized (have permission) then redirect to a page that says "I know you're logged in and not allowed to be here". – Ben Cull Nov 04 '14 at 20:56
  • 2
    filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl) - For me this actually does perform the redirect yet also throws a "System.Web.HttpException (0x80004005): Cannot redirect after HTTP headers have been sent." under the hood. Can this be avoided and if so how? – ProNotion Apr 22 '15 at 15:48
  • Potentially you could call the base method *after* the redirect to see if that helps? – Ben Cull Apr 23 '15 at 01:39
  • 1
    @ProNotion @BenCull This code is incorrect. You should use the `filterContext.Result` as shown by @divide_byzero's answer. `Response.Redirect` is not supported by MVC, as you can tell from the `System.Web.HttpException (0x80004005)`. – Daniel Lidström Jun 10 '15 at 08:01
  • @daniel-lidström You're absolutely right, well done! I've updated the code with the correct implementation. – Ben Cull Jun 20 '15 at 10:40
  • Note that the AuthorizeAttribute listed here is in the *System.Web.Mvc* namespace. When I tried this solution, I accidentally used the System.Web.Http namespace. See my answer below for more details. – Brian Vander Plaats Feb 05 '16 at 16:04
  • @Willy if you want HandleUnauthorizedRequest to be ran from OnAuthorization, you should add this piece of code: filterContext.Result = new HttpUnauthorizedResult(); – Francisco Aug 11 '16 at 22:00
30

You can look for the ?ReturnUrl= querystring value, or you can create your own authorization filter & set a field in TempData indicating the reason.

Here is a simple custom filter that will do the trick:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : AuthorizeAttribute
{

    // NOTE: This is not thread safe, it is much better to store this
    // value in HttpContext.Items.  See Ben Cull's answer below for an example.
    private bool _isAuthorized;

    protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
    {
        _isAuthorized = base.AuthorizeCore(httpContext);
        return _isAuthorized;
    }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if(!_isAuthorized)
        {
            filterContext.Controller.TempData.Add("RedirectReason", "Unauthorized");
        }
    }
}

Then in your view, you can do something like this:

@if(TempData["RedirectReason"] == "Unauthorized")
{
    <b>You don't have permission to access that area</b>
}

(Though I'd recommend a better approach than these magic strings, but you get the point)

Ben Scheirman
  • 40,531
  • 21
  • 102
  • 137
6

Ben Cull's method works well, but remember there are two AuthorizeAttribute classes - one in System.Web.HTTP (used by Web API), and the other in System.Web.Mvc. Ben's method uses the System.Web.Mvc class. For clarity, I suggest using the fully qualified path.

If you're using Web API alongside MVC, you will need to implement two filters:

public class AuthorizeRedirectMVCAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        base.HandleUnauthorizedRequest(filterContext);

        if (filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
        {
            filterContext.Result = new RedirectResult("~/Account/AccessDenied");
        }
    }
}

public class AuthorizeRedirectAPIAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);

        if (actionContext.RequestContext.Principal.Identity.IsAuthenticated)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Forbidden);
        }
    }
}

Note that asp.net will let you decorate your MVC controller with an API filter - it just won't work the way you expect, so keep your attribute names explicit.

Brian Vander Plaats
  • 2,257
  • 24
  • 28
4

If you have a controller and don't want to have a url in you code you can redirect this way as well. It will not change the url in the address bar of the browser so the user will never see the url for the unauthorized page. This was written in MVC 3. This method will also work if you want to redirect them to a login page or if you want to redirect them to a page to just tell them they aren't authorized. I had section in the program that some user didn't have rights to but they were logged in so this is what I used.

public class AuthorizedRedirect : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        bool isAuthorized = base.AuthorizeCore(httpContext);
        return isAuthorized;
    }
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
    filterContext.RequestContext.RouteData.Values["controller"] = "error";
    filterContext.Result = new ViewResult { ViewName = "unauthorized" };
}
Ole Albers
  • 8,715
  • 10
  • 73
  • 166
divide_byzero
  • 790
  • 2
  • 9
  • 24
  • As another variation to show the message in a Shared View, e.g. `var vr = new ViewResult(); vr.ViewName = "Info"; vr.ViewBag.Message = "You are unauthorized for this page; contact us. "; filterContext.Result = vr;` – subsci Jan 29 '15 at 00:59
2

And an even simpler version that utilizes FormsAuthentication settings. For those not familiar with Contract, Contract.Requires is a .NET 4 addition. Pros and cons to using Code Contracts.

public class RequiresAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        Contract.Requires(filterContext != null);

        HttpContextBase context = filterContext.RequestContext.HttpContext;

        if (context.User.Identity.IsAuthenticated)
        {
            // user does not possess the required role permission
            string url = context.GetCustomErrorUrl(401);
            context.Response.Redirect(url);
        }
        else
        {

            // redirect the user to the login page
            string extraQueryString  = context.Request.RawUrl;
            FormsAuthentication.RedirectToLoginPage(extraQueryString);
        }
    }
}
n00begon
  • 3,503
  • 3
  • 29
  • 42
mousedoc
  • 21
  • 1
1

Going further from divide_byzero's answer even if you don't have a controller you can still use the HandleUnauthorizedRequest to change the redirect.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AuthoriseRedirect : AuthorizeAttribute
    {
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.RequestContext.HttpContext.Response.Redirect("UrlToRedirectTo");
        }
    }

Comes in handy if you have a legacy webforms site that you will be converting to MVC over a longer period of time.....!

Bindi
  • 41
  • 1
  • 6
0

I like what Brian Vander Plaats posted, just added few improvements:

/// <summary>
/// Authorize or redirect to an unauthorized MVC action if the user does not have the required roles
/// (an unauthenticated user will be redirected to the defualt sign in action)
/// <para>Decorate an action or a controller like this [AuthorizeAndRedirect(Roles = "RoleName")]</para>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AuthorizeOrRedirectAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        base.HandleUnauthorizedRequest(filterContext);

        if (filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
        {
            var routeData = new RouteData();
            routeData.Values.Add("controller", "Error");
            routeData.Values.Add("action", "Unauthorized");
            filterContext.Result = new RedirectToRouteResult(routeData.Values);
        }
    }
}

/// <summary>
/// Authorize or redirect to an unauthorized API action if the user does not have the required roles
/// (an unauthenticated user will be redirected to the defualt sign in action)
/// <para>Decorate an action or a controller like this [AuthorizeAndRedirect(Roles = "RoleName")]</para>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AuthorizeOrRedirectApiFilterAttribute : System.Web.Http.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        base.HandleUnauthorizedRequest(actionContext);

        if (actionContext.RequestContext.Principal.Identity.IsAuthenticated)
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }
}
Yovav
  • 2,557
  • 2
  • 32
  • 53