Authentication overview

NextForge uses NextAuth v5 with JWT sessions. Four complete flows are wired: registration, login, email verification, and password reset.

How it fits together

Authentication is split across three layers:

  • lib/auth.ts - NextAuth configuration, providers, JWT and session callbacks
  • actions/auth.ts - server actions for registration, login, verification, logout
  • actions/email.ts - server actions for OTP delivery and password reset

The form components in components/auth/ call these server actions using useTransition - the submit button is disabled and shows a spinner for the entire duration of the action.

Registration flow

  1. User submits name, email, password via RegisterForm
  2. registerAction() sanitizes input with sanitizeAndStrip()
  3. Zod registerSchema validates - returns fieldErrors if invalid
  4. authRatelimit checks the IP - returns error if exceeded
  5. connectMongo() / Supabase / Firebase client initializes
  6. Duplicate email check - returns error if exists
  7. User created - password hashed by bcrypt pre-save hook (cost 12)
  8. generateOTP() creates 6-digit CSPRNG code
  9. hashOTP() hashes it with SHA-256 - OTP record saved with 10-min expiry
  10. sendVerificationEmail() delivers OTP via Resend
  11. Client redirects to /verify-email

Login flow

  1. User submits email, password via LoginForm
  2. loginAction() sanitizes and validates with loginSchema
  3. authRatelimit check
  4. signIn('credentials', ...) called - NextAuth Credentials provider runs authorize()
  5. User looked up by email with .select('+password')
  6. comparePassword() runs bcrypt.compare()
  7. On success: JWT minted with id, role, emailVerified - stored as HTTP-only cookie
  8. Client redirects to /dashboard

Email verification flow

  1. User submits 6-digit OTP from their email via VerifyEmailForm
  2. verifyEmailAction() sanitizes and validates
  3. OTP record queried: matching email, purpose: 'email-verification', used: false, expiresAt > now
  4. verifyOTP() runs crypto.timingSafeEqual - constant-time comparison
  5. OTP marked used - user.emailVerified set to true
  6. Client redirects to /dashboard

Password reset flow

  1. User submits email via ForgotPasswordForm
  2. sendPasswordResetOTPAction() - always returns the same success message regardless of whether email exists (prevents email enumeration)
  3. If user found: OTP generated, hashed, saved, email sent
  4. User submits email + OTP + new password via ResetPasswordForm
  5. resetPasswordAction() validates OTP, marks used, updates password (pre-save hook re-hashes)
  6. Client redirects to /login

Accessing the session

In server components and server actions:

app/dashboard/page.tsx
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const session = await auth()
  if (!session) redirect('/login')

  return <div>Welcome, {session.user.name}</div>
}

In client components:

components/nav/Navbar.tsx
'use client'
import { useSession } from 'next-auth/react'

export function Navbar() {
  const { data: session, status } = useSession()

  if (status === 'loading') return <NavSkeleton />
  if (!session) return <GuestNav />
  return <AuthNav user={session.user} />
}