# MarrowStack Auth System A production-focused authentication block for **Next.js 14+ SaaS apps** using **NextAuth.js v4**, **Supabase**, **bcryptjs**, **Zod**, **credentials auth**, **Google/GitHub OAuth**, **RBAC**, **email verification**, **password reset**, and **auth audit logs**. This setup follows common authentication hardening guidance from OWASP and uses NextAuth page/API protection patterns with secure session handling expectations. [1][2][3] This README is written for someone who wants to **copy, paste, wire up, and launch** this auth system inside a SaaS app with minimal confusion. The flow below takes you from a clean app to a working auth setup step by step. [2][1] ## What this block includes - Email/password login with hashed passwords using bcryptjs. [1] - Google OAuth and GitHub OAuth via NextAuth providers. [2] - Role-based access control with `user`, `admin`, and `super_admin`. [3] - Email verification and password reset token flows. [1] - Audit log table for auth activity tracking. [4] - Next.js API and page protection helpers for authenticated and role-restricted routes. [2] ## Before you start You should already have these ready before pasting the auth block: - A **Next.js 14+ app** using the App Router. - A **Supabase project**. - A **Google OAuth app** if you want Google login. - A **GitHub OAuth app** if you want GitHub login. - An email sending setup for verification/reset emails, because this block generates tokens but does not send emails by itself. OWASP recommends careful handling of verification and recovery flows rather than exposing raw account state. [1] ## Recommended folder structure Use this structure so the integration stays clean: ```txt app/ api/ auth/ [...nextauth]/ route.ts auth/ signin/ page.tsx verify-email/ page.tsx reset-password/ page.tsx blocks/ auth.ts lib/ email.ts middleware.ts .env.local ``` You can rename folders if you want, but if you keep the auth pages exactly as shown in the config, the paths will work immediately. ## Step 1: Install dependencies Install the packages used by the auth block: ```bash npm install next-auth @supabase/supabase-js bcryptjs zod ``` If you plan to add request throttling, also install a rate-limiter package. OWASP recommends protection against brute-force and abuse on login and recovery endpoints. [1][3] Example: ```bash npm install @upstash/ratelimit @upstash/redis ``` ## Step 2: Create Supabase tables Open your **Supabase SQL Editor** and run the SQL migration that comes with the auth block. Use the hardened version of the schema, including these fields: - `verify_token_expires` - `failed_login_attempts` - `locked_until` - `email_verify_token` - `reset_token` - `reset_token_expires` These fields support token expiry, recovery flows, and basic lockout behavior, which align with OWASP and ASVS expectations around authentication and account protection. [1][3] After running the SQL, confirm the following tables exist: - `profiles` - `auth_events` Also confirm these are in place: - RLS enabled on both tables. - Policies for own-profile access. - Policy for admin access. - `updated_at` trigger. - Failed login trigger if you included lockout logic. ## Step 3: Add environment variables Create a `.env.local` file in the root of your app. Use this template: ```env NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=replace-with-a-long-random-secret NEXT_PUBLIC_SUPABASE_URL=your-supabase-url SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret ``` ### Important notes - `SUPABASE_SERVICE_ROLE_KEY` must stay **server-only** because it bypasses RLS. [3] - `NEXTAUTH_SECRET` should be long and random because NextAuth uses it to protect session-related data. [2] - `NEXTAUTH_URL` must match your real site URL in production, or OAuth callbacks may fail. [2] You can generate a secret with: ```bash openssl rand -base64 32 ``` ## Step 4: Add the auth block file Create this file: ```txt blocks/auth.ts ``` Paste the hardened auth code into that file. This file contains: - NextAuth config - Zod schemas - registration helper - email verification helper - password reset helpers - profile helpers - RBAC helpers - route protection wrapper - audit log helpers ## Step 5: Create the NextAuth route Create this file: ```ts // app/api/auth/[...nextauth]/route.ts import NextAuth from 'next-auth' import { authOptions } from '@/blocks/auth' const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ``` This is the main route NextAuth uses for sign-in, callbacks, sessions, and provider flows. NextAuth documents page and API protection through its route/session model. [2] ## Step 6: Extend NextAuth types Because the auth block adds custom fields like `id`, `role`, and `emailVerified` to `session.user`, create a type declaration file so TypeScript stops complaining. Create: ```txt types/next-auth.d.ts ``` Add: ```ts import NextAuth, { DefaultSession } from 'next-auth' declare module 'next-auth' { interface Session { user: { id: string role: 'user' | 'admin' | 'super_admin' emailVerified: boolean } & DefaultSession['user'] } interface User { role?: 'user' | 'admin' | 'super_admin' emailVerified?: boolean } } declare module 'next-auth/jwt' { interface JWT { id?: string role?: 'user' | 'admin' | 'super_admin' emailVerified?: boolean } } ``` If you skip this step, your code may still run, but TypeScript will usually show errors when you access `session.user.id` or `session.user.role`. ## Step 7: Add middleware protection Create `middleware.ts` in the project root: ```ts import { withAuth } from 'next-auth/middleware' export default withAuth({ pages: { signIn: '/auth/signin', }, }) export const config = { matcher: ['/dashboard/:path*', '/admin/:path*'], } ``` This protects routes like `/dashboard` and `/admin` at the middleware layer. NextAuth recommends this pattern for securing pages and API routes. [2] If you only want certain routes protected, change the `matcher` array. ## Step 8: Build your sign-in page The auth config already points to this custom page: ```txt /auth/signin ``` So create: ```txt app/auth/signin/page.tsx ``` At minimum, this page should include: - email input - password input - sign-in button - Google sign-in button - GitHub sign-in button - forgot-password link - link to sign-up page Use NextAuth’s `signIn()` client helper on that page. Keep error messages generic for failed login attempts so you do not reveal whether an account exists. OWASP explicitly recommends avoiding account enumeration in auth flows. [1] ## Step 9: Build your sign-up flow Create a sign-up page, for example: ```txt app/auth/signup/page.tsx ``` On submit: 1. Validate input using the `RegisterSchema`. 2. Call `registerUser({ name, email, password })` from the auth block. 3. Get back `user` and `verifyToken`. 4. Send a verification email containing a URL like: ```txt https://yourdomain.com/auth/verify-email?token=TOKEN_HERE ``` 5. Show a success message like: “Check your email to verify your account.” Do not auto-log the user in before verification unless you intentionally want that product behavior. ## Step 10: Send verification emails The auth block creates the token, but **you** need to send the email. A simple email helper can look like this: ```ts // lib/email.ts export async function sendVerificationEmail(email: string, token: string) { const url = `${process.env.NEXTAUTH_URL}/auth/verify-email?token=${token}` // send email with your provider here // resend / nodemailer / postmark / mailgun / ses } ``` Then, after registration: ```ts const { user, verifyToken } = await registerUser({ name, email, password }) await sendVerificationEmail(user.email, verifyToken) ``` OWASP recommends secure, time-limited verification flows rather than open-ended token handling. [1] ## Step 11: Create the email verification page Create: ```txt app/auth/verify-email/page.tsx ``` In this page: 1. Read the `token` from `searchParams`. 2. Call `verifyEmail(token)`. 3. Show success or failure UI. Example server component logic: ```ts import { verifyEmail } from '@/blocks/auth' export default async function VerifyEmailPage({ searchParams }: { searchParams: { token?: string } }) { const token = searchParams.token const ok = token ? await verifyEmail(token) : false return