Next.js course Β· Module 7: Authentication and Authorization
Implementing NextAuth.js / Auth.js
In this lesson14
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.
Why use NextAuth.js?
NextAuth.js offers many benefits:
- Ready-made solution - no need to implement authentication from scratch
- Support for various providers - offers integration with over 50 authentication providers
- Security - implements security best practices
- Session management - handles sessions on both server and client side
- Flexibility - can be customized to specific application needs
- App Router support - works with the new Next.js architecture
- Extended TypeScript typings - offers full type support
Installation and basic configuration
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@latestBasic configuration for the App Router
To 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.
Adding the SessionProvider component
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}Environment variables configuration
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_URLshould be the full URL of your applicationNEXTAUTH_SECRETshould be a long, random string of characters. You can generate it using the commandopenssl rand -base64 32.
Configuration of different authentication providers
NextAuth.js supports many authentication providers. Let's look at how to configure a few popular ones:
1. Authentication with GitHub
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};2. Authentication with Google
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};3. Authentication with credentials (email/password)
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};4. Authentication from multiple providers
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};Configuring session and JWT options
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};Custom authentication pages
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}Callbacks and events
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};Extending types for TypeScript
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}Database adapters
To use the database-based session strategy or store user accounts, NextAuth.js offers adapters for various databases:
Configuration with Prisma
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}Other available adapters
NextAuth.js offers adapters for many databases:
- SQL adapters: PostgreSQL, MySQL, SQLite, SQL Server
- Non-relational adapters: MongoDB, DynamoDB
- ORM/ODM: Prisma, Mongoose, Typeorm, Sequelize, DrizzleORM
- Others: Firebase, Supabase, Kysely, Planetscale, Xata
Using authentication in the application
1. Fetching session data on the client side
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}2. Fetching session data on the server side
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}3. Authentication state management
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}Adapting the UI to NextAuth.js
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}Authentication with API Routes
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}Best practices for implementing NextAuth.js
Security first
- Use HTTPS in production
- Generate a strong NEXTAUTH_SECRET
- Never store sensitive data in JWT tokens
Performance optimization
- Use session strategies appropriate to your needs
- Consider using the JWT strategy for greater scalability
Error handling
- Implement custom error pages
- Log authentication errors for analysis
User experience
- Design user-friendly login forms
- Allow smooth switching between providers
Testing
- Test different authentication scenarios
- Check how the application reacts to expired sessions
Advanced scenarios
1. Multi-step authentication
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})2. Implementing custom registration logic
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}Summary
NextAuth.js (Auth.js) is a comprehensive tool for implementing authentication in Next.js applications. It offers:
- Flexibility - supports multiple authentication strategies
- Integration - easily integrates with various providers
- Security - implements best security practices
- Scalability - works with various databases and architectures
- Extensibility - can be adapted to specific needs
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.
Code for this lesson: App.tsx
1import React, { useState } from 'react';
2
3// Demo: NextAuth.js / Auth.js - configuration and providers
4interface AuthConfig {
5 provider: string;
6 callbackUrl: string;
7 strategy: 'jwt' | 'database';
8 maxAge: number;
9}
10
11const defaultProviders: AuthConfig[] = [
12 { provider: 'credentials', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 3600 },
13 { provider: 'google', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 86400 },
14 { provider: 'github', callbackUrl: '/dashboard', strategy: 'database', maxAge: 604800 },
15];
16
17function AuthConfigBuilder() {
18 const [configs, setConfigs] = useState<AuthConfig[]>(defaultProviders);
19 const [newProvider, setNewProvider] = useState('');
20 const [strategy, setStrategy] = useState<'jwt' | 'database'>('jwt');
21 const [showCode, setShowCode] = useState(false);
22
23 const addProvider = () => {
24 if (!newProvider.trim()) return;
25 setConfigs(prev => [...prev, {
26 provider: newProvider.trim(),
27 callbackUrl: '/dashboard',
28 strategy,
29 maxAge: strategy === 'jwt' ? 3600 : 86400,
30 }]);
31 setNewProvider('');
32 };
33
34 const removeProvider = (index: number) => {
35 setConfigs(prev => prev.filter((_, i) => i !== index));
36 };
37
38 // Keywords from variables: the editor preview strips import/export lines from the code, even inside template text
39 const kw = { imp: 'import', exp: 'export' };
40 const generatedCode = `// app/api/auth/[...nextauth]/route.ts
41${kw.imp} NextAuth from 'next-auth';
42${configs.map(c => {
43 if (c.provider === 'credentials') return kw.imp + " CredentialsProvider from 'next-auth/providers/credentials';";
44 return kw.imp + " " + c.provider.charAt(0).toUpperCase() + c.provider.slice(1) + "Provider from 'next-auth/providers/" + c.provider + "';";
45}).join('\n')}
46
47${kw.exp} const authOptions = {
48 providers: [
49${configs.map(c => " // " + c.provider + " provider\n // strategy: " + c.strategy).join(',\n')}
50 ],
51 session: {
52 strategy: '${configs[0]?.strategy || 'jwt'}',
53 maxAge: ${configs[0]?.maxAge || 3600},
54 },
55 callbacks: {
56 async jwt({ token, user }) {
57 if (user) token.role = user.role;
58 return token;
59 },
60 async session({ session, token }) {
61 session.user.role = token.role;
62 return session;
63 },
64 },
65};`;
66
67 return (
68 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
69 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>NextAuth.js / Auth.js Configuration</h1>
70 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Interactive NextAuth.js configurator - add providers and generate code</p>
71
72 <div style={{ display: 'flex', gap: '12px', marginBottom: '24px', flexWrap: 'wrap' }}>
73 <input
74 value={newProvider}
75 onChange={e => setNewProvider(e.target.value)}
76 placeholder="Provider name (e.g. discord)"
77 style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff', flex: 1, minWidth: '200px' }}
78 />
79 <select
80 value={strategy}
81 onChange={e => setStrategy(e.target.value as 'jwt' | 'database')}
82 style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff' }}
83 >
84 <option value="jwt">JWT Strategy</option>
85 <option value="database">Database Strategy</option>
86 </select>
87 <button onClick={addProvider} style={{ padding: '8px 16px', borderRadius: '6px', background: '#64ffda', color: '#0f0f23', border: 'none', cursor: 'pointer', fontWeight: 'bold' }}>
88 + Add Provider
89 </button>
90 </div>
91
92 <div style={{ display: 'grid', gap: '12px', marginBottom: '24px' }}>
93 {configs.map((config, i) => (
94 <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', background: '#1a1a2e', borderRadius: '8px', border: '1px solid #333' }}>
95 <div>
96 <strong style={{ color: '#64ffda' }}>{config.provider}</strong>
97 <span style={{ color: '#8892b0', marginLeft: '12px', fontSize: '14px' }}>
98 Strategy: {config.strategy} | MaxAge: {config.maxAge}s
99 </span>
100 </div>
101 <button onClick={() => removeProvider(i)} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer' }}>
102 Remove
103 </button>
104 </div>
105 ))}
106 </div>
107
108 <button
109 onClick={() => setShowCode(!showCode)}
110 style={{ padding: '10px 20px', borderRadius: '6px', background: showCode ? '#333' : '#7c3aed', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}
111 >
112 {showCode ? 'Hide code' : 'Generate configuration code'}
113 </button>
114
115 {showCode && (
116 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', overflow: 'auto', fontSize: '13px', lineHeight: 1.5, border: '1px solid #333' }}>
117 <code style={{ color: '#e0e0e0' }}>{generatedCode}</code>
118 </pre>
119 )}
120 </div>
121 );
122}
123
124export default AuthConfigBuilder;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Authentication means:
2. Authorization means:
Hands-on tasks in the game
- Code editor
Build an interface with protected sections that require login
- Vertical ordering
Arrange the elements in the correct order: import 'server-only' β export async β fu
- Click in order
Arrange the elements in the correct order: import β NextAuth β from
- Code editor
Create a login component with JWT authentication