
Adding Authentication Context to Next.js App Router with JWT
This article builds on a previous setup for JWT-based authentication with HTTP-only cookies in a Next.js application using the App Router. We'll create an authentication context to manage user authentication state and provide easy access to user data and authentication methods across the app.
Prerequisites
- A Next.js project with JWT authentication and HTTP-only cookies, as described in the previous article.
- Familiarity with React Context API and TypeScript.
Step 1: Setting Up the Authentication Context
Create a context to manage the authentication state and provide methods for login and logout.
File: app/context/AuthContext.tsx
'use client';
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from 'react';
import { useRouter } from 'next/navigation';
import jwt from 'jsonwebtoken';
// Define types for the user and context
interface User {
userId: number;
email: string;
}
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
// Verify token on initial load
useEffect(() => {
const verifyToken = async () => {
try {
const res = await fetch('/api/auth/verify', {
credentials: 'include',
});
if (res.ok) {
const { user } = await res.json();
setUser(user);
} else {
setUser(null);
}
} catch (error) {
setUser(null);
} finally {
setLoading(false);
}
};
verifyToken();
}, []);
// Login function
const login = async (email: string, password: string) => {
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
credentials: 'include',
});
if (res.ok) {
const { user } = await res.json();
setUser(user);
router.push('/dashboard');
} else {
throw new Error('Login failed');
}
} catch (error) {
throw new Error('Invalid credentials');
}
};
// Logout function
const logout = async () => {
try {
const res = await fetch('/api/auth/logout', {
credentials: 'include',
});
if (res.ok) {
setUser(null);
router.push('/login');
}
} catch (error) {
console.error('Logout failed:', error);
}
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
// Custom hook to use the AuthContext
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
This code:
- Creates a React Context for authentication state.
- Provides
user,login,logout, andloadingproperties. - Verifies the JWT on initial load to restore the user session.
- Handles login and logout operations, updating the context state accordingly.
Step 2: Creating the Verify API Route
Add an API route to verify the JWT and return user data.
File: app/api/auth/verify/route.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
export async function GET(req: NextRequest) {
const token = req.cookies.get('token')?.value;
if (!token) {
return NextResponse.json({ error: 'No token provided' }, { status: 401 });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as {
userId: number;
email: string;
};
return NextResponse.json({ user: { userId: decoded.userId, email: decoded.email } });
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}
This route:
- Extracts the JWT from the HTTP-only cookie.
- Verifies the token and returns the user data if valid.
Step 3: Wrapping the App with AuthProvider
Wrap the entire application with the AuthProvider to make the authentication context available.
File: app/layout.tsx
import { AuthProvider } from './context/AuthContext';
import './globals.css';
export const metadata = {
title: 'My Auth App',
description: 'Next.js App with JWT Authentication',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}
This ensures all components in the app can access the authentication context.
Step 4: Updating the Login Page
Modify the login page to use the authentication context.
File: app/login/page.tsx
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '../context/AuthContext';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { login, loading } = useAuth();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await login(email, password);
} catch (err) {
setError('Invalid credentials');
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="flex min-h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="flex flex-col gap-4 p-4">
<h1 className="text-2xl font-bold">Login</h1>
{error && <p className="text-red-500">{error}</p>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className="border p-2"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="border p-2"
required
/>
<button
type="submit"
className="bg-blue-500 text-white p-2 rounded"
disabled={loading}
>
Login
</button>
</form>
</div>
);
}
This updated login page:
- Uses the
useAuthhook to access theloginfunction andloadingstate. - Handles login via the context, reducing redundant fetch logic.
Step 5: Updating the Dashboard Page
Update the dashboard to use the authentication context.
File: app/dashboard/page.tsx
'use client';
import { useAuth } from '../context/AuthContext';
import Link from 'next/link';
export default function DashboardPage() {
const { user, logout, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="flex min-h-screen items-center justify-center">
<div className="p-4">
<h1 className="text-2xl font-bold">Dashboard</h1>
{user ? (
<>
<p>Welcome, {user.email}!</p>
<button
onClick={logout}
className="text-blue-500 underline"
>
Logout
</button>
</>
) : (
<p>
Please <Link href="/login" className="text-blue-500">log in</Link>.
</p>
)}
</div>
</div>
);
}
This updated dashboard:
- Uses the
useAuthhook to accessuser,logout, andloading. - Displays user information and a logout button if authenticated.
- Shows a login link if not authenticated.
Step 6: Testing the Authentication Context
- Run the development server:
npm run dev
- Navigate to
http://localhost:3000/login. - Log in with credentials (e.g.,
user@example.comandpassword123). - Verify that the dashboard displays the user’s email and a logout button.
- Test the logout functionality, ensuring it redirects to the login page.
- Refresh the dashboard page to confirm the context restores the user session via the
/api/auth/verifyendpoint.
Benefits of Using Auth Context
- Centralized State Management: The context centralizes user state and authentication methods.
- Simplified Component Logic: Components can access authentication data and methods without repetitive fetch calls.
- Session Persistence: The
useEffectinAuthProviderensures the user session is restored on page refresh. - Type Safety: TypeScript ensures type-safe access to user data and methods.
Security Considerations
- Secure API Calls: Ensure all API calls include
credentials: 'include'to send HTTP-only cookies. - Error Handling: Add robust error handling in the context for network failures or invalid tokens.
- Refresh Tokens: For production, consider adding refresh tokens to extend sessions securely.
- Context Scope: Avoid storing sensitive data (e.g., the JWT itself) in the context; keep it in HTTP-only cookies.
Conclusion
By adding an authentication context, you enhance the maintainability and scalability of your Next.js authentication system. The context provides a clean way to access user data and authentication methods across components, while the existing JWT and HTTP-only cookie setup ensures security. Extend this system with refresh tokens and additional error handling for production use.