Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
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:

  1. Google Cloud Console → APIs & Services → Credentials
  2. Create OAuth 2.0 Client ID
  3. Authorized redirect URIs: http://localhost:3000/api/auth/callback/google
  4. Copy Client ID and Secret to .env.local

GitHub:

  1. GitHub Settings → Developer settings → OAuth Apps
  2. Authorization callback URL: http://localhost:3000/api/auth/callback/github
  3. Copy Client ID and Secret to .env.local

Next lesson: Protected routes and middleware — securing routes at the edge.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!