IdentityServer Authentication with an MVC Client & Unauthorized Loops

I’ve learned a lot about authentication this past month. My team has been working on migrating an application from using Forms Auth to IdentityServer single sign-on. Our goal is to provide a centralized authentication for all parts of the system.

This process took several steps, including:

    • Migrating user accounts over to IdentityServer
    • Syncing account creation/management across the application and IdentityServer
    • Authenticating the user with IdentityServer

Once IdentityServer is set up, the client needs to be able to communicate and authenticate the user with IdentityServer. In the case that the user is not logged in and navigates to a protected view or action–one that they must be logged in to access–we want the user to be re-directed to IdentityServer and given the option to sign in with their IdentityServer credentials.

Any controller action that requires the user to sign in (i.e. is protected) should have [Authorize] above it. Once everything is hooked up, having [Authorize] on a controller action will re-direct the user to IdentityServer and trigger the authentication handshake.

For example, in the client application we are updating, the user should only be able to access the dashboard page once they are logged in. To protect the dashboard page, we put [Authorize] above the Dashboard get method.


//GET: /Home/Dashboard
[Authorized(Menu = "Home/Dashboard")]
public ActionResult Dashboard(int? id)
{
//code to configure dashboard view
}

So, when a user has not yet signed in but tries to view the dashboard, they will receive an unauthorized response. Next, we need to add OpenIdConnect middleware that will re-direct the user to IdentityServer on receiving this unauthorized response.

To do this, we use the OpenIdConnectAuthentication standard (OpenId Connect is a layer on top of the OAuth2 protocol, and you can learn more about OAuth from my colleague). OpenIDConnect provides a framework to communicate between the relying party (in our case, the client) and the identity provider (in our case, IdentityServer).

To enable our MVC client to use OpenIdConnectAuthentication, we installed the Microsoft.Owin.Security.OpenIdConnect package. We also installed the Microsoft.Owin.Security.Cookies package for cookie authentication, which I’ll get to later.

Since we are using OWIN, the Startup class is the entry point of the application and the place to add the middleware. Thus, thanks to our Microsoft.Owin.Security.OpenIdConnect package, we are essentially plugging in OpenIdConnect authentication as middleware.

The middleware will add the necessary headings etc. to our IdentityServer requests, allowing the client to talk to IdentityServer using the OpenIdConnect protocol without too much work on our end. This middleware is also responsible for re-directing any unauthorized response to the identity provider.  So, if a user attempts to access one of those actions marked [Authorize], and is not authorized  (i.e. does not have a cookie set), the user will be redirected to IdentityServer.

For a model of how to configure using OpenIdConnect authentication in MVC, see the IdentityServer client configuration GitHub example.

It is important that the client ID, client secret, and redirect url match the ones in IdentityServer.  The authority should be set to the URL that IdentityServer is running on.

Set the Cookie

We use cookie authentication to track whether or not the user is authenticated (if the user has a correct cookie in the browser, they are authenticated).

Add the cookie authentication to the startup file. For example:


app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieDomain = “localhost”
});

This cookie middleware is then invoked indirectly once the user’s credentials have been validated (see OWIN cookie authentication). The cookie will be stored in the user’s browser once the user is signed in.

Overview of Current Workflow

The MVC client is now set up to redirect a user to IdentityServer, sign in the user with their IdentityServer credentials, and store the user information as a cookie in the browser.

It helps me to think of the user as a traveler and their home country as the identity provider. For instance, say the user is Lydia and she is going to Turkey. Then Lydia is relying on America (the identity provider) to provide her identity. The step-by-step process for Lydia’s admittance to Turkey would be:

      • Lydia is in Turkey, but she doesn’t have her passport.
        The user is not authorized to view page.
      • Lydia gets sent back to America to get her passport.
        User gets directed to IdentityServer.
      • Lydia gives the American consulate her birth certificate and other important documents and gets a passport.
        User logs in via IdentityServer.
      • This time, Lydia carries her passport back to Turkey with her.
        IdentityServer sets cookie in client browser.
      • The Turkish government accepts Lydia’s passport as valid form of identification, so Lydia can now go visit her parents in Turkey.
        User now has cookie from IdentityServer proving authentication, so the user can now view the page.

Unauthorized Loops

What if the user is logged in, but not authorized to view a specific page?

The above workflow is awesome, as long as the client application verifies that the logged in user can view the page. What if, however, the user has been authenticated via IdentityServer, but the client application does not authorize the authenticated user to view the page?

Or, going back to our Turkey example, what if Lydia went all the way back to America to get her passport and then the Turkish government decided that, even though they knew who she was, she was not allowed into the country?

Worse yet, since the client always redirects an unauthorized user to IdentityServer, the user might get caught in an infinite authorization loop (the client says the user is not authorized, the unauthorized user is re-directed to IdentityServer, IdentityServer sends the user back to the client, the user is still not authorized to view the page so gets directed back to IdentityServer…).

This would be like poor Lydia going to Turkey, Turkey saying that she wasn’t allowed to visit, Lydia going back to the U.S. to check to make sure the problem wasn’t with her passport, then returning back to Turkey, then getting sent back to the U.S. to check her passport again…Eventually, Lydia would end up crying on a plane, which doesn’t seem like a good thing.

Instead of the default behavior which re-directs all unauthorized users to IdentityServer, we want our client to only re-direct the unauthenticated users but show the unauthorized users (users who are logged in, but don’t have permission to view the page) an “unauthorized” page. Then, the client and IdentityServer would not get stuck in an infinite loop.

To do this, my team created a custom Authorize class implementing System.Web.Mvc.AuthorizeAttribute.  Because of the installed OpenIdConnect package, by default, the ‘HandleUnauthorizedRequest’ method in Authorize Attribute re-directs an unauthorized user to the identity provider. To change the default behavior, I overrode Authorize’s ‘HandleUnauthorizedRequest’ method with a new method. This new approach intercepted the requests where the user was authenticated but unauthorized, and re-directed the user.


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

//Intercept results where person is authenticated but still doesn't have permissions
if (filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult(ConfigSettings.KPToolsURL + "/Error/HttpError401");
return;
}

base.HandleUnauthorizedRequest(filterContext);
}

Now, the user can login via IdentityServer and, even if the client does not allow the user to access a specific page, the user will not get caught in an infinite unauthorized loop. And, Lydia won’t get stuck crying on a plane.