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

asp.net MvcAuthenticationAuthorization

asp.net Mvc Problem Overview


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?

asp.net Mvc Solutions


Solution 1 - asp.net Mvc

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);
        }
    }
}

Solution 2 - asp.net Mvc

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.

Solution 3 - asp.net Mvc

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.

Solution 4 - asp.net Mvc

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.

Solution 5 - asp.net Mvc

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");
}
    

Solution 6 - asp.net Mvc

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;
			}
		}
	}
}

Solution 7 - asp.net Mvc

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" });
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRoger LipscombeView Question on Stackoverflow
Solution 1 - asp.net MvcShadowChaserView Answer on Stackoverflow
Solution 2 - asp.net MvcAlan JacksonView Answer on Stackoverflow
Solution 3 - asp.net MvcKeltexView Answer on Stackoverflow
Solution 4 - asp.net MvcRobView Answer on Stackoverflow
Solution 5 - asp.net MvcKareem CambridgeView Answer on Stackoverflow
Solution 6 - asp.net MvcGreg GumView Answer on Stackoverflow
Solution 7 - asp.net MvcCésar LeónView Answer on Stackoverflow