Vulnerabilities in Authentication with JWT

And how to implement it properly

Vulnerabilities in Authentication with JWT

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. It’s encoded in base64 to be used as a string.

Payload

The payload is also a JSON object, where you define the user information. it’s 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 (it’s stateless/self-contained), the API doesn’t 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 you’re 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 you’re 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 shouldn’t 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 don’t want to ask a user credentials every five minutes.

That’s why you need to implement refresh tokens. It’s often treated as beyond the scope of basic learning materials, but it’s 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 it’s 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 I’ve 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 isn’t 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. It’s 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 they’re not perfect. With cookies, you don’t control when they are sent, your browser sends them with every request.

It’s 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 shouldn’t. 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 doesn’t 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.

Author’s note

I advocate for the use of cookies over localStorage to mitigate XSS attacks, but that’s 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