5.6 KiB
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:- redirect to
CLOUDRON_OIDC_AUTH_ENDPOINT, - receive the code at
loginRedirectUri(e.g./auth/openid/callback), - exchange it at
CLOUDRON_OIDC_TOKEN_ENDPOINT, - validate the ID token (RS256, keys from
CLOUDRON_OIDC_KEYS_ENDPOINT), - establish an app-side session cookie.
- redirect to
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:
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/*asX-Member-User. The portal resolves username → email (it holds the Cloudron admin token). - admin-panel: checks the username against an
ADMIN_USERNAMESallowlist, then calls the portal's/admin/*withX-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:
- Reads
CLOUDRON_OIDC_*env vars. - On an unauthenticated request, redirects to the auth endpoint.
- Handles the callback (
loginRedirectUri) — exchange code for ID token, validate signature + issuer + audience. - Sets a signed session cookie; reads it on subsequent requests.
- 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.coopand 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
oidcaddon) but does not actually implement a client — itsoidcblock hasloginRedirectUri: "/", 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_USERNAMESmust 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).