Full-Stack Authentication with Next.js, Prisma, and JWT Rotation
Introduction
Building a robust and secure authentication system is a crucial aspect of full-stack development. In this article, we'll explore how to implement full-stack authentication in a Next.js application using Prisma as the ORM (Object-Relational Mapping) tool and JSON Web Tokens (JWT) with rotation. This approach ensures secure and scalable user management, making it an ideal choice for modern web applications.
Prerequisites
Before diving into the implementation, make sure you have the following installed:
- Node.js (version 14 or higher)
- Next.js (version 12 or higher)
- Prisma (version 3 or higher)
- A code editor or IDE of your choice
Setting up the Project
Create a new Next.js project using the following command:
npx create-next-app my-app
Install the required dependencies, including Prisma and JWT:
npm install @prisma/client jsonwebtoken
Initialize Prisma by running the following command:
npx prisma init
This will create a prisma folder with the necessary configuration files.
Defining the User Model
In the prisma/schema.prisma file, define the User model as follows:
model User { id String @id @default(cuid()) email String @unique password String name String? createdAt DateTime @default(now()) updatedAt DateTime @default(now()) }
This model includes the basic fields for a user, including id, email, password, name, createdAt, and updatedAt.
Generating the Prisma Client
Run the following command to generate the Prisma client:
npx prisma generate
This will create a prisma/client folder with the generated client code.
Implementing Authentication with JWT
Create a new file lib/auth.js with the following code:
import { PrismaClient } from '@prisma/client'; import jwt from 'jsonwebtoken'; const prisma = new PrismaClient(); const secretKey = process.env.SECRET_KEY; const tokenExpiration = 3600; // 1 hour const generateToken = (user) => { const payload = { id: user.id, email: user.email, }; return jwt.sign(payload, secretKey, { expiresIn: tokenExpiration }); }; const verifyToken = (token) => { try { const decoded = jwt.verify(token, secretKey); return decoded; } catch (error) { return null; } }; const authenticate = async (email, password) => { const user = await prisma.user.findFirst({ where: { email } }); if (!user || user.password !== password) { return null; } const token = generateToken(user); return token; }; export { authenticate, verifyToken };
This code defines the authentication logic using JWT. The generateToken function creates a new token based on the user's ID and email, while the verifyToken function verifies the token by checking its signature and expiration. The authenticate function checks the user's credentials and returns a token if they are valid.
Implementing JWT Rotation
To implement JWT rotation, we'll use a simple approach where we store the last issued token in the user's session. When the user makes a request, we'll check if the token has expired or if it's been more than 15 minutes since the last issued token. If either condition is true, we'll generate a new token and update the user's session.
Create a new file lib/auth.js with the following code:
import { authenticate } from './auth'; const tokenRotationInterval = 15 * 60 * 1000; // 15 minutes const rotateToken = async (req, res) => { const user = req.user; const lastIssuedToken = user.lastIssuedToken; const currentTime = new Date().getTime(); if (!lastIssuedToken || currentTime - lastIssuedToken > tokenRotationInterval) { const newToken = await authenticate(user.email, user.password); user.lastIssuedToken = currentTime; res.cookie('token', newToken, { httpOnly: true, maxAge: tokenExpiration * 1000 }); } }; export { rotateToken };
This code defines the token rotation logic. The rotateToken function checks if the token has expired or if it's been more than 15 minutes since the last issued token. If either condition is true, it generates a new token and updates the user's session.
Integrating with Next.js
Create a new file pages/api/auth.js with the following code:
import { NextApiRequest, NextApiResponse } from 'next'; import { authenticate, verifyToken, rotateToken } from '../../lib/auth'; const authHandler = async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const { email, password } = req.body; const token = await authenticate(email, password); if (!token) { return res.status(401).json({ error: 'Invalid credentials' }); } res.cookie('token', token, { httpOnly: true, maxAge: 3600 * 1000 }); return res.json({ token }); } else if (req.method === 'GET') { const token = req.cookies.token; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } const decoded = verifyToken(token); if (!decoded) { return res.status(401).json({ error: 'Invalid token' }); } await rotateToken(req, res); return res.json({ user: decoded }); } }; export default authHandler;
This code defines the authentication API endpoint. The authHandler function handles both POST and GET requests. When a user submits their credentials, it authenticates them and returns a token. When a user makes a GET request, it verifies the token and rotates it if necessary.
Conclusion
In this article, we've implemented full-stack authentication with Next.js, Prisma, and JWT rotation. This approach ensures secure and scalable user management, making it an ideal choice for modern web applications. By using JWT rotation, we've added an extra layer of security to our authentication system, making it more resistant to token theft and replay attacks.