docs(auth): design the mobile SSO authorization bridge (M9)

Record the plan before coding: native Android "Sign in with Google/Discord"
via a Mobile SSO Authorization Bridge that extends the existing /auth/sso/*
redirect flow and terminates in the existing mobile bearer tokens.

- BACKEND_DESIGN.md: mobile_auth_sessions / mobile_auth_codes schema, the
  /auth/mobile/sso/{start,exchange} contract, the two PKCE layers, state/CSRF,
  exact-match redirect-URI allowlist, TOTP parity, and the documented
  revocation-latency window.
- android/PLAN.md: promote §4.2's "possible later enhancement" to milestone M9
  (backend-first, mirroring M7); status note.
- android/APP_LINKS.md: new architecture note on the per-shard assetlinks.json
  / pairing multi-tenancy question (App Links deferred; custom scheme only now).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-20 16:41:23 -05:00
parent 5a7bbc26fa
commit 1aba1ff93d
3 changed files with 231 additions and 9 deletions

View File

@@ -1,6 +1,6 @@
# Android App — Plan
Status: **M0M7 landed; M7 (push notifications) both parts done — Part 1 backend (website#78) and Part 2 app (Android-app#15) plus a small `push.ntfyUrl` settings addition (website#79). Remaining: set the shard's `NTFY_*` deploy config so push lights up, and cut the v1 tag.** This document is the
Status: **M0M7 landed; M7 (push notifications) both parts done — Part 1 backend (website#78) and Part 2 app (Android-app#15) plus a small `push.ntfyUrl` settings addition (website#79). Remaining: set the shard's `NTFY_*` deploy config so push lights up, and cut the v1 tag. M9 (native SSO login) is now underway backend-first — the Mobile SSO Authorization Bridge is being built in `website/` + `docs/` ahead of the app-side client (§4.2, §9 M9); custom-scheme callback only for now, App Links deferred (see [`APP_LINKS.md`](./APP_LINKS.md)).** This document is the
design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the
API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API
reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated
@@ -467,14 +467,19 @@ completes them in a Custom Tab, then returns and signs in natively (§4.1):
new username + password. (No mobile register/invite endpoints needed.)
- **Forgot / reset password** — the app links to the website's reset page (the flow being built in §8
before app work). The user resets there, then signs into the app. (No mobile reset endpoint needed.)
- **SSO (Google / Discord / OIDC)** — SSO stays the website's browser redirect flow (`/auth/sso/*`),
**link-only** (no auto-provisioning). For v1 the app does **not** do one-tap in-app SSO; instead an
SSO user links their identity and sets a password on the website (the existing "set initial password"
path for SSO-provisioned accounts), then uses password login in the app. `GET /auth/sso/providers`
can still be shown so the login screen can direct users to "sign in with … on the website."
- *Possible later enhancement (out of v1):* true in-app SSO via a Custom-Tab flow that hands a
one-time code back to an app link, exchanged for mobile tokens — a small new backend endpoint. Only
build it if password-for-SSO-users proves too clunky.
- **SSO (Google / Discord / OIDC)** — **v1** shipped this as a website browser hand-off: an SSO user
links their identity and sets a password on the website, then uses password login in the app.
`GET /auth/providers` is shown so the login screen can direct users to "sign in with … on the website."
- **Native in-app SSO — now being built (M9), post-v1 additive.** The "possible later enhancement"
noted here is now the **Mobile SSO Authorization Bridge**: a Custom-Tab flow that hands a one-time
code back to the app's fixed callback (`runicgateway://auth/callback`), exchanged for the *existing*
mobile bearer tokens. It **extends** the existing `/auth/sso/*` redirect flow rather than adding a
parallel auth path — same PKCE-vs-IdP, same link-only + opt-in-provisioning policy, same TOTP gate,
same token shape as `/auth/mobile/login`. The bridge adds a **second** PKCE layer (app ↔ website)
and an app-generated `state` (CSRF, verified by the app before exchange). Backend + docs land first
(this document's canonical API ref is `../website/BACKEND_DESIGN.md` → "Mobile SSO Authorization
Bridge"); the native app client is M9. Custom-scheme callback only for now — App Links are deferred
(see [`APP_LINKS.md`](./APP_LINKS.md)).
### 4.3 Session model (all paths)
- **Refresh:** `POST /auth/mobile/refresh` `{ refreshToken }` → new pair. **Single-use / rotated:** store
@@ -704,6 +709,23 @@ push, and Play (M6M8) follow the designed app.
its `NTFY_*` deploy config (§13).
9. **M8 — Google Play**: Play Console listing, signing/upload key, and (optionally) an FCM build flavor
— after the direct-APK release is stable.
10. **M9 — Native SSO login** (post-v1, additive; independent of M8): in-app "Sign in with Google /
Discord" via the **Mobile SSO Authorization Bridge** (§4.2). **Backend-first**, mirroring M7's
split:
- **Part 1 — backend + docs (in progress):** `mobile_auth_sessions` + `mobile_auth_codes` bridge
tables; `GET /auth/mobile/sso/start` (seeds a bridge session, reuses the existing SSO redirect
tagged `mode:'mobile'`); a mobile branch in the SSO callback + TOTP-completion that mints a
single-use, hashed, PKCE-bound authorization code and redirects to the fixed app callback instead
of setting a cookie; `POST /auth/mobile/sso/exchange` (code + PKCE verifier → the existing mobile
bearer token pair); an exact-match redirect-URI allowlist; boot-time + opportunistic cleanup of
the bridge tables. Reuses `GET /auth/providers` for discovery and `POST /auth/mobile/{refresh,
logout}` unchanged. See `../website/BACKEND_DESIGN.md`.
- **Part 2 — app client:** register the `runicgateway://auth/callback` intent-filter; generate
`code_verifier`/`code_challenge` + `state`; open the Custom Tab at `/auth/mobile/sso/start`;
verify `state` on the callback; `POST …/exchange`; store the returned pair in the existing
`TokenStore` (M3). No new token-storage or refresh code — it feeds the M3 session machinery.
- **Deferred:** App Links / per-shard `assetlinks.json` / pairing — custom scheme only for now
([`APP_LINKS.md`](./APP_LINKS.md)).
---