The Common Approach

The common approach to JWT authentication seems to be using a short-lived JWT in javascript's memory and a longer-lived JWT in an HTTPOnly cookie for re-issuing the in-memory token after it's expired. The security benefits of this have never made much sense to me since the cookie's token can just be used to retrieve an in-memory token itself. More work with no benefit.

The longer-lived token for re-issuing the short-lived token generally contains information for invalidating the session. This means session access cannot be guaranteed revoked for however long the short-lived token is potentially active.

The Best of Both Worlds Approach

This approach allows for fast throughput requests and infrastructure savings by reducing database lookups to "dangerous" actions while allowing session revocation before any dangerous action is performed.

What's a dangerous action? That's up to you and your app needs to determine. While developing this approach any Create, Update, or Delete action or token re-issue was considered dangerous, while any Read action is safe enough that they're allowed during the short-lived token's window without verification.

This means any revoked session will have, at most, minutes to view the account, but cannot alter any data associated with it.

The Basics

This is the basic setup:

The Implementation:

Storing the JWT in an HTTPOnly cookie allows us to create a persistent, hidden from javascript authentication method. Attaching it as credentials to our requests will mean needing to implement cross-site request forgery (CSRF) protection, but we won't dive into that here because of the many possible solutions depending on your needs.

Shared Sessions

By creating a unique ID session field on each user we eliminate the need to store a separate session database/table that stores multiple sessions for each user depending on device, browser, etc. The shared session ID is deemed a current valid session until it is changed in the database. This is how we'll revoke sessions.

This means we'll have to revoke all sessions associated with a single account rather than individual sessions, but this is usually the general approach anyways.

Revoking Sessions

What reasons can we use to revoke sessions? Password changes, compromised account detection, and more. When we need to invalidate a session, we simply need to generate a new session ID for the user in the database. All currently issued tokens will be invalid for re-issue or unsafe actions.

User ID

By storing the user's ID in the token we can implicitly trust for any safe action that the user of a valid, unexpired token is who they say they are, and should have access to the account data associated with the ID.

This will save a database lookup for every safe action performed. A possibly large accumulation over time with a speedup for the user on each safe request as well.

Expiration

With a short-lived expiration we can re-verify valid sessions as soon as a token expires, limiting the window revoked sessions have to safe actions as well.

Rather than having a separate token for re-issuing the short-lived token we can just use the expired token + its session ID to verify it should be re-issued.

We check the database that the session ID is still valid, compare the issued time against a longer window of time and if both are accepted issue a new short-lived cookie token.

Most JWT implementations (like node-jsonwebtoken) make this easy by throwing an expiration specific error on validation, and then a method for decoding the token while ignoring the expiration:


				verify(token, 'secret').catch((err) => {
					if (err.name === 'TokenExpiredError') {
						// Check against re-issue time window & validate session ID
						return validateSessionAndReissue(token);
					}
				});
				

This way our single token can "re-issue" itself when needed.

Considerations:

The JWT Window

If you need instantly revocable sessions for all requests and can't deem any safe vs unsafe then a JWT isn't the solution for you at all. Classic sessions stored in a cookie are smaller in size, will use the same number of DB lookups, and are often easier to implement.

Server Side Handling

Your server implementation may need to accommodate the approach in several ways. The user's ID will typically be attached to the request as included credentials from the token rather than included as an argument/parameter or REST endpoint. This can be nice since endpoints, graphQL queries, etc. can be simplified.

Different actions/resources will need to be marked as safe for implicit trust and some for unsafe requiring explicit validated session trust. This may be a higher order function around different routes or resolvers.

A Single Session

If your security needs to track/differentiate between two sessions under a single account then a shared session likely over complicates the fingerprinting you'd want to attach to actions and classic sessions are probably a better choice.

In Summary

This approach has the same security implications as most JWT implementations, but offers more safety around certain actions giving the performance and infrastructure benefits of the normal JWT approach for all other actions. And the single JWT being responsible for re-issuing itself reduces complexity greatly.