iron-session
Authentication Library
iron-session
Overview
iron-session is a lightweight and secure session management library designed specifically for Next.js applications. As of 2025, it fully supports both Next.js App Router and Pages Router, achieving stateless session management through encrypted cookie-based state persistence. It requires no JWT tokens or database session storage, using the @hapi/iron algorithm to securely encrypt and sign session data stored as cookies. It provides type safety through TypeScript integration and works seamlessly with server-side rendering, API Routes, and middleware.
Details
iron-session is a Next.js-specific library that provides cookie-based encrypted session management. Key features:
- Encrypted Sessions: Strong encryption and HMAC signing using @hapi/iron algorithm
- Next.js Optimization: Complete integration with App Router, Pages Router, API Routes, and middleware
- TypeScript Integration: Type-safe session objects and auto-completion features
- Stateless: Completely stateless design requiring no database or Redis
- Security: CSRF protection, session rotation, TTL settings
- Lightweight: Minimal dependencies with reduced bundle size
- Flexibility: Custom cookie settings, session structure, and encryption options
Pros and Cons
Pros
- Stateless design eliminates database requirements, significantly reducing operational costs and infrastructure load
- Strong encryption completely prevents session data eavesdropping and tampering
- Deep Next.js integration provides high performance with server-side rendering
- Full TypeScript support ensures type safety and maintainability during development
- Lightweight design optimizes page load speed and bundle size
- Simple API design has low learning curve and easy adoption
Cons
- Next.js-specific, cannot be used with other frameworks
- Cookie size limit (4KB) makes storing large amounts of session data difficult
- Immediate session invalidation is difficult (due to encrypted cookies)
- Requires same encryption key when sharing sessions across multiple servers
- Advanced session analytics features require separate implementation
Reference Pages
- GitHub - vvo/iron-session
- iron-session Documentation
- Next.js Authentication Examples
- @hapi/iron Documentation
Code Examples
Basic Setup and Session Configuration
// lib/session.ts - Session configuration and helper functions
import { getIronSession, IronSession } from 'iron-session';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
// Session data type definition
export interface SessionData {
userId?: string;
username?: string;
email?: string;
role?: 'admin' | 'user' | 'guest';
isLoggedIn: boolean;
loginAt?: number;
expiresAt?: number;
}
// Session configuration
export const sessionOptions = {
password: process.env.SESSION_SECRET_KEY!,
cookieName: 'myapp-session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 60 * 60 * 24 * 7, // 7 days
sameSite: 'lax' as const,
path: '/',
},
ttl: 60 * 60 * 24 * 7, // 7 days (in seconds)
};
// Default session data
export const defaultSession: SessionData = {
isLoggedIn: false,
};
// Get session on server-side
export async function getSession(): Promise<IronSession<SessionData>> {
const session = await getIronSession<SessionData>(cookies(), sessionOptions);
// Set default values if session is empty
if (!session.isLoggedIn) {
Object.assign(session, defaultSession);
}
return session;
}
// Get session for API Routes
export async function getSessionFromRequest(
request: NextRequest
): Promise<IronSession<SessionData>> {
const response = new NextResponse();
const session = await getIronSession<SessionData>(
request,
response,
sessionOptions
);
if (!session.isLoggedIn) {
Object.assign(session, defaultSession);
}
return session;
}
// Session validity check
export function isSessionValid(session: SessionData): boolean {
if (!session.isLoggedIn || !session.expiresAt) {
return false;
}
return Date.now() < session.expiresAt;
}
// Session refresh
export async function refreshSession(session: IronSession<SessionData>): Promise<void> {
if (session.isLoggedIn) {
session.loginAt = Date.now();
session.expiresAt = Date.now() + (sessionOptions.ttl * 1000);
await session.save();
}
}
Login/Logout Implementation with App Router
// app/api/auth/login/route.ts - Login API
import { NextRequest, NextResponse } from 'next/server';
import { getIronSession } from 'iron-session';
import { sessionOptions } from '@/lib/session';
import bcrypt from 'bcryptjs';
interface LoginRequest {
email: string;
password: string;
rememberMe?: boolean;
}
export async function POST(request: NextRequest) {
try {
const { email, password, rememberMe }: LoginRequest = await request.json();
// Validation
if (!email || !password) {
return NextResponse.json(
{ error: 'Email and password are required' },
{ status: 400 }
);
}
// User authentication (fetch from database in practice)
const user = await authenticateUser(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Create session
const response = new NextResponse(
JSON.stringify({ success: true, user: { id: user.id, email: user.email, role: user.role } })
);
const session = await getIronSession(request, response, {
...sessionOptions,
cookieOptions: {
...sessionOptions.cookieOptions,
maxAge: rememberMe ? 60 * 60 * 24 * 30 : sessionOptions.cookieOptions.maxAge, // 30 days or 7 days
},
});
// Set session data
session.userId = user.id;
session.username = user.username;
session.email = user.email;
session.role = user.role;
session.isLoggedIn = true;
session.loginAt = Date.now();
session.expiresAt = Date.now() + (rememberMe ? 30 * 24 * 60 * 60 * 1000 : 7 * 24 * 60 * 60 * 1000);
await session.save();
return response;
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// User authentication function (dummy implementation)
async function authenticateUser(email: string, password: string) {
// In practice, fetch user from database
const users = [
{
id: '1',
username: 'admin',
email: '[email protected]',
password: await bcrypt.hash('password123', 10),
role: 'admin' as const,
},
{
id: '2',
username: 'user',
email: '[email protected]',
password: await bcrypt.hash('userpass', 10),
role: 'user' as const,
},
];
const user = users.find(u => u.email === email);
if (!user) return null;
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) return null;
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
};
}
// app/api/auth/logout/route.ts - Logout API
export async function POST(request: NextRequest) {
try {
const response = new NextResponse(
JSON.stringify({ success: true, message: 'Logged out successfully' })
);
const session = await getIronSession(request, response, sessionOptions);
// Clear session
session.destroy();
return response;
} catch (error) {
console.error('Logout error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// app/api/auth/me/route.ts - Get current user information
export async function GET(request: NextRequest) {
try {
const response = new NextResponse();
const session = await getIronSession(request, response, sessionOptions);
if (!session.isLoggedIn || !isSessionValid(session)) {
return NextResponse.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
// Extend session
await refreshSession(session);
return NextResponse.json({
user: {
userId: session.userId,
username: session.username,
email: session.email,
role: session.role,
loginAt: session.loginAt,
},
});
} catch (error) {
console.error('Get user error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
Login Form and Client-Side Implementation
// components/LoginForm.tsx - Login form component
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
interface LoginFormProps {
onSuccess?: () => void;
}
export default function LoginForm({ onSuccess }: LoginFormProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [rememberMe, setRememberMe] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
rememberMe,
}),
});
const data = await response.json();
if (response.ok) {
router.refresh(); // Re-render server components
onSuccess?.();
router.push('/dashboard');
} else {
setError(data.error || 'Login failed');
}
} catch (err) {
setError('Network error occurred');
console.error('Login error:', err);
} finally {
setIsLoading(false);
}
};
return (
<div className="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
{error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="[email protected]"
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="••••••••"
/>
</div>
<div className="mb-6">
<label className="flex items-center">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<span className="ml-2 text-sm text-gray-700">
Keep me logged in (30 days)
</span>
</label>
</div>
<button
type="submit"
disabled={isLoading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Processing...' : 'Login'}
</button>
</form>
<div className="mt-4 text-center">
<p className="text-sm text-gray-600">
Test accounts:<br />
Admin: [email protected] / password123<br />
User: [email protected] / userpass
</p>
</div>
</div>
);
}
// hooks/useAuth.ts - Authentication state management custom hook
import { useState, useEffect } from 'react';
interface User {
userId: string;
username: string;
email: string;
role: 'admin' | 'user' | 'guest';
loginAt: number;
}
interface AuthState {
user: User | null;
isLoading: boolean;
error: string | null;
}
export function useAuth() {
const [state, setState] = useState<AuthState>({
user: null,
isLoading: true,
error: null,
});
const fetchUser = async () => {
try {
setState(prev => ({ ...prev, isLoading: true, error: null }));
const response = await fetch('/api/auth/me', {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setState({
user: data.user,
isLoading: false,
error: null,
});
} else {
setState({
user: null,
isLoading: false,
error: null,
});
}
} catch (error) {
setState({
user: null,
isLoading: false,
error: 'Failed to fetch user',
});
}
};
const logout = async () => {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
setState({
user: null,
isLoading: false,
error: null,
});
} catch (error) {
console.error('Logout error:', error);
}
};
useEffect(() => {
fetchUser();
}, []);
return {
...state,
refetch: fetchUser,
logout,
};
}
Session Authentication in Server Components
// app/dashboard/page.tsx - Protected dashboard page
import { redirect } from 'next/navigation';
import { getSession, isSessionValid } from '@/lib/session';
import UserProfile from '@/components/UserProfile';
import LogoutButton from '@/components/LogoutButton';
export default async function DashboardPage() {
const session = await getSession();
// Session authentication check
if (!session.isLoggedIn || !isSessionValid(session)) {
redirect('/login');
}
return (
<div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Dashboard</h1>
<LogoutButton />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* User information card */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">User Information</h2>
<div className="space-y-2">
<p><strong>Username:</strong> {session.username}</p>
<p><strong>Email:</strong> {session.email}</p>
<p><strong>Role:</strong> {session.role}</p>
<p><strong>Login Time:</strong> {
session.loginAt ? new Date(session.loginAt).toLocaleString() : 'Unknown'
}</p>
</div>
</div>
{/* Session information card */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Session Information</h2>
<div className="space-y-2">
<p><strong>User ID:</strong> {session.userId}</p>
<p><strong>Login Status:</strong> {session.isLoggedIn ? 'Active' : 'Inactive'}</p>
<p><strong>Expires:</strong> {
session.expiresAt ? new Date(session.expiresAt).toLocaleString() : 'Not set'
}</p>
</div>
</div>
{/* Feature card */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-semibold mb-4">Available Features</h2>
<div className="space-y-2">
{session.role === 'admin' && (
<p className="text-green-600">• Admin Functions</p>
)}
<p className="text-blue-600">• Profile Management</p>
<p className="text-blue-600">• Data Viewing</p>
<p className="text-blue-600">• Settings</p>
</div>
</div>
</div>
<UserProfile session={session} />
</div>
);
}
// components/UserProfile.tsx - User profile management
import { SessionData } from '@/lib/session';
interface UserProfileProps {
session: SessionData;
}
export default function UserProfile({ session }: UserProfileProps) {
return (
<div className="mt-8 bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-semibold mb-6">Profile Settings</h2>
<form className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Display Name
</label>
<input
type="text"
defaultValue={session.username}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
type="email"
defaultValue={session.email}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Update Profile
</button>
</form>
</div>
);
}
// components/LogoutButton.tsx - Logout button
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
export default function LogoutButton() {
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const handleLogout = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
if (response.ok) {
router.push('/login');
router.refresh();
}
} catch (error) {
console.error('Logout error:', error);
} finally {
setIsLoading(false);
}
};
return (
<button
onClick={handleLogout}
disabled={isLoading}
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 disabled:opacity-50"
>
{isLoading ? 'Logging out...' : 'Logout'}
</button>
);
}
Authentication Guards with Middleware
// middleware.ts - Authentication with Next.js middleware
import { NextRequest, NextResponse } from 'next/server';
import { getIronSession } from 'iron-session';
import { sessionOptions, SessionData, isSessionValid } from '@/lib/session';
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
try {
const session = await getIronSession<SessionData>(
request,
response,
sessionOptions
);
const { pathname } = request.nextUrl;
// Define protected routes
const protectedRoutes = ['/dashboard', '/profile', '/admin'];
const adminRoutes = ['/admin'];
const authRoutes = ['/login', '/register'];
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
const isAdminRoute = adminRoutes.some(route =>
pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some(route =>
pathname.startsWith(route)
);
// Redirect logged-in users from auth pages
if (isAuthRoute && session.isLoggedIn && isSessionValid(session)) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// Redirect unauthenticated users from protected pages
if (isProtectedRoute && (!session.isLoggedIn || !isSessionValid(session))) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
// Admin access control
if (isAdminRoute && session.role !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// API route authentication check
if (pathname.startsWith('/api/') && !pathname.startsWith('/api/auth/')) {
if (!session.isLoggedIn || !isSessionValid(session)) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
}
return response;
} catch (error) {
console.error('Middleware error:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|public).*)',
],
};
// lib/auth-guards.ts - Authentication guard functions
import { redirect } from 'next/navigation';
import { getSession, isSessionValid } from './session';
export async function requireAuth() {
const session = await getSession();
if (!session.isLoggedIn || !isSessionValid(session)) {
redirect('/login');
}
return session;
}
export async function requireAdmin() {
const session = await requireAuth();
if (session.role !== 'admin') {
redirect('/dashboard');
}
return session;
}
export async function requireRole(role: 'admin' | 'user') {
const session = await requireAuth();
if (session.role !== role && session.role !== 'admin') {
redirect('/dashboard');
}
return session;
}
// Usage example: app/admin/page.tsx
export default async function AdminPage() {
const session = await requireAdmin();
return (
<div>
<h1>Admin Page</h1>
<p>Welcome, {session.username}</p>
</div>
);
}
Advanced Session Management Features
// lib/advanced-session.ts - Advanced session management features
import { getIronSession, IronSession } from 'iron-session';
import { cookies } from 'next/headers';
import { sessionOptions, SessionData } from './session';
// Session activity log
interface SessionActivity {
timestamp: number;
action: 'login' | 'logout' | 'access' | 'refresh';
ip?: string;
userAgent?: string;
path?: string;
}
// Extended session data
export interface ExtendedSessionData extends SessionData {
activities?: SessionActivity[];
lastAccess?: number;
deviceFingerprint?: string;
csrfToken?: string;
}
// Log session activity
export async function logActivity(
session: IronSession<ExtendedSessionData>,
action: SessionActivity['action'],
metadata?: Partial<SessionActivity>
) {
if (!session.activities) {
session.activities = [];
}
const activity: SessionActivity = {
timestamp: Date.now(),
action,
...metadata,
};
session.activities.push(activity);
// Keep only latest 10 activities
if (session.activities.length > 10) {
session.activities = session.activities.slice(-10);
}
session.lastAccess = Date.now();
await session.save();
}
// CSRF token generation and validation
export function generateCSRFToken(): string {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
export async function setCSRFToken(session: IronSession<ExtendedSessionData>) {
session.csrfToken = generateCSRFToken();
await session.save();
return session.csrfToken;
}
export function validateCSRFToken(
session: ExtendedSessionData,
token: string
): boolean {
return session.csrfToken === token;
}
// Session analysis
export function analyzeSession(session: ExtendedSessionData) {
const now = Date.now();
const loginDuration = session.loginAt ? now - session.loginAt : 0;
const lastAccessDuration = session.lastAccess ? now - session.lastAccess : 0;
return {
loginDuration: Math.floor(loginDuration / 1000 / 60), // minutes
lastAccessMinutes: Math.floor(lastAccessDuration / 1000 / 60), // minutes
activityCount: session.activities?.length || 0,
recentActions: session.activities?.slice(-5) || [],
sessionHealth: lastAccessDuration < 30 * 60 * 1000 ? 'active' : 'idle', // 30 minutes
};
}
// Session cleanup
export async function cleanupExpiredSession(
session: IronSession<ExtendedSessionData>
) {
if (!isSessionValid(session)) {
await logActivity(session, 'logout', {
path: '/cleanup',
userAgent: 'system'
});
session.destroy();
return true;
}
return false;
}
// Security audit
export function auditSession(session: ExtendedSessionData) {
const now = Date.now();
const issues: string[] = [];
// Long session duration warning
if (session.loginAt && (now - session.loginAt) > 24 * 60 * 60 * 1000) {
issues.push('Long session duration detected');
}
// Inactive session warning
if (session.lastAccess && (now - session.lastAccess) > 2 * 60 * 60 * 1000) {
issues.push('Inactive session detected');
}
// Unusual activity patterns
const recentActivities = session.activities?.filter(
a => now - a.timestamp < 60 * 60 * 1000
) || [];
if (recentActivities.length > 50) {
issues.push('Unusual activity pattern detected');
}
return {
issues,
riskLevel: issues.length > 0 ? 'medium' : 'low',
recommendations: issues.map(issue => {
switch (issue) {
case 'Long session duration detected':
return 'Consider implementing session rotation';
case 'Inactive session detected':
return 'Consider automatic logout for inactive sessions';
case 'Unusual activity pattern detected':
return 'Review recent activity for potential security issues';
default:
return 'Review session security settings';
}
}),
};
}
// app/api/session/info/route.ts - Session information API
export async function GET() {
try {
const session = await getSession() as IronSession<ExtendedSessionData>;
if (!session.isLoggedIn) {
return NextResponse.json(
{ error: 'Not authenticated' },
{ status: 401 }
);
}
const analysis = analyzeSession(session);
const audit = auditSession(session);
// Log activity
await logActivity(session, 'access', {
path: '/api/session/info'
});
return NextResponse.json({
session: {
userId: session.userId,
username: session.username,
role: session.role,
loginAt: session.loginAt,
lastAccess: session.lastAccess,
},
analysis,
audit,
activities: session.activities?.slice(-5) || [],
});
} catch (error) {
console.error('Session info error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}