Adding JWTs to a Fastify Server
A live session walkthrough of how to add JWTs to a Fastify server, covering practical implementation steps and best practices for JWT authentication.
Planted August 15, 2024
Adding JWTs to a Fastify Server
This blog post accompanies the YouTube video: Watch on YouTube
This tutorial walks through the practical steps of implementing JWT authentication in a Fastify server. This was recorded as a live coding session, showing real-world implementation challenges and solutions.
Overview
In this hands-on tutorial, we’ll implement JWT authentication in a Fastify application from scratch, covering:
- Setting up the Fastify JWT plugin
- Creating login endpoints
- Protecting routes with JWT middleware
- Handling token validation and errors
Project Setup
Let’s start by setting up our Fastify server with JWT support:
npm init -ynpm install fastify @fastify/jwt bcryptjs dotenvnpm install -D nodemon
Basic Server Setup
import Fastify from 'fastify';import jwt from '@fastify/jwt';import dotenv from 'dotenv';
dotenv.config();
const fastify = Fastify({ logger: true });
// Register JWT pluginawait fastify.register(jwt, { secret: process.env.JWT_SECRET || 'your-secret-key'});
const start = async () => { try { await fastify.listen({ port: 3000 }); fastify.log.info('Server listening on http://localhost:3000'); } catch (err) { fastify.log.error(err); process.exit(1); }};
start();
User Authentication Setup
Mock User Database
For this example, we’ll use a simple in-memory user store:
import bcrypt from 'bcryptjs';
// Mock user databaseconst users = [ { id: 1, username: 'admin', email: 'admin@example.com', passwordHash: await bcrypt.hash('password123', 10) }, { id: 2, username: 'user', email: 'user@example.com', passwordHash: await bcrypt.hash('userpass', 10) }];
// Helper function to find userconst findUser = (username) => { return users.find(user => user.username === username);};
// Helper function to validate passwordconst validatePassword = async (plainPassword, hashedPassword) => { return await bcrypt.compare(plainPassword, hashedPassword);};
Implementing Authentication Routes
Login Route
// Login endpointfastify.post('/login', async (request, reply) => { const { username, password } = request.body;
// Validate input if (!username || !password) { return reply.code(400).send({ error: 'Username and password are required' }); }
// Find user const user = findUser(username); if (!user) { return reply.code(401).send({ error: 'Invalid credentials' }); }
// Validate password const isValidPassword = await validatePassword(password, user.passwordHash); if (!isValidPassword) { return reply.code(401).send({ error: 'Invalid credentials' }); }
// Generate JWT token const token = fastify.jwt.sign({ userId: user.id, username: user.username, email: user.email }, { expiresIn: '1h' // Token expires in 1 hour });
return { message: 'Login successful', token, user: { id: user.id, username: user.username, email: user.email } };});
Registration Route (Optional)
fastify.post('/register', async (request, reply) => { const { username, email, password } = request.body;
// Check if user already exists const existingUser = findUser(username); if (existingUser) { return reply.code(409).send({ error: 'Username already exists' }); }
// Hash password const passwordHash = await bcrypt.hash(password, 10);
// Create new user const newUser = { id: users.length + 1, username, email, passwordHash };
users.push(newUser);
// Generate token for new user const token = fastify.jwt.sign({ userId: newUser.id, username: newUser.username, email: newUser.email }, { expiresIn: '1h' });
return { message: 'Registration successful', token, user: { id: newUser.id, username: newUser.username, email: newUser.email } };});
Protecting Routes with JWT
JWT Verification Hook
// JWT verification functionconst verifyJWT = async (request, reply) => { try { // This will verify the JWT and populate request.user await request.jwtVerify(); } catch (err) { reply.code(401).send({ error: 'Authentication required', message: 'Please provide a valid token' }); }};
Protected Routes
// Protected route examplefastify.get('/profile', { preHandler: [verifyJWT]}, async (request, reply) => { // request.user is available here after successful JWT verification const user = findUser(request.user.username);
if (!user) { return reply.code(404).send({ error: 'User not found' }); }
return { message: 'Profile data', user: { id: user.id, username: user.username, email: user.email } };});
// Another protected routefastify.get('/dashboard', { preHandler: [verifyJWT]}, async (request, reply) => { return { message: `Welcome to your dashboard, ${request.user.username}!`, data: { userId: request.user.userId, lastLogin: new Date().toISOString() } };});
Advanced JWT Handling
Token Refresh
fastify.post('/refresh', { preHandler: [verifyJWT]}, async (request, reply) => { // Generate new token with extended expiration const newToken = fastify.jwt.sign({ userId: request.user.userId, username: request.user.username, email: request.user.email }, { expiresIn: '1h' });
return { message: 'Token refreshed', token: newToken };});
Logout Route
// Note: With JWTs, logout is typically handled client-side// by removing the token. For server-side logout, you'd need// to maintain a blacklist of invalidated tokensfastify.post('/logout', { preHandler: [verifyJWT]}, async (request, reply) => { // In a real application, you might: // 1. Add token to a blacklist // 2. Store invalidated tokens in Redis // 3. Use shorter expiration times
return { message: 'Logout successful' };});
Error Handling
Global Error Handler
// Global error handler for authentication errorsfastify.setErrorHandler((error, request, reply) => { // JWT related errors if (error.statusCode === 401) { reply.code(401).send({ error: 'Unauthorized', message: 'Invalid or expired token' }); return; }
// Other errors fastify.log.error(error); reply.code(500).send({ error: 'Internal Server Error' });});
Testing the Implementation
Using curl
# Login to get tokencurl -X POST http://localhost:3000/login \ -H "Content-Type: application/json" \ -d '{"username": "admin", "password": "password123"}'
# Use token to access protected routecurl -X GET http://localhost:3000/profile \ -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"
Frontend Integration
// Frontend exampleconst login = async (username, password) => { const response = await fetch('/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) });
const data = await response.json();
if (response.ok) { // Store token (consider using httpOnly cookies in production) localStorage.setItem('token', data.token); return data; } else { throw new Error(data.error); }};
// Making authenticated requestsconst fetchProfile = async () => { const token = localStorage.getItem('token');
const response = await fetch('/profile', { headers: { 'Authorization': `Bearer ${token}` } });
return await response.json();};
Security Best Practices
- Use HTTPS in production - Never send JWTs over unencrypted connections
- Strong secrets - Use cryptographically secure random secrets
- Short expiration times - Implement refresh token mechanism
- Validate all inputs - Never trust client data
- Rate limiting - Implement rate limiting on auth endpoints
- Proper error handling - Don’t leak sensitive information in errors
Additional Resources
Next Steps
- Implement refresh token mechanism
- Add role-based authorization
- Set up token blacklisting for logout
- Integrate with a real database
- Add rate limiting and security middleware
This post is part of my YouTube tutorial series. Subscribe to my channel for more tutorials!