AiTechWorlds
AiTechWorlds
University learning companion
React & Next.js Complete Course · NextAuth.js v5 Setup
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “NextAuth.js v5 Setup” in the larger system. It complements—not replaces—a topic-specific worked diagram.
NextAuth.js v5 (Auth.js) is the standard authentication library for Next.js it handles Google, GitHub, and 60+ OAuth providers out of the box, plus email/password credentials.
Think of it as a universal building access system that already speaks the protocol for every visitor badge type Google ID, GitHub ID, or a plain key card instead of you wiring up a separate reader for each one. This lesson walks through a complete production ready setup.
npm install next-auth@beta @auth/prisma-adapter# Generate a secure secret
openssl rand -base64 32
# Or use: npx auth secretAdd to .env.local:
AUTH_SECRET=your-generated-secret-here
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret
AUTH_GITHUB_ID=your-github-client-id
AUTH_GITHUB_SECRET=your-github-client-secret
DATABASE_URL=postgresql://localhost:5432/myappAdd NextAuth tables to your Prisma schema:
// prisma/schema.prisma
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
password String? // for credentials auth
role String @default("user")
accounts Account[]
sessions Session[]
@@map("users")
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
}npx prisma migrate dev --name add-auth-tables// src/auth.ts — the central auth config
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { db } from "@/lib/db";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(db),
providers: [
// OAuth providers — just add them
Google,
GitHub,
// Email + password credentials
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await db.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) return null;
const isValid = await bcrypt.compare(
credentials.password as string,
user.password
);
if (!isValid) return null;
return { id: user.id, email: user.email, name: user.name, role: user.role };
},
}),
],
session: {
strategy: "jwt", // JWT stored in HttpOnly cookie
},
callbacks: {
// Add extra fields to the JWT token
async jwt({ token, user }) {
if (user) {
token.role = user.role;
token.id = user.id;
}
return token;
},
// Make token fields available in the session
async session({ session, token }) {
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
},
pages: {
signIn: "/login", // Custom login page
error: "/auth/error", // Custom error page
},
});// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;This one file handles all NextAuth routes: /api/auth/signin, /api/auth/signout, /api/auth/callback/google, etc.
Extend NextAuth types to include your custom fields:
// src/types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
interface User {
role?: string;
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string;
role?: string;
}
}In Server Components:
// src/app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect("/login");
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Role: {session.user.role}</p>
</div>
);
}In Client Components:
"use client";
import { useSession } from "next-auth/react";
function UserMenu() {
const { data: session, status } = useSession();
if (status === "loading") return <Spinner />;
if (!session) return <LoginButton />;
return (
<div className="flex items-center gap-3">
<img src={session.user.image ?? ""} className="w-8 h-8 rounded-full" alt=""/>
<span>{session.user.name}</span>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}Wrap your app in SessionProvider for client side session access:
// src/providers/session-provider.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export default function AuthSessionProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}// src/app/login/page.tsx
"use client";
import { signIn } from "next-auth/react";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
async function handleCredentialsLogin(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError("");
const formData = new FormData(e.currentTarget);
const result = await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirect: false,
});
if (result?.error) {
setError("Invalid email or password");
setLoading(false);
} else {
router.push("/dashboard");
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-md">
<h1 className="text-2xl font-bold text-center mb-8">Sign in</h1>
{/* OAuth buttons */}
<div className="space-y-3 mb-6">
<button
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
className="w-full flex items-center justify-center gap-3 border border-gray-300 rounded-lg px-4 py-3 hover:bg-gray-50 transition-colors font-medium"
>
<GoogleIcon />
Continue with Google
</button>
<button
onClick={() => signIn("github", { callbackUrl: "/dashboard" })}
className="w-full flex items-center justify-center gap-3 bg-gray-900 text-white rounded-lg px-4 py-3 hover:bg-gray-800 transition-colors font-medium"
>
<GitHubIcon />
Continue with GitHub
</button>
</div>
<div className="flex items-center gap-4 mb-6">
<hr className="flex-1 border-gray-200"/>
<span className="text-gray-400 text-sm">or</span>
<hr className="flex-1 border-gray-200"/>
</div>
{/* Credentials form */}
<form onSubmit={handleCredentialsLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input name="email" type="email" required className="w-full border rounded-lg px-3 py-2.5 focus:border-blue-500 outline-none"/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input name="password" type="password" required className="w-full border rounded-lg px-3 py-2.5 focus:border-blue-500 outline-none"/>
</div>
{error && <p className="text-red-600 text-sm">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2.5 rounded-lg transition-colors disabled:opacity-60"
>
{loading ? "Signing in..." : "Sign in"}
</button>
</form>
</div>
</div>
);
}Google:
http://localhost:3000/api/auth/callback/google.env.localGitHub:
http://localhost:3000/api/auth/callback/github.env.localNext lesson: Protected routes and middleware securing routes at the edge.
University learning companion
React & Next.js Complete Course · NextAuth.js v5 Setup
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises