Move SSO migration plan out of public docs (tracked internally)
This commit is contained in:
1 parent
c96de68f21
commit
7f3dc0903e
1 file changed
-125
@@ -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).
|
||||
Reference in new issue
Block a user