0%
Vibe44 Logo

Authentication

Supabase Auth with 5 sign-in methods, universal MFA enforcement, passkeys, RLS, and session lifecycle.

Sign-In Methods

MethodImplementationMFA Enforced?
Email + PasswordSupabase signInWithPassword(), Zod (min 8 chars)Yes
OAuth (enabled)Google, GitHubYes
Magic LinkSupabase signInWithOtp(), email deliveryYes
Passkey (WebAuthn)Challenge-response, 60s expiry,passkeys tableYes
TOTP MFAAuthenticator app,getAuthenticatorAssuranceLevel()—

Authentication Flow

User submits credentials
    ↓
Supabase Auth validates (server-side)
    ↓
PKCE code exchange at /auth/callback
    ↓
Session created (httpOnly, secure, sameSite=lax cookies)
    ↓
proxy.ts middleware on every subsequent request:
    ├── Refreshes session token if needed
    ├── Checks MFA (AAL2 required)
    │   ├── AAL2 met → continue
    │   └── AAL2 NOT met → redirect /login?mfa=pending (fail-closed on error → /login?mfa=error)
    ├── Waitlist Gating (if enabled)
    │   ├── Approved → sync metadata → continue
    │   └── NOT Approved → delete auth user → redirect /signup?error=waitlist_not_approved
    ├── CSRF validation for mutating requests (HMAC-bound double-submit)
    ├── CSRF cookie seeding/refresh for authenticated page requests
    ├── Feature flag redirects (disabled → /dashboard)
    └── Rate limiting (per-tier, production fail-closed)
    ↓
Protected page renders

️ MFA (Multi-Factor Authentication)

Universal Enforcement

The root middleware (proxy.ts) implements a sophisticated security gate that detects the user's Authenticator Assurance Level (AAL). This gate applies to every auth method, not just email/password:

const {
  data: { currentLevel, nextLevel },
} = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();

// Sophisticated gate: Detects if user is at aal1 but has aal2 enrolled
if (nextLevel === "aal2" && currentLevel !== nextLevel) {
  // Fail-closed: errors (e.g. Supabase API down) redirect to /login?mfa=error
  return NextResponse.redirect(new URL("/login?mfa=pending", request.url));
}

This ensures multi-layered enforcement: if a user has MFA enrolled, they cannot bypass it by using a magic link or enabled OAuth provider that would normally only provide an aal1 session.

Covers:

  • Email/password → MFA challenge before dashboard
  • OAuth (Google, GitHub) → MFA after callback
  • Magic link → MFA after code exchange
  • Passkey → inline MFA after verifyOtp()

Enrollment

Settings → Security:

  1. supabase.auth.mfa.enroll({ factorType: "totp" }) → QR code
  2. User scans with authenticator app
  3. Verification code confirms enrollment
  4. Future logins require TOTP after primary auth

Challenge Component

  • 6-digit input with auto-submit

  • supabase.auth.mfa.challengeAndVerify() validation

  • Success → window.location.href = "/dashboard" (hard nav for layout refresh)

  • Brute force protection via Supabase Auth rate limits

Disablement (Step-Up Authentication)

To prevent unauthorized MFA disablement on an active session, users must pass identity re-verification (Step-Up Authentication) by providing their current password.

  • Verification is executed securely via the server action disableMfaWithPassword using FormData to prevent plain-text password leakage in development logs.
  • The action uses the service_role client to verify the password and concurrently unenroll all factors server-side, bypassing client-side AAL2 constraints.
  • Protects against session hijacking and unauthorized security downgrades.

Passkeys (WebAuthn)

Registration

"Add Passkey" in Settings → Security
    ↓
/api/auth/passkey/register POST `{ "phase": "options" }`
    ↓
Browser prompts biometric/security key
    ↓
/api/auth/passkey/register POST `{ "phase": "verify", "response": ..., "friendlyName": ... }`
    ↓
Challenge status updated to `registration_completed` for audit trail
    ↓
credential_id + public_key → passkeys table

Authentication

"Sign in with Passkey"
    ↓
/api/auth/passkey/authenticate POST `{ "phase": "options" }` → challenge
    ↓
Browser prompts biometric
    ↓
/api/auth/passkey/authenticate POST `{ "phase": "verify", "response": ... }` → signature validated
    ↓
Supabase session created
    ↓
MFA enabled? → inline MfaChallenge
    ↓
Redirect /dashboard

Requirements

  • HTTPS required (localhost for dev)
  • Chrome 67+, Firefox 60+, Safari 14+, Edge 79+
  • rpID auto-matches current domain
  • Registration and authentication POST bodies are validated with local Zod schemas before challenge lookup, credential verification, or DB mutation

OAuth Configuration

ProviderCallback URLStatus
Google{NEXT_PUBLIC_APP_URL}/auth/callbackEnabled in OAUTH_PROVIDERS
GitHub{NEXT_PUBLIC_APP_URL}/auth/callbackEnabled in OAUTH_PROVIDERS
Discord{NEXT_PUBLIC_APP_URL}/auth/callbackCommented scaffold
Twitter/X{NEXT_PUBLIC_APP_URL}/auth/callbackCommented scaffold
Apple{NEXT_PUBLIC_APP_URL}/auth/callbackCommented scaffold
Facebook{NEXT_PUBLIC_APP_URL}/auth/callbackCommented scaffold
LinkedIn{NEXT_PUBLIC_APP_URL}/auth/callbackCommented scaffold

Setup

  1. Create OAuth app in provider console
  2. Set redirect URL: {NEXT_PUBLIC_APP_URL}/auth/callback
  3. Copy Client ID + Secret
  4. Enable in Supabase Dashboard → Auth → Providers
  5. Paste credentials

All flows share /auth/callback — handles code exchange + redirect (or MFA challenge).


Password Reset

Unauthenticated (Forgot Password)

"Forgot Password" → submit email
    ↓
signInWithOtp() → redirectTo: {NEXT_PUBLIC_APP_URL}/auth/callback?next=/reset-password
    ↓
Email link → /auth/callback exchanges code
    ↓
/reset-password (NOT in AUTH_ROUTES — allows authenticated access)
    ↓
updateUser({ password }) → signOut() → /login with success toast

Authenticated (Change Password)

When changing passwords from Dashboard → Settings:

  1. User clicks "Reset Password"
  2. Step-Up Authentication: Identity verification dialog requires current password
  3. Success → Redirects to /reset-password
  4. updateUser({ password }) → signOut() → /login

/reset-password must NOT be in AUTH_ROUTES in proxy.ts — otherwise middleware redirects before password set.


️ Email Verification (Scanner Mitigation)

The Threat

Enterprise email gateways and security scanners (e.g., Proofpoint, Mimecast) aggressively prefetch links in incoming emails to detect phishing. When these scanners hit standard href links pointing to the Supabase /verify endpoint, they unintentionally consume the one-time password (OTP) token before the user can click the link, resulting in an "Email link is invalid or has expired" error for the actual user.

Secure Architecture

To protect verification links from automated consumption, Vibe44 implements a secure, user-initiated verification flow:

  1. Intermediate Page: Verification emails point to an intermediate Next.js route (/confirm-email?token_hash=xxx&type=signup) rather than the direct Supabase API endpoint.
  2. Scanner Mitigation: Automated scanners will load the /confirm-email page but do not execute the client-side JavaScript required to click the "Confirm Email" button. The token remains unused.
  3. User Action: The human user opens the email, clicks the link, lands on the /confirm-email page, and explicitly clicks "Confirm Email".
  4. Server-Side Verification: The button triggers a Server Action or POST request to /auth/confirm, which calls supabase.auth.verifyOtp() server-side to validate the token and establish the session.
  5. Audit Logging: Upon successful verification, the system logs an email_verified action to the activity_logs table, recording the email_verified_at timestamp.

Important

Supabase Configuration: The "Confirm signup" email template in the Supabase Dashboard MUST be updated to use the intermediate route: {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup Do not use the default {{ .ConfirmationURL }} variable, as it points directly to the vulnerable /verify endpoint.


Session Management

PropertyValue
Token typeJWT (access) + refresh
StoragehttpOnly, secure, sameSite=lax cookies
RefreshAutomatic in proxy.ts (transparent)
ExpiryConfigured in Supabase (default: 1h access, 7d refresh)
InvalidationsignOut() clears cookies + revokes refresh
Post-verification/verified calls signOut() to clear stale session
Login navigationwindow.location.href (hard nav = layout refresh)

Role-Based Access Control

Platform Roles

RoleAccess
userDashboard, billing, settings, projects, orgs, AI chat
super-adminEverything + 17-module modular super-admin panel + impersonation

profiles.role column. Value is super-admin (hyphenated) — super-admin and super_admin are invalid.

Organization Roles

RolePermissions
ownerFull control, billing, delete org
super-adminManage members, settings
memberView and participate

Stored in org_members.role. Independent of platform roles.

Super-Admin Protections

  • Self-shielding — cannot be deleted via super-admin UI
  • Impersonation — HMAC-signed cookie-based context, 1h auto-expiry, read-time super-admin/target revalidation, context propagation via x-impersonating-* headers, getEffectiveUserId() isolation. Signing uses IMPERSONATION_COOKIE_SECRET, falling back to NEXT_AUTH_SECRET or CSRF_SECRET.
  • All actions → activity_logs (36 typed categories)
  • Every super-admin page checks role === 'super-admin'

️ Row-Level Security (RLS)

RLS enabled on all 32 tables. Policies in supabase/schema.sql.

-- Users read own data
CREATE POLICY "Users can view own data"
  ON [table] FOR SELECT
  USING ((select auth.uid()) = user_id);

-- Users write own data
CREATE POLICY "Users can insert own data"
  ON [table] FOR INSERT
  WITH CHECK ((select auth.uid()) = user_id);

-- Super-Admin override
CREATE POLICY "Admins can view all"
  ON [table] FOR SELECT
  USING (
    EXISTS (SELECT 1 FROM profiles WHERE id = (select auth.uid()) AND role = 'super-admin')
  );

Always use (select auth.uid()) subquery — cached once per query, not per row.


Signup & Duplicate Handling

  • Supabase returns empty identities array for duplicate emails
  • Signup inspects data.user?.identities → "email already in use" toast
  • Prevents account enumeration (200 in both cases)

Callback Error Handling

/auth/callback parses error, error_code, error_description from URL. Redirects to /login with toast instead of crash.


️ Waitlist Gating & Metadata Sync

Authentication Gating

When isWaitlistEnabled() is true, the system enforces a "Waitlist Only" mode:

  1. Auth Callback Enforcement: The callback route (app/auth/callback/route.ts) queries the waitlist_subscribers table for the authenticating user's email.
  2. Real-time Deletion: If the user is not approved, their Supabase Auth account is immediately deleted via the service_role super-admin client to prevent ghost account accumulation.
  3. User Feedback: The user is redirected to the signup page with a localized waitlist_not_approved error toast.

Profile Metadata Synchronization

During the first successful login of an approved waitlist subscriber:

  • The system retrieves the fullName stored in the waitlist record.
  • It automatically updates the profiles table for that user.
  • This ensures that the Onboarding Wizard displays the user's correct name immediately, even if they signed up via a passwordless "Magic Link" which typically lacks metadata.

Auth Page UI Architecture

Layout

All auth pages use a split-screen layout:

  • Form Panel (left on login, right on others): Contains the form, OAuth buttons, and action buttons.
  • Branding Panel: AuthBranding component with animated testimonial carousel and gradient effects.

On mobile/tablet (< lg), the branding panel is hidden and the form panel floats as a glassmorphism card with backdrop-blur-2xl, rounded-3xl, and ambient gradient orbs in the background.

Theme & Locale Controls

Every auth form includes <ThemeToggle /> and <LocaleSwitcher /> positioned absolutely at top-2 right-2 of the form container:

<div className="absolute top-2 right-2 flex items-center z-50">
  {isI18nEnabled() && <LocaleSwitcher />}
  <ThemeToggle />
</div>

This ensures both controls are accessible on desktop, tablet, and mobile across all auth routes.

Button Styling

Button TypeStyle
Primary Actions (Email Sign-in, Magic Link, Passkey)bg-primary text-primary-foreground → hover: hover:text-primary hover:bg-primary/5 hover:border hover:border-primary
OAuth Buttons (Google, GitHub enabled)bg-background dark:bg-input/30 → hover: hover:bg-accent dark:hover:bg-input/50

Mobile Logo Branding

All auth forms display the brand logo above the heading on mobile/tablet (lg:hidden) using the standard dynamic theme logo pattern with next/image.


Copyright © 2026 Vibe44. Built with ❤️ by Scale with Alap.