In previous lessons, we explored JWT fundamentals, common vulnerabilities, and defenses like token blacklisting and short-lived token expiration. While these improve security, they can negatively impact user experience by requiring frequent re-authentication.
Refresh tokens address this by allowing clients to obtain new access tokens without re-authentication. They work alongside access tokens:
- Access tokens: Short-lived, used for API access
- Refresh tokens: Long-lived, used to get new access tokens
This separation follows the principle of least privilege. Token rotation enhances security by generating a new refresh token each time one is used, preventing replay attacks. Let's implement this system to address identified vulnerabilities.
To implement refresh tokens, we need to modify our authentication system to generate, store, and validate both access and refresh tokens. Let's start by examining how we can create and manage these tokens.
Note: In most applications, users don’t manually request a new access token. Instead, the frontend (UI) automatically handles it in the background. This is done using refresh tokens whenever an access token expires.
First, we'll need to generate both tokens during login. We'll add a unique jti
(JWT ID) claim to each refresh token to help us track and manage them securely. We generate this jti
using Math.random().toString(36).substring(2)
, which creates a random string for each token:
Why use the jti
claim in refresh tokens?
The jti
(JWT ID) claim provides a unique identifier for each refresh token. By assigning a unique jti
to every token, the server can track and manage individual tokens—allowing it to invalidate a specific token after use (token rotation) or in case of compromise. This is essential for security because it prevents replay attacks: if a refresh token is stolen, it cannot be reused once its jti
has been invalidated. Using jti
enables fine-grained control over token validity and enforces single-use refresh tokens during rotation.
Now, let's see how these tokens are used during login:
In this code, we're generating two tokens: a short-lived access token (15 minutes) and a longer-lived refresh token (7 days) with a unique jti
claim. Both are stored as HTTP-only cookies to protect against XSS attacks. The access token is used for regular API access, while the refresh token will be used to obtain new access tokens when the current one expires.
Next, we need to create a /refresh
endpoint that allows clients to exchange a valid refresh token for a new access token:
This endpoint extracts the refresh token from cookies, verifies it, and if valid, issues a new access token. The client can call this endpoint whenever the access token expires, allowing for continuous authentication without requiring the user to log in again.
However, this implementation still has a security issue: if a refresh token is stolen, it can be used repeatedly until it expires (7 days in our example). This is where token rotation comes in.
Token rotation adds an important security layer by ensuring that refresh tokens are single-use. Each time a refresh token is used, it's invalidated and a new one is issued. This prevents an attacker who has stolen a refresh token from using it after the legitimate user has already used it.
To implement token rotation, we need to:
- Track all issued refresh tokens (using their
jti
) - Invalidate a refresh token after it's used
- Issue a new refresh token with each refresh request
Let's break down the implementation step by step.
First, we need a way to keep track of issued refresh tokens. We'll use the jti
claim as a unique identifier for each refresh token. For demonstration, we'll use an in-memory store (a Map
). In production, use a persistent database.
When a user logs in, generate a refresh token with a unique jti
and store the jti
:
When a refresh token is used, we need to:
- Check if its
jti
exists in our store - Remove (invalidate) it
- Issue a new refresh token and store its
jti
Refresh tokens solve the user experience problem of frequent re-authentication when using short-lived access tokens. This lesson introduces the dual-token system where short-lived access tokens handle API access while long-lived refresh tokens obtain new access tokens without requiring users to log in again. We implemented refresh token functionality in our authentication system, including secure token generation, storage, and validation. We then enhanced security through token rotation, making refresh tokens single-use by invalidating them after use and issuing new ones, tracked by their unique jti
claim. The jti
is generated using Math.random().toString(36).substring(2)
to ensure each token is unique and unpredictable, which is critical for preventing replay attacks and enforcing single-use tokens.
In the upcoming practice, you'll apply these concepts by implementing a secure token rotation system to protect your application. 🔄🔐
