localStorage
Types of tokens
- Access tokens are usually short-lived JWTs signed by the server. They are included in every HTTP request a client makes to a server. Tokens are used to authorize requests.
- Refresh tokens are usually long-lived tokens stored in a database and used to obtain a new access token when the previous token expires.
Where exactly should the tokens be stored on the client?
There are 2 common ways to store tokens on the client: local browser storage and cookies. There is a lot of debate about which method is better. Most people lean towards cookies because of their better security.
Let's compare local storage and cookies. Our comparison is based primarily on this material and on the comments to it.
Local storage
▍Advantages
The main advantage of local storage is that it is convenient to use.
- Working with local storage is very convenient, pure JavaScript is used here. If your application does not have a backend and you rely on third-party APIs, you may not always be able to request these APIs to set specific cookies for your site.
- Using local storage, it is convenient to work with APIs that require placing an access token in the request header. For example - as follows:
Authorization Bearer ${access_token}
.
▍Disadvantages
The main disadvantage of local storage is its vulnerability to XSS attacks.
- When performing an XSS attack, an attacker can run their JavaScript code on your site. This means that an attacker can gain access to the access token stored in
localStorage
. - The source of the XSS attack can be third-party JavaScript code included in your site. It could be something like React, Vue, jQuery, Google Analytics script, and so on. In modern conditions, it is almost impossible to develop a site that does not include third-party libraries.
Cookies
▍Advantages
The main advantage of cookies is that they are not accessible from JavaScript. As a result, they are not as vulnerable to XSS attacks as local storage.
- If you use a flag
HttpOnly
and secure cookies, it means that JavaScript cannot access these files. That is, even if an attacker can run his code on your page, he will not be able to read the access token from the cookie. - Cookies are automatically sent in every HTTP request to the server.
▍Disadvantages
Depending on the specific circumstances, it may happen that the tokens in the cookies cannot be stored.
- The size of the cookies is limited to 4 KB. Therefore, if you use large JWTs, storing them in cookies will not work for you.
- There are scenarios where you cannot pass cookies to your API server. It is also possible that some API requires placing a token in the header
Authorization
. In this case, you will not be able to store tokens in cookies.
XSS attacks
Local storage is vulnerable to XSS attacks because it is very easy to work with using JavaScript. Therefore, an attacker can gain access to the token and use it to their advantage. However, although HttpOnly cookies are not reachable from JavaScript, this does not mean that you are protected from XSS attacks by using cookies to steal an access token.
If an attacker can run his JS code in your application, this means that he can simply send a request to your server, and the token will be included in this request automatically. Such a scheme of work is simply not so convenient for the attacker, since he cannot read the contents of the token. But attackers rarely need this. In addition, with this scheme of work, it may be more profitable for an attacker to attack the server using the victim's computer, rather than his own.
Cookies and CSRF attacks
CSRF attacks are attacks in which a user is somehow coerced into making a special request. For example, the site accepts requests to change the email address:
POST /email/change HTTP/1.1
Host: site.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
Cookie: session=abcdefghijklmnopqrstu
email=myemail.example.com
In such a situation, an attacker can create a form with a hidden field for entering an email address that sends a POST request to
https://site.com/email/change
. In this case, session cookies will automatically be included in such a request.
However, this threat can be easily protected by using the attribute
SameSite
in the response header and anti-CSRF tokens.
Subtotals
Although cookies are not completely immune to attacks, the best way to store tokens is, whenever possible, to choose them over
localStorage
. Why?
- Both local storage and cookies are vulnerable to XSS attacks, but it will be more difficult for an attacker to attack if HttpOnly cookies are used.
- Cookies are vulnerable to CSRF attacks, but the risk of such attacks can be mitigated by using the attribute
SameSite
and anti-CSRF tokens.
Cookies can be used even when you need to use a header
Authorization: Bearer
or when the JWT is larger than 4KB. This is also consistent with the OWASP guidelines : “Do not store session IDs in local storage, as the corresponding data is always available from JavaScript. Cookies can help mitigate the risk with HttpOnly
. "
Using cookies to store OAuth 2.0 tokens
Let's briefly list the ways to store tokens:
- Method 1: storing tokens in local storage. This method is susceptible to XSS attacks.
- Method 2: storing tokens in HttpOnly cookies. This method is susceptible to CSRF attacks, but the risk of such attacks can be mitigated. This token storage option is slightly better protected from XSS attacks than the first.
- Method 3: store refresh tokens in HttpOnly cookies and access tokens in memory. This way of storing tokens is safer in terms of CSRF attacks and slightly better protected against XSS attacks.
Below we will take a closer look at the third method of storing tokens, since it, of the three listed, looks the most interesting.
Why is storing the refresh token in an HttpOnly cookie safer in terms of CSRF attacks?
An attacker could create a form that accesses
/refresh_token
. A new access token is returned in response to this request. But the attacker cannot read the response if he uses an HTML form. In order to prevent an attacker from successfully executing fetch or AJAX requests and reading responses, the authorization server's CORS policy must be configured correctly, namely, so that the server does not respond to requests from unauthorized websites.
How do you set it up?
Step 1: return access token and refresh token when authenticating user
After the user authenticates, the authentication server returns
access_token
(access token) and refresh_token
(refresh token). The access token will be included in the response body and the refresh token in the cookie.
Here's what you need to use to set up cookies for storing refresh tokens:
- Flag
HttpOnly
- to prevent JavaScript from reading the token. - A flag
secure=true
that will cause data to be transmitted only over HTTPS. - The flag
SameSite=strict
should be used whenever possible to protect against CSRF attacks. This approach can only be used if the authorization server belongs to the same site as the system frontend. If this is not the case, then the authorization server must set CORS headers on the backend, or use other methods to ensure that a request with a refresh token can only be made by an authorized website.
Step 2: store the access token in memory
Storing the access token in memory means that the token, in the frontend code, is written to a variable. This, of course, means that the token will be lost if the user closes the tab where the site is open or refreshes the page. This is why we have a refresh token.
Step 3: getting a new access token using the refresh token
If the access token is lost or invalid, you need to contact the endpoint
/refresh_token
. In this case, the refresh token, which, in step 1, was saved in the cookie, will be included in the request. You will then receive a new access token that you can use to make API requests.
All this means that JWTs can be larger than 4KB, and that they can be placed in the header
Authorization
.
Outcome
What we've covered here should give you some basic information about storing JWTs on the client and how to make your project more secure.
How do you store JWT on the client?