I have been trying to model a “best of both worlds” approach to API Authentication specific to SPAs. These types of applications run entirely in the clients browser and use asynchronous API calls to pull in the data for each page or action. This is great for the user experience because things appear to be faster and the page never has to reload.
However, it does become a logistical nightmare when handling API keys. A common approach to this scenario is using an OAuth server to authenticate and issue tokens. There are many different types of tokens, but most common are the Access Token/Refesh Token model. This is called a Password Grant type in the OAuth world.
The idea is when a user authenticates to your App they will receive these two tokens in the response. The Refresh Token is long lived and is used to get another Access Token when it expires. The Access Token is actually what grants access to the application and needs to be passed with every API call.
What this also means for the frontend developer is they need to store the Refresh/Access tokens in long-lived storage. In a traditional website, there is a persistent session that is kept on the server that knows if the user is authenticated. But APIs are supposed to be stateless and it becomes a pain to try and use sessions in this manner. So with the SPA, when a user closes their tab or browser they will expect to be authenticated still. That is why these tokens need to be persistent.
Two choices for storing these tokens are in a cookie or local storage. My requirements for storing these keys are:
- They can’t be accessible by scripts.
- They can’t be recovered by the local computer or browser being compromised.
What could we possibly do to fulfill these requirements…
Http-Only cookies to the rescue! 🍪
An Http-Only cookie is one that is returned from your API and can’t be read by scripts! So when you first authenticate to your App, instead of returning the tokens, you return the cookie. Cookies have a Url field and most client libraries (i.e. Axios, Apollo) will pass any cookies that match the Url you are sending your request to. You can also set the lifetime on these cookies to limit the logon session. To log a user out, you simply delete the cookie.
At this point we don’t need to use the Password Grant Auth flow since the SPA doesn’t have access to this cookie. We can opt for a single access token. These are normally referred to as API keys or Personal Access Tokens. In the OAuth world it is known as an Implicit Grant Auth flow. Whatever you call it, it is going to greatly simplify things.
There are some security concerns using this type of Auth Flow due to these tokens being long-lived and providing full access to your resources. However, you can change the lifetime of the tokens to offset the security concerns.
So passing this token in our Http-Only cookie is great in all, but it still has to be inaccessible if the computer is compromised…
Encrypt those Cookies!
Nearly every backend framework comes with the ability to encrypt your client side cookies. Normally this uses an App Key environment variable that is stored server side to encrypt your data. This allows you to save the cookie in the browser with confidence.
Ensuring Compatibility with OAuth Implementions
Some OAuth implementations don’t support accepting cookies out of the box. They almost always support using the Authorization header of an Http request to pass the token. You can always create a Middleware to extract the token(after it is decrypted) and add an Authorization header on the inbound request. This will ensure that your OAuth server will handle the request without any modifications needed on the OAuth server.
Of course, you will need to check if there is an Authorization header already present. This way you don’t disturb other API calls outside your application.
CSRF Protection and JSON Web Tokens (bonus)
To provide more security, we can encode a JWT with a CSRF token and the API key. This will ensure that the requests are actually coming from your application. OAuth servers should support returning JWTs out of the box. Instead of returning the actual API key in your cookie you would instead return the JWT. The same applies extracting the JWT through Middleware.
That is it! To recap:
- We completely removed the need to handle tokens on the Frontend.
- We are storing the token in a secure fashion
- We can optionally preventing CSF attacks
And that is Simplicity without compromising Security!
If you want to see a full demo on how to implement everything here. Enjoy!