After working with JWT more in-depth for the past few months, I realized most of the learning materials are of poor quality.
Today, I want to make it clear how JWT should be used in your authentication flow, what are its security vulnerabilities, and how to avoid them.
What is a JWT
From its introduction page, we learn the following:
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object
Content
In practice, a JWT is a string that looks like xxxxx.yyyyy.zzzzz
, where the sections are respectively the header (xxxxx
), payload (yyyyy
), and signature (zzzzz
).
Header
The header is a JSON object, which typically defines the algorithm used, and the type of the token JWT. Its encoded in base64 to be used as a string.
Payload
The payload is also a JSON object, where you define the user information. its also encoded in base64.
Signature
The signature is generated based on the header and payload, using an algorithm (such as HMAC SHA256) and your secret.
Result
In the end, you generate a JWT, which anyone can read the information of. But you are the only one able to verify a JWT has not been tampered with. Nobody can change a JWT payload and sign it with your own secret.
Processes
Authentication
In the authentication process, a user typically sends his credentials to an API, which tries to find the corresponding account in a database.
If an account is found, a JWT is generated with the user information, such as id, name, or even his roles (admin? user?).
Authorization
Then, a user is able to request a private endpoint, where he needs to be authenticated. This is known as the authorization process:
Because a JWT contains user information (its stateless/self-contained), the API doesnt need to request a database. This is amazing in terms of performance, and even better on distributed architecture.
This is the normal use case of a JWT, if youre making requests to your database during authorization, you defeat the purpose of JWT.
Limitations
On one side, the self-contained aspect of JWT makes it amazing. On the other, because youre not requesting to your database, you cannot invalidate a JWT.
This is an issue for both functionality & security. If a user gets his token stolen, or if you have a role system and someone gets a role removed, he can still use a previous token when he shouldnt be allowed to (while the token is not expired).
This problem is solved by limiting the lifetime of tokens to a short duration, such as 5 minutes. But you dont want to ask a user credentials every five minutes.
Thats why you need to implement refresh tokens. Its often treated as beyond the scope of basic learning materials, but its mandatory.
Refresh tokens
Refresh tokens are completely different from the regular JWT you use for authorization (which are called identity tokens). They are long-lived tokens (~7 days) and can be used a single time to generate a new identity token.
If you respect those properties, you can implement identity tokens in different ways. You can have a table that stores refresh tokens and the corresponding user, which is updated every time its used.
For refresh tokens, I usually generate a JWT where the payload contains two properties, a sub
, and userId
. The sub contains a UUID, which is stored in a database, and map to its corresponding user.
When a user tries to log in based on a refresh token, I find the corresponding sub
in my database and verify the userId
it maps to is the right one (it avoids a potential situation where a user connects with a previous user refresh token UUID).
In the end, you should have two endpoints for login, one with user credentials, and one with a refresh token. Now, there are multiple ways to generate and store those tokens, which leads us to the next section: vulnerabilities.
Security vulnerabilities
XSS attack
There is one implementation issue Ive seen too many times, how to store a JWT. In most online examples, you can see a JWT being returned inside a request response (body), and stored in localStorage or simply in memory.
This is the source of a huge security vulnerability, XSS attack.
An XSS attack is possible when a malicious third party manage to inject code inside your application.
From here, anything allowed by your runtime is possible. A third party could secretly read the content of localStorage
and send it to their own server, stealing JWTs for example.
Storing your JWT in-memory isnt enough. A third party can easily intercept a request response, and read users JWTs from there. There is a single solution: storing them inside secured cookies
.
A secure cookie is configured with, at least, the Secure and HttpOnly attributes. Its also a good practice to use SameSite to avoid CSRF.
There is also some arguments in favor of storing refresh tokens inside localStorage instead of cookies. The impact of a refresh token being stolen is reduced by its one-time only validity.
CSRF
Cookies are much better than localStorage
for our use case, but theyre not perfect. With cookies, you dont control when they are sent, your browser sends them with every request.
Its the source of CSRF, short for Cross Site Request Forgery.
From a general perspective, CSRF can happen when a third party trick a user into making a malicious request. The CSRF page from OWASP gives an amazing scenario with a bank transfer endpoint.
Using cookies with SameSite
mitigates CSRF, but only a CSRF token can completely get rid of it.
Signing
There are two potential vulnerabilities when you sign a JWT token, bad algorithm and secret key.
JWT can be signed with different algorithms. The list can differ depending on which library you are using. Library authors are responsible to implement those.
The default algorithm is usually HS256, but using a bad implementation or wrong configuration might end up with you using the none algorithm.
What this algorithm does is nothing! It generates an empty signature, which allows any third party to modify the JWT payload, and your server will still believe the modified JWT is valid.
My advice is to use well-known/maintained libraries and not try to use the none algorithm. You can also verify the content of tokens you generate using jwt.io.
Previously, we talked about refresh tokens, but there is a vulnerability introduced by using different types of tokens.
If you configure your refresh tokens to be signed with the same secret as the identity tokens, a malicious user could send an identity token where you expect a refresh token and vice-versa.
Depending on your implementation, you might grant access to a malicious user where you shouldnt. There is a single solution: use a different secret for your identity & refresh tokens.
Validation
During the validation phase, bad implementation could introduce security vulnerabilities.
There is an example with the NodeJS jsonwebtoken library. The right implementation is to use the verify method to ensure the token is valid and decode it.
On the other hand, it provides a decod method that doesnt check if the token is valid. Some developers not used to working with JWT might use the second method instead, virtually accepting any JWT token.
Ensure you are using well-known libraries and learn them properly before implementing anything sensitive.
Authors note
I advocate for the use of cookies over localStorage to mitigate XSS attacks, but thats not a reason to ignore potential XSS attacks altogether. I definitely recommend following good practices regarding XSS attacks.
For example, using JS eval should be avoided. You can also verify your dependencies using tools like snyk.io, and only use trusted CDN.
Building your own authentication system is an arduous task, I would recommend anyone to either use an authentication provider such as Auth0, or have a dedicated team working full time on authentication.
Do you want to learn how to create a backend application, add a secure authentication system , and much more?
Cover photo by Edwin Hooper on Unsplash
Top comments (1)
Good article. Interesting stuff with cookies over localstorage, I will definitely check that.