Skip to main content
Security & Identity

Authentication & Authorization

Authentication flow

   /auth (email + password)
        │
        ▼
   login-rate-limit (pre-check)
        │       │
        │ 429   │ ok
        ▼       ▼
   block   supabase.auth.signInWithPassword
                │
                ▼
       login-rate-limit (post-check: record outcome)
                │
                ▼
        Session established (AAL1)
                │
                ▼
    /app/* beforeLoad → check AAL + TOTP factor
        │             │
        │ enrolled?   │
        ▼             ▼
   /mfa-setup    AAL2? — yes → /app, no → /mfa-setup (step-up)

Implementation:

  • src/routes/auth.tsx — sign-in UI and rate-limit wiring.
  • src/routes/mfa-setup.tsx — TOTP enrolment and step-up challenge.
  • src/routes/app.tsxbeforeLoad enforces AAL2.
  • supabase/functions/login-rate-limit/index.ts — IP-based lockout.

Multi-factor authentication

  • Factor type: TOTP (RFC 6238).
  • Library: supabase.auth.mfa plus the qrcode package for local QR generation (the TOTP secret never leaves the browser).
  • Enrolment: required on first sign-in; cannot be skipped.
  • Step-up: AAL1 sessions are redirected to /mfa-setup to satisfy the gate.
  • Recovery: managed by the tenant administrator via Supabase Auth; documented in the Administrator Guide.

Session management

PropertyValue
Token storagelocalStorage via @supabase/supabase-js
RefreshAutomatic, rotation on use
AAL level required for /app/*aal2
Idle timeout30 minutes → silent signOut(); 8-hour absolute session maximum
Sign-out on logoutClears local session and refresh token

RBAC model

Roles are an enum in Postgres and live in a dedicated public.user_roles table.

create type public.app_role as enum (
  'owner', 'admin', 'auditor',
  'risk_officer', 'compliance_officer', 'model_owner', 'user'
);

Role checks use a SECURITY DEFINER function to avoid recursive RLS:

create or replace function public.has_role(_user_id uuid, _role app_role)
returns boolean language sql stable security definer
set search_path = public
as $$
  select exists (
    select 1 from public.user_roles
    where user_id = _user_id and role = _role
  )
$$;

src/lib/rbac/roles.ts exports the TypeScript enum that mirrors the Postgres type.

Permission matrix

Capabilityowneradminauditorrisk_officercompliancemodel_owneruser
Read dashboards
Register / edit AI models
Run AI assessments
Approve AI deployment
Manage controls / compliance
Issue / revoke access grants
Attest access reviews
Read full audit log viewer
Switch role (governance only)
Read GDPR requests
Edit security / platform config

The matrix above is enforced at three layers: route gating (TanStack Start beforeLoad), server function middleware (requireSupabaseAuth + has_role check), and database RLS policies.

Audit

Every authentication and authorisation event writes to:

  • public.sec_login_attempts — sign-in attempts (IP, outcome, timestamp).
  • public.access_audit_log — role changes, grant issue/revoke, review attestations.

Both are append-only from the application's perspective; service_role only writes.