Where to save a JWT in a browser-based application and how to use it
As of 2021 things evolved a bit with the introduction of the SameSite: Lax/Strict option for cookie on most nowdays browsers
So to elaborate on João Angelo answer, i would say the most secure way is now:
Store the JWT in a cookie with the following options
- HttpOnly
- Secure
- SameSite: Lax or Strict
This will avoid XSS and CSRF both together
This blog post has excellent side by side comparison of browser storage vs. cookies and tackles every potential attack in each case. https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/
The shorter answer / spoiler: cookies and add xsrf token in the jwt. Detailed explanation in the blog post.
Choosing the storage is more about trade-offs than trying to find a definitive best choice. Let's go through a few options:
Option 1 - Web Storage (localStorage
or sessionStorage
)
Pros
- The browser will not automatically include anything from Web storage into HTTP requests making it not vulnerable to CSRF
- Can only be accessed by Javascript running in the exact same domain that created the data
- Allows to use the most semantically correct approach to pass token authentication credentials in HTTP (the
Authorization
header with aBearer
scheme) - It's very easy to cherry pick the requests that should contain authentication
Cons
- Cannot be accessed by Javascript running in a sub-domain of the one that created the data (a value written by
example.com
cannot be read bysub.example.com
) - ⚠️ Is vulnerable to XSS
- In order to perform authenticated requests you can only use browser/library API's that allow for you to customize the request (pass the token in the
Authorization
header)
Usage
You leverage the browser localStorage
or sessionStorage
API to store and then retrieve the token when performing requests.
localStorage.setItem('token', 'asY-x34SfYPk'); // write
console.log(localStorage.getItem('token')); // read
Option 2 - HTTP-only cookie
Pros
- It's not vulnerable to XSS
- The browser automatically includes the token in any request that meets the cookie specification (domain, path and lifetime)
- The cookie can be created at a top-level domain and used in requests performed by sub-domains
Cons
- ⚠️ It's vulnerable to CSRF
- You need to be aware and always consider the possible usage of the cookies in sub-domains
- Cherry picking the requests that should include the cookie is doable but messier
- You may (still) hit some issues with small differences in how browsers deal with cookies
- ⚠️ If you're not careful you may implement a CSRF mitigation strategy that is vulnerable to XSS
- The server-side needs to validate a cookie for authentication instead of the more appropriate
Authorization
header
Usage
You don't need to do anything client-side as the browser will automatically take care of things for you.
Option 3 - Javascript accessible cookie ignored by server-side
Pros
- It's not vulnerable to CSRF (because it's ignored by the server)
- The cookie can be created at a top-level domain and used in requests performed by sub-domains
- Allows to use the most semantically correct approach to pass token authentication credentials in HTTP (the
Authorization
header with aBearer
scheme) - It's somewhat easy to cherry pick the requests that should contain authentication
Cons
- ⚠️ It's vulnerable to XSS
- If you're not careful with the path where you set the cookie then the cookie is included automatically by the browser in requests which will add unnecessary overhead
- In order to perform authenticated requests you can only use browser/library API's that allow for you to customize the request (pass the token in the
Authorization
header)
Usage
You leverage the browser document.cookie
API to store and then retrieve the token when performing requests. This API is not as fine-grained as the Web storage (you get all the cookies) so you need extra work to parse the information you need.
document.cookie = "token=asY-x34SfYPk"; // write
console.log(document.cookie); // read
Additional Notes
This may seem a weird option, but it does has the nice benefit that you can have storage available to a top-level domain and all sub-domains which is something Web storage won't give you. However, it's more complex to implement.
Conclusion - Final Notes
My recommendation for most common scenarios would be to go with Option 1, mostly because:
- If you create a Web application you need to deal with XSS; always, independently of where you store your tokens
- If you don't use cookie-based authentication CSRF should not even pop up on your radar so it's one less thing to worry about
Also note that the cookie based options are also quite different, for Option 3 cookies are used purely as a storage mechanism so it's almost as if it was an implementation detail of the client-side. However, Option 2 means a more traditional way of dealing with authentication; for a further read on this cookies vs token thing you may find this article interesting: Cookies vs Tokens: The Definitive Guide.
Finally, none of the options mention it, but use of HTTPS is mandatory of course, which would mean cookies should be created appropriately to take that in consideration.
[EDIT] This answer is the accepted one, however the response from João Angelo is way more detailed and should be considered. One remark though and because the security pratices evolved since Nov. 2016, the Option 2 should be implemented in favour of the Option 1.
Look at this web site: https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/
If you want to store them, you should use the localStorage or sessionStorage if available or cookies. You should also use the Authorization header, but instead of Basic scheme, use the Bearer one:
curl -v -X POST -H "Authorization: Bearer YOUR_JWT_HERE"
With JS, you could use the following code:
<script type='text/javascript'>
// define vars
var url = 'https://...';
// ajax call
$.ajax({
url: url,
dataType : 'jsonp',
beforeSend : function(xhr) {
// set header if JWT is set
if ($window.sessionStorage.token) {
xhr.setRequestHeader("Authorization", "Bearer " + $window.sessionStorage.token);
}
},
error : function() {
// error handler
},
success: function(data) {
// success handler
}
});
</script>