77

I have a controller in ASP.NET MVC that I've restricted to the admin role:

[Authorize(Roles = "Admin")]
public class TestController : Controller
{
   ...

If a user who is not in the Admin role navigates to this controller they are greeted with a blank screen.

What I would like to do is redirect them to View that says "you need to be in the Admin role to be able to access this resource."

One way of doing this that I've thought of is to have a check in each action method on IsUserInRole() and if not in role then return this informational view. However, I'd have to put that in each Action which breaks the DRY principal and is obviously cumbersome to maintain.

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
Guy
  • 65,082
  • 97
  • 254
  • 325

8 Answers8

71

Create a custom authorization attribute based on AuthorizeAttribute and override OnAuthorization to perform the check how you want it done. Normally, AuthorizeAttribute will set the filter result to HttpUnauthorizedResult if the authorization check fails. You could have it set it to a ViewResult (of your Error view) instead.

EDIT: I have a couple of blog posts that go into more detail:

Example:

    [AttributeUsage( AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false )]
    public class MasterEventAuthorizationAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// The name of the master page or view to use when rendering the view on authorization failure.  Default
        /// is null, indicating to use the master page of the specified view.
        /// </summary>
        public virtual string MasterName { get; set; }

        /// <summary>
        /// The name of the view to render on authorization failure.  Default is "Error".
        /// </summary>
        public virtual string ViewName { get; set; }

        public MasterEventAuthorizationAttribute()
            : base()
        {
            this.ViewName = "Error";
        }

        protected void CacheValidateHandler( HttpContext context, object data, ref HttpValidationStatus validationStatus )
        {
            validationStatus = OnCacheAuthorization( new HttpContextWrapper( context ) );
        }

        public override void OnAuthorization( AuthorizationContext filterContext )
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException( "filterContext" );
            }

            if (AuthorizeCore( filterContext.HttpContext ))
            {
                SetCachePolicy( filterContext );
            }
            else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
                // auth failed, redirect to login page
                filterContext.Result = new HttpUnauthorizedResult();
            }
            else if (filterContext.HttpContext.User.IsInRole( "SuperUser" ))
            {
                // is authenticated and is in the SuperUser role
                SetCachePolicy( filterContext );
            }
            else
            {
                ViewDataDictionary viewData = new ViewDataDictionary();
                viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
                filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
            }

        }

        protected void SetCachePolicy( AuthorizationContext filterContext )
        {
            // ** IMPORTANT **
            // Since we're performing authorization at the action level, the authorization code runs
            // after the output caching module. In the worst case this could allow an authorized user
            // to cause the page to be cached, then an unauthorized user would later be served the
            // cached page. We work around this by telling proxies not to cache the sensitive page,
            // then we hook our custom authorization code into the caching mechanism so that we have
            // the final say on whether a page should be served from the cache.
            HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
            cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
            cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
        }


    }
tvanfosson
  • 524,688
  • 99
  • 697
  • 795
  • 1
    I don't suppose there's a link I can go to that breaks this down into a little easier to follow reasonings? – Maslow Jan 18 '10 at 02:12
  • 1
    What's not clear? It first uses AuthorizeCore to check if the user is authorized and in an allowed role. If not, the if the user isn't authenticated it returns an Unauthorized response by setting the result on the filter's context. If it is authenticated, it then checks if it is in the additional role of "SuperUser" (a default role, not specified in the attribute). If not, it returns an error indicating that while authorized, the user is not in a valid role for the action. When the user is authorized and in a valid role (or SuperUser), it set the cache policy to prevent downstream caching – tvanfosson Jan 18 '10 at 14:10
  • I found a better answer here: http://stackoverflow.com/questions/1498727/asp-net-mvc-how-to-show-unauthorized-error-on-login-page – bluee Aug 19 '14 at 10:12
  • It is left to mention that with this solution, you will have to "decorate" the class or method you want to control with this attribute: [MasterEventAuthorizationAttribute] – netfed Aug 23 '17 at 07:37
  • @netfed you could also add it as a global attribute, though you'd need to add in handling for AllowAnonymousAttribute (which didn't exist when I wrote this). – tvanfosson Aug 23 '17 at 12:59
28

You can work with the overridable HandleUnauthorizedRequest inside your custom AuthorizeAttribute

Like this:

protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
    // Returns HTTP 401 by default - see HttpUnauthorizedResult.cs.
    filterContext.Result = new RedirectToRouteResult(
    new RouteValueDictionary 
    {
        { "action", "YourActionName" },
        { "controller", "YourControllerName" },
        { "parameterName", "YourParameterValue" }
    });
}

You can also do something like this:

private class RedirectController : Controller
{
    public ActionResult RedirectToSomewhere()
    {
        return RedirectToAction("Action", "Controller");
    }
}

Now you can use it in your HandleUnauthorizedRequest method this way:

filterContext.Result = (new RedirectController()).RedirectToSomewhere();
Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
10

The code by "tvanfosson" was giving me "Error executing Child Request".. I have changed the OnAuthorization like this:

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

        if (!_isAuthorized)
        {
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else if (filterContext.HttpContext.User.IsInRole("Administrator") || filterContext.HttpContext.User.IsInRole("User") ||  filterContext.HttpContext.User.IsInRole("Manager"))
        {
            // is authenticated and is in one of the roles 
            SetCachePolicy(filterContext);
        }
        else
        {
            filterContext.Controller.TempData.Add("RedirectReason", "You are not authorized to access this page.");
            filterContext.Result = new RedirectResult("~/Error");
        }
    }

This works well and I show the TempData on error page. Thanks to "tvanfosson" for the code snippet. I am using windows authentication and _isAuthorized is nothing but HttpContext.User.Identity.IsAuthenticated...

MrBoJangles
  • 12,127
  • 17
  • 61
  • 79
sajoshi
  • 2,733
  • 1
  • 18
  • 22
5

I had the same issue. Rather than figure out the MVC code, I opted for a cheap hack that seems to work. In my Global.asax class:

member x.Application_EndRequest() =
  if x.Response.StatusCode = 401 then 
      let redir = "?redirectUrl=" + Uri.EscapeDataString x.Request.Url.PathAndQuery
      if x.Request.Url.LocalPath.ToLowerInvariant().Contains("admin") then
          x.Response.Redirect("/Login/Admin/" + redir)
      else
          x.Response.Redirect("/Login/Login/" + redir)
MichaelGG
  • 9,976
  • 1
  • 39
  • 82
2

This problem has hounded me for some days now, so on finding the answer that affirmatively works with tvanfosson's answer above, I thought it would be worthwhile to emphasize the core part of the answer, and address some related catch ya's.

The core answer is this, sweet and simple:

filterContext.Result = new HttpUnauthorizedResult();

In my case I inherit from a base controller, so in each controller that inherits from it I override OnAuthorize:

protected override void OnAuthorization(AuthorizationContext filterContext)
{
    base.OnAuthorization(filterContext);
    YourAuth(filterContext); // do your own authorization logic here
}

The problem was that in 'YourAuth', I tried two things that I thought would not only work, but would also immediately terminate the request. Well, that is not how it works. So first, the two things that DO NOT work, unexpectedly:

filterContext.RequestContext.HttpContext.Response.Redirect("/Login"); // doesn't work!
FormsAuthentication.RedirectToLoginPage(); // doesn't work!

Not only do those not work, they don't end the request either. Which means the following:

if (!success) {
    filterContext.Result = new HttpUnauthorizedResult();
}
DoMoreStuffNowThatYouThinkYourAuthorized();

Well, even with the correct answer above, the flow of logic still continues! You will still hit DoMoreStuff... within OnAuthorize. So keep that in mind (DoMore... should be in an else therefore).

But with the correct answer, while OnAuthorize flow of logic continues till the end still, after that you really do get what you expect: a redirect to your login page (if you have one set in Forms auth in your webconfig).

But unexpectedly, 1) Response.Redirect("/Login") does not work: the Action method still gets called, and 2) FormsAuthentication.RedirectToLoginPage(); does the same thing: the Action method still gets called!

Which seems totally wrong to me, particularly with the latter: who would have thought that FormsAuthentication.RedirectToLoginPage does not end the request, or do the equivalant above of what filterContext.Result = new HttpUnauthorizedResult() does?

Nicholas Petersen
  • 9,104
  • 7
  • 59
  • 69
1

Would have left this as a comment but I need more rep, anyways I just wanted to mention to Nicholas Peterson that perhaps passing the second argument to the Redirect call to tell it to end the response would have worked. Not the most graceful way to handle this but it does in fact work.

So

filterContext.RequestContext.HttpContext.Response.Redirect("/Login", true);

instead of

filterContext.RequestContext.HttpContext.Response.Redirect("/Login);

So you'd have this in your controller:

 protected override void OnAuthorization(AuthorizationContext filterContext)
 {
      if(!User.IsInRole("Admin")
      {
          base.OnAuthorization(filterContext);
          filterContext.RequestContext.HttpContext.Response.Redirect("/Login", true);
      }
 }
Lazy Coder
  • 1,157
  • 1
  • 8
  • 18
1

Perhaps you get a blank page when you run from Visual Studio under development server using Windows authentication (previous topic).

If you deploy to IIS you can configure custom error pages for specific status codes, in this case 401. Add httpErrors under system.webServer:

<httpErrors>
  <remove statusCode="401" />
  <error statusCode="401" path="/yourapp/error/unauthorized" responseMode="Redirect" />
</httpErrors>

Then create ErrorController.Unauthorized method and corresponding custom view.

Community
  • 1
  • 1
Mark Meyerovich
  • 193
  • 3
  • 9
1

You should build your own Authorize-filter attribute.

Here's mine to study ;)

Public Class RequiresRoleAttribute : Inherits ActionFilterAttribute
    Private _role As String

    Public Property Role() As String
        Get
            Return Me._role
        End Get
        Set(ByVal value As String)
            Me._role = value
        End Set
    End Property

    Public Overrides Sub OnActionExecuting(ByVal filterContext As System.Web.Mvc.ActionExecutingContext)
        If Not String.IsNullOrEmpty(Me.Role) Then
            If Not filterContext.HttpContext.User.Identity.IsAuthenticated Then
                Dim redirectOnSuccess As String = filterContext.HttpContext.Request.Url.AbsolutePath
                Dim redirectUrl As String = String.Format("?ReturnUrl={0}", redirectOnSuccess)
                Dim loginUrl As String = FormsAuthentication.LoginUrl + redirectUrl

                filterContext.HttpContext.Response.Redirect(loginUrl, True)
            Else
                Dim hasAccess As Boolean = filterContext.HttpContext.User.IsInRole(Me.Role)
                If Not hasAccess Then
                    Throw New UnauthorizedAccessException("You don't have access to this page. Only " & Me.Role & " can view this page.")
                End If
            End If
        Else
            Throw New InvalidOperationException("No Role Specified")
        End If

    End Sub
End Class
Ropstah
  • 17,538
  • 24
  • 120
  • 194