NextAuth.js (now also known as Auth.js) is a comprehensive authentication solution for Next.js applications. It provides ready-made infrastructure for managing sessions, tokens, login, registration, and integration with external authentication providers. In this module, we will cover the detailed implementation of this tool in Next.js applications.
NextAuth.js offers many benefits:
Let's start by installing NextAuth.js:
1npm install next-auth@latest
2# or
3yarn add next-auth@latest
4# or
5pnpm add next-auth@latestTo configure NextAuth.js in the App Router architecture, we need to create a file to handle the Route API for authentication:
1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GithubProvider from "next-auth/providers/github";
4
5export const authOptions = {
6 providers: [
7 GithubProvider({
8 clientId: process.env.GITHUB_ID!,
9 clientSecret: process.env.GITHUB_SECRET!
10 })
11 ],
12 // Additional configuration options...
13};
14
15const handler = NextAuth(authOptions);
16export { handler as GET, handler as POST };This file creates a dynamic API route that handles all necessary authentication endpoints.
To enable the use of the hook
useSession in client-side components, we need to add SessionProvider to our application. It is best to do this in a Provider file or directly in the main layout: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}NextAuth.js requires several environment variables. Create or update the file
.env.local in the project's main directory:1# Basic configuration for NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=your_secret_key_for_jwt_encryption
4
5# GitHub authentication data (example)
6GITHUB_ID=your_github_client_id
7GITHUB_SECRET=your_github_client_secretIn a production environment:
NEXTAUTH_URL should be the full URL of your applicationNEXTAUTH_SECRET should be a long, random string of characters. You can generate it using the command openssl rand -base64 32.NextAuth.js supports many authentication providers. Let's look at how to configure a few popular ones:
1import GithubProvider from "next-auth/providers/github";
2
3export const authOptions = {
4 providers: [
5 GithubProvider({
6 clientId: process.env.GITHUB_ID!,
7 clientSecret: process.env.GITHUB_SECRET!,
8 // Optional parameters
9 scope: "read:user user:email", // Specifies access scope
10 profile(profile) {
11 // Customizing user profile
12 return {
13 id: profile.id.toString(),
14 name: profile.name || profile.login,
15 email: profile.email,
16 image: profile.avatar_url,
17 // Additional fields
18 username: profile.login,
19 };
20 },
21 }),
22 ],
23 // Other configuration options...
24};1import GoogleProvider from "next-auth/providers/google";
2
3export const authOptions = {
4 providers: [
5 GoogleProvider({
6 clientId: process.env.GOOGLE_CLIENT_ID!,
7 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
8 // Optional parameters
9 authorization: {
10 params: Promise<{
11 prompt: "consent",
12 access_type: "offline",
13 response_type: "code"
14 }>
15 }
16 }),
17 ],
18 // Other configuration options...
19};1import CredentialsProvider from "next-auth/providers/credentials";
2import { compare } from "bcrypt";
3import { prisma } from "@/lib/prisma"; // We assume you have a configured Prisma instance
4
5export const authOptions = {
6 providers: [
7 CredentialsProvider({
8 // Name displayed on the login page
9 name: "Credentials",
10 // Login form field configuration
11 credentials: {
12 email: { label: "Email", type: "email" },
13 password: { label: "Password", type: "password" }
14 },
15 async authorize(credentials) {
16 // Check if credentials exist
17 if (!credentials?.email || !credentials?.password) {
18 return null;
19 }
20
21 try {
22 // Find the user in the database
23 const user = await prisma.user.findUnique({
24 where: { email: credentials.email },
25 });
26
27 if (!user || !user.password) {
28 return null;
29 }
30
31 // Check if the password is correct
32 const isPasswordValid = await compare(
33 credentials.password,
34 user.password
35 );
36
37 if (!isPasswordValid) {
38 return null;
39 }
40
41 // Return the user object (without password)
42 return {
43 id: user.id,
44 name: user.name,
45 email: user.email,
46 role: user.role,
47 // Other fields you want to include
48 };
49 } catch (error) {
50 console.error("Authentication error:", error);
51 return null;
52 }
53 },
54 }),
55 ],
56 // Other configuration options...
57};You can combine multiple authentication providers:
1export const authOptions = {
2 providers: [
3 GithubProvider({
4 clientId: process.env.GITHUB_ID!,
5 clientSecret: process.env.GITHUB_SECRET!,
6 }),
7 GoogleProvider({
8 clientId: process.env.GOOGLE_CLIENT_ID!,
9 clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
10 }),
11 CredentialsProvider({
12 // Configuration...
13 async authorize(credentials) {
14 // Authentication logic...
15 },
16 }),
17 // You can add more providers...
18 ],
19 // Other configuration options...
20};NextAuth.js offers different session strategies:
1export const authOptions = {
2 // Provider configuration...
3
4 session: {
5 // Choose strategy: 'jwt' or 'database'
6 strategy: 'jwt',
7
8 // Maximum session lifetime (in seconds)
9 maxAge: 30 * 24 * 60 * 60, // 30 days
10
11 // Session update on each request, extending its validity
12 updateAge: 24 * 60 * 60, // 24 hours
13 },
14
15 // JWT configuration (only for the 'jwt' strategy)
16 jwt: {
17 // Secret key used to sign JWT tokens
18 // By default uses NEXTAUTH_SECRET
19 secret: process.env.JWT_SECRET,
20
21 // You can customize JWT encoder/decoder
22 // encode: async ({ secret, token, maxAge }) => {},
23 // decode: async ({ secret, token }) => {},
24 },
25};By default, NextAuth.js provides simple login pages, but you can replace them with your own:
1export const authOptions = {
2 // Provider configuration...
3
4 pages: {
5 signIn: '/auth/login',
6 signOut: '/auth/logout',
7 error: '/auth/error', // Authentication error (e.g. invalid credentials)
8 verifyRequest: '/auth/verify-request', // Used for Magic Links
9 newUser: '/auth/new-user' // Page shown after first registration
10 },
11};Then you need to create these pages in your application. Here is an example login page:
1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useState, useEffect } from 'react';
7
8export default function LoginPage() {
9 const router = useRouter();
10 const searchParams = useSearchParams();
11 const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
12 const { status } = useSession();
13
14 const [email, setEmail] = useState('');
15 const [password, setPassword] = useState('');
16 const [error, setError] = useState('');
17 const [isLoading, setIsLoading] = useState(false);
18
19 // Redirect the logged-in user
20 useEffect(() => {
21 if (status === 'authenticated') {
22 router.push(callbackUrl);
23 }
24 }, [status, router, callbackUrl]);
25
26 // GitHub login handling
27 const handleGitHubSignIn = () => {
28 setIsLoading(true);
29 signIn('github', { callbackUrl });
30 };
31
32 // Google login handling
33 const handleGoogleSignIn = () => {
34 setIsLoading(true);
35 signIn('google', { callbackUrl });
36 };
37
38 // Credentials login handling
39 const handleSubmit = async (e: React.FormEvent) => {
40 e.preventDefault();
41 setIsLoading(true);
42 setError('');
43
44 try {
45 const result = await signIn('credentials', {
46 redirect: false,
47 email,
48 password,
49 });
50
51 if (result?.error) {
52 setError('Invalid email or password');
53 setIsLoading(false);
54 } else {
55 router.push(callbackUrl);
56 }
57 } catch (error) {
58 setError('An error occurred during login');
59 setIsLoading(false);
60 }
61 };
62
63 if (status === 'loading') {
64 return <div>Loading...</div>;
65 }
66
67 return (
68 <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
69 <h1 className="text-2xl font-bold mb-6 text-center">Log in</h1>
70
71 {error && (
72 <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
73 {error}
74 </div>
75 )}
76
77 <div className="space-y-4 mb-6">
78 <button
79 type="button"
80 onClick={handleGitHubSignIn}
81 disabled={isLoading}
82 className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
83 >
84 <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
85 {/* Logo GitHub */}
86 <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
87 </svg>
88 Continue with GitHub
89 </button>
90
91 <button
92 type="button"
93 onClick={handleGoogleSignIn}
94 disabled={isLoading}
95 className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
96 >
97 <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
98 {/* Logo Google */}
99 <path d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm0-22.5C6.21 1.5 1.5 6.21 1.5 12S6.21 22.5 12 22.5 22.5 17.79 22.5 12 17.79 1.5 12 1.5zm-5.25 9v3h3v3h3v-3h3v-3h-3v-3h-3v3h-3z" />
100 </svg>
101 Continue with Google
102 </button>
103 </div>
104
105 <div className="relative my-6">
106 <div className="absolute inset-0 flex items-center">
107 <div className="w-full border-t border-gray-300"></div>
108 </div>
109 <div className="relative flex justify-center text-sm">
110 <span className="px-2 bg-white text-gray-500">
111 or sign in with email
112 </span>
113 </div>
114 </div>
115
116 <form onSubmit={handleSubmit} className="space-y-4">
117 <div>
118 <label htmlFor="email" className="block text-sm font-medium mb-1">
119 Email
120 </label>
121 <input
122 id="email"
123 type="email"
124 value={email}
125 onChange={(e) => setEmail(e.target.value)}
126 required
127 className="w-full px-3 py-2 border border-gray-300 rounded-md"
128 />
129 </div>
130
131 <div>
132 <label htmlFor="password" className="block text-sm font-medium mb-1">
133 Password
134 </label>
135 <input
136 id="password"
137 type="password"
138 value={password}
139 onChange={(e) => setPassword(e.target.value)}
140 required
141 className="w-full px-3 py-2 border border-gray-300 rounded-md"
142 />
143 </div>
144
145 <button
146 type="submit"
147 disabled={isLoading}
148 className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:opacity-50"
149 >
150 {isLoading ? 'Logging in...' : 'Log in'}
151 </button>
152 </form>
153 </div>
154 );
155}NextAuth.js allows reacting to various events in the authentication lifecycle through callbacks:
1export const authOptions = {
2 // Provider configuration...
3
4 callbacks: {
5 // Called when creating a session
6 async session({ session, token }) {
7 // Add token data to the session
8 if (token.sub && session.user) {
9 session.user.id = token.sub;
10 }
11
12 // Add custom fields, e.g. user role
13 if (token.role && session.user) {
14 session.user.role = token.role as string;
15 }
16
17 return session;
18 },
19
20 // Called when creating a JWT
21 async jwt({ token, user, account, profile }) {
22 // Add user data on first login
23 if (user) {
24 token.role = user.role;
25 // Other custom fields...
26 }
27
28 return token;
29 },
30
31 // Called after login
32 async signIn({ user, account, profile, email, credentials }) {
33 // Check if the user has access to the application
34 return true; // Return false to deny access
35 },
36
37 // Called during redirect
38 async redirect({ url, baseUrl }) {
39 // Redirect rules
40 if (url.startsWith(baseUrl)) {
41 return url;
42 }
43 // By default redirect to the base page
44 return baseUrl;
45 },
46 },
47
48 // Events
49 events: {
50 async signIn(message) {
51 // Called after login
52 console.log('User signed in:', message);
53 },
54 async signOut(message) {
55 // Called after logout
56 console.log('User signed out:', message);
57 },
58 async createUser(message) {
59 // Called after user creation
60 console.log('User created:', message);
61 },
62 async linkAccount(message) {
63 // Called after account linking
64 console.log('Account linked:', message);
65 },
66 async session(message) {
67 // Called after session creation
68 console.log('Session created/updated:', message);
69 },
70 },
71};If you use TypeScript, you can extend NextAuth.js types to include custom fields:
1// In the type extension file (e.g. types/next-auth.d.ts)
2import 'next-auth';
3import 'next-auth/jwt';
4
5declare module 'next-auth' {
6 interface Session {
7 user: {
8 id: string;
9 name?: string | null;
10 email?: string | null;
11 image?: string | null;
12 role?: string;
13 // Add other custom fields...
14 };
15 }
16
17 interface User {
18 id: string;
19 name?: string | null;
20 email?: string | null;
21 image?: string | null;
22 role?: string;
23 // Add other custom fields...
24 }
25}
26
27declare module 'next-auth/jwt' {
28 interface JWT {
29 role?: string;
30 // Add other custom fields...
31 }
32}To use the database-based session strategy or store user accounts, NextAuth.js offers adapters for various databases:
1import { PrismaAdapter } from "@auth/prisma-adapter";
2import { PrismaClient } from "@prisma/client";
3
4const prisma = new PrismaClient();
5
6export const authOptions = {
7 adapter: PrismaAdapter(prisma),
8 providers: [
9 // Provider configuration...
10 ],
11 // Other options...
12};The Prisma schema should contain the appropriate models:
1// prisma/schema.prisma
2model Account {
3 id String @id @default(cuid())
4 userId String
5 type String
6 provider String
7 providerAccountId String
8 refresh_token String? @db.Text
9 access_token String? @db.Text
10 expires_at Int?
11 token_type String?
12 scope String?
13 id_token String? @db.Text
14 session_state String?
15
16 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
17
18 @@unique([provider, providerAccountId])
19}
20
21model Session {
22 id String @id @default(cuid())
23 sessionToken String @unique
24 userId String
25 expires DateTime
26 user User @relation(fields: [userId], references: [id], onDelete: Cascade)
27}
28
29model User {
30 id String @id @default(cuid())
31 name String?
32 email String? @unique
33 emailVerified DateTime?
34 image String?
35 password String?
36 role String? @default("user")
37 accounts Account[]
38 sessions Session[]
39}
40
41model VerificationToken {
42 identifier String
43 token String @unique
44 expires DateTime
45
46 @@unique([identifier, token])
47}NextAuth.js offers adapters for many databases:
After configuring NextAuth.js, you can use the hook
useSession in client components:1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function Dashboard() {
7 const { data: session, status } = useSession({
8 required: true,
9 onUnauthenticated() {
10 redirect('/auth/login?callbackUrl=/dashboard');
11 },
12 });
13
14 if (status === 'loading') {
15 return <div>Loading...</div>;
16 }
17
18 return (
19 <div>
20 <h1>Dashboard</h1>
21 <p>Hello, {session.user.name}!</p>
22 <p>Your email: {session.user.email}</p>
23 {session.user.role === 'admin' && (
24 <div>
25 <h2>Administrator panel</h2>
26 {/* Content only for administrators */}
27 </div>
28 )}
29 </div>
30 );
31}In server-rendered components, you can use
getServerSession:1// app/profile/page.tsx
2import { getServerSession } from 'next-auth/next';
3import { redirect } from 'next/navigation';
4import { authOptions } from '../api/auth/[...nextauth]/route';
5
6export default async function ProfilePage() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 redirect('/auth/login?callbackUrl=/profile');
11 }
12
13 return (
14 <div>
15 <h1>Profile</h1>
16 <p>Hello, {session.user.name}!</p>
17 <p>Your email: {session.user.email}</p>
18 {/* Rest of the profile content */}
19 </div>
20 );
21}NextAuth.js provides functions for managing authentication state:
1'use client';
2
3import { signIn, signOut, useSession } from 'next-auth/react';
4
5export function AuthButton() {
6 const { data: session } = useSession();
7
8 if (session) {
9 return (
10 <button
11 onClick={() => signOut({ callbackUrl: '/' })}
12 className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
13 >
14 Log out
15 </button>
16 );
17 }
18
19 return (
20 <button
21 onClick={() => signIn()}
22 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
23 >
24 Log in
25 </button>
26 );
27}We can create a component that renders different interface elements depending on the authentication state:
1'use client';
2
3import { useSession } from 'next-auth/react';
4import Link from 'next/link';
5import { AuthButton } from './AuthButton';
6
7export function Navbar() {
8 const { data: session, status } = useSession();
9 const isLoading = status === 'loading';
10
11 return (
12 <nav className="bg-white shadow-md p-4">
13 <div className="container mx-auto flex justify-between items-center">
14 <Link href="/" className="text-xl font-bold">
15 MyApp
16 </Link>
17
18 <div className="flex items-center space-x-4">
19 <Link href="/" className="hover:text-blue-500">
20 Home page
21 </Link>
22
23 {session ? (
24 <>
25 <Link href="/dashboard" className="hover:text-blue-500">
26 Dashboard
27 </Link>
28 <Link href="/profile" className="hover:text-blue-500">
29 Profile
30 </Link>
31 {session.user.role === 'admin' && (
32 <Link href="/admin" className="hover:text-blue-500">
33 Admin
34 </Link>
35 )}
36 </>
37 ) : (
38 <Link href="/features" className="hover:text-blue-500">
39 Features
40 </Link>
41 )}
42
43 {!isLoading && (
44 <div className="flex items-center space-x-2">
45 {session && (
46 <div className="flex items-center">
47 {session.user.image && (
48 <img
49 src={session.user.image}
50 alt={session.user.name || 'User'}
51 className="w-8 h-8 rounded-full mr-2"
52 />
53 )}
54 <span className="text-sm font-medium">
55 {session.user.name}
56 </span>
57 </div>
58 )}
59 <AuthButton />
60 </div>
61 )}
62 </div>
63 </div>
64 </nav>
65 );
66}NextAuth.js can also be used to secure API endpoints:
1// app/api/protected-data/route.ts
2import { getServerSession } from 'next-auth/next';
3import { NextResponse } from 'next/server';
4import { authOptions } from '../auth/[...nextauth]/route';
5
6export async function GET() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 return NextResponse.json(
11 { message: 'Unauthorized access' },
12 { status: 401 }
13 );
14 }
15
16 // Check permissions based on role (optional)
17 if (session.user.role !== 'admin') {
18 return NextResponse.json(
19 { message: 'Insufficient permissions' },
20 { status: 403 }
21 );
22 }
23
24 // Data available only to logged-in users (and admins if we check roles)
25 return NextResponse.json({
26 message: 'Success',
27 data: {
28 secretContent: 'This data is protected',
29 timestamp: new Date().toISOString()
30 }
31 });
32}Security first
Performance optimization
Error handling
User experience
Testing
In some cases, you may want to implement multi-step authentication (e.g. password + SMS code):
1// Implement a custom CredentialsProvider
2CredentialsProvider({
3 name: 'TwoFactor',
4 credentials: {
5 email: { label: "Email", type: "email" },
6 password: { label: "Password", type: "password" },
7 code: { label: "Verification code", type: "text" }
8 },
9 async authorize(credentials) {
10 if (!credentials?.email || !credentials?.password) {
11 return null;
12 }
13
14 // First step: verify password
15 const user = await prisma.user.findUnique({
16 where: { email: credentials.email }
17 });
18
19 if (!user || !await compare(credentials.password, user.password)) {
20 return null;
21 }
22
23 // Second step: verify the verification code
24 if (credentials.code) {
25 const isValidCode = await verifyTwoFactorCode(user.id, credentials.code);
26
27 if (!isValidCode) {
28 return null;
29 }
30 } else {
31 // If the code was not provided and is required
32 if (user.twoFactorEnabled) {
33 // Sending the code to email/phone
34 await sendTwoFactorCode(user);
35
36 // Return a special object so App Router knows we need a code
37 throw new Error('TwoFactorRequired');
38 }
39 }
40
41 // Authentication successful
42 return {
43 id: user.id,
44 email: user.email,
45 name: user.name,
46 role: user.role
47 };
48 }
49})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 role: 'user' // Default role
52 }
53 });
54
55 // Do not return password in response
56 const { password: _, ...userWithoutPassword } = user;
57
58 return NextResponse.json(
59 { message: 'Registration completed successfully', user: userWithoutPassword },
60 { status: 201 }
61 );
62 } catch (error) {
63 console.error('Registration error:', error);
64 return NextResponse.json(
65 { message: 'A server error occurred' },
66 { status: 500 }
67 );
68 }
69}NextAuth.js (Auth.js) is a comprehensive tool for implementing authentication in Next.js applications. It offers:
Thanks to NextAuth.js, you can quickly implement a complete authentication system in your Next.js application, saving time and minimizing the risk of security-related errors.
In the following modules, we will discuss more advanced aspects of authentication, including authentication strategies, session and token management, and implementation of roles and permissions.