Skip to content

Marrow-Stack/ms-block-auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

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:

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:

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:

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:

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:

openssl rand -base64 32

Step 4: Add the auth block file

Create this file:

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:

// 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:

types/next-auth.d.ts

Add:

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:

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:

/auth/signin

So create:

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:

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:
https://yourdomain.com/auth/verify-email?token=TOKEN_HERE
  1. 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:

// 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:

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:

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:

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 <div>{ok ? 'Email verified successfully' : 'Invalid or expired verification link'}</div>
}

Step 12: Create forgot-password flow

Create a page such as:

app/auth/forgot-password/page.tsx

On submit:

  1. Validate the email.
  2. Call requestPasswordReset(email).
  3. If the account exists, you will get a token.
  4. Send an email with a reset link.
  5. Always show a generic confirmation message like: “If an account exists, a reset link has been sent.”

That generic response helps avoid user enumeration, which OWASP recommends. [1]

Example reset URL:

https://yourdomain.com/auth/reset-password?token=TOKEN_HERE

Step 13: Create reset-password page

Create:

app/auth/reset-password/page.tsx

On that page:

  1. Read the token from the URL.
  2. Collect new password + confirm password.
  3. Validate on the client.
  4. Call resetPassword(token, password) on submit.
  5. Show success or expired/invalid-link state.

The hardened block uses time-limited reset tokens, which matches OWASP guidance for secure recovery flows. [1]

Step 14: Protect server components

In a server component:

import { getServerSession } from 'next-auth'
import { authOptions, requireRole } from '@/blocks/auth'

export default async function AdminPage() {
  const session = await getServerSession(authOptions)
  requireRole(session, 'admin')

  return <div>Admin only</div>
}

Use requireRole(session, 'admin') or requireRole(session, 'super_admin') when you want role-based access control.

Step 15: Protect API routes

Example protected route:

import { withAuth } from '@/blocks/auth'

export const POST = withAuth(async (req, session) => {
  return Response.json({
    ok: true,
    userId: session.user.id,
    role: session.user.role,
  })
}, 'admin')

This ensures:

  • unauthenticated users get 401
  • authenticated but underprivileged users get 403
  • authorized users reach the handler

Step 16: Use session data in your app

Anywhere you need current-user info from the server:

import { getServerSession } from 'next-auth'
import { authOptions } from '@/blocks/auth'

const session = await getServerSession(authOptions)

Useful fields available on session.user:

  • id
  • email
  • name
  • image
  • role
  • emailVerified

Step 17: Add rate limiting

This part is strongly recommended before production. OWASP and ASVS both emphasize defenses against brute-force and abuse on authentication endpoints. [1][3]

Recommended routes to rate limit:

  • sign in
  • sign up
  • forgot password
  • reset password
  • resend verification email

If you use Upstash, wire it into the relevant route handlers or server actions. Even a simple IP-based limit is much better than none. [1]

Step 18: Test locally

Before deploying, test these flows manually:

Core auth tests

  • Register a new account.
  • Verify email.
  • Sign in with credentials.
  • Sign out.
  • Request password reset.
  • Reset password with valid token.
  • Confirm old password no longer works.
  • Confirm new password works.

Security tests

  • Try signing in with wrong password multiple times.
  • Try reset token after expiry.
  • Try invalid verification token.
  • Try visiting admin route as regular user.
  • Try API route with no session.
  • Confirm generic error behavior for invalid auth attempts.

OWASP recommends testing both normal and abuse cases in authentication and recovery flows. [1][3]

Step 19: Configure OAuth providers

GitHub OAuth

In GitHub Developer Settings:

  • Set the callback URL to:
http://localhost:3000/api/auth/callback/github

For production:

https://yourdomain.com/api/auth/callback/github

Google OAuth

In Google Cloud Console:

  • Add authorized redirect URI:
http://localhost:3000/api/auth/callback/google

For production:

https://yourdomain.com/api/auth/callback/google

If these callback URLs are wrong, provider sign-in will fail. NextAuth’s provider setup depends on matching callback configuration. [2]

Step 20: Production deployment checklist

Before going live, confirm all of this:

  • NEXTAUTH_URL points to your real domain. [2]
  • NEXTAUTH_SECRET is set. [2]
  • SUPABASE_SERVICE_ROLE_KEY is present only on the server. [3]
  • OAuth provider production callback URLs are correct. [2]
  • Verification and reset emails use your real domain.
  • Cookies are secure in production.
  • Rate limiting is active.
  • RLS is enabled in Supabase.
  • Protected routes actually reject unauthorized users.
  • Password reset and verification links expire correctly.

Common integration examples

Get current profile

import { getProfile } from '@/blocks/auth'

const profile = await getProfile(session.user.id)

Update profile

import { updateProfile } from '@/blocks/auth'

await updateProfile(session.user.id, {
  name: 'Samarth',
  avatar_url: 'https://example.com/avatar.png',
})

Change password

import { changePassword } from '@/blocks/auth'

await changePassword(session.user.id, currentPassword, newPassword)

Get audit events

import { getAuthEvents } from '@/blocks/auth'

const events = await getAuthEvents(session.user.id)

What you still need to build yourself

This auth block handles the auth logic, but these pieces are still product-specific and should be added per SaaS:

  • Email sending provider integration
  • onboarding flow after signup
  • billing/subscription checks
  • team/invite logic if your SaaS is multi-user
  • MFA if you want stronger account security
  • bot protection or captcha if abuse becomes a concern

Troubleshooting

session.user.id type error

You probably forgot the types/next-auth.d.ts file.

OAuth login fails after redirect

Usually one of these is wrong:

  • NEXTAUTH_URL
  • provider callback URL
  • client ID / client secret

Supabase permission error

Check:

  • SQL migration fully ran
  • table exists
  • policy exists
  • service role key is valid
  • server-side code is using the admin client

Password reset link says invalid

Possible causes:

  • token expired
  • token already used
  • wrong environment URL in email
  • token not included correctly in query string

Admin route is accessible by normal user

Check:

  • requireRole(session, 'admin') is actually called
  • route uses withAuth(..., 'admin')
  • session token contains role
  • JWT callback is populating token.role

Security notes for SaaS owners

This system is strong, but copy-pasting auth does not automatically make an app secure. OWASP advises secure design for login, recovery, authorization, and abuse prevention as a whole, and ASVS treats authentication, session management, configuration, and access control as separate verification areas. [1][3][5]

That means you should still review:

  • access control on every sensitive route. [5]
  • secure environment variable handling. [3]
  • rate limiting and lockout behavior. [1][3]
  • audit logging for important auth actions. [4][3]
  • production cookie/session behavior. [2][3]

Quick start summary

If you want the shortest version, do these in order:

  1. Install dependencies.
  2. Run the Supabase SQL migration.
  3. Add environment variables.
  4. Paste the auth block into blocks/auth.ts.
  5. Create app/api/auth/[...nextauth]/route.ts.
  6. Add types/next-auth.d.ts.
  7. Add middleware.ts.
  8. Build /auth/signin, /auth/signup, /auth/verify-email, /auth/forgot-password, and /auth/reset-password.
  9. Add email sending.
  10. Test every auth flow locally.
  11. Configure OAuth callbacks.
  12. Deploy with production env vars.

Suggested buyer note

If you are distributing this to SaaS founders, include this note with the block:

This auth block provides the backend auth foundation, but you are still responsible for email delivery, frontend pages, production environment configuration, and route-level authorization inside your app. Authentication security depends on correct deployment and correct use of protected routes. [1][3][2]

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors