Disclaimer: I am not a security expert. This article is a result of an investigation about authentication and authorization in Single Page Applications.
Requirements for authentication in Browser-Based Apps:
- Logging in MUST authenticate a user;
- Logging in SHOULD authenticate a user in every window;
- A remember me functionality MAY be provided;
- Remember me functionality MUST reauthenticate a user;
- Logging out MUST invalidate the authentication of a user;
- Logging out MUST invalidate remember me functionality if there is any;
- Closing the browser MUST invalidate the authentication of a user;
- Data for identifying a user MUST kept secure.
All of these are covered in regular multi page applications using good old fashioned session cookie.
Requirements for an API endpoint:
- An API endpoint SHOULD be stateless.
However this requirement is not a must but it is recommended to avoid state for scalability. To identifying a user without state we need some kind of token for example JWT or PASETO. For later on I will mainly refer to JWT tokens.
Available storage options on client side:
When a token stored in a cookie a site can be vulnerable against CRSF/XSRF attacks because it is attached to the header
of requests automatically by the browser without the correct
SameSite value. Gladly
SameSite is implemented by
many browsers by now. The
HttpOnly directive makes cookies protected against XSS.
This value can be set only on the backend and not on the frontend. The use of
HttpOnly is de facto now but it is
better to mention it.
Session Storage can be discarded right away as it exists per window/tab which means that a user has to authenticate every one of them. Furthermore, storing a token in Web Storage is discouraged. You can read about it in details on the “Security Risks” section of “Session Management Cheat Sheet” by OWASP. It does not means that it cannot be done the “Token storage on client side” section of “JSON Web Token Cheat Sheet for Java” by OWASP provides a possible workaround but it is easy to implement it incorrectly. The “Local Storage” section of “HTML5 Security Cheat Sheet” by OWASP says the following:
- A single Cross Site Scripting can be used to steal all the data in these objects, so again it’s recommended not to store sensitive information in local storage.
- A single Cross Site Scripting can be used to load malicious data into these objects too, so don’t consider objects in these to be trusted.
There are many tutorials on the web which shows how to create a JWT token after a successful authentication and then store it in web storage and let sanitization take care of protecting against XSS. As well there are some libraries which follow these tutorials. Sanitization is something you shouldn’t trust. People can still find issues in sanitizers not necessarily because the implementation is bad but because the web is complicated and a special char in a browser can cause some unexpected results during render.
The problem with JWT. JWT is a token which has a validity date. This means that no matter where you store it, it will be valid even after the user logs out. The recommendation for resolving this is to keep track of the invalidated tokens which means adding state to the API. In my opinion this is simply not the best way of dealing with the problem. Moreover, when the browser is closed the logout is not automatic. It can be handled with a use of a session cookie which stores a random string and that string is also added to the token and the two can be compared. The absence of that cookie would mean the browser was closed or the client cleared the cookies. In my opinion the problem is that JWT is not meant for using it in browser-based applications.
My recommendation is to use an API gateway and handle the authentication in the gateway. This means using a regular
session cookie in the browser and prevent CSRF/XSRF with
X-CSRF-TOKEN. On the server side a JWT token
can be stored and never exposed to the user. This is not something I came up with IETF contains a draft for “OAuth 2.0
for Browser-Based Apps” originally named draft-parecki-oauth-browser-based-apps
which was then renamed to draft-ietf-oauth-browser-based-apps. In both cases
section 6.2 covers the scenario I described.