SPA with cookie authentication in Asp.net Core
By: Marcos De Aguiar
Tags: Dotnet Core, Web Security, Jwt Token, Aspnetcore, Authentication

Asp.net core is a great framework for web applications, and a big improvement in architecture design in comparison to the Asp.net MVC. But when I started to work with it, I quickly realized that it’s very opinionated about the way some things should be. These opinions are also reflected on the many tutorials and blog posts about the framework.

If you want to do things a bit differently, you’re kind of out of luck getting help. One of those opinions, even though not explicit, is to use Authorization header token authentication, such as JWT for Single Page Applications (SPA). I personally had a hard time finding a tutorial or documentation that described how to do a SPA with cookie authentication in Asp.net core.

I won’t dive into the debate whether this is a good idea or not, there are many blog posts and videos about this discussion already. What I’ll do in this article is to present how I was able to implement cookie authentication in SPA using Asp.net core SPA template, and the gotchas I found along the way. It’s also a great opportunity to hack a little bit and see how some internal parts of the framework works.

The Design:

When I started to develop software, authentication and authorization were always a big topic of discussion. The main point is that there is not a one size fit all solution, and the strategy hangs on the requirements of the application.

The design I’ll present in this article is particular to this application and doesn’t mean it’s the best or a silver bullet solution.

There are a couple of ways to go about this. One is to scrap everything and do your own middlewares and filters, and do all this code. Another is to try to use what the framework offers as much as possible, customizing only the relevant parts. I’m taking the later approach.

The concept is quite simple, when the user authenticates, I create a JWT token and send it as “Http Only” cookie, so XSS scripts cannot capture it.

I’ll not present how to create the claims and generate the JWT cookie since there are many tutorials about it online. The main point is that the JWT is being returned as a “Http Only” cookie and not for the client application to control directly.

For the authentication scheme we use the cookie authentication instead of the standard JWT authentication.

And now is the part where Asp.net core has an opinion on it. I didn’t find a ready to use solution to decode the JWT in a cookie, only the default that decodes it in the header. So I created a custom middleware that all it does is to check if the JWT is still valid and fills the HttpContext User (ClaimsPrincipal) with the data in the JWT.

CSRF Protection:

As expected with this authentication cookie approach, it brings the Cross Site Request Forgery (CSRF) threat. And that’s where another gotcha is in Asp.net core framework. The CSRF protection has 2 elements. A “Http Only” cookie with the CSRF token that stays for the session, and a request token that must be sent together on the request, either in the form body, or in a header. When using the MVC approach, it’s easier to just put the token as a hidden field in the form, but in SPA, normally the POST requests are sent in json format so the CSRF request token must be provided in the header.

The gotcha here is, that the CSRF request token is calculated taking into consideration the CSRF cookie and the claims in the HttpContext User ClaimsPrincipal. If any claim is different for the CSRF request token, it won’t work. This is one of the reasons we remove the “iat”, “exp” and “nbf” from the claims when setting the ClaimsPrincipal, because if we refresh (not covered in this article) the token, we don’t have to re generate the CSRF request token, or have it fail in the current request.

So in a SPA everytime the user authenticates a new request token must be passed back to the client via a cookie that is not “Http Only”, so the client can access it and put on the header. When the user logs out, the CSRF request to the anonymous user must be sent back to the client as well. We can see this in the logout listing below.

As we can see in the middleware code, when the JWT token expires, we send back a CSRF request token for the anonymous user (as with the logout action), so the client can work again with the login endpoint and so on.

CSRF for unsecure actions:

Everything works fine now, but there is one scenario that this approach may generate an unexpected error. If the user is authenticated, and makes a POST for an action that doesn’t require authentication, and the token is expired. That request will fail because the token sent is for a certain user (specific set of claims), and now the anonymous user is the one in the ClaimsPrincipal. Note that because the middleware updates the cookie when the JWT token expires, the next request will work.

But is that the only way to get around it? Well, there are others. One is not to require CSRF protection to this endpoint anyway. In your application design, any insecure action should not make any changes that will use the user authentication. That being done, there is no gain for someone to trick your user with a CSRF malicious page, because they can do it themselves.

And this is a big question in general whether to use CSRF for any action that doesn’t require authentication. The login page for instance, wouldn’t necessarily need CSRF since the user is not authenticated anyway.

But in my approach I do use CSRF in unsecure actions just to be on the safe side, especially on the login page. So in order to have the CSRF token when the user loads your SPA, you also need a middleware to send this request token, as shown in the listing below.

This middleware should seat just before the app.UseSpa() middleware, because the the request passes through the previous middlewares, it’s because it didn’t match any file or action, so this UseSpa middleware will send your Index.html with the SPA code to be loaded to the user. The JWT Cookie middleware should be used where you would normally use the Asp.net core default authentication middleware.

Conclusions:

As we can see, it’s not a whole lot of code to make it work. But researching the information and putting it together took longer than I thought, specially in the part that says that the CSRF request token is checked based on the claims set in the HttpContext User.

What is presented here is not the complete solution of course, but the building blocks for someone who wants to create a custom authentication solution in Asp.net core.