RBAC-3: trust platform forwardAuth headers (X-Plat-*) instead of in-app OAuth (plat/mitosis#30) #3

Merged
plat merged 1 commit from feat/rbac-3-forward-auth-headers into main 2026-07-01 17:44:03 +00:00
Owner

Implements RBAC-3 (roadmap plat/mitosis#30): the app can now TRUST the platform edge's forwardAuth headers instead of running its own better-auth OAuth flow. RBAC-2 (the forwardAuth service + Traefik middleware) is merged to plat/mcp main and injects X-Plat-User / X-Plat-Perm / X-Plat-Manage after stripping any inbound X-Plat-*, so those headers are authoritative inside the app.

Two modes (default OFF — existing apps byte-identical)

PLAT_FORWARD_AUTH Mode Behavior
unset (default) in-app better-auth OAuth Unchanged. /api/auth/* bridge, session-cookie /api/me, Forgejo sign-in button. Verified byte-identical homepage + /api/me.
=1 header-trust Zero app auth code. Reads verified X-Plat-*; /api/me returns {login,perm,manage}; example /api/manage gated on X-Plat-Manage=1.

better-auth (src/auth.ts) is now imported lazily, so a header-trust app needs no OAuth/DB-auth env at all.

Safety

  • Fail closed: no X-Plat-User → 401 (never an anonymous allow), so a deploy that bypasses the edge cannot leak. A smuggled X-Plat-Manage=1 with no user is denied.
  • The edge is the gate: no app-side re-verification by design (that's the point). In header-trust mode the app MUST be reachable only through Traefik, which strips client X-Plat-* before injecting the verified trio — documented in the README.

Files

  • src/platform-auth.ts — header-trust identity resolver + manage gate (case-insensitive, fail-closed).
  • src/platform-auth.test.ts — node:test units for the decision (manage only with X-Plat-Manage=1; missing X-Plat-User → denied).
  • src/server.ts — two-mode wiring, lazy better-auth import, example /api/manage.
  • src/homepage.ts — header-trust auth-box variant (no in-app sign-in); default variant byte-identical.
  • README.md — documents both modes + the edge-is-the-gate / fail-closed contract.

Validation

  • biome ci clean, tsc --noEmit clean, tsc build clean.
  • 6/6 unit tests pass.
  • Live boot smoke in header-trust mode (no DB/OAuth env): /api/me no-header→401, with user→200, /api/manage user-only→403, user+manage→200, manage-without-user→401.
  • Default mode still eagerly loads auth.ts (same env requirements) — behavior unchanged. CI's default-mode smoke covers the OAuth path.
  • Live E2E on a germinated cluster with the forwardAuth edge is deferred (needs the RBAC-2 edge deployed + an app with PLAT_FORWARD_AUTH=1); header logic is unit- and boot-validated here.

🤖 Generated with Claude Code

Implements **RBAC-3** (roadmap plat/mitosis#30): the app can now TRUST the platform edge's forwardAuth headers instead of running its own better-auth OAuth flow. RBAC-2 (the forwardAuth service + Traefik middleware) is merged to `plat/mcp` main and injects `X-Plat-User` / `X-Plat-Perm` / `X-Plat-Manage` after stripping any inbound `X-Plat-*`, so those headers are authoritative inside the app. ## Two modes (default OFF — existing apps byte-identical) | `PLAT_FORWARD_AUTH` | Mode | Behavior | | --- | --- | --- | | unset (default) | in-app better-auth OAuth | Unchanged. `/api/auth/*` bridge, session-cookie `/api/me`, Forgejo sign-in button. Verified byte-identical homepage + `/api/me`. | | `=1` | header-trust | **Zero app auth code.** Reads verified `X-Plat-*`; `/api/me` returns `{login,perm,manage}`; example `/api/manage` gated on `X-Plat-Manage=1`. | better-auth (`src/auth.ts`) is now imported **lazily**, so a header-trust app needs no OAuth/DB-auth env at all. ## Safety - **Fail closed**: no `X-Plat-User` → `401` (never an anonymous allow), so a deploy that bypasses the edge cannot leak. A smuggled `X-Plat-Manage=1` with no user is denied. - **The edge is the gate**: no app-side re-verification by design (that's the point). In header-trust mode the app MUST be reachable only through Traefik, which strips client `X-Plat-*` before injecting the verified trio — documented in the README. ## Files - `src/platform-auth.ts` — header-trust identity resolver + manage gate (case-insensitive, fail-closed). - `src/platform-auth.test.ts` — `node:test` units for the decision (manage only with `X-Plat-Manage=1`; missing `X-Plat-User` → denied). - `src/server.ts` — two-mode wiring, lazy better-auth import, example `/api/manage`. - `src/homepage.ts` — header-trust auth-box variant (no in-app sign-in); default variant byte-identical. - `README.md` — documents both modes + the edge-is-the-gate / fail-closed contract. ## Validation - `biome ci` clean, `tsc --noEmit` clean, `tsc` build clean. - 6/6 unit tests pass. - Live boot smoke in header-trust mode (no DB/OAuth env): `/api/me` no-header→401, with user→200, `/api/manage` user-only→403, user+manage→200, manage-without-user→401. - Default mode still eagerly loads `auth.ts` (same env requirements) — behavior unchanged. CI's default-mode smoke covers the OAuth path. - **Live E2E on a germinated cluster with the forwardAuth edge is deferred** (needs the RBAC-2 edge deployed + an app with `PLAT_FORWARD_AUTH=1`); header logic is unit- and boot-validated here. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
RBAC-3: trust platform forwardAuth headers (X-Plat-*) instead of in-app OAuth (plat/mitosis#30)
Some checks failed
check / check (push) Successful in 1m40s
check / check (pull_request) Successful in 1m34s
preview / preview (pull_request) Failing after 19s
1601945102
Add a "header-trust" auth mode selected by PLAT_FORWARD_AUTH=1. When enabled the
app runs ZERO auth code of its own: RBAC-2's edge forwardAuth service has already
authenticated the caller against Forgejo, run the repo-permission probe, and
injected verified X-Plat-User / X-Plat-Perm / X-Plat-Manage headers (it strips
inbound X-Plat-* first, so they are authoritative in-process). The app reads them
via src/platform-auth.ts, gates the example /api/manage route on X-Plat-Manage=1,
and FAILS CLOSED when X-Plat-User is absent (401) so a misconfigured deploy that
bypasses the edge cannot leak.

Default (flag unset) is unchanged: the in-app better-auth Forgejo OAuth flow runs
exactly as before — verified byte-identical homepage + default /api/me. better-auth
(src/auth.ts) is now imported lazily so header-trust apps need no OAuth/DB-auth env
at all.

- src/platform-auth.ts: header-trust identity + manage gate (case-insensitive read)
- src/platform-auth.test.ts: unit tests for the decision (node:test)
- src/server.ts: two-mode wiring; lazy better-auth import; /api/manage example
- src/homepage.ts: header-trust auth box variant (no in-app sign-in)
- README.md: documents the two modes + the edge-is-the-gate / fail-closed contract

Live E2E on a germinated cluster with the forwardAuth edge is deferred; the header
decision is unit-validated and boot-smoked here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
plat merged commit cabd6b8bc3 into main 2026-07-01 17:44:03 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
open-platform/app-template!3
No description provided.