Refresh Tokens in Authentication using Node.js: Benefits and Implementation with Code
Introduction
Authentication is a critical aspect of building secure applications. When dealing with user authentication, especially in modern web applications, JWT (JSON Web Tokens) are often used for managing user sessions. A common pattern in token-based authentication is the use of access tokens and refresh tokens.
While access tokens are short-lived to enhance security, refresh tokens come in to extend user sessions seamlessly without needing the user to log in again. In this blog, we’ll explore how refresh tokens work, their benefits, and how to implement them in a Node.js application.
Why Use Refresh Tokens?
In a typical JWT-based authentication system, the server issues an access token upon successful login, which the client stores and sends with every request to authenticate itself. However, since access tokens are short-lived (e.g., 15 minutes), they expire quickly to reduce the attack surface in case a token gets compromised.
But constantly asking users to re-authenticate can be frustrating. This is where refresh tokens come in handy.
Benefits of Refresh Tokens:
Extended Sessions Without Frequent Logins: Refresh tokens allow users to maintain sessions over longer periods (days, weeks, or months) without frequently logging in again.
Security Enhancement: Since access tokens expire quickly, even if they are compromised, the attacker only has a small window to exploit them. Refresh tokens, typically stored securely, can be used to generate new access tokens.
Revocation Control: Refresh tokens give you control over invalidating tokens, allowing you to revoke them in case of suspicious activity.
Better User Experience: With refresh tokens, users enjoy seamless sessions without unnecessary interruptions from frequent re-authentication.
How Refresh Tokens Work
Login: The client sends login credentials to the server.
Token Generation: Upon successful login, the server generates an access token (short-lived) and a refresh token (long-lived).
Access Token Usage: The client uses the access token to make authenticated requests to the server.
Access Token Expiry: Once the access token expires, the client sends the refresh token to the server to request a new access token.
New Access Token: If the refresh token is valid, the server issues a new access token without requiring the user to log in again.
Let’s now dive into the implementation of this flow in a Node.js application.
Setting Up Node.js with JWT and Refresh Tokens
1. Install Dependencies
npm init -y
npm install express jsonwebtoken bcryptjs dotenv
2. Configure Environment Variables
Create a .env
file to store sensitive information like JWT secret keys.
ACCESS_TOKEN_SECRET=youraccesstokensecret
REFRESH_TOKEN_SECRET=yourrefreshtokensecret
3. Server Setup with JWT Tokens
Here’s a simple authentication flow using access and refresh tokens in Node.js.
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
require('dotenv').config();
const app = express();
app.use(express.json());
const users = []; // Store users (In real applications, use a database)
let refreshTokens = []; // Store refresh tokens (In a database)
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = { username, password: hashedPassword };
users.push(user);
res.status(201).send("User Registered!");
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(403).send('Invalid credentials');
}
const accessToken = generateAccessToken(user);
const refreshToken = jwt.sign(user, process.env.REFRESH_TOKEN_SECRET);
refreshTokens.push(refreshToken);
res.json({ accessToken, refreshToken });
});
app.post('/token', (req, res) => {
const { token } = req.body;
if (!token) return res.sendStatus(401);
if (!refreshTokens.includes(token)) return res.sendStatus(403);
jwt.verify(token, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
const accessToken = generateAccessToken({ username: user.username });
res.json({ accessToken });
});
});
app.post('/logout', (req, res) => {
const { token } = req.body;
refreshTokens = refreshTokens.filter(t => t !== token);
res.sendStatus(204);
});
function generateAccessToken(user) {
return jwt.sign({ username: user.username }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
}
app.listen(4000, () => console.log('Server running on port 4000'));
4. Explanation of the Code
User Registration: New users are registered with hashed passwords using
bcrypt
.Login: Upon successful login, an access token (expires in 15 minutes) and a refresh token (long-lived) are generated.
Token Route: When the access token expires, the client can use the refresh token to get a new access token without re-authenticating.
Logout: The refresh token is removed from the server, which ensures it can no longer be used.
5. Testing the Flow
- Register: First, register a user using the
/register
endpoint with aPOST
request.
{
"username": "testuser",
"password": "password123"
}
- Login: Log in using
/login
. The response will include both the access and refresh tokens.
{
"accessToken": "your_access_token_here",
"refreshToken": "your_refresh_token_here"
}
- Token Refresh: Use the refresh token in the
/token
endpoint to get a new access token after the current one expires.
{
"token": "your_refresh_token_here"
}
- Logout: Call the
/logout
endpoint with the refresh token to invalidate it.
Conclusion
In a modern web application, refresh tokens enhance user experience and improve security by allowing the use of short-lived access tokens without disrupting user sessions. In this blog, we walked through the benefits of using refresh tokens and implemented a simple example in Node.js using JWTs.
By implementing this token-based authentication system, your app can achieve the balance between security and usability, ensuring that users remain logged in seamlessly while mitigating the risk of token compromise.