Authentication is a fundamental aspect of modern web applications. It allows you to verify user identity, which is essential for personalizing experiences, protecting data, and providing features only to authorized users. In this module, we will learn the basics of implementing authentication in Next.js applications, which combines the benefits of server-side rendering (SSR) and client-side rendering.
Authentication is the process of verifying a user's identity. It is a key element of web application security because:
It is important to distinguish between two related but separate concepts:
In many cases, both processes are interconnected, but they serve different functions in the security architecture.
Next.js offers a hybrid rendering model, which has a significant impact on authentication implementation:
Several main authentication models can be applied in Next.js applications:
A traditional approach based on maintaining session state on the server:
1// Example API route for session-based login
2import { NextApiRequest, NextApiResponse } from 'next';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '../auth/[...nextauth]';
5
6export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7 const session = await getServerSession(req, res, authOptions);
8
9 if (!session) {
10 return res.status(401).json({ message: 'Unauthorized' });
11 }
12
13 // User authenticated, can proceed
14 return res.status(200).json({ user: session.user });
15}A popular approach using JWT (JSON Web Tokens):
1// Example API route for JWT token verification
2import { NextApiRequest, NextApiResponse } from 'next';
3import jwt from 'jsonwebtoken';
4
5export default function handler(req: NextApiRequest, res: NextApiResponse) {
6 const { authorization } = req.headers;
7
8 if (!authorization || !authorization.startsWith('Bearer ')) {
9 return res.status(401).json({ message: 'Unauthorized' });
10 }
11
12 const token = authorization.replace('Bearer ', '');
13
14 try {
15 const decoded = jwt.verify(token, process.env.JWT_SECRET!);
16 // Token valid, can proceed
17 return res.status(200).json({ user: decoded });
18 } catch (error) {
19 return res.status(401).json({ message: 'Invalid token' });
20 }
21}Using external services like Google, Facebook, or GitHub:
1// Example NextAuth.js configuration with Google authentication
2import NextAuth from 'next-auth';
3import GoogleProvider from 'next-auth/providers/google';
4
5export default NextAuth({
6 providers: [
7 GoogleProvider({
8 clientId: process.env.GOOGLE_CLIENT_ID!,
9 clientSecret: process.env.GOOGLE_CLIENT_SECRET!
10 })
11 ],
12 // Additional configuration...
13});In newer versions of Next.js (13+) with App Router, the authentication approach has been improved. Below are the basic steps:
First, we need to configure the authentication provider. In this example, we will use Auth.js (formerly NextAuth.js):
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from 'next-auth';
3import CredentialsProvider from 'next-auth/providers/credentials';
4import { compare } from 'bcrypt';
5import { prisma } from '@/lib/prisma';
6
7export const authOptions = {
8 providers: [
9 CredentialsProvider({
10 name: 'Credentials',
11 credentials: {
12 email: { label: "Email", type: "email" },
13 password: { label: "Password", type: "password" }
14 },
15 async authorize(credentials) {
16 if (!credentials?.email || !credentials?.password) {
17 return null;
18 }
19
20 const user = await prisma.user.findUnique({
21 where: {
22 email: credentials.email
23 }
24 });
25
26 if (!user) {
27 return null;
28 }
29
30 const isPasswordValid = await compare(
31 credentials.password,
32 user.password
33 );
34
35 if (!isPasswordValid) {
36 return null;
37 }
38
39 return {
40 id: user.id,
41 email: user.email,
42 name: user.name
43 };
44 }
45 })
46 ],
47 pages: {
48 signIn: '/login',
49 signOut: '/logout',
50 error: '/login',
51 },
52 session: {
53 strategy: 'jwt'
54 }
55};
56
57const handler = NextAuth(authOptions);
58export { handler as GET, handler as POST };Next, we need a user interface for login:
1// app/login/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { signIn } from 'next-auth/react';
6import { useRouter } from 'next/navigation';
7
8export default function LoginPage() {
9 const router = useRouter();
10 const [email, setEmail] = useState('');
11 const [password, setPassword] = useState('');
12 const [error, setError] = useState('');
13 const [loading, setLoading] = useState(false);
14
15 async function handleSubmit(e: React.FormEvent) {
16 e.preventDefault();
17 setLoading(true);
18 setError('');
19
20 try {
21 const result = await signIn('credentials', {
22 redirect: false,
23 email,
24 password
25 });
26
27 if (result?.error) {
28 setError('Invalid email or password');
29 } else {
30 router.push('/dashboard');
31 router.refresh();
32 }
33 } catch (error) {
34 setError('An error occurred during login');
35 } finally {
36 setLoading(false);
37 }
38 }
39
40 return (
41 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
42 <h1 className="text-2xl font-bold mb-6">Login</h1>
43
44 {error && (
45 <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
46 {error}
47 </div>
48 )}
49
50 <form onSubmit={handleSubmit} className="space-y-4">
51 <div>
52 <label htmlFor="email" className="block text-sm font-medium mb-1">
53 Email
54 </label>
55 <input
56 id="email"
57 type="email"
58 value={email}
59 onChange={(e) => setEmail(e.target.value)}
60 required
61 className="w-full px-3 py-2 border border-gray-300 rounded"
62 />
63 </div>
64
65 <div>
66 <label htmlFor="password" className="block text-sm font-medium mb-1">
67 Password
68 </label>
69 <input
70 id="password"
71 type="password"
72 value={password}
73 onChange={(e) => setPassword(e.target.value)}
74 required
75 className="w-full px-3 py-2 border border-gray-300 rounded"
76 />
77 </div>
78
79 <button
80 type="submit"
81 disabled={loading}
82 className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
83 >
84 {loading ? 'Logging in...' : 'Log in'}
85 </button>
86 </form>
87 </div>
88 );
89}And a registration page:
1// app/register/page.tsx
2'use client';
3
4import { useState } from 'react';
5import { useRouter } from 'next/navigation';
6
7export default function RegisterPage() {
8 const router = useRouter();
9 const [name, setName] = useState('');
10 const [email, setEmail] = useState('');
11 const [password, setPassword] = useState('');
12 const [confirmPassword, setConfirmPassword] = useState('');
13 const [error, setError] = useState('');
14 const [loading, setLoading] = useState(false);
15
16 async function handleSubmit(e: React.FormEvent) {
17 e.preventDefault();
18 setLoading(true);
19 setError('');
20
21 // Validate form
22 if (password !== confirmPassword) {
23 setError('Passwords do not match');
24 setLoading(false);
25 return;
26 }
27
28 try {
29 const response = await fetch('/api/register', {
30 method: 'POST',
31 headers: { 'Content-Type': 'application/json' },
32 body: JSON.stringify({ name, email, password })
33 });
34
35 const data = await response.json();
36
37 if (!response.ok) {
38 throw new Error(data.message || 'An error occurred during registration');
39 }
40
41 // Registration successful, redirect to login page
42 router.push('/login?registered=true');
43 } catch (error) {
44 setError(error instanceof Error ? error.message : 'An error occurred');
45 } finally {
46 setLoading(false);
47 }
48 }
49
50 return (
51 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
52 <h1 className="text-2xl font-bold mb-6">Registration</h1>
53
54 {error && (
55 <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
56 {error}
57 </div>
58 )}
59
60 <form onSubmit={handleSubmit} className="space-y-4">
61 <div>
62 <label htmlFor="name" className="block text-sm font-medium mb-1">
63 Full name
64 </label>
65 <input
66 id="name"
67 type="text"
68 value={name}
69 onChange={(e) => setName(e.target.value)}
70 required
71 className="w-full px-3 py-2 border border-gray-300 rounded"
72 />
73 </div>
74
75 <div>
76 <label htmlFor="email" className="block text-sm font-medium mb-1">
77 Email
78 </label>
79 <input
80 id="email"
81 type="email"
82 value={email}
83 onChange={(e) => setEmail(e.target.value)}
84 required
85 className="w-full px-3 py-2 border border-gray-300 rounded"
86 />
87 </div>
88
89 <div>
90 <label htmlFor="password" className="block text-sm font-medium mb-1">
91 Password
92 </label>
93 <input
94 id="password"
95 type="password"
96 value={password}
97 onChange={(e) => setPassword(e.target.value)}
98 required
99 minLength={8}
100 className="w-full px-3 py-2 border border-gray-300 rounded"
101 />
102 </div>
103
104 <div>
105 <label htmlFor="confirmPassword" className="block text-sm font-medium mb-1">
106 Confirm password
107 </label>
108 <input
109 id="confirmPassword"
110 type="password"
111 value={confirmPassword}
112 onChange={(e) => setConfirmPassword(e.target.value)}
113 required
114 className="w-full px-3 py-2 border border-gray-300 rounded"
115 />
116 </div>
117
118 <button
119 type="submit"
120 disabled={loading}
121 className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
122 >
123 {loading ? 'Registering...' : 'Sign up'}
124 </button>
125 </form>
126 </div>
127 );
128}To enable registration, we need an API endpoint:
1// app/api/register/route.ts
2import { NextResponse } from 'next/server';
3import { hash } from 'bcrypt';
4import { prisma } from '@/lib/prisma';
5import { z } from 'zod';
6
7// Validation schema
8const UserSchema = z.object({
9 name: z.string().min(2, 'First name must be at least 2 characters'),
10 email: z.string().email('Invalid email address'),
11 password: z.string().min(8, 'Password must be at least 8 characters')
12});
13
14export async function POST(request: Request) {
15 try {
16 const body = await request.json();
17
18 // Validate data
19 const result = UserSchema.safeParse(body);
20
21 if (!result.success) {
22 return NextResponse.json(
23 { message: result.error.errors[0].message },
24 { status: 400 }
25 );
26 }
27
28 const { name, email, password } = result.data;
29
30 // Check if user already exists
31 const existingUser = await prisma.user.findUnique({
32 where: { email }
33 });
34
35 if (existingUser) {
36 return NextResponse.json(
37 { message: 'A user with this email address already exists' },
38 { status: 409 }
39 );
40 }
41
42 // Hash the password
43 const hashedPassword = await hash(password, 10);
44
45 // Create new user
46 const user = await prisma.user.create({
47 data: {
48 name,
49 email,
50 password: hashedPassword
51 }
52 });
53
54 // Return response without password
55 const { password: _, ...userWithoutPassword } = user;
56
57 return NextResponse.json(
58 { message: 'Registration completed successfully', user: userWithoutPassword },
59 { status: 201 }
60 );
61 } catch (error) {
62 console.error('Registration error:', error);
63 return NextResponse.json(
64 { message: 'A server error occurred' },
65 { status: 500 }
66 );
67 }
68}To protect pages that should only be accessible to logged-in users, we can use middleware:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7 const token = await getToken({ req: request });
8
9 // Check if user is logged in
10 if (!token) {
11 const url = new URL('/login', request.url);
12 url.searchParams.set('callbackUrl', request.nextUrl.pathname);
13 return NextResponse.redirect(url);
14 }
15
16 return NextResponse.next();
17}
18
19// Define paths that should be protected
20export const config = {
21 matcher: ['/dashboard/:path*', '/profile/:path*', '/settings/:path*']
22};To use information about the logged-in user in a component, we can use the hook
useSession:1// components/ProfileSection.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { signOut } from 'next-auth/react';
6
7export default function ProfileSection() {
8 const { data: session, status } = useSession();
9
10 if (status === 'loading') {
11 return <div>Loading...</div>;
12 }
13
14 if (status === 'unauthenticated' || !session) {
15 return <div>You are not logged in</div>;
16 }
17
18 return (
19 <div className="p-4 bg-white rounded shadow">
20 <h2 className="text-xl font-bold mb-4">Your profile</h2>
21 <p>Hello, {session.user?.name || 'user'}!</p>
22 <p>Email: {session.user?.email}</p>
23
24 <button
25 onClick={() => signOut({ callbackUrl: '/' })}
26 className="mt-4 px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
27 >
28 Log out
29 </button>
30 </div>
31 );
32}For authentication to work properly, we need to add
SessionProvider at the layout level:1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from 'next-auth/react';
5import { ReactNode } from 'react';
6
7export function Providers({ children }: { children: ReactNode }) {
8 return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from './providers';
13
14export default function RootLayout({
15 children,
16}: {
17 children: React.ReactNode;
18}) {
19 return (
20 <html lang="en">
21 <body>
22 <Providers>{children}</Providers>
23 </body>
24 </html>
25 );
26}When implementing authentication, it is important to keep in mind several key security mechanisms:
Never store passwords in plain text! Always use hashing functions such as
bcrypt:1import { hash, compare } from 'bcrypt';
2
3// Hashing password during registration
4const hashedPassword = await hash(password, 10);
5
6// Verifying password during login
7const isValid = await compare(password, hashedPassword);Cross-Site Request Forgery (CSRF) is a type of attack that exploits an authenticated user's session to perform unauthorized actions. Next.js and NextAuth.js provide some protections by default, but it is worth ensuring additional protection:
1// Adding a CSRF token to the form
2export default function LoginForm() {
3 const csrfToken = await getCsrfToken();
4
5 return (
6 <form method="post" action="/api/auth/callback/credentials">
7 <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
8 {/* Rest of the form */}
9 </form>
10 );
11}Using encrypted HTTPS connections is essential for protecting authentication credentials and session tokens. In a production environment, make sure your application is accessible only via HTTPS.
If you use JWT tokens, set an appropriate expiration time to limit risk in case of token theft:
1// JWT token expiration configuration in NextAuth.js
2export const authOptions = {
3 // ... other options
4 session: {
5 strategy: 'jwt',
6 maxAge: 30 * 24 * 60 * 60, // 30 days
7 },
8 jwt: {
9 secret: process.env.JWT_SECRET,
10 maxAge: 30 * 24 * 60 * 60, // 30 days
11 }
12};A challenge can be securing pages that are statically generated during build:
Solution: Add an additional client-side verification layer:
1'use client';
2
3import { useSession } from 'next-auth/react';
4import { useRouter } from 'next/navigation';
5import { useEffect } from 'react';
6
7export default function ProtectedPage() {
8 const { status } = useSession();
9 const router = useRouter();
10
11 useEffect(() => {
12 if (status === 'unauthenticated') {
13 router.push('/login');
14 }
15 }, [status, router]);
16
17 if (status === 'loading') {
18 return <div>Loading...</div>;
19 }
20
21 if (status === 'unauthenticated') {
22 return null; // Instead of rendering content, redirect to login page
23 }
24
25 // Actual content of the protected page
26 return (
27 <div>
28 <h1>Protected page</h1>
29 {/* Rest of content */}
30 </div>
31 );
32}Sometimes we need different access levels for users:
Solution: Extend the token/session with role information and implement role checking:
1// Extending NextAuth types
2declare module 'next-auth' {
3 interface Session {
4 user: {
5 id: string;
6 name?: string | null;
7 email?: string | null;
8 image?: string | null;
9 role: string;
10 };
11 }
12
13 interface User {
14 id: string;
15 name?: string | null;
16 email?: string | null;
17 image?: string | null;
18 role: string;
19 }
20}
21
22// Using role information
23function canAccessAdmin(session: Session | null) {
24 return session?.user?.role === 'admin';
25}
26
27// In the component
28const { data: session } = useSession();
29const isAdmin = session?.user?.role === 'admin';
30
31if (!isAdmin) {
32 return <div>Access denied. This section is for administrators only.</div>;
33}Authentication in Next.js can be implemented in many ways, but the most commonly used approaches include:
Choosing the right solution depends on the specific requirements of your application, such as:
In the following modules, we will look at more advanced aspects of authentication, including detailed NextAuth.js implementation, session and token management, route protection using middleware, and security topics.