Understanding Authentication in Fastify
Authentication is the backbone of modern web applications. In Fastify, one of Node.js's fastest web frameworks, implementing robust authentication requires understanding the ecosystem of plugins, strategies, and patterns that integrate seamlessly with its high-performance architecture. This tutorial covers three essential authentication mechanisms — JWT (JSON Web Tokens), session-based auth, and OAuth integration — providing complete, production-ready code examples for each.
Fastify's plugin architecture and its encapsulation system make it uniquely suited for modular authentication setups. Unlike Express, Fastify enforces a clear separation of concerns through its plugin registration scope, which prevents cross-plugin pollution and enhances security. Before diving into code, let's understand why authentication matters in the Fastify context: Fastify's high throughput means authentication logic must be equally performant. A slow authentication middleware can bottleneck your entire application, negating Fastify's speed advantages.
Project Setup and Dependencies
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Create a new Fastify project and install the core dependencies we'll use across all authentication strategies:
mkdir fastify-auth-tutorial && cd fastify-auth-tutorial
npm init -y
npm install fastify @fastify/jwt @fastify/cookie @fastify/session @fastify/csrf-protection @fastify/formbody fastify-oauth2 bcrypt dotenv
Here's what each package does:
@fastify/jwt— Official Fastify plugin for JWT signing and verification@fastify/cookie— Parse and set cookies with signature support@fastify/session— Server-side session management@fastify/csrf-protection— CSRF token generation and validationfastify-oauth2— Third-party plugin for OAuth2 providersbcrypt— Password hashing librarydotenv— Environment variable management
Part 1: JWT Authentication in Fastify
What Are JWTs and Why Use Them in Fastify?
JSON Web Tokens are compact, URL-safe tokens that carry claims (user data, permissions, expiry) as a self-contained JSON payload. They are cryptographically signed, allowing stateless authentication — the server doesn't need to store session data. This aligns perfectly with Fastify's design philosophy: minimal overhead, maximum throughput. JWTs shine in microservices architectures, API gateways, and mobile backends where session state is impractical.
Configuring @fastify/jwt
The @fastify/jwt plugin is the official Fastify module for JWT operations. It decorates the Fastify instance with jwt utilities and adds a verifyJWT decorator for route-level protection. Here's a complete setup with environment-based secrets:
// .env
JWT_SECRET=your-super-secret-key-at-least-32-chars
JWT_REFRESH_SECRET=your-refresh-secret-key
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
// src/config/jwt.config.js
import dotenv from 'dotenv';
dotenv.config();
export const jwtConfig = {
secret: process.env.JWT_SECRET,
refreshSecret: process.env.JWT_REFRESH_SECRET,
signOptions: {
expiresIn: process.env.JWT_EXPIRES_IN || '15m',
issuer: 'fastify-auth-tutorial',
},
refreshSignOptions: {
expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
issuer: 'fastify-auth-tutorial',
},
};
Building the JWT Auth Plugin
Fastify encourages encapsulating authentication logic in dedicated plugins. This prevents secret leakage across scopes and keeps your codebase organized. Below is a complete JWT authentication plugin that handles token signing, verification, refresh logic, and route decoration:
// src/plugins/jwt-auth.plugin.js
import fp from 'fastify-plugin';
import { jwtConfig } from '../config/jwt.config.js';
async function jwtAuthPlugin(fastify, opts) {
// Register the JWT plugin
await fastify.register(import('@fastify/jwt'), {
secret: jwtConfig.secret,
sign: jwtConfig.signOptions,
});
// Decorate Fastify with a custom authenticate method
fastify.decorate('authenticate', async function (request, reply) {
try {
await request.jwtVerify();
} catch (err) {
reply.code(401).send({
error: 'Unauthorized',
message: 'Invalid or expired token',
statusCode: 401,
});
}
});
// Decorate with role-based authorization helper
fastify.decorate('authorize', function (requiredRole) {
return async function (request, reply) {
// authenticate runs first
if (!request.user) {
return reply.code(401).send({ error: 'Not authenticated' });
}
if (request.user.role !== requiredRole) {
return reply.code(403).send({
error: 'Forbidden',
message: `Role '${requiredRole}' required`,
statusCode: 403,
});
}
};
});
// Generate access token
fastify.decorate('signAccessToken', function (payload) {
return fastify.jwt.sign(payload, {
expiresIn: jwtConfig.signOptions.expiresIn,
});
});
// Generate refresh token with longer expiry
fastify.decorate('signRefreshToken', function (payload) {
return fastify.jwt.sign(payload, {
expiresIn: jwtConfig.refreshSignOptions.expiresIn,
secret: jwtConfig.refreshSecret, // Different secret for refresh tokens
});
});
// Verify refresh token
fastify.decorate('verifyRefreshToken', async function (token) {
return fastify.jwt.verify(token, {
secret: jwtConfig.refreshSecret,
});
});
}
export default fp(jwtAuthPlugin, {
name: 'jwt-auth',
dependencies: [],
});
User Registration and Login Routes
Now let's build the authentication routes. We'll use bcrypt for password hashing and generate token pairs (access + refresh) on login:
// src/routes/auth.routes.js
import bcrypt from 'bcrypt';
// In-memory user store for demo — use a real database in production
const users = new Map();
export default async function authRoutes(fastify, opts) {
// Registration endpoint
fastify.post('/register', {
schema: {
body: {
type: 'object',
required: ['email', 'password', 'name'],
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 },
name: { type: 'string', minLength: 2 },
role: { type: 'string', enum: ['user', 'admin'], default: 'user' },
},
},
},
}, async (request, reply) => {
const { email, password, name, role } = request.body;
// Check if user exists
if (users.has(email)) {
return reply.code(409).send({ error: 'Email already registered' });
}
// Hash password
const saltRounds = 12;
const passwordHash = await bcrypt.hash(password, saltRounds);
// Store user
const user = {
id: crypto.randomUUID(),
email,
name,
role: role || 'user',
passwordHash,
createdAt: new Date().toISOString(),
};
users.set(email, user);
// Generate tokens
const tokenPayload = { sub: user.id, email: user.email, role: user.role };
const accessToken = fastify.signAccessToken(tokenPayload);
const refreshToken = fastify.signRefreshToken(tokenPayload);
reply.code(201).send({
message: 'User registered successfully',
user: { id: user.id, email: user.email, name: user.name, role: user.role },
accessToken,
refreshToken,
});
});
// Login endpoint
fastify.post('/login', {
schema: {
body: {
type: 'object',
required: ['email', 'password'],
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string' },
},
},
},
}, async (request, reply) => {
const { email, password } = request.body;
const user = users.get(email);
if (!user) {
return reply.code(401).send({ error: 'Invalid credentials' });
}
const passwordValid = await bcrypt.compare(password, user.passwordHash);
if (!passwordValid) {
return reply.code(401).send({ error: 'Invalid credentials' });
}
const tokenPayload = { sub: user.id, email: user.email, role: user.role };
const accessToken = fastify.signAccessToken(tokenPayload);
const refreshToken = fastify.signRefreshToken(tokenPayload);
reply.send({
message: 'Login successful',
user: { id: user.id, email: user.email, name: user.name, role: user.role },
accessToken,
refreshToken,
});
});
// Token refresh endpoint
fastify.post('/refresh', async (request, reply) => {
const { refreshToken } = request.body;
if (!refreshToken) {
return reply.code(400).send({ error: 'Refresh token required' });
}
try {
const decoded = await fastify.verifyRefreshToken(refreshToken);
const user = Array.from(users.values()).find(u => u.id === decoded.sub);
if (!user) {
return reply.code(401).send({ error: 'User not found' });
}
const newPayload = { sub: user.id, email: user.email, role: user.role };
const newAccessToken = fastify.signAccessToken(newPayload);
const newRefreshToken = fastify.signRefreshToken(newPayload);
reply.send({ accessToken: newAccessToken, refreshToken: newRefreshToken });
} catch (err) {
reply.code(401).send({ error: 'Invalid refresh token' });
}
});
// Logout (client-side token deletion — JWT is stateless)
fastify.post('/logout', async (request, reply) => {
// In production, add the refresh token to a deny list
reply.send({ message: 'Logged out. Please discard tokens client-side.' });
});
}
Protected Routes Using JWT
With the plugin and routes in place, you can now protect any route using the authenticate decorator. Here's how to set up protected endpoints with role-based access:
// src/routes/protected.routes.js
export default async function protectedRoutes(fastify, opts) {
// All routes in this scope require authentication
fastify.addHook('onRequest', fastify.authenticate);
// Any authenticated user can access this
fastify.get('/profile', async (request, reply) => {
reply.send({
message: 'Protected profile data',
user: request.user, // { sub, email, role, iat, exp, iss }
timestamp: new Date().toISOString(),
});
});
// Admin-only route using the authorize decorator
fastify.get('/admin/dashboard', {
preHandler: fastify.authorize('admin'),
}, async (request, reply) => {
reply.send({
message: 'Admin dashboard — sensitive data',
stats: { totalUsers: users.size, uptime: process.uptime() },
});
});
// Route with multiple roles allowed
fastify.get('/manage', {
preHandler: async (request, reply) => {
const allowedRoles = ['admin', 'manager'];
if (!allowedRoles.includes(request.user.role)) {
return reply.code(403).send({ error: 'Insufficient permissions' });
}
},
}, async (request, reply) => {
reply.send({ message: 'Management area' });
});
}
Assembling the JWT Application
Here's the complete server file that wires everything together:
// src/server-jwt.js
import Fastify from 'fastify';
import jwtAuthPlugin from './plugins/jwt-auth.plugin.js';
import authRoutes from './routes/auth.routes.js';
import protectedRoutes from './routes/protected.routes.js';
async function buildApp() {
const fastify = Fastify({
logger: true,
trustProxy: true,
});
// Register JWT auth plugin
await fastify.register(jwtAuthPlugin);
// Register routes
await fastify.register(authRoutes, { prefix: '/api/auth' });
await fastify.register(protectedRoutes, { prefix: '/api' });
// Health check
fastify.get('/health', async () => ({ status: 'ok', timestamp: Date.now() }));
return fastify;
}
const app = await buildApp();
try {
await app.listen({ port: 3000, host: '0.0.0.0' });
console.log('JWT Auth server running on http://localhost:3000');
} catch (err) {
app.log.error(err);
process.exit(1);
}
Testing JWT Endpoints
You can test the flow with curl or any HTTP client:
# Register a user
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"secretpass123","name":"Alice"}'
# Login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"secretpass123"}'
# Access protected profile (replace TOKEN with accessToken from login response)
curl http://localhost:3000/api/profile \
-H "Authorization: Bearer TOKEN"
# Refresh token
curl -X POST http://localhost:3000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"YOUR_REFRESH_TOKEN"}'
Part 2: Session-Based Authentication
When Sessions Beat JWTs
While JWTs excel in stateless scenarios, session-based authentication remains superior for traditional server-rendered applications, multi-page web apps, and situations requiring immediate token invalidation. Sessions store user state on the server, giving you full control over session lifecycle. Fastify's @fastify/session plugin makes session management straightforward and secure.
Session Configuration
Set up a session store. For production, use Redis or a database. For development, an in-memory store works but won't survive restarts:
// src/config/session.config.js
export const sessionConfig = {
secret: process.env.SESSION_SECRET || 'a-very-long-session-secret-string-32-chars-min',
cookieName: 'session_id',
cookie: {
maxAge: 86400000, // 24 hours
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
httpOnly: true, // Prevent JavaScript access
sameSite: 'lax', // CSRF protection
path: '/',
},
saveUninitialized: false,
};
Session Auth Plugin
Create a dedicated session plugin that handles login verification and user deserialization:
// src/plugins/session-auth.plugin.js
import fp from 'fastify-plugin';
import { sessionConfig } from '../config/session.config.js';
async function sessionAuthPlugin(fastify, opts) {
// Register cookie parser (required for sessions)
await fastify.register(import('@fastify/cookie'));
// Register session plugin
await fastify.register(import('@fastify/session'), {
...sessionConfig,
store: opts.store || new Map(), // Pass a custom store or use in-memory
});
// CSRF protection — essential for session-based auth
await fastify.register(import('@fastify/csrf-protection'), {
session: true,
cookieOpts: {
httpOnly: true,
sameSite: 'strict',
path: '/',
secure: process.env.NODE_ENV === 'production',
},
});
// Decorate with session authentication check
fastify.decorate('ensureAuthenticated', async function (request, reply) {
if (!request.session || !request.session.userId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'Please log in to access this resource',
});
}
// Attach user data to request for downstream handlers
request.currentUser = request.session.userData;
});
// Decorate with login helper
fastify.decorate('loginUser', async function (request, userData) {
request.session.userId = userData.id;
request.session.userData = {
id: userData.id,
email: userData.email,
name: userData.name,
role: userData.role,
};
request.session.lastActivity = Date.now();
await request.session.save();
});
// Decorate with logout helper
fastify.decorate('logoutUser', async function (request) {
request.session.destroy();
});
}
export default fp(sessionAuthPlugin, {
name: 'session-auth',
dependencies: [],
});
Session-Based Auth Routes
Unlike JWT routes, session endpoints use cookies for transport and include CSRF protection:
// src/routes/session-auth.routes.js
import bcrypt from 'bcrypt';
const users = new Map(); // Shared store — use DB in production
export default async function sessionAuthRoutes(fastify, opts) {
// CSRF token endpoint — must be called before form submissions
fastify.get('/csrf-token', async (request, reply) => {
const token = await reply.generateCsrf();
reply.send({ csrfToken: token });
});
// Register with session
fastify.post('/register', {
schema: {
body: {
type: 'object',
required: ['email', 'password', 'name'],
properties: {
email: { type: 'string', format: 'email' },
password: { type: 'string', minLength: 8 },
name: { type: 'string', minLength: 2 },
},
},
},
}, async (request, reply) => {
const { email, password, name } = request.body;
if (users.has(email)) {
return reply.code(409).send({ error: 'Email already registered' });
}
const passwordHash = await bcrypt.hash(password, 12);
const user = {
id: crypto.randomUUID(),
email,
name,
role: 'user',
passwordHash,
createdAt: new Date().toISOString(),
};
users.set(email, user);
// Log user in immediately after registration
await fastify.loginUser(request, user);
reply.code(201).send({
message: 'Registered and logged in',
user: { id: user.id, email: user.email, name: user.name },
});
});
// Login with session
fastify.post('/login', async (request, reply) => {
const { email, password } = request.body;
const user = users.get(email);
if (!user) {
return reply.code(401).send({ error: 'Invalid credentials' });
}
const valid = await bcrypt.compare(password, user.passwordHash);
if (!valid) {
return reply.code(401).send({ error: 'Invalid credentials' });
}
await fastify.loginUser(request, user);
reply.send({
message: 'Logged in successfully',
user: { id: user.id, email: user.email, name: user.name },
});
});
// Logout — destroys server-side session
fastify.post('/logout', async (request, reply) => {
await fastify.logoutUser(request);
reply.send({ message: 'Logged out successfully' });
});
// Session status check
fastify.get('/session', {
preHandler: fastify.ensureAuthenticated,
}, async (request, reply) => {
reply.send({
session: {
userId: request.session.userId,
userData: request.session.userData,
lastActivity: new Date(request.session.lastActivity).toISOString(),
maxAge: request.session.cookie.maxAge,
},
});
});
}
Session-Based Protected Routes
// src/routes/session-protected.routes.js
export default async function sessionProtectedRoutes(fastify, opts) {
fastify.addHook('preHandler', fastify.ensureAuthenticated);
fastify.get('/dashboard', async (request, reply) => {
reply.send({
message: `Welcome back, ${request.currentUser.name}!`,
user: request.currentUser,
sessionActive: true,
});
});
// Route that updates session data
fastify.post('/update-activity', async (request, reply) => {
request.session.lastActivity = Date.now();
await request.session.save();
reply.send({ message: 'Activity timestamp updated' });
});
// Admin-only route
fastify.get('/admin', async (request, reply) => {
if (request.currentUser.role !== 'admin') {
return reply.code(403).send({ error: 'Admin access required' });
}
reply.send({ message: 'Admin area', users: Array.from(users.values()) });
});
}
Session Server Assembly
// src/server-session.js
import Fastify from 'fastify';
import sessionAuthPlugin from './plugins/session-auth.plugin.js';
import sessionAuthRoutes from './routes/session-auth.routes.js';
import sessionProtectedRoutes from './routes/session-protected.routes.js';
async function buildSessionApp() {
const fastify = Fastify({ logger: true, trustProxy: true });
// Custom session store (in-memory Map for demo)
const sessionStore = new Map();
// Implement basic store interface
const storeAdapter = {
set: (sid, session, cb) => { sessionStore.set(sid, session); cb(null); },
get: (sid, cb) => { cb(null, sessionStore.get(sid) || null); },
destroy: (sid, cb) => { sessionStore.delete(sid); cb(null); },
};
await fastify.register(sessionAuthPlugin, { store: storeAdapter });
await fastify.register(sessionAuthRoutes, { prefix: '/api/auth' });
await fastify.register(sessionProtectedRoutes, { prefix: '/api' });
return fastify;
}
const sessionApp = await buildSessionApp();
try {
await sessionApp.listen({ port: 3001, host: '0.0.0.0' });
console.log('Session Auth server running on http://localhost:3001');
} catch (err) {
sessionApp.log.error(err);
process.exit(1);
}
Testing Session Auth
# Register (session cookie is set automatically)
curl -X POST http://localhost:3001/api/auth/register \
-H "Content-Type: application/json" \
-c cookies.txt -b cookies.txt \
-d '{"email":"bob@example.com","password":"securepass456","name":"Bob"}'
# Access protected dashboard using stored cookies
curl http://localhost:3001/api/dashboard -b cookies.txt
# Logout
curl -X POST http://localhost:3001/api/auth/logout -b cookies.txt
# Verify session is destroyed
curl http://localhost:3001/api/dashboard -b cookies.txt
# Should return 401
Part 3: OAuth Integration with Fastify
OAuth Strategy Overview
OAuth 2.0 enables users to authenticate via third-party providers like Google, GitHub, or Microsoft. Fastify integrates with OAuth through the fastify-oauth2 plugin, which handles the authorization code flow. This approach delegates authentication to trusted providers, reducing your application's security surface while offering users a familiar login experience.
OAuth Plugin Configuration
Set up OAuth providers. We'll configure Google OAuth as an example, but the pattern works identically for GitHub, Facebook, and others:
// src/config/oauth.config.js
export const googleOAuthConfig = {
name: 'googleOAuth2',
scope: ['profile', 'email'],
credentials: {
client: {
id: process.env.GOOGLE_CLIENT_ID,
secret: process.env.GOOGLE_CLIENT_SECRET,
},
auth: {
authorizeHost: 'https://accounts.google.com',
authorizePath: '/o/oauth2/v2/auth',
tokenHost: 'https://oauth2.googleapis.com',
tokenPath: '/token',
},
},
callbackUri: 'http://localhost:3002/api/auth/google/callback',
verifyUser: async (fastify, credentials, data) => {
// data contains: access_token, refresh_token, id_token, expires_in
// Use the access token to fetch user profile from Google
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
headers: { Authorization: `Bearer ${data.access_token}` },
});
const profile = await response.json();
return {
id: profile.sub,
email: profile.email,
name: profile.name,
picture: profile.picture,
provider: 'google',
};
},
};
OAuth Plugin Implementation
// src/plugins/oauth.plugin.js
import fp from 'fastify-plugin';
import oauth2 from 'fastify-oauth2';
import { googleOAuthConfig } from '../config/oauth.config.js';
async function oauthPlugin(fastify, opts) {
// Register cookie and session support for OAuth state management
await fastify.register(import('@fastify/cookie'));
await fastify.register(import('@fastify/session'), {
secret: process.env.SESSION_SECRET || 'oauth-session-secret-key',
cookie: { secure: false, httpOnly: true, sameSite: 'lax' },
saveUninitialized: true,
});
// Register OAuth2 plugin
await fastify.register(oauth2, {
name: googleOAuthConfig.name,
scope: googleOAuthConfig.scope,
credentials: googleOAuthConfig.credentials,
callbackUri: googleOAuthConfig.callbackUri,
startRedirectPath: '/api/auth/google',
callbackUriPath: '/api/auth/google/callback',
generateState: true, // CSRF protection via state parameter
verifyUser: googleOAuthConfig.verifyUser,
});
// Decorate with OAuth login helper
fastify.decorate('oauthLogin', async function (request, user) {
// Store user in session after OAuth verification
request.session.userId = user.id;
request.session.userData = user;
request.session.authenticatedVia = user.provider;
await request.session.save();
});
// Decorate to check OAuth authentication
fastify.decorate('ensureOAuth', async function (request, reply) {
if (!request.session || !request.session.userId) {
return reply.code(401).send({
error: 'Unauthorized',
message: 'Please authenticate via OAuth provider',
});
}
request.currentUser = request.session.userData;
});
}
export default fp(oauthPlugin, {
name: 'oauth-auth',
dependencies: [],
});
OAuth Routes
// src/routes/oauth.routes.js
export default async function oauthRoutes(fastify, opts) {
// The OAuth plugin automatically creates:
// GET /api/auth/google — redirects to Google's consent screen
// GET /api/auth/google/callback — handles the callback from Google
// Override the callback handler to add custom logic
fastify.get('/api/auth/google/callback', async (request, reply) => {
// The plugin processes the callback and calls verifyUser
// We then handle the verified user
try {
// The plugin attaches the verified user to request
// Access via the plugin's internal mechanism
const user = request.session?.userData;
if (!user) {
return reply.code(401).send({ error: 'OAuth verification failed' });
}
await fastify.oauthLogin(request, user);
reply.send({
message: 'OAuth login successful',
user: {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
provider: user.provider,
},
});
} catch (err) {
request.log.error(err);
reply.code(500).send({ error: 'OAuth callback processing failed' });
}
});
// Logout — clears session
fastify.post('/logout', async (request, reply) => {
request.session.destroy();
reply.send({ message: 'Logged out. Session destroyed.' });
});
// User info endpoint
fastify.get('/me', {
preHandler: fastify.ensureOAuth,
}, async (request, reply) => {
reply.send({
user: request.currentUser,
authenticatedVia: request.session.authenticatedVia,
});
});
}
OAuth Server Assembly
// src/server-oauth.js
import Fastify from 'fastify';
import oauthPlugin from './plugins/oauth.plugin.js';
import oauthRoutes from './routes/oauth.routes.js';
async function buildOAuthApp() {
const fastify = Fastify({ logger: true, trustProxy: true });
await fastify.register(oauthPlugin);
await fastify.register(oauthRoutes);
// Protected route example
fastify.get('/api/dashboard', {
preHandler: fastify.ensureOAuth,
}, async (request, reply) => {
reply.send({
message: `Welcome, ${request.currentUser.name}!`,
provider: request.currentUser.provider,
email: request.currentUser.email,
});
});
return fastify;
}
const oauthApp = await buildOAuthApp();
try {
await oauthApp.listen({ port: 3002, host: '0.0.0.0' });
console.log('OAuth server running on http://localhost:3002');
console.log('Visit http://localhost:3002/api/auth/google to authenticate');
} catch (err) {
oauthApp.log.error(err);
process.exit(1);
}
Setting Up Google OAuth Credentials
For the OAuth flow to work, you need actual Google Cloud credentials:
# .env additions for OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
Steps to obtain these:
- Go to the Google Cloud Console
- Create a new project or select an existing one
- Navigate to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth client ID"
- Set application type to "Web application"
- Add
http://localhost:3002/api/auth/google/callbackto authorized redirect URIs - Copy the client ID and secret to your .env file
Part 4: Combining Authentication Strategies
Real-world applications often need multiple authentication methods. Fastify's plugin encapsulation allows you to compose strategies cleanly. Here's how to combine JWT and session auth in a single application:
// src/plugins/composite-auth.plugin.js
import fp from 'fastify-plugin';
async function compositeAuthPlugin(fastify, opts) {
// Register both JWT and session plugins in their own scopes
await fastify.register(import('./jwt-auth.plugin.js'));
await fastify.register(import('./session-auth.plugin.js'), {
store: opts.sessionStore,
});
// Unified authentication decorator — tries JWT first, falls back to session
fastify.decorate('unifiedAuth', async function (request, reply) {
const authHeader = request.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
// Try JWT authentication
try {
await request.jwtVerify();
request.authMethod = 'jwt';
return;
} catch (jwtErr) {
// JWT failed, fall through to session check
}
}
// Fall back to session authentication
if (request.session && request.session.userId) {
request.currentUser = request.session.userData;
request.authMethod = 'session';
return;
}
return reply.code(401).send({
error: 'Unauthorized',
message: 'Please authenticate using JWT (Bearer token) or session cookie',
});
});
// Route that works with either auth method
fastify.get('/api/unified/profile', {
preHandler: fastify.unifiedAuth,
}, async (request, reply) => {
reply.send({
message: 'Authenticated via either method',
authMethod: request.authMethod,
user: request.user || request.currentUser,
});
});
}
export default fp(compositeAuthPlugin, { name: 'composite-auth' });
Best Practices for Fastify Authentication
1. Use Environment Variables for Secrets
Never hardcode secrets, keys, or credentials. Use dotenv or a secrets manager. Rotate JWT signing keys periodically and use separate secrets for access and refresh tokens as demonstrated above.
2. Implement Token Deny Lists for JWT
Since JWTs are stateless, immediate invalidation is impossible without a deny list. For refresh tokens, maintain a Redis set of revoked token IDs. Check this set during token refresh:
// Example deny list check (conceptual)
const isRevoked = await redis.sismember('revoked_tokens', decoded.jti);
if (isRevoked) {
return reply.code(401).send({ error: 'Token has been revoked' });
}
3. Set Appropriate Cookie Flags for Sessions
Always use httpOnly: true, secure: true in production (HTTPS), and sameSite: 'lax' or 'strict'. These flags prevent XSS attacks from stealing session cookies and mitigate CS