Why does AuthorizeAttribute redirect to the login page for authentication and authorization failures?
Asked Answered
R

7

273

In ASP.NET MVC, you can mark up a controller method with AuthorizeAttribute, like this:

[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

This means that, if the currently logged-in user is not in the "CanDeleteTags" role, the controller method will never be called.

Unfortunately, for failures, AuthorizeAttribute returns HttpUnauthorizedResult, which always returns HTTP status code 401. This causes a redirection to the login page.

If the user isn't logged in, this makes perfect sense. However, if the user is already logged in, but isn't in the required role, it's confusing to send them back to the login page.

It seems that AuthorizeAttribute conflates authentication and authorization.

This seems like a bit of an oversight in ASP.NET MVC, or am I missing something?

I've had to cook up a DemandRoleAttribute that separates the two. When the user isn't authenticated, it returns HTTP 401, sending them to the login page. When the user is logged in, but isn't in the required role, it creates a NotAuthorizedResult instead. Currently this redirects to an error page.

Surely I didn't have to do this?

Rodrigues answered 26/10, 2008 at 18:51 Comment(5)
Excellent question and I agree, it should be throwing an HTTP Not Authorized status.Bost
I like your solution, Roger. Even if you don't.Treviso
My Login page has a check to simply redirect the user to the ReturnUrl, if s/he is already autnenticated. So I managed to create an infinite loop of 302 redirects :D woot.Aziza
Check out this.Altimeter
Roger, good article on your solution -- red-gate.com/simple-talk/dotnet/asp-net/… It seems your solution is the only way to do this cleanlyPaske
D
315

When it was first developed, System.Web.Mvc.AuthorizeAttribute was doing the right thing - older revisions of the HTTP specification used status code 401 for both "unauthorized" and "unauthenticated".

From the original specification:

If the request already included Authorization credentials, then the 401 response indicates that authorization has been refused for those credentials.

In fact, you can see the confusion right there - it uses the word "authorization" when it means "authentication". In everyday practice, however, it makes more sense to return a 403 Forbidden when the user is authenticated but not authorized. It's unlikely the user would have a second set of credentials that would give them access - bad user experience all around.

Consider most operating systems - when you attempt to read a file you don't have permission to access, you aren't shown a login screen!

Thankfully, the HTTP specifications were updated (June 2014) to remove the ambiguity.

From "Hyper Text Transport Protocol (HTTP/1.1): Authentication" (RFC 7235):

The 401 (Unauthorized) status code indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.

From "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content" (RFC 7231):

The 403 (Forbidden) status code indicates that the server understood the request but refuses to authorize it.

Interestingly enough, at the time ASP.NET MVC 1 was released the behavior of AuthorizeAttribute was correct. Now, the behavior is incorrect - the HTTP/1.1 specification was fixed.

Rather than attempt to change ASP.NET's login page redirects, it's easier just to fix the problem at the source. You can create a new attribute with the same name (AuthorizeAttribute) in your website's default namespace (this is very important) then the compiler will automatically pick it up instead of MVC's standard one. Of course, you could always give the attribute a new name if you'd rather take that approach.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}
Directly answered 30/4, 2011 at 22:0 Comment(11)
+1 Very good approach. A small suggestion: instead of checking filterContext.HttpContext.User.Identity.IsAuthenticated, you can just check filterContext.HttpContext.Request.IsAuthenticated, which comes with null checks built in. See #1380066Equidistance
> You can create a new attribute with the same name (AuthorizeAttribute) in your website's default namespace then the compiler will automatically pick it up instead of MVC's standard one. This results in an error: The type or namespace 'Authorize' could not be found ( are you missing a directive or an assembly reference?) Both using System.Web.Mvc; and the namespace for my custom AuthorizeAttribute class are referenced in the controller. To solve this I had to use [MyNamepace.Authorize]Luehrmann
@DePeter the spec never says anything about a redirect so why is a redirect a better solution? This alone kills ajax requests without a hack in place to solve it.Leban
That should be logged on MS Connect because it is clearly a behavioural bug. Thanks.Airlee
BTW, why are we redirected to the login page? Why not just output a 401 code and the login page directly within the same request?Osculation
Actually, from my limited experience with limited accounts on Windows and Ubuntu, I remember seeing the window for inputing credentials show up when I tried doing something that I was not allowed to do. But it still is different from what Simple Membership. In those operating systems, you could authorize a single operation by inputing those credentials, but here your authentication will change entirely (simply put you'll relogin as a different user).Lighterage
Incidentally the cast is not necessary, at least now (there is a constructor for HttpStatusCodeResult that takes HttpStatusCode)Thayer
Love the references to protocol standard. I've always wondered why there wasn't a behavior distinction between authn and authz in ASP.NET, so your explanation is spot on. And following written standards is a good thing.Pisistratus
I also added a custom HTTP error rule in web.config to override IIS's behavior for a 403: <system.webServer> <httpErrors errorMode="DetailedLocalOnly"> <remove statusCode="403" /> <error statusCode="403" path="/Error/NotAuthorized" responseMode="ExecuteURL" />Pisistratus
Only problem I have with this approach is it only helps when permissions are simple enough that an AuthorizeAttribute gets the job done. In more complex scenarios where you throw an UnauthorizedException in specific scenarios programmatically such as in the business layer, you need to handle it in a HandleErrorAttribute instead. Not really a criticism, but just unfortunately the framework doesn't provide one all inclusive place to handle authorization failures.Cavesson
I tried this (and this #34942377) and it didn't fix the redirect loop. I've obviously done something wrong but I don't know what. I put a breakpoint in my code and it was never reachedMitigate
A
24

Add this to your Login Page_Load function:

// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

When the user is redirected there but is already logged in, it shows the unauthorized page. If they are not logged in, it falls through and shows the login page.

Astrogeology answered 1/4, 2009 at 13:29 Comment(7)
Page_Load is a webforms mojoCallie
@Callie - then do that in the default ActionMethod for the controller that is called where FormsAuthencation has been setup to call.Bost
This actually works really good though for MVC it should be something like if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized"); where Unauthorized is a defined route name.Pelfrey
So you ask a resource, you get redirected to a login page and you get redirected again to a 403 page? Seems bad to me. I even can't tolerate one redirect at all. IMO this thing is very badly built anyway.Osculation
According to your solution, If you have already logged in and go to Login page by typing the URL ... this would throw you to Unauthorized page. which is not right.Militant
As @Reddy has stated, this is not good if the user bookmarked the login page for example, and he hits that even though he has already authenticated.Breadbasket
I use Kendo UI Controls and the Menu widget use the default behavior of AuthorizeAttribute to show/hide the menu items so using this approach I show a warning to the user "The current user isn't authorized to access this option"Conker
S
4

Unfortunately, you're dealing with the default behavior of ASP.NET forms authentication. There is a workaround (I haven't tried it) discussed here:

http://www.codeproject.com/KB/aspnet/Custon401Page.aspx

(It's not specific to MVC)

I think in most cases the best solution is to restrict access to unauthorized resources prior to the user trying to get there. By removing/graying out the link or button that might take them to this unauthorized page.

It probably would be nice to have an additional parameter on the attribute to specify where to redirect an unauthorized user. But in the meantime, I look at the AuthorizeAttribute as a safety net.

Shylashylock answered 26/10, 2008 at 22:2 Comment(2)
I plan on removing the link based on authorization as well (I saw a question on here about that somewhere), so I'll code an HtmlHelper extension method up later.Rodrigues
I still have to prevent the user from going directly to the URL, which is what this attribute is all about. I'm not too happy with the Custom 401 solution (seems a bit global), so I'll try modelling my NotAuthorizedResult on RedirectToRouteResult...Rodrigues
C
4

I always thought this did make sense. If you're logged in and you try to hit a page that requires a role you don't have, you get forwarded to the login screen asking you to log in with a user who does have the role.

You might add logic to the login page that checks to see if the user is already authenticated. You could add a friendly message that explains why they've been bumbed back there again.

Cle answered 27/10, 2008 at 16:28 Comment(2)
It's my feeling that most people don't tend to have more than one identity for a given web app. If they do, then they're smart enough to think "my current ID doesn't have mojo, I'll log back in as the other one".Rodrigues
Although your other point about displaying something on the login page is a good one. Thanks.Rodrigues
C
0

Try this in your in the Application_EndRequest handler of your Global.ascx file

if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}
Culicid answered 8/1, 2015 at 21:44 Comment(0)
L
0

If your using aspnetcore 2.0, use this:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}
Lexine answered 14/2, 2018 at 23:5 Comment(0)
C
0

In my case the problem was "HTTP specification used status code 401 for both "unauthorized" and "unauthenticated"". As ShadowChaser said.

This solution works for me:

if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}
Creature answered 18/3, 2019 at 18:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.