Front Matter
Field | Value |
|---|---|
Title | W0.5 — Sensitive Action Re-Authentication |
Author | scorevi (Sean Patrick Caintic) |
Reviewers | @armaminter |
Created | 2026-05-26 |
Status | Approved (Merged to develop) |
Introduction & Goals
Problem Summary
Destructive admin actions (Delete User, Change Role, Delete Agency) previously lacked proper identity verification. The existing implementation had a placeholder password field that was commented out with the note: "Clerk doesn't provide password verification in the SDK — we'll skip password verification." This created a security gap where compromised admin sessions could perform destructive actions without re-confirming the admin's identity.
Goals
Implement a time-bounded (≤5 min) re-authentication gate before all destructive admin actions
Support both email/password and OAuth (Google) authentication methods
Enforce single-use tokens to prevent replay attacks
Log all re-authentication attempts (success/failure) to
audit_logsEnforce ADMIN role unconditionally — bypass dev/staging View-As mode role-check skip
Non-Goals
MFA/2FA support (future enhancement)
Session-wide re-auth (each destructive action requires its own token)
Client-side token caching (tokens are ephemeral)
Glossary
Term | Definition |
|---|---|
Re-auth Token | Time-bounded, single-use UUID issued after identity verification |
OAuth Account | Admin account authenticated via Google (no password set) |
Mixed Account | Account with both Google OAuth linked AND a password set |
View-As Mode | Dev/staging feature allowing role simulation — re-auth bypasses this |
High-Level Architecture
System Diagram

Component Interaction

Technologies Used
Category | Technology |
|---|---|
Framework | Next.js 15 (App Router) |
Auth Provider | Clerk SDK ( |
Database | Supabase (PostgreSQL) |
Validation | Zod |
UI | Shadcn/UI (Dialog, Input, Button) |
Icons | Lucide React |
Detailed Design & Implementation
Data Model / Schemaadmin_reauth_tokens Table
CREATE TABLE admin_reauth_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), admin_clerk_id TEXT NOT NULL, -- Clerk string ID (e.g. "user_...") token TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, -- created_at + 5 minutes used BOOLEAN NOT NULL DEFAULT FALSE, ip_address TEXT); -- IndexesCREATE INDEX idx_admin_reauth_tokens_token ON admin_reauth_tokens (token);CREATE INDEX idx_admin_reauth_tokens_clerk_expires ON admin_reauth_tokens (admin_clerk_id, expires_at);
RLS Policy
-- Deny all client access — service role onlyCREATE POLICY "No direct client access to reauth tokens" ON admin_reauth_tokens FOR ALL USING (false) WITH CHECK (false);
ERD

Zod Schemas
// shared/schemas/reauthSchema.ts // POST /api/admin/reauth — verify identityexport const reauthSchema = z .object({ password: z.string().min(1).optional(), confirmEmail: z.string().email().optional(), }) .refine( (data) => data.password !== undefined || data.confirmEmail !== undefined, { message: "Either password or email confirmation is required" } ); // Destructive action requests — attach tokenexport const reauthTokenHeaderSchema = z.object({ reauthToken: z.string().uuid("Re-auth token must be a valid UUID"),});
API SpecificationGET /api/admin/reauth
Detects the admin's authentication method without triggering audit logs.
Field | Value |
|---|---|
Auth | Clerk session + ADMIN role (unconditional) |
Response |
|
POST /api/admin/reauth
Verifies identity and issues a time-bounded token.
Field | Value |
|---|---|
Auth | Clerk session + ADMIN role (unconditional) |
Body (password) |
|
Body (OAuth) |
|
Success |
|
Failure | 401 Unauthorized |
Protected Endpoints
All require reauthToken in request body:
Endpoint | Method | Action |
|---|---|---|
| PATCH | Transfer agency ownership |
| DELETE | Permanently delete user |
| POST | Change user role |
| DELETE | Delete agency |
Logic & Workflows
Token Issuance (issueReAuthToken)
export async function issueReAuthToken( adminClerkId: string, adminInternalId: string, ipAddress?: string): Promise<string> { const token = randomUUID(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes await supabase.from("admin_reauth_tokens").insert({ admin_clerk_id: adminClerkId, token, expires_at: expiresAt.toISOString(), used: false, ip_address: ipAddress ?? null, }); return token;}
Token Consumption (consumeReAuthToken)
export async function consumeReAuthToken( adminClerkId: string, token: string): Promise<void> { const { data } = await supabase .from("admin_reauth_tokens") .select("id, expires_at, used, admin_clerk_id") .eq("token", token) .maybeSingle(); // Validations: // 1. Token exists // 2. Token belongs to requesting admin // 3. Token not already used // 4. Token not expired // Mark as used (single-use) await supabase .from("admin_reauth_tokens") .update({ used: true }) .eq("id", data.id);}
Clerk Error Handling
// Inline type guard — avoids @clerk/backend subpath importsfunction isClerkApiError(err: unknown): err is ClerkApiError { return ( typeof err === "object" && err !== null && "clerkError" in err && (err as Record<string, unknown>).clerkError === true );}
Infrastructure & Operations
Dependencies
Dependency | Purpose |
|---|---|
Clerk SDK | Password verification, user metadata |
Supabase | Token storage, audit logs |
Next.js API Routes | Server-side processing |
Monitoring & AlertingAudit Log Events
Action | Trigger | Details |
|---|---|---|
| Identity verified |
|
| Wrong password/email |
|
| Token validation failed |
|
Recommended Alerts
High REAUTH_FAILED rate (>5 in 10 min) → Potential brute force
REAUTH_TOKEN_INVALID spikes → Possible replay attack attempt
Clerk API errors → Service degradation
Deployment Plan
Pre-deployment: Run migration
20260526_create_admin_reauth_tokens.sqlFeature flag: None required (always enabled for ADMIN)
Rollback: Revert API changes; tokens table can remain (harmless)
Testing & Quality Assurance
Test Strategy
Test Type | Coverage |
|---|---|
Unit |
|
Integration | POST/GET |
E2E | Full flow: Modal → Verify → Delete User |
Manual QA Checklist
ReAuthModal opens for Delete User (password input for email accounts)
ReAuthModal opens for Delete User (email confirm for OAuth accounts)
ReAuthModal before Change Role
ReAuthModal before Delete Agency
Mixed account (Google + password) shows contextual hint
Non-admin session shows ShieldAlert error, Verify disabled
Wrong password → REAUTH_FAILED in audit_logs
Correct password → REAUTH_SUCCESS in audit_logs
Token expires after 5 minutes
Token cannot be reused (single-use)
View-As mode cannot bypass re-auth
Known Limitations
No MFA support — Future enhancement
Token cleanup — Expired tokens require nightly cron job
Turbopack issue —
@clerk/backendsubpath imports mis-resolve; inline type guard used
Maintenance & Support
Troubleshooting
Symptom | Cause | Resolution |
|---|---|---|
"Admin role required" error | View-As mode active | Exit View-As, sign in as real admin |
"Could not reach verification service" | Clerk API down/rate-limited | Check Clerk status, retry |
Token always invalid | Clock skew on server | Sync server time (NTP) |
Modal shows email input for password user | Clerk user.passwordEnabled incorrect | Check Clerk dashboard |
Cleanup Job
-- Run nightly to remove expired tokensDELETE FROM admin_reauth_tokensWHERE expires_at < NOW() - INTERVAL '1 day';
Changelog
Version | Status | Description | Date |
|---|---|---|---|
1.0 | Approved | Merged to develop branch | 2026-05-26 |
1.1 | Current | Technical documentation created | 2026-06-02 |