Thanks for visiting my blog! See more about me here: About Me
As I’ve been digging into building apps with ASP.NET 5, I’ve had to get used to some of the new metaphors. Some of these make sense (especially if you’ve used Node before), but some are brand new to me. One of these metaphors I ran into was the idea of Identity notifications.
The problem I was running into was one I thought many people would run into: using Identity (e.g. authentication/authorization) with REST APIs. Here is the scenario:
I was using Cookie Authentication (similar to old ASP.NET’s Forms Authentication) to log in users and to protect certain pages of the site to non-anonymous users. It’s simple to set up and works well.
Then I added controllers that returned JSON instead of HTML (aka an API). At first it worked great. The API couldn’t be reached except if you were logged in. The API was only used on pages that were accessible to only authenticated users. This was important because when data was queried or modified, I was gating it to the logged in user. But what if someone tried to use it without being logged in?
Typically if an authorized user attempts to reach a Controller’s action that is protected by the Authorize attribute, it looks like this:
This is an over-simplification, but it’s important to see how it works. When a request comes in, it gets routed to a controller’s action and if it requires authentication, it stops the request and returns a redirect request to the login page (typically). In ASP.NET 4 and below it is dependent on the controller setting the response’s StatusCode to Unauthorized (401) and then the redirect would happen. In ASP.NET 5 this is different and better. It knows the request is failing so it get’s notified that it requires a redirection to happen and this is all in the CookieAuthentication support so it doesn’t get hijacked if you split your site into different authentication for the API and Site side. But that’s not what I wanted to do.
The problem is that when you call the API and using Cookie Authentication alone, you want to get the Unauthorized in the client-code so you know to handle login your own way (especially in Angular/React/etc. apps). Instead, the response is the redirection and you end up getting the HTML document of the redirected login page. Not a good solution. The status code is OK (200) instead of Unauthorized so the client-code has to write nasty “if HTML then failed login” logic which sucks.
Back in earlier versions of ASP.NET (including Web API), they handled it in a less than graceful way: they checked to see if there was a header that indicated that the request was using XmlHttpRequest object (which JavaScript does). It works but somewhat inelegant.
To be clear, when writing an API, typically you’re not going to protect it with cookie authentication. Typically you want it to be protected by something with more functionality and protection like OAuth2 (or similar). It’s also good to have a better non-web specific authentication scheme since it’s likely to be used in a variety of scenarios (e.g. Mobile Apps)
But my need was specific to teaching ASP.NET 5. I wanted to warn the users to use OAuth2 but since I only have their attention for so long, taking a tangent and teaching OAuth2 was a distraction I couldn’t afford. I had to find a way to make it work that was a teachable moment too. In came notifications.
So I explain all of this to explain this, I like this metaphor that we’re likely to see. Essentially notifications are middleware events. So let’s see what it looks like. Here is the code from my Startup.cs Configure method:
services.Configure<CookieAuthenticationOptions>(opt =>
{
opt.LoginPath = PathString.FromUriComponent("/Auth/Login");
opt.Notifications = new CookieAuthenticationNotifications()
{
OnApplyRedirect = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api") && ctx.Response.StatusCode == 200)
{
ctx.Response.StatusCode = 401;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
};
});
During the configuration of the CookieAuthenticationOptions, you can set a new Notifications object and supply a lambda function for OnApplyRedirect. This is called when a redirect is about to happen. It allows you to take over the operation. In this case, I just check to see if it’s part of the API (luckily in it’s own URI segment) and if the status code wasn’t another type of error. Essentially, this is saying, “a redirect was requested and the status code isn’t for another reason.” I could have checked for the header too, but I thought this was clear enough.
While this isn’t bulletproof, I’m hoping that it’s adequate for an example with enough caveats. More importantly I am hoping that this pattern of supplying middleware notifications is used in more areas as it provides hooks without taking over the entire job of the middleware.