iron-session

authentication-libraryNext.jsTypeScriptsession-managementsecurityencryptioncookie

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

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 }
    );
  }
}