Web application security is crucial in today's digital landscape, especially when sensitive data and user information are involved. One of the most important aspects of web application security is authentication, which verifies the identity of users and devices seeking access to resources.
JSON Web Tokens (JWT) is a popular and straightforward method for implementing authentication in web applications. JWT is a self-contained, digitally signed token in JSON format that can encode various data, such as user ID, expiration time, and other custom claims. It is a stateless mechanism that enables cross-domain authentication and prevents server-side storage of sessions and cookies.
In this blog, we will discuss how to implement JWT authentication with access tokens and refresh tokens in Node.js, a popular backend platform. We will cover the following topics:
Installing Dependencies
Understanding Access Tokens and Refresh Tokens
Registering and Logging in Users
Generating and Refreshing Tokens
Protecting Endpoints with JWT Authentication
Storing and Revoking Refresh Tokens
Conclusion
So let's begin.
Installing Dependencies
First, we need to install some dependencies that we'll use in this project. We'll use Express as the web framework, Mongoose as the object modeling library for MongoDB, bcrypt for password hashing, and jsonwebtoken for generating and verifying JWT.
npm install express mongoose bcrypt jsonwebtoken
Understanding Access Tokens and Refresh Tokens
Before we dive into code, let's briefly discuss access tokens and refresh tokens and their roles in JWT authentication.
Access tokens are short-lived tokens that grant the holder permission to access a protected resource, such as an API endpoint, a user's data, or other system resources. The token contains information about the user, including their identity and privileges, along with a limited lifespan (expiry), to enforce revocation and reduce security risks.
Refresh tokens are long-lived tokens that enable a user to obtain a new access token without re-authenticating with their credentials, such as username and password. The refresh token is used to renew the access token if it has expired or invalidated due to revocation, without exposing the user's credentials again.
Registering and Logging in Users
The first step to implement JWT authentication is to register new users and provide them with access credentials. We'll start by defining a user model using Mongoose that will store a user's credentials.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
module.exports = mongoose.model('User', userSchema);
Next, we'll define an API endpoint for creating a new user. This endpoint will receive the user's credentials, hash the password using bcrypt, and save the user to the database.
const express = require('express');
const bcrypt = require('bcrypt');
const User = require('./models/User');
const app = express();
app.use(express.json());
app.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const user = new User({
username,
password: hashedPassword
});
await user.save();
res.send('User created successfully');
} catch (err) {
console.error(err);
res.status(500).send('Server error');
}
});
Note that we use the async/await pattern to handle asynchronous operations such as hashing the password and saving the user to the database.
Next, we'll define an API endpoint for logging in a user and issuing a JWT access token and a refresh token. This endpoint will receive the user's credentials, validate them, generate and return tokens, and store the refresh token in the database or a store.
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'my-secret-key';
app.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user) throw new Error('Invalid username or password');
const isPasswordCorrect = await bcrypt.compare(password, user.password);
if (!isPasswordCorrect) throw new Error('Invalid username or password');
const accessToken = jwt.sign({ userId: user._id }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId: user._id }, JWT_SECRET);
addRefreshToken(refreshToken);
res.json({ accessToken, refreshToken });
} catch (err) {
console.error(err);
res.status(401).send('Invalid username or password');
}
});
Here, we use the jsonwebtoken package to generate an access token and a refresh token. We encode the user ID as a payload of the token to enable the server to verify the token's authenticity later when it is presented. We set the access token's lifespan to 15 minutes to ensure timely expiration of the token.
Note that we use the throw keyword to indicate an error instead of sending a response when an error occurs. We catch this error in the catch block, log it for debugging purposes, and send an appropriate response to the user.
Generating and Refreshing Tokens
Now, let's define an API endpoint for refreshing an access token using a refresh token. This endpoint will receive the refresh token, validate it, decode it to obtain the user ID, and generate and return a new access token.
app.post('/refresh-token', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!isRefreshTokenValid(refreshToken)) throw new Error('Invalid refresh token');
const decoded = jwt.verify(refreshToken, JWT_SECRET);
const accessToken = jwt.sign({ userId: decoded.userId }, JWT_SECRET, { expiresIn: '15m' });
res.json({ accessToken });
} catch (err) {
console.error(err);
res.status(401).send('Invalid refresh token');
}
});
Here, we define a helper function isRefreshTokenValid() that checks if the refresh token exists in the database or store and has not expired yet. If the refresh token is valid, we verify and decode the refresh token to obtain the user ID embedded in the token. Then, we generate a new access token with the user ID and return it to the user.
Note that we use the same jsonwebtoken package and the JWT_SECRET key to generate and verify the access token and refresh token. We also set the access token's lifespan to be the same as the original access token to maintain consistency.
Protecting Endpoints with JWT Authentication
Now, let's use the access token to secure endpoints that require authentication. We'll define a protected endpoint that only authorized users can access, using the authenticateToken() middleware function.
app.get('/protected', authenticateToken, (req, res) => {
res.send('This is a protected endpoint');
});
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const accessToken = authHeader && authHeader.split(' ')[1];
if (!accessToken) return res.status(401).send('Access token not found');
jwt.verify(accessToken, JWT_SECRET, (err, decoded) => {
if (err) return res.status(403).send('Access token invalid');
req.userId = decoded.userId;
next();
});
}
Here, we define a middleware function authenticateToken() that extracts the access token from the request header, verifies it using the jsonwebtoken package, and sets the user ID in the request object for further processing by the endpoint handler. If the access token is invalid or not found, we send an appropriate response to the user.
We use the middleware function authenticateToken() to secure the /protected endpoint, which requires a valid access token to access.
Storing and Revoking Refresh Tokens
Finally, let's discuss how to store and revoke refresh tokens securely. We recommend storing refresh tokens in a database or an in-memory store like Redis, and associate each refresh token with a user ID to enable quick retrieval and revocation.
function addRefreshToken(refreshToken) {
}
function isRefreshTokenValid(refreshToken) {
return true;
}
Here, we define two helper functions: addRefreshToken() to store a refresh token and isRefreshTokenValid() to check if a refresh token is valid. We recommend implementing appropriate cache eviction and refresh token revocation mechanisms to ensure the refresh tokens' security and integrity.
Conclusion
In this blog, we have discussed how to implement JWT authentication with access tokens and refresh tokens in Node.js. We have covered how to install dependencies, register and log in users, generate and refresh tokens, protect endpoints with JWT authentication, and store and revoke refresh tokens. Implementing JWT authentication in your web application can significantly improve its security and enhance user experience. We hope you find this blog useful and informative.