Introduction
JSON Web Tokens (JWT) are a popular method for handling authentication in web applications. JWTs are compact, URL-safe tokens that can be used to securely transmit information between parties. This article provides a step-by-step guide on implementing JWT authentication in a Next.js application, including generating and verifying tokens, handling user login, and protecting routes.
Setting Up the Project
Create a New Next.js Project
If you haven’t already set up a Next.js project, create one using.
npx create-next-app@latest my-next-app
cd my-next-app
Install Required Packages
You’ll need a few packages to handle JWTs and manage environment variables.
npm install jsonwebtoken bcryptjs dotenv
- jsonwebtoken: Library to sign and verify JWTs.
- bcryptjs: Library to hash passwords.
- dotenv: Library to manage environment variables.
Setting up Environment Variables
Create a .env.local file at the root of your project to store your secret key and other sensitive information.
JWT_SECRET=your-secret-key
Replace your secret key with a strong, random string.
Creating Utility Functions
JWT Utility Functions
Create a file named auth.js in the lib directory to manage JWT operations.
mkdir lib
touch lib/auth.js
Add the following code to auth.js.
// lib/auth.js
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET;
export function signToken(user) {
return jwt.sign(user, SECRET, { expiresIn: '1h' });
}
export function verifyToken(token) {
try {
return jwt.verify(token, SECRET);
} catch (err) {
return null;
}
}
Password Hashing Utility
Add password hashing functionality in auth.js.
import bcrypt from 'bcryptjs';
export async function hashPassword(password) {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
}
export async function verifyPassword(password, hash) {
return bcrypt.compare(password, hash);
}
Setting Up API Routes
User Registration
Create a registration route to handle user registration and password hashing.
mkdir pages/api
touch pages/api/register.js
Add the following code to register.js.
// pages/api/register.js
import { hashPassword } from '../../lib/auth';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({ error: 'Missing username or password' });
}
// Hash the password
const hashedPassword = await hashPassword(password);
// Save user to the database (pseudo-code)
// await saveUserToDatabase(username, hashedPassword);
res.status(201).json({ message: 'User registered' });
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
User Login
Create a login route to handle user authentication and token generation.
touch pages/api/login.js
Add the following code to login.js.
// pages/api/login.js
import { signToken, verifyPassword } from '../../lib/auth';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { username, password } = req.body;
// Validate input
if (!username || !password) {
return res.status(400).json({ error: 'Missing username or password' });
}
// Fetch user from the database (pseudo-code)
// const user = await fetchUserFromDatabase(username);
// Verify password (pseudo-code)
// const isPasswordValid = await verifyPassword(password, user.password);
if (/* user exists and password is valid */) {
// Generate JWT token
const token = signToken({ username });
res.status(200).json({ token });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Protected Route
Create a protected API route that requires authentication.
touch pages/api/protected.js
Add the following code to protected.js.
// pages/api/protected.js
import { verifyToken } from '../../lib/auth';
export default function handler(req, res) {
if (req.method === 'GET') {
const { authorization } = req.headers;
if (authorization && authorization.startsWith('Bearer ')) {
const token = authorization.replace('Bearer ', '');
const user = verifyToken(token);
if (user) {
res.status(200).json({ message: 'Protected data', user });
} else {
res.status(401).json({ error: 'Invalid token' });
}
} else {
res.status(401).json({ error: 'No token provided' });
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Client-Side Authentication
Login Page
Create a login page to allow users to authenticate.
touch pages/login.js
Add the following code to login.js.
// pages/login.js
import { useState } from 'react';
export default function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
const res = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
const { token } = await res.json();
if (token) {
localStorage.setItem('token', token);
// Redirect or show success message
} else {
// Handle login error
}
};
return (
<div>
<h1>Login</h1>
<input
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Username"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button onClick={handleLogin}>Login</button>
</div>
);
}
Protected Page
Create a protected page that requires authentication.
touch pages/protected.js
Add the following code to protected.js.
// pages/protected.js
import { useEffect, useState } from 'react';
export default function ProtectedPage() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const token = localStorage.getItem('token');
const res = await fetch('/api/protected', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const result = await res.json();
setData(result);
};
fetchData();
}, []);
return (
<div>
<h1>Protected Page</h1>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}
</div>
);
}
Best Practices
- Secure Your JWT Secret: Keep your JWT secret key secure, and do not expose it in your codebase.
- Token Expiration: Set an appropriate expiration time for tokens and handle token renewal or refreshing.
- HTTPS: Ensure your application uses HTTPS to protect token transmissions.
- Error Handling: Implement proper error handling and user feedback mechanisms for authentication failures.
Summary
Implementing JWT authentication in a Next.js application provides a secure and efficient way to handle user authentication and authorization. By creating utility functions for JWT operations, setting up registration and login routes, and protecting API routes, you can build a robust authentication system. Proper handling of tokens and security practices will ensure a secure application environment.