Next.js authentication patterns deep dive
Next.js Authentication Patterns Deep Dive
Authentication is a critical aspect of modern web applications, and Next.js offers multiple approaches to implement secure and scalable authentication. Whether you're building a client-side app, a server-rendered solution, or a hybrid approach, understanding the available patterns is essential. In this deep dive, we'll explore three primary authentication strategies in Next.js, their trade-offs, and practical implementation examples.
1. Client-Side Authentication with JWT
Client-side authentication is a common approach where the frontend handles user sessions using JSON Web Tokens (JWT). This method is straightforward to implement but requires careful consideration of security best practices.
How It Works
- The user logs in via an API endpoint, which returns a JWT.
- The token is stored in memory or a secure HTTP-only cookie.
- Subsequent requests include the token in the
Authorization
header.
Implementation Example
Here’s how you can implement JWT-based auth in a Next.js app:
Login API Route (pages/api/login.js
)
export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }); } const { email, password } = req.body; // Validate credentials (e.g., against a database) const user = await validateUser(email, password); if (!user) { return res.status(401).json({ message: 'Invalid credentials' }); } // Generate JWT const token = generateJWT(user); // Set HTTP-only cookie (recommended for security) res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Path=/; Secure; SameSite=Strict`); res.status(200).json({ user }); }
Protecting Client-Side Routes
To restrict access to authenticated users, use a custom hook like useAuth
:
import { useEffect } from 'react'; import { useRouter } from 'next/router'; export function useAuth() { const router = useRouter(); useEffect(() => { const token = localStorage.getItem('token'); if (!token) { router.push('/login'); } }, []); }
Trade-offs
✅ Simple to implement.
❌ Vulnerable to XSS attacks if tokens are stored in localStorage
.
❌ Requires additional logic for server-side rendering (SSR).
2. Server-Side Authentication with Next.js Middleware
Next.js Middleware (introduced in v12) allows running logic before a request completes, making it ideal for authentication. This approach is well-suited for apps requiring SSR or static generation with protected routes.
How It Works
- Middleware intercepts requests and checks authentication status.
- Redirects unauthenticated users to a login page.
- Works seamlessly with both SSR and static pages.
Implementation Example
Middleware (middleware.js
)
import { NextResponse } from 'next/server'; export function middleware(request) { const token = request.cookies.get('token')?.value; if (!token && !request.nextUrl.pathname.startsWith('/login')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } // Apply middleware to specific paths export const config = { matcher: ['/dashboard/:path*', '/profile'], };
Server-Side Props Validation
For SSR pages, validate the session in getServerSideProps
:
export async function getServerSideProps(context) { const token = context.req.cookies.token; if (!token) { return { redirect: { destination: '/login', permanent: false, }, }; } return { props: {} }; }
Trade-offs
✅ Secure (handles auth before page rendering).
✅ Works with SSR and static pages.
❌ Requires careful cookie management.
3. Authentication Providers (NextAuth.js / Auth.js)
For a production-ready solution, libraries like NextAuth.js (now Auth.js) provide built-in support for OAuth, email/password, and other providers with minimal setup.
How It Works
- Configure providers (Google, GitHub, etc.) in
authOptions
. - Use built-in hooks (
useSession
) for client-side auth state. - Integrates with middleware for route protection.
Implementation Example
Setup (pages/api/auth/[...nextauth].js
)
import NextAuth from 'next-auth'; import GoogleProvider from 'next-auth/providers/google'; export const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), ], secret: process.env.NEXTAUTH_SECRET, }; export default NextAuth(authOptions);
Client-Side Session Handling
import { useSession, signIn, signOut } from 'next-auth/react'; export default function Dashboard() { const { data: session } = useSession(); if (!session) { return <button onClick={() => signIn()}>Sign in</button>; } return ( <div> <p>Welcome, {session.user.name}!</p> <button onClick={() => signOut()}>Sign out</button> </div> ); }
Trade-offs
✅ Supports multiple auth providers out-of-the-box.
✅ Built-in session management.
❌ Slightly heavier bundle size.
Conclusion
Choosing the right authentication pattern in Next.js depends on your application's requirements:
- Client-side JWT: Best for SPAs with minimal SSR.
- Middleware + SSR: Ideal for secure, server-rendered apps.
- NextAuth.js: Perfect for apps needing OAuth or quick setup.
By understanding these patterns, you can implement scalable and secure authentication tailored to your Next.js project. Always prioritize security by using HTTP-only cookies, CSRF protection, and secure token storage.
For further reading, explore Next.js documentation on Authentication and the Auth.js library.