The Full-Stack Guide to JWT Authentication (with Node.js and React)

By Md. Abu Sufian
The Full-Stack Guide to JWT Authentication (with Node.js and React)
Why Do We Need Authentication? In any real-world application, you have data or pages that shouldn't be public. You need a way to: Authenticate a user (prove they are who they say they are, usually with a login). Authorize a user (check if they have permission to access a specific resource, like their own dashboard). While old methods used server-side sessions, the modern, stateless way to do this is with JSON Web Tokens (JWT). What is a JWT? A JWT is just a long, secure string. This string is an open standard (RFC 7519) that safely transmits information between two parties (like your backend server and your frontend app). A JWT consists of three parts separated by dots (.): Header: Contains the token type (JWT) and the signing algorithm (like HS256). Payload: Contains the data (called "claims"). This is where you'd put user information, like userId or email. Signature: This is a cryptographic signature. Your server uses a secret key to create this. If the user changes anything in the payload, the signature will become invalid, and your server will reject the token. This "stateless" approach is powerful: your server doesn't need to store session information. It just needs to check the token's signature to trust the data inside it. Part 1: The Backend (Node.js & Express.js) Let's build the API endpoints for registration, login, and a protected route. What you'll need: npm install express jsonwebtoken bcryptjs jsonwebtoken: The library for creating and verifying tokens. bcryptjs: The library for securely "hashing" (scrambling) passwords before saving them. Never store plain-text passwords! Step 1: User Registration (/register) When a user signs up, you hash their password and save them to your database (e.g., using PostgreSQL and Prisma). JavaScript // /routes/auth.js const express = require('express'); const bcrypt = require('bcryptjs'); const prisma = require('../prismaClient'); // Assuming you have Prisma set up const router = express.Router(); router.post('/register', async (req, res) => { const { email, password } = req.body; // Hash the password const hashedPassword = await bcrypt.hash(password, 10); try { const user = await prisma.user.create({ data: { email, password: hashedPassword, }, }); res.status(201).json({ message: 'User created successfully' }); } catch (error) { res.status(400).json({ error: 'Email already exists' }); } }); module.exports = router; Step 2: User Login (/login) When a user logs in, you find them, check their password, and—if it's correct—you create and send them a JWT. JavaScript // /routes/auth.js (continued) const jwt = require('jsonwebtoken'); // !! IMPORTANT: Store your secret in a .env file, not in the code! const JWT_SECRET = process.env.JWT_SECRET || 'your-very-secret-key-that-is-long'; router.post('/login', async (req, res) => { const { email, password } = req.body; // 1. Find the user const user = await prisma.user.findUnique({ where: { email }, }); if (!user) { return res.status(400).json({ error: 'Invalid email or password' }); } // 2. Check the password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(400).json({ error: 'Invalid email or password' }); } // 3. Create the JWT const token = jwt.sign( { userId: user.id, email: user.email }, JWT_SECRET, { expiresIn: '1h' } // Token expires in 1 hour ); // 4. Send the token res.json({ message: 'Login successful', token: token, }); }); Step 3: Auth Middleware (To Protect Routes) This is the most important part. We need a "gatekeeper" function (middleware) to check for a valid token on any route we want to protect. JavaScript // /middleware/auth.js const jwt = require('jsonwebtoken'); const JWT_SECRET = process.env.JWT_SECRET; const authMiddleware = (req, res, next) => { // 1. Get the token from the header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Access denied. No token provided.' }); } const token = authHeader.split(' ')[1]; // "Bearer TOKEN" // 2. Verify the token try { const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; // Add the user payload to the request object next(); // Move to the next function (the protected route) } catch (ex) { res.status(400).json({ error: 'Invalid token.' }); } }; module.exports = authMiddleware; Now, you can easily protect any route: JavaScript // /routes/protected.js const express = require('express'); const router = express.Router(); const authMiddleware = require('../middleware/auth'); // This route is now protected! router.get('/profile', authMiddleware, (req, res) => { // Because of the middleware, we have access to req.user res.json({ message: `Welcome to your profile, ${req.user.email}`, userId: req.user.userId, }); }); module.exports = router; Part 2: The Frontend (React/Next.js) Your frontend is now responsible for: Storing the token after login. Sending the token with every request to a protected route. Redirecting users who aren't logged in. Step 1: Login and Store the Token When your login form is submitted, you call your API, get the token, and store it in localStorage. JavaScript // /pages/login.js import { useState } from 'react'; import axios from 'axios'; import { useRouter } from 'next/router'; export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const router = useRouter(); const handleSubmit = async (e) => { e.preventDefault(); try { const res = await axios.post('/api/login', { email, password }); // 1. Store the token localStorage.setItem('token', res.data.token); // 2. Redirect to the protected page router.push('/dashboard'); } catch (error) { console.error('Login failed', error); } }; // ... return login form JSX } Step 2: Send the Token with API Requests For any API call to a protected endpoint, you must include the token in the Authorization header. A great way to do this is to create a central axios instance (e.g., in /lib/api.js). JavaScript // /lib/api.js import axios from 'axios'; const api = axios.create({ baseURL: '/api', // Your API base URL }); // Interceptor: This runs BEFORE each request api.interceptors.request.use( (config) => { const token = localStorage.getItem('token'); if (token) { // Set the auth header config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } ); export default api; Now, you can import this api instance instead of axios everywhere, and the token will be attached automatically! JavaScript // /pages/dashboard.js import { useEffect, useState } from 'react'; import api from '../lib/api'; // <-- Import our custom instance export default function Dashboard() { const [profile, setProfile] = useState(null); useEffect(() => { api.get('/profile') // The token is auto-attached! .then(res => setProfile(res.data)) .catch(err => { // If token is invalid or expired, server sends 400/401 console.error(err); // We should redirect to login here }); }, []); if (!profile) return <div>Loading...</div>; return <h1>{profile.message}</h1>; } Step 3: Handle Logout Logout is simple: just remove the token and redirect. JavaScript // /components/Navbar.js import { useRouter } from 'next/router'; export default function Navbar() { const router = useRouter(); const handleLogout = () => { // 1. Remove the token localStorage.removeItem('token'); // 2. Redirect to login router.push('/login'); }; return ( <nav> {/* ... other links */} <button onClick={handleLogout}>Logout</button> </nav> ); } Conclusion & Next Steps You've now implemented a full-stack authentication flow! Backend: Creates a secure token on login and verifies it on protected routes using middleware. Frontend: Stores the token on login, attaches it to all API requests, and removes it on logout. Security Note: Storing the token in localStorage is common, but for maximum security, you should use HttpOnly cookies. This prevents malicious JavaScript from stealing the token (XSS attacks). This is a more complex setup but is the industry best practice.

Tags

#Authentication#JWT#Node.js#Express.js#React#Next.js#Web Development#Security#Full-Stack