Implementing JWT Refresh Tokens in Node.js for Secure User Authentication
While JWTs (JSON Web Tokens) are a secure way to handle authentication, they are often short-lived, with a limited expiration time to reduce security risks if compromised. To maintain user sessions without requiring frequent re-authentication, refresh tokens allow you to renew access tokens safely. This guide walks through implementing refresh tokens in a Node.js application using Express and Mongoose, allowing for secure and scalable session management.
Why Use Refresh Tokens?
Access tokens usually have short expiration times (e.g., 15 minutes to 1 hour). If an access token expires, the user would have to log in again, which could lead to poor user experience. Refresh tokens enable the client to obtain a new access token without requiring the user to log in, allowing for a seamless experience while keeping sessions secure.
How Refresh Tokens Work
- Access Token: Short-lived token used for authorizing requests.
- Refresh Token: Long-lived token stored securely on the client side. It’s used to request a new access token when the access token expires.
The flow looks like this:
- The server issues an access token and a refresh token during login.
- The client stores the refresh token securely (e.g., in an HTTP-only cookie).
- When the access token expires, the client uses the refresh token to obtain a new access token.
- The server verifies the refresh token, issues a new access token, and, optionally, a new refresh token.
Setting Up the Project
We’ll build upon the previous JWT setup by adding refresh token functionality. Ensure you have Express, Mongoose, jsonwebtoken, and bcryptjs installed.
Step 1: Updating the Auth Routes
In the auth.js
route file, we’ll add functionality to handle refresh tokens.
Generate Access and Refresh Tokens
Let’s create functions to generate an access token and a refresh token.
Here, the generateAccessToken
function generates a short-lived token (e.g., 15 minutes), and generateRefreshToken
generates a token with a longer lifespan (e.g., 7 days).
Modify the Register and Login Routes
Update the register
and login
routes to issue both access and refresh tokens.
routes/auth.js
In this update:
- Both routes now return an
accessToken
andrefreshToken
to the client upon successful authentication.
Step 2: Implementing the Refresh Token Endpoint
Next, let’s add a new endpoint to handle token refreshing.
Refresh Token Route
Create a refreshToken
route that verifies the refresh token and issues a new access token.
routes/auth.js
This route:
- Accepts the refresh token in the request body.
- Verifies the refresh token using the
JWT_REFRESH_SECRET
. - If valid, it issues a new access token.
Step 3: Storing and Handling Tokens on the Client
To keep tokens secure, follow best practices for storing and sending tokens.
Securely Storing Tokens
- Access Tokens: Store in memory (not in local storage or session storage) to prevent XSS attacks.
- Refresh Tokens: Store in an HTTP-only, secure cookie. This prevents JavaScript access to the refresh token, reducing the risk of XSS attacks.
Example: Sending Tokens as Cookies
In the login
and register
routes, set the refresh token as an HTTP-only cookie.
The refresh token is sent as a cookie, while the access token is returned in the response body for client-side storage in memory.
Using Tokens in Requests
For each API request, include the access token in the Authorization
header:
When the access token expires, send a request to the /refresh-token
endpoint, and retrieve a new access token.
Step 4: Middleware for Protected Routes
Create a middleware to verify the access token for protected routes, as done in the previous JWT guide.
middleware/authMiddleware.js
This middleware checks the access token validity before granting access to protected routes.
Best Practices for JWT and Refresh Tokens
- Short Access Token Expiration: Use a short expiration (e.g., 15 minutes) to minimize security risks if the token is compromised.
- Longer Refresh Token Expiration: Use a longer expiration (e.g., 7 days or more) for refresh tokens.
- HTTP-Only Cookies for Refresh Tokens: Store refresh tokens in HTTP-only cookies to prevent JavaScript access and mitigate XSS risks.
- Secure Token Storage: Avoid storing tokens in local storage. Use memory storage for access tokens and HTTP-only cookies for refresh tokens.
- Logout and Token Revocation: Implement a mechanism to revoke refresh tokens on logout or detect compromised tokens.
Conclusion
Implementing refresh tokens with JWT in a Node.js application adds a layer of security and flexibility to user authentication. With a combination of short-lived access tokens and long-lived refresh tokens, you can improve the user experience by allowing continuous sessions while maintaining robust security.
This approach is ideal for modern applications that need scalable, stateless authentication. Integrate these techniques in your projects to secure user sessions and manage authentication with ease and security.