Mastering Next.js authentication patterns
Introduction
Authentication is a critical component of modern web applications, and Next.js provides several powerful patterns to implement secure authentication flows. Whether you're building a simple blog with protected content or a complex SaaS application, understanding these patterns will help you create robust, secure authentication systems that scale with your application's needs.
In this post, we'll explore the most effective authentication patterns in Next.js, covering server-side strategies, JWT handling, and integration with popular authentication providers. We'll provide practical examples and discuss the trade-offs of each approach.
Session-Based Authentication with NextAuth.js
One of the most popular authentication patterns in Next.js involves using session-based authentication with NextAuth.js. This library simplifies the process of adding authentication to your application while supporting multiple providers (OAuth, email/password, etc.) and database adapters.
Setting Up NextAuth.js
First, install the required packages:
npm install next-auth
Then, create an API route for authentication at pages/api/auth/[...nextauth].js
:
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
// Add more providers as needed
],
database: process.env.DATABASE_URL,
callbacks: {
async session(session, user) {
session.user.id = user.id
return session
},
},
})
Protecting Routes
You can protect routes using NextAuth.js's built-in session handling:
import { getSession } from 'next-auth/react' export async function getServerSideProps(context) { const session = await getSession(context) if (!session) { return { redirect: { destination: '/auth/signin', permanent: false, }, } } return { props: { session }, } } function ProtectedPage({ session }) { return <div>Welcome, {session.user.name}!</div> } export default ProtectedPage
JWT Authentication with API Routes
For applications that need more control over the authentication flow, implementing JWT (JSON Web Token) authentication with Next.js API routes is a powerful alternative.
Creating the Authentication API
Here's a basic implementation of a JWT authentication API:
import jwt from 'jsonwebtoken' import { compare } from 'bcryptjs' 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 // In a real app, you would fetch the user from your database const user = await getUserByEmail(email) if (!user || !(await compare(password, user.password))) { return res.status(401).json({ message: 'Invalid credentials' }) } const token = jwt.sign( { userId: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '1h' } ) res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Path=/; Max-Age=3600`) res.status(200).json({ user: { id: user.id, email: user.email } }) }
Protecting Client-Side Routes
To protect client-side routes with JWT, you can create a custom hook:
import { useState, useEffect } from 'react' import { useRouter } from 'next/router' import { parseCookies } from 'nookies' export function useAuth() { const [user, setUser] = useState(null) const router = useRouter() useEffect(() => { const cookies = parseCookies() const token = cookies.token if (!token) { router.push('/login') return } try { const decoded = jwt.verify(token, process.env.NEXT_PUBLIC_JWT_SECRET) setUser(decoded) } catch (err) { router.push('/login') } }, [router]) return { user } }
Authentication with Middleware (Next.js 12+)
Next.js 12 introduced middleware that runs before a request is completed. This is perfect for authentication checks that need to happen before the page loads.
Implementing Authentication Middleware
Create a middleware.js
file in your project root:
import { NextResponse } from 'next/server' import { verify } from 'jsonwebtoken' const secret = process.env.JWT_SECRET export function middleware(req) { const { cookies } = req const token = cookies.token const url = req.url if (url.includes('/dashboard')) { if (!token) { return NextResponse.redirect('/login') } try { verify(token, secret) return NextResponse.next() } catch (e) { return NextResponse.redirect('/login') } } return NextResponse.next() }
Edge-Compatible Authentication
For even better performance, you can use edge-compatible authentication solutions:
import { NextResponse } from 'next/server' import { createEdgeRouter } from 'next-connect' import { getToken } from 'next-auth/jwt' const router = createEdgeRouter() router.use(async (req, event, next) => { const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }) if (!token) { return new NextResponse( JSON.stringify({ message: 'Authentication required' }), { status: 401, headers: { 'content-type': 'application/json' } } ) } return next() }) export default router
Conclusion
Next.js offers flexible patterns for implementing authentication that can adapt to your application's specific requirements. Whether you choose NextAuth.js for its simplicity and provider integrations, JWT for fine-grained control, or middleware for edge-based authentication, each approach has its strengths.
Remember to always:
- Store secrets securely using environment variables
- Implement proper session expiration
- Use HTTPS in production
- Consider CSRF protection for sensitive operations
- Regularly audit your authentication flows for security vulnerabilities
By mastering these authentication patterns, you'll be able to build secure, scalable applications with Next.js that meet your users' needs while protecting their data.