From 7f3dc0903e1989045ecec81a32194eda86ea22d1 Mon Sep 17 00:00:00 2001 From: inference-bot Date: Wed, 23 Sep 2026 08:09:58 -0600 Subject: [PATCH] Move SSO migration plan out of public docs (tracked internally) --- sso-migration.md | 125 ----------------------------------------------- 1 file changed, 125 deletions(-) delete mode 100644 sso-migration.md diff --git a/sso-migration.md b/sso-migration.md deleted file mode 100644 index 18e49c1..0000000 --- a/sso-migration.md +++ /dev/null @@ -1,125 +0,0 @@ -# SSO migration plan — `proxyAuth` → OIDC - -Migrating our two custom apps (`member-dashboard`, `admin-panel`) off the legacy -`proxyAuth` addon to OpenID Connect, matching the rest of the stack and Cloudron -10's direction. - -**Status: planned, not started.** Both apps work today on `proxyAuth`; this is -future-proofing done deliberately while the membership is small. - -## Why - -- Cloudron 10 is moving packages from LDAP/proxyAuth → OIDC so that "only - Cloudron sees the password." Our two custom apps are on the legacy path - (proxyAuth); the portal and all App Store apps are already OIDC. -- proxyAuth is the minority path now (11 packages vs 93 on OIDC). Migrating - removes a dependency on a mechanism Cloudron is deprioritizing. - -## The core technical fact (shapes the whole plan) - -**proxyAuth and OIDC are fundamentally different auth models — this is a real -code change, not a manifest swap.** - -- **proxyAuth** puts an auth *wall* in front of the app. Cloudron handles login - and **injects the authenticated user's username via a header** - (`X-Remote-User` / `X-Forwarded-User`). The app just reads the header. -- **OIDC** does *not* inject a header. Cloudron exports `CLOUDRON_OIDC_*` env - vars (`CLOUDRON_OIDC_ISSUER`, `..._CLIENT_ID`, `..._CLIENT_SECRET`, - `..._DISCOVERY_URL`, `..._TOKEN_ENDPOINT`, `..._PROFILE_ENDPOINT`, etc.) and - the app must implement the full **authorization-code flow**: - 1. redirect to `CLOUDRON_OIDC_AUTH_ENDPOINT`, - 2. receive the code at `loginRedirectUri` (e.g. `/auth/openid/callback`), - 3. exchange it at `CLOUDRON_OIDC_TOKEN_ENDPOINT`, - 4. validate the ID token (RS256, keys from `CLOUDRON_OIDC_KEYS_ENDPOINT`), - 5. establish an app-side session cookie. - -So each app needs an OIDC client implementation (redirect, callback, token -exchange, session) where today it only reads a header. - -## Current state of the two apps - -Both read identity the same way — a small header-union helper: - -```python -def get_identity(request): - for header in ("x-remote-user", "x-forwarded-user", "x-auth-request-user", - "x-auth-request-email", "x-forwarded-email"): - val = request.headers.get(header) - if val: - return val.strip() - return "" -``` - -- **member-dashboard**: passes that identity (a Cloudron *username*, e.g. - `ntnsndr`) to the portal's `/broker/*` as `X-Member-User`. The portal resolves - username → email (it holds the Cloudron admin token). -- **admin-panel**: checks the username against an `ADMIN_USERNAMES` allowlist, - then calls the portal's `/admin/*` with `X-Admin-Token`. - -## Open question to resolve first (before code) - -The portal's broker resolves **username → email**, but OIDC's userinfo / ID -token may return the **email** directly (or the username, or both). Which claim -Cloudron's OIDC profile endpoint exposes determines whether the dashboard keeps -sending `X-Member-User` (username) or switches to `X-Member-Email` (email). Both -work today (the broker accepts either and normalizes to email), but we should -confirm what the ID token actually contains rather than assume. - -**Action:** once the OIDC client is wired in a test app, log the decoded ID -token claims and note whether `email`, `preferred_username`, and/or `sub` are -present. Then pick the identity field accordingly. - -## Migration steps - -### Phase 1 — add OIDC plumbing to each app (no manifest change yet) - -For each app, add a small OIDC client that: - -1. Reads `CLOUDRON_OIDC_*` env vars. -2. On an unauthenticated request, redirects to the auth endpoint. -3. Handles the callback (`loginRedirectUri`) — exchange code for ID token, - validate signature + issuer + audience. -4. Sets a signed session cookie; reads it on subsequent requests. -5. Replaces `get_identity()` with "read identity from the validated session" - (falling back to the header during the transition). - -The dashboard's `/logout` link and the panel's `ADMIN_USERNAMES` allowlist stay; -only the *identity source* changes. - -### Phase 2 — swap the manifest addon - -Replace `"proxyAuth": {}` with `"oidc": { "loginRedirectUri": "/auth/openid/callback", "logoutRedirectUri": "/" }` in each `CloudronManifest.json`, then redeploy. - -### Phase 3 — verify - -- A member can log into `dashboard.inference.coop` and see their usage/keys. -- A non-admin is **denied** on `panel.inference.coop`; an admin gets in. -- The broker still resolves identity correctly (usage/keys load). -- Logout works and re-presents the login. - -### Phase 4 — remove the header fallback - -Once OIDC is verified, drop the `X-Remote-User`/`X-Forwarded-User` fallback so -the apps no longer trust a spoofable header (a header the app now controls is -fine internally, but a client-supplied `X-Remote-User` would otherwise be -trusted if the wall were ever bypassed). - -## Rollback - -Until Phase 4, the header fallback means we can revert the manifest to -`proxyAuth` and redeploy with a single change. Keep that path open until -verification passes. - -## Notes / gotchas - -- **The portal itself is already OIDC** (via the `oidc` addon) but does *not* - actually implement a client — its `oidc` block has `loginRedirectUri: "/"`, - and it's effectively used for the admin/broker token surfaces, not member - login. Don't pattern-match on the portal; the dashboard/panel need a real - client implementation. -- **`admin-panel`'s allowlist is orthogonal** to SSO — `ADMIN_USERNAMES` must - still gate *which* authenticated users reach the panel. OIDC only changes how - we learn *who* the user is. -- **Do the dashboard first**, the panel second (the dashboard has more members - relying on it, so it validates the pattern; the panel is admin-only and lower - traffic, so a bug there is less visible).