diff --git a/docs/design/2026-10-13-files-oidc-sso.md b/docs/design/2026-10-13-files-oidc-sso.md new file mode 100644 index 0000000..ceaae12 --- /dev/null +++ b/docs/design/2026-10-13-files-oidc-sso.md @@ -0,0 +1,86 @@ +# Files SSO design — Hub as the OIDC issuer for oCIS + +Status: proposed (design pass; not implemented) +Date: 2026-10-13 +Related: docs/design/2026-10-12-collectives-architecture.md, brand review P0 #2 +(files.occupymedia.cc navy login wall), matrix-sso module (chat SSO, shipped). + +## Problem + +The Hub's Drive view embeds oCIS web (files.occupymedia.cc). Today: + +- No Hub user has an oCIS account. The API reaches oCIS through one shared + service account (`FILES_OCID_USER`); the collective Library lives in that + account's space. +- The embedded oCIS web shows its own sign-in screen for a login the user + does not have, in ownCloud's default navy/white theme — the single worst + brand break measured in the review. + +## Decision + +The Hub becomes the OIDC identity provider; oCIS trusts it as its external +issuer. This mirrors what just shipped for chat: the Hub is already the +identity provider for Matrix (JWT broker). Files becomes the second relying +party of the same identity, not a third account system. + +Rejected alternative: a standalone IdP (Authelia/Authentik/Keycloak). It adds +a service to run and a second admin surface for one relying party; the Hub +already holds the session, the RBAC roles, and the username namespace. + +## Flow (target state) + +1. User opens `/files/drive` in the Hub. Their Hub session cookie proves who + they are. +2. The embedded oCIS web redirects to `https://api.occupymedia.cc/oidc/authorize` + (client id `web`, PKCE required). +3. The Hub is already signed in → no login prompt, no consent screen for this + first-party client (auto-approve). A short-lived code returns to oCIS. +4. oCIS exchanges the code at `/oidc/token`, reads claims + (`preferred_username` = Hub username, `email`, `name`), and provisions the + oCIS account on first login (oCIS does this natively when + `OCIS_OIDC_ISSUER` points at an external issuer). +5. The user lands in their own drive. The navy wall is gone because there is + no wall. + +## What the Hub must add + +- `apps/api/src/oidc/`: an OIDC provider module — + `/.well-known/openid-configuration`, `/oidc/authorize`, `/oidc/token`, + `/oidc/userinfo`, `/oidc/jwks`. Signing keys in the existing vault pattern + (ES256 or RS256, rotated like DEKs). Session binding to the existing Hub + session cookie; codes single-use, ≤60 s TTL, PKCE S256 enforced. +- Auto-consent policy: client id `web` (the oCIS web app) is first-party and + skips the consent screen. Any other client id requires explicit consent — + fail closed. +- Claim mapping: `preferred_username` must equal the Hub username verbatim + (same invariant the Matrix SSO module already enforces: Hub usernames are a + subset of the localpart grammar, case-folded identity is refused). + +## What oCIS must change + +- `OCIS_OIDC_ISSUER=https://api.occupymedia.cc/oidc` +- `WEB_OIDC_CLIENT_ID=web`, scope `openid profile email` +- oCIS auto-provisions accounts from claims (default behaviour with an + external issuer). The `FILES_OCID_USER` service account stays for the + Library's WebDAV path. + +## Phases + +1. **Provider + sign-in** (≈1–2 days): the module above; Drive view switches + `authMode` to `"oidc"`; the navy wall disappears. +2. **Personal drives** (≈1 day): uploaded files land in the user's own drive; + the Library keeps serving collective folders from the service account. +3. **Share-through** (≈1 day, optional): group folders shared into members' + drives so the Drive tab also shows collective Library content; only then + consider deprecating the Library tab. + +## Security notes + +- The OIDC provider must never accept the service-account identity for a + browser session; tokens are minted only from a live Hub session. +- Logout: Hub logout revokes the session cookie; oCIS tokens expire on their + own TTL (oCIS refresh behaviour is issuer-driven). Single-logout signaling + to oCIS is a nice-to-have, not a blocker. +- The issuer URL must be exactly `https://api.occupymedia.cc/oidc` everywhere + (issuer discovery, oCIS config, token audience) — mismatch breaks token + validation.