Authentication Guide
NextAuth.js v5 setup with Credentials + OAuth (Google/GitHub), JWT sessions, role-based admin access, signup/register flow, and Edge-safe middleware for Vercel.
Overview
Authentication is handled by NextAuth.js v5 (Auth.js) with Credentials (email/password) and optional OAuth providers (Google/GitHub). Sessions use JWT strategy (no database sessions). Shared options (pages, session, JWT/session callbacks) live in src/lib/auth.config.ts; the full app config in src/lib/auth.ts spreads that file and adds providers plus Prisma/bcrypt logic. The admin panel at /admin/* is protected by Next.js middleware that validates the JWT session without importing the database layer.
The auth UI includes both /admin/login and /admin/signup. Email sign-up posts to /api/auth/register, then signs in automatically. OAuth sign-in/up buttons are shown when public feature flags are enabled.
NextAuth Configuration
The route handler at src/app/api/auth/[...nextauth]/route.ts imports handlers from src/lib/auth.ts. That file merges shared config with the Credentials provider:
// src/lib/auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import bcrypt from "bcryptjs";
import { authConfig } from "./auth.config";
import { prisma } from "./prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
pages: authConfig.pages,
session: authConfig.session,
providers: [
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
? [Google({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET })]
: []),
...(process.env.GITHUB_ID && process.env.GITHUB_SECRET
? [GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET })]
: []),
Credentials({
/* authorize → prisma.user.findUnique + bcrypt.compare */
}),
],
});
Edge-safe defaults (no Prisma/bcrypt) are defined in src/lib/auth.config.ts — see Edge vs Node (Vercel). OAuth providers are conditionally added only when their server-side credentials exist, so local/dev can run without social login.
Edge vs Node (Vercel)
middleware.ts runs on the Edge. Importing auth from @/lib/auth pulled in Prisma Client and bcrypt, which inflated the middleware bundle past Vercel Hobby’s ~1 MB Edge Function limit and caused deploy failures after a successful build.
The fix is to initialize middleware with only authConfig (JWT/session logic, empty providers array — credentials run on the server only):
// middleware.ts (project root)
import NextAuth from "next-auth";
import { NextResponse } from "next/server";
import { authConfig } from "@/lib/auth.config";
export default NextAuth(authConfig).auth((req) => {
/* redirect unauthenticated /admin/* → /admin/login, etc. */
});
export const config = {
matcher: ["/admin/:path*"],
};
Sign-in and API routes still use src/lib/auth.ts (Node/serverless) where Prisma and bcrypt are appropriate.
Credentials Provider
The Credentials provider accepts email and password. It queries the database for a user with the given email, then verifies the password with bcrypt.compare().
- If the user is found and the password matches, the user object (with
id,email,name,role) is returned. - If no user is found or the password doesn't match,
nullis returned (login fails).
OAuth Providers (Google & GitHub)
src/lib/auth.ts also supports Google and GitHub providers. They are enabled only if these env vars are present:
GOOGLE_CLIENT_ID+GOOGLE_CLIENT_SECRETGITHUB_ID+GITHUB_SECRET
For UI gating, the login/signup forms read public flags:
NEXT_PUBLIC_GOOGLE_OAUTH_ENABLED=trueNEXT_PUBLIC_GITHUB_OAUTH_ENABLED=true
On first successful OAuth sign-in, the signIn callback checks whether the user exists by email; if not, it creates a user row automatically with a generated bcrypt-hashed password. This keeps credentials and social accounts in one user table.
JWT & Session Callbacks
JWT Callback
The JWT callback keeps id, role, email, and name in sync with the database. This ensures Credentials and OAuth users both get a normalized token payload.
jwt({ token, user }) {
const tokenEmail = (user?.email ?? token.email)?.toLowerCase();
if (tokenEmail && (user || !token.id || !token.role)) {
const dbUser = await prisma.user.findUnique({ where: { email: tokenEmail } });
if (dbUser) {
token.id = dbUser.id;
token.role = dbUser.role;
token.email = dbUser.email;
token.name = dbUser.name;
}
}
return token;
}
Session Callback
The session callback copies role and id from the JWT token into the session object, making them available in client components via useSession().
Auth Middleware
The middleware at middleware.ts (project root) protects all /admin/* routes:
- Unauthenticated users accessing
/admin/*(except/admin/loginand/admin/signup) are redirected to/admin/login(withcallbackUrlpreserved). - Authenticated users accessing
/admin/loginor/admin/signupare redirected to/admin(dashboard).
It uses NextAuth(authConfig).auth(...) so the Edge bundle contains only Auth.js JWT handling — not Prisma or bcrypt. Do not switch this back to import { auth } from "@/lib/auth" without checking middleware size on Vercel.
NEXTAUTH_SECRET and callbacks as auth.ts.Admin Layout
The admin layout (src/app/admin/layout.tsx) wraps all admin pages with:
- AuthProvider — wraps children in NextAuth's
SessionProviderfor client-side session access. - AdminSidebar — navigation sidebar with links to dashboard, posts, courses, contacts, and a sign-out button.
Login Page
The login page at /admin/login is split into two components:
page.tsx— Server Component wrapper with<Suspense>boundary.LoginForm.tsx— Client Component with field-level validation, loading states, OAuth buttons (when enabled), callback URL handling, and error display.
The Suspense boundary is required because useSearchParams() must be wrapped in Suspense to avoid SSR warnings in the App Router (Next.js 15+).
Signup & Register API
The signup page at /admin/signup mirrors the login UX and adds first-name/last-name fields, password confirmation, and a password-strength indicator. It supports both email signup and social signup buttons (Google/GitHub) when enabled.
src/app/admin/signup/page.tsx— Server wrapper with<Suspense>.src/app/admin/signup/SignupForm.tsx— Client form with validation and submit handling.src/app/api/auth/register/route.ts— POST endpoint for server-side validation and user creation.
/api/auth/register validates names/email/password, blocks duplicate emails, hashes passwords with bcrypt, and creates a user in Prisma. After successful registration, the client attempts immediate credentials sign-in and redirects to the callback URL or admin dashboard.
AuthProvider
src/components/providers/AuthProvider.tsx is a "use client" component that wraps children with NextAuth's SessionProvider. It's used in the admin layout so all admin pages can access session data via useSession().
Authentication Flow
1. User visits /admin → middleware redirects to /admin/login
2. User enters email + password → LoginForm calls signIn("credentials")
3. NextAuth Credentials provider → prisma.user.findUnique → bcrypt.compare
4. On success → JWT created with { role, id } → redirect to /admin
5. Subsequent requests → Edge middleware validates JWT (no DB) → allows access
6. AdminSidebar reads useSession() → shows user info, sign-out button
1. User clicks Google/GitHub on /admin/login or /admin/signup
2. NextAuth redirects to OAuth provider and back to callback
3. signIn callback checks user by email in Prisma
4. If user does not exist → create user row (generated bcrypt hash)
5. JWT/session callbacks hydrate { id, role, email, name } from database
6. User is redirected to callbackUrl or /admin