Auth Architecture¶
Document: docs/product/auth-architecture.md
Status: Canonical
Last updated: 2026-04-06
Authority: Tim Rignold, RTOpacks Pty Ltd
Auth Flow¶
User visits my.rtopacks.com.au
│
▼
Middleware checks rtp_session cookie
│
┌────┴────┐
│ No │ Yes
│ cookie │
▼ ▼
/auth Route handler
│ (KV validation
│ at page level)
▼
Email form
│
▼
POST /api/auth/magic-link
│
├── Check access_allowlist (ops-db)
│ ├── Not listed → "invitation only" message (no email sent)
│ └── Listed → continue
│
├── Generate 32-byte token → KV (magic:{token}, 15min TTL)
│
└── Send via Resend (noreply@rtopacks.com.au)
│
▼
User clicks link → GET /auth/verify?token=...
│
├── Validate token in KV (single use — deleted immediately)
├── Upsert user in workspace-db
├── resolveUserTier(email, userId, workspaceDb, opsDb)
├── Build RTPSession with resolved ucca_layer
├── Write session to KV (session:{id}, 24hr TTL)
├── Log session_started to platform_audit_log
│
└── Set-Cookie: rtp_session={id} → redirect to /
Session Model¶
interface RTPSession {
session_id: string // UUID
user_id: string // UUID — from workspace-db users table
email: string
ucca_layer: 1 | 2 | 3 | 4 | 4.5
world_id: string // 'au-vet'
tenant_id: string // org identifier
org_id: string | null
role_template_id: string | null
permissions: string[]
seat_limit_reached: boolean
identity_source: 'ucca_managed' | 'federated' | 'stepdown'
impersonation: boolean
impersonating_layer: number | null
created_at: number // epoch ms
expires_at: number // epoch ms (24hr from creation)
last_active: number // epoch ms (slid on each access)
}
Cookie: rtp_session — HttpOnly, Secure, SameSite=Strict, Max-Age=86400, Domain=my.rtopacks.com.au
KV storage: session:{session_id} in SESSION_KV namespace (293964a187084f10a5eae8b566cff5a4). TTL slides on each access via getSessionFromKV().
Tier Resolution¶
Resolved once at login, stored in session. Not recalculated per-request.
resolveUserTier(email, userId, workspaceDb, opsDb)
│
├── FEDERATION-HOOK-01 (disabled)
│ Future: validate UCCA assertion token here
│
├── Look up user in workspace-db → get org_id
│
├── No org_id → L4A (4.5) — unresolved, pending onboarding
│
├── Has org_id → check rto_clients in ops-db
│ ├── Not a client → L4A (4.5)
│ └── Active client → check group membership
│ ├── Admin group → L4 (4)
│ ├── No groups (first user) → L4 (4) — org owner default
│ └── Standard group → L4A (4.5)
│
└── Return: { ucca_layer, org_id, team_id, group_ids, display_name }
The 4.5 sentinel: L4A is stored as 4.5 (not a string). This allows numeric comparisons: ucca_layer <= 4 correctly includes L1-L4 and excludes L4A. Documented in all KN gating and compliance mode gating locations.
Permissions by Tier¶
| Tier | Permissions |
|---|---|
| L1-L3 | Full: qualifications:read, units:read, scope:read/write, audit:read/write, members:manage, billing:read/write, org-settings:manage, compliance:read/write |
| L4 | Org admin: qualifications:read, units:read, scope:read/write, audit:read, members:manage, billing:read, org-settings:manage, compliance:read |
| L4A | Member: qualifications:read, units:read, scope:read, audit:read |
Content Gating¶
KN content (kn_criteria_reasoning column):
- L1-L4 (ucca_layer <= 4): KN content included when ?include=kn is passed
- L4A (ucca_layer = 4.5): KN content stripped from response
Compliance mode: - L1-L4: full access to compliance mode - L4A: compliance mode option visible but disabled
Allowlist (ONBOARD-01)¶
All magic link delivery gated by access_allowlist table in ops-db.
| Type | Behaviour |
|---|---|
email |
Exact email match |
domain |
Any address at that domain |
Unlisted emails: no magic link sent, "invitation only" message shown. Response timing identical to allowed path (800ms fixed delay) to prevent timing attacks.
Rejected attempts logged to auth_rejection_log in ops-db.
Worker Secret Dependencies¶
| Secret | Worker | Purpose |
|---|---|---|
RESEND_API_KEY |
workspace, internal-api | Transactional email (magic links, alerts) |
FEDERATION-HOOK-01¶
Location: apps/workspace/lib/tier-resolution.ts
When UCCA ships B-AUTH-OAUTH-01, this hook will validate a signed assertion token from auth.ucca.online. Until then:
- No UCCA domain bypass in tier resolution
- admin@rtopacks.com.au resolves via allowlist + org lookup like any other user
- The hook is a comment block, not dead code — it marks where future validation will land
Amendment Log¶
| Date | Change | Authority |
|---|---|---|
| 2026-04-06 | Initial document — Session 44 | Tim Rignold |