[0.5] Sensitive Action Re-Authentication

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

  1. Implement a time-bounded (≤5 min) re-authentication gate before all destructive admin actions

  2. Support both email/password and OAuth (Google) authentication methods

  3. Enforce single-use tokens to prevent replay attacks

  4. Log all re-authentication attempts (success/failure) to audit_logs

  5. Enforce 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

image.png

Component Interaction

image.png

Technologies Used

Category

Technology

Framework

Next.js 15 (App Router)

Auth Provider

Clerk SDK (@clerk/nextjs)

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
);
 
-- Indexes
CREATE 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 only
CREATE POLICY "No direct client access to reauth tokens"
ON admin_reauth_tokens
FOR ALL
USING (false)
WITH CHECK (false);

ERD

image.png

Zod Schemas

// shared/schemas/reauthSchema.ts
 
// POST /api/admin/reauth — verify identity
export 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 token
export 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

{ method: "password" | "oauth", hasOAuth: boolean }

POST /api/admin/reauth

Verifies identity and issues a time-bounded token.

Field

Value

Auth

Clerk session + ADMIN role (unconditional)

Body (password)

{ password: string }

Body (OAuth)

{ confirmEmail: string }

Success

{ token: string, expiresInSeconds: 300 }

Failure

401 Unauthorized

Protected Endpoints

All require reauthToken in request body:

Endpoint

Method

Action

/api/admin/agencies/[id]

PATCH

Transfer agency ownership

/api/admin/delete-user

DELETE

Permanently delete user

/api/admin/change-role

POST

Change user role

/api/admin/agencies/[id]

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 imports
function 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

REAUTH_SUCCESS

Identity verified

{ method: "password" | "oauth_email_confirm" }

REAUTH_FAILED

Wrong password/email

{ reason, method, clerkCode? }

REAUTH_TOKEN_INVALID

Token validation failed

{ reason: "expired" | "used" | "not_found" | "wrong_admin" }

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

  1. Pre-deployment: Run migration 20260526_create_admin_reauth_tokens.sql

  2. Feature flag: None required (always enabled for ADMIN)

  3. Rollback: Revert API changes; tokens table can remain (harmless)

Testing & Quality Assurance

Test Strategy

Test Type

Coverage

Unit

issueReAuthToken, consumeReAuthToken, Zod schemas

Integration

POST/GET /api/admin/reauth with mock Clerk

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

  1. No MFA support — Future enhancement

  2. Token cleanup — Expired tokens require nightly cron job

  3. Turbopack issue@clerk/backend subpath 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 tokens
DELETE FROM admin_reauth_tokens
WHERE 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


Was this article helpful?