March 18, 2020

Are you using JWTs for user sessions in the correct way?

JSON Web Tokens (or JWTs) have become incredibly popular and you’ve likely heard of them before. What you may not have heard is that JWTs were originally designed for use in OAuth – which is fundamentally different to user sessions.

While the use of JWTs for OAuth is widely accepted, its use for authenticating users sessions is controversial (see this post). In this article, I will attempt to make a comprehensive list of the pros and cons of using JWT for this context. I do not intend to solve this debate, since devs (especially devs) are often strongly opinionated. I only aim to summarize all the perspectives.

However, I do offer my opinion on the best solution for session management (spoiler: it has the advantages of JWTs without any of its disadvantages!)

The flow of the content is as follows:

A cursory note on session management.

User sessions involve managing tokens across your app’s backend and frontend. These tokens act as a proxy to your’s identity and can either be:

Non opaque tokens have a special property that enables the backend to verify that the token is legitimate. This is achieved by cryptographically signing them, and in doing so, we get what is known as a JWT – a signed, non-opaque token.

A clarification note: I am only concerned with session management between an app’s backend APIs and frontend. There is no third party service involved (i.e. no OAuth 2.0).

The Pros

The following is a list of all the pros for using JWTs – aggregated across multiple sources. These are benchmarked relative to opaque tokens (the only other type of token for sessions). I have also included some common misconceptions and have labeled them as “myths”:

1)Fact: No database lookups: It’s generally known that for most APIs, network calls add the most latency. Hence, it’s reasonable to expect that having no network calls (no database lookups) for session verification is beneficial.

To prove this, I ran a test to see latency times (requests per second or RPS) of APIs that used JWTs and not. The displayed RPS are an average of running the tests 60 times. Following are the different APIs that were tested:

For each API, I set up the database / cache in three locations:

1) The same machine (as the API process)

2) A different machine, but within the same WiFi network

3) A different machine with a different network (to the API process), but within the same city (an AWS EC2 instance). All machines have roughly the same spec in terms of processing power and RAM.

Results for API-1 (No extra database read):

Results for API-2 (one extra database read in API):

As it can be seen, database lookups are indeed much slower, especially over distributed machines (which is very often the case). However, there are counters to this point:

2) Myth: Saving on database space: Since JWT’s don’t need to be stored in the database, it’s true that it does save space. To get a sense of how much, let’s do a back of an envelope calculation:

So by using JWTs, we are saving 300 MB of database space per million users. This doesn’t make much difference since it would cost approximately $0.03 extra per month on AWS as per their pricing.

3) Myth: More secure because it’s signed: The signing of the JWT token is only required so that clients cannot manipulate the content in the token. Whereas, opaque tokens cannot be manipulated since the string itself doesn’t have any meaning. Just having a long opaque token (high entropy) is good enough. Hence, the signing of JWTs doesn’t add any extra security in comparison to opaque tokens, it simply matches the security level.

4) Myth: JWTs are easier to use: It is true that JWTs are easier to get started with since we don’t have to take the effort to build a system that reads the database for session verification, or a cron job to remove expired tokens… However, these are quite easy to implement anyway.

5) Myth: JWTs are more flexible: Flexibility comes because we can put anything in a JWT. However, we can do the same with opaque tokens. Any data can be stored in the database against an issued opaque access token.

6) Myth: JWTs automatically prevent CSRF: As long as we are using cookies for JWT (which is recommended), we also have to take care of CSRF attacks, just like if we use an opaque token. This attack vector will have to be prevented using anti CSRF tokens or SameSite cookie attribute, both of which are independent of if we use JWT or opaque tokens.

7) Myth: No need to ask users for ‘cookie consent’: Cookie consent which is required for GDPR, applies only to cookies used for analytics and tracking. Not for keeping users logged in securly. JWTs and opaque tokens are the same in regards to this point.

8) Other myths: I have also read people claim that JWTs work better than opaque tokens for mobile and also work even if cookies are blocked. Both of these are simply not true.

Overall, it seems that the only advantage of JWT over opaque token is lesser latency in API requests (which is a major win). Now let’s have a look at the cons.

The cons

Like the above section, the following is a list of all the cons that I have thought about, as well as what I have read from other sources:

1) Fact: Non revocable: Since verifying JWTs doesn’t require any lookup to a single source of truth (database), revoking them before they expire can be difficult. I say difficult and not impossible because one can always change the JWT signing key and then all issued JWTs will be immediately revoked. Revocation is important in many cases:

One solution that people recommend is to use revocation lists. This is where you keep a list of revoked JWTs and check against that list when verifying the JWT. But if we do this, it’s almost the same as opaque tokens since we will have to do a database / cache lookup in each API. I say almost since here, we have the option to choose which APIs should check against the blacklist and which should not. So this may be an advantage in certain scenarios over opaque tokens.

One more solution is to keep the lifetime of the JWT very small (~10 mins). However, this also means that users will be logged out every 10 mins. There are various session flows that one can implement to have short lived JWTs while maintaining a long session as explained in this blog post. We will be exploring the recommended method later in this post.

2) Fact: Bottlenecked against one secret key: If the signing key of the JWTs is compromised, then the attacker can use that to change the userId in their JWT to any other user’s. This allows them to hijack any user’s account in a system. This secret key can be compromised in a variety of ways like employees making a mistake (by pushing the key to github) or purposely leaking the key. Attacks to your servers might also leak this key.

A counter to this is that even opaque tokens from the database can be leaked. However, those are much harder to leak  (because of their sheer volume) and cannot be used to compromise new accounts or accounts that don’t have an active session during the time of attack.

3) Fact: Crypto deprecation: Signing of JWTs requires use of a cryptography instrument called hashing. It is usually recommended to use SHA256 for this. However, what happens when this gets deprecated? At that point, one may want to switch to a newer algorithm. While making this change is relatively straightforward, the problem is that developers are very busy and often will miss out on such deprecations. That being said, such deprecations are very infrequent.

4) Fact: Monitoring user devices: In the most simple implementation, if one is using JWTs for their sessions without any session information stored in the database, their app will not be able to know which devices or how many devices a user is using. This may often cause business logic and analytics issues. This being said, it’s easy to add some information to the database when a JWT is issued and remove it once it expires. This way, this disadvantage can be mitigated. However, this is something that needs to be done purely outside the scope of a JWT (hence this point).

5) Myth: Cookie size is too large: A typical JWT can be 500 bytes long[1], versus a 36 or 64 bytes sized opaque token. These are to be sent to the frontend via cookies and these are sent to the backend on each API request. This causes two problems:

6) Myth: Data in JWT is visible to everyone: First, the priority should be that JWTs themselves should not be accessible by anyone malicious because then they can gain unauthorised access to an account (which is a far bigger problem than being able to see the contents of the JWT). However, if that does happen, one should also refrain from putting any sensitive information in a JWT. Instead, one can store this information in the database. Either way, this is not a con of using JWTs.

Seeing the pros and the cons above, my opinion is that just using JWTs is probably not worth it. The risks, I feel, outweigh the benefits. However, what if we could use a different approach where we use both, opaque tokens and JWTs. Perhaps, this would allow us to eliminate the cons whilst keeping the pros?

The new approach

Once the user logs in, the backend issues a short lived JWT (access token) and a long lived opaque token (refresh token). Both of these are sent to the frontend via httpOnly and secure cookies. The JWT is sent for each API call and is used to verify the session. Once the JWT expires, the frontend uses the opaque token to get a new JWT and a new opaque token. This is known as rotating refresh tokens. The new JWT is used to make subsequent API calls and the session continues normally. This flow is illustrated in the diagram below:

Now let’s revisit pros and cons for this new session flow.

Revisiting the pros:

1) No database lookups: Since most API calls still use the JWT, this advantage still holds. We will need to call the database when refreshing the session, but this is a relatively rare event (relative to the number of session verifications that do not require a database lookup).

Cookie 3; Localstorage 1

2) Added security via session hijacking detection: Using rotating refresh tokens, we are now able to detect stolen tokens in a reliable way. This will help prevent session hijacking attacks. Learn more about it here.

We can see that the main advantage of using JWTs still holds, and we have also added a new advantage!

Revisiting the cons:

1) Partially Solved: Non revocable: We can use short lived JWTs and long lived refresh tokens to maintain a long session as well as get substantially more control on revocability. To revoke a session, we must now simply remove the opaque token from the database. This way, when the refresh API is called, we can detect that the session has expired and log out the user. Note that this will not immediately revoke a session – it depends on the lifetime of the JWT. But it makes this problem much more bearable.

2) Solved: Bottlenecked against one secret key: We can keep changing the JWT signing key every fixed interval of time. When the key is changed, all current JWTs will be immediately invalidated. In this event, the frontend can simply use its refresh token to get a new JWT (and a new refresh token) signed with the new key. This way, we can vastly minimise our dependency on this secret key.

3) Not solved: Crypto deprecation: This point is still a problem, however, changing the hashing algorithm can be done smoothly and immediately just like how we change the signing key.

4) Solved: Monitoring user devices: Since we have an opaque token for each session, we can easily monitor the devices each user has.

We can see that most of the cons have been roughly resolved and are now all acceptable risks.

Conclusion:

My opinion is that using JWTs, especially for long lived sessions, is not a good idea. Using short lived JWTs with long lived opaque (refresh) tokens in the following scenarios:

When I think of consumer apps that I want to develop, most of them meet the criteria above. I feel that it is a perfect balance between scalability and security. For all other requirements, stick to short lived opaque access tokens and long lived opaque refresh tokens.

Note that we have not spoken about the applicability of JWTs for OAuth and have only focused on sessions between an app’s backend API and frontend. JWTs are generally an excellent use case for delegation of access to third party services (OAuth). In fact, they were originally designed for this exact purpose.

If you like the session flow I described, please checkout SuperTokens. It is a robust solution that has implemented rotating refresh tokens with JWTs (and opaque tokens coming out soon). It provides all the benefits mentioned above and also prevents all session related attacks.

Written by the Folks at SuperTokens — hope you enjoyed! We are always available on our Discord server. Join us if you have any questions or need any help.

This is a total of 315 bytes. The JWT header is normally between 36 and 50 bytes and finally the signature is between 43 and 64 bytes. So this gives us a maximum of 429 bytes which would take about 10% of cookie space.