Are you having problems with redirect loops in your MVC app? Maybe you are using ADFS or another identity server/security token service, if so read on.
In ASP.NET, whatever the authentication mechanism being used (FormsAuth, CookieAuthentication Middleware, ADFS or any other identity provider) the 401 http status code is always the starting point of the authentication process.
When you annotate a controller action with the AuthorizeAttribute what happens behind the covers is a check to see if a user is authenticated, and when the user is not, the http response status code is set to 401 Unauthorized (if you are unsure about what the AuthorizeAttribute is doing check this)
The user never actually sees that response though. The authentication mechanism (they all do this) will look for a response with that status code, before it is sent to the client, and change it to a 302 Redirect to a login page.
The redirect loop problem happens when you have an authenticated user without the required privileges. For example, if you are using roles and you annotate a controller action with the authorize attribute and specify the role “Admin”.
[Authorize(Roles="Admin")]
public ActionResult Users()
{
...
}
If you go the url handled by that action (e.g. http://localhost/Account/Users
) the Authorize attribute will check if the current user is authenticated and has the role “Admin”.
There are three possible scenarios in this situation
- The user is not authenticated
- The user is authenticated but does not have the Admin role
- The user is authenticated and has the Admin role
When the user is not authenticated (1) the AuthorizeAttribute will change the response to 401.
When the user is authenticated but does not have the Admin role (2) the authorize attribute will also change the response to 401. It is only when the user is authenticated and has the Admin role (3) that the authorize attribute won’t change the response.
If you are using FormsAuthentication or the OWIN Cookie Authentication Middleware and the user is already logged in (scenarios 1 and 2), he will be redirected to the login page again, which is kind of weird if you thing about it. “I’ve already logged in, and now I’m back do the log in page just because I clicked some link, and no one told me why this just happened.”
This problem becomes a redirect loop when you are using an identity provider (aka identity server, security token service, etc), for example ADFS or Identity Server.
The way it becomes a redirect loop has to do with the single sign-on feature that identity servers enable.
When you use an identity server, you are delegating the responsibility of authenticating the user to the identity server. So, instead of the 401 being transformed into a redirect to your login page, it will be transformed to a redirect to the identity server. After the user logs in the identity server, s/he is redirected back to your web application.
The user information is sent in the redirect response’s query string from the identity server to your web app as an encrypted token (e.g. /yourwebapp?accessToken=…). As a developer you don’t have to manually deal with this, for example if you use ADFS this is all done for you by a pair of http modules.
There’s a step in the middle of all this. A cookie is issued to the users by the identity server so that the user does not have to provide his credentials again (until the cookie expires).
This enables a user of two different web applications, that share the same identity server, to only enter his credentials once. For example, when you login to Outlook.com and then go to Onedrive.com you don’t have to enter your email and password again.
In this example, when the user goes to Outlook.com for the first time he’s redirected to Microsoft’s identity server (login.live.com). The user enters his email and password and is redirected back to Outlook.com. When the user now goes to Onedrive.com, he’ll also be redirected to login.live.com, but he’ll be immediately redirected to Onedrive.com (with the access token sent in the redirect response) because a cookie was stored for login.live.com the first time he logged in.
Do you see where the redirect loop happens yet? It happens because the default behaviour when using the Authorize attribute in ASP.NET is to issue a 401 when the user is not authorized (even if the user is authenticated). When the user is authenticated and is redirected to the identity provider, the identity provider redirects the user back to the url it came from, which will then cause a redirect of the user back to the identity provider…
The best way to solve this is to use the extensibility points in the AuthorizeAttribute to redefine its behaviour.
Custom AuthorizeAttribute
AuthorizeAttribute provides a protected virtual method named HandleUnauthorizedRequest
that you can override. In it you can test if the user is authenticated and if so (this will definitely be the case of a user being authenticated with insufficient permissions), use a different response code.
You could return instead 403 Forbidden. Although the spec is not very clear about when to use this status code, it feels like a good fit.
using System.Net;
using System.Web.Mvc;
namespace YouAppNamespace
{
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden);
}
else
{
base.HandleUnauthorizedRequest(filterContext);
}
}
}
}
This might not be what you want though, you might just redirect the user to a page that explains that he does not have sufficient privileges to perform the action that he just tried to perform. If this is the case, just use the RedirectToRouteResult
(e.g. new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary(new { action = "NotAuthorized", controller = "Error" }));
).
There you go. If this was helpful don’t forget to subscribe!