26 minLesson 26 of 33
Authentication
NextAuth.js v5 Setup
NextAuth.js v5 Setup
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. This lesson walks through a complete production-ready setup.
Installation
npm install next-auth@beta @auth/prisma-adapter
# Generate a secure secret
openssl rand -base64 32
# Or use: npx auth secret
Add 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/myapp
Database Schema for Auth
Add 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
Auth Configuration
// 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
},
});
Route Handler
// 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.
TypeScript Types
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;
}
}
Getting the Session
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>;
}
Login and Sign-Up Pages
// 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>
);
}
OAuth Provider Setup
Google:
- Google Cloud Console → APIs & Services → Credentials
- Create OAuth 2.0 Client ID
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google - Copy Client ID and Secret to
.env.local
GitHub:
- GitHub Settings → Developer settings → OAuth Apps
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github - Copy Client ID and Secret to
.env.local
Next lesson: Protected routes and middleware — securing routes at the edge.
📱
Get Notes Free →Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises