docs: record Q12-Q18 (D353-D359) and plan Steam sign-in on the website (D360)
The org lead answered §13's and §14's questions on 2026-10-14: D353 public feed with name and grid, staff adds the Steam id; D354 an event can call a vanilla event, kept at teardown; D355 every world event is a trigger; D356 Safecracker and Scavenger; D357 protocol 14; D358 BotReSpawn stays out; D359 the end walk waits for RaidableBases (against the proposal), recorded in §9 too. D360: Steam sign-in in the same manner as Google and Discord, kept on the website's plan (D171). New website/STEAM_SIGNIN.md (OpenID 2.0 adapter, the build per repo, security, tests, Q1-Q4), indexed in the README, linked from PLAN_REDESIGNS §15 and PLAN_FIXES §4.8, and on the roadmap as the website milestone "Steam sign-in" (website#214). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
107
website/STEAM_SIGNIN.md
Normal file
107
website/STEAM_SIGNIN.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Sign in with Steam — plan
|
||||
|
||||
> **Status: plan, written 2026-10-14. Nothing is built.** Its four questions (§7) are for the org lead before code.
|
||||
> It is the website's own plan for Steam sign-in, as Rust's
|
||||
> [`PLAN_FIXES.md`](../modules/rust/PLAN_FIXES.md) D171 set out (§4.8 there), and it is on the roadmap as the
|
||||
> `website` milestone *Steam sign-in*, issue website#214. Rust's [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §15 links
|
||||
> here for what the Rust module needs from it.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
**Steam becomes a sign-in provider in the same manner as the others** (the org lead, 2026-10-14; D360 in
|
||||
PLAN_REDESIGNS). That means:
|
||||
|
||||
- It is a **built-in provider beside Google and Discord**. An admin turns it on in Admin → Auth.
|
||||
- It follows the **same policy**: link-only, with opt-in sign-up when `player_registration` allows SSO.
|
||||
- It creates sessions through the **same session service**, with the TOTP gate and the trusted-device skip.
|
||||
- It is linked and unlinked from a player's account page, as the others are.
|
||||
- It works **in the Android app** through the existing mobile SSO bridge (M9), like the others.
|
||||
|
||||
What it adds beyond that: **modules can learn a signed-in account's verified Steam ids**. That is what the Rust
|
||||
module has needed since its dry run (`modules/rust-dryrun.md` finding 1, MODULE_SYSTEM.md's "a module cannot
|
||||
register an identity provider").
|
||||
|
||||
Out of scope: a module registering its own identity provider. Steam is core's, so session creation stays a part of
|
||||
core that no module can weaken (MODULE_SYSTEM.md).
|
||||
|
||||
## 2. What exists
|
||||
|
||||
| Piece | Where | Shape |
|
||||
|---|---|---|
|
||||
| Providers | `server/src/auth/providers/` | `registry.js` maps a `kind` to a class. Built-ins `google` and `discord` always exist as defaults; `oidc`/`oauth2` are custom. Each class implements `getAuthorizationUrl(state, options)` and `handleCallback(params)` → `{ subject, email, emailVerified, name }` |
|
||||
| Provider config | `auth_providers` | `kind ENUM('google','discord','oidc','oauth2')`, `client_id`, `client_secret_enc` (AES-256-GCM via `secretBox`), URLs, scopes |
|
||||
| Identities | `user_identities` | `(provider, subject)` unique, `user_id` indexed. **Several identities per provider per user are already allowed**, which several Steam accounts need |
|
||||
| The flow | `router/v1/auth/sso.controller.js` | `beginFlow` (modes `login`, `link`, `mobile`) signs a single-use `sso_tx` cookie. `callback` reads `code` and `state` from the query, verifies the tx, calls `handleCallback`, then `finishLogin` / `finishLink` / `finishMobileLogin`. `provisionSsoPlayer` creates a player when registration allows SSO sign-up |
|
||||
| Android | `/auth/mobile/sso/*` (M9) | A Custom Tab runs the same `/auth/sso/:provider/*` flow; the app receives a one-time code and exchanges it with PKCE |
|
||||
|
||||
## 3. How Steam differs: OpenID 2.0, not OAuth2
|
||||
|
||||
| OAuth2 / OIDC (today) | Steam |
|
||||
|---|---|
|
||||
| Redirect to an authorize URL with `client_id`, `state`, PKCE | Redirect to `https://steamcommunity.com/openid/login` with `openid.mode=checkid_setup`, `openid.return_to`, `openid.realm` and the identifier-select fields. **No client id, no secret, no `state` parameter** |
|
||||
| Callback carries `code` and `state` | Callback carries `openid.*` fields. **Our `state` must ride inside `return_to`**, and the tx cookie verifies it as today |
|
||||
| Exchange the code at the token endpoint | **Verify the assertion** by POSTing the same fields back to Steam with `openid.mode=check_authentication`, and accepting only `is_valid:true` |
|
||||
| `subject` from the ID token or userinfo | `subject` is the 17-digit SteamID64 at the end of `openid.claimed_id` (`https://steamcommunity.com/openid/id/<id>`) |
|
||||
| Email and name from the IdP | **No email.** No name either, unless the site holds a Steam Web API key (Q3) |
|
||||
|
||||
The checks the adapter must make: `openid.op_endpoint` is Steam's, `return_to` matches the URL we sent, the
|
||||
claimed id matches `^https://steamcommunity\.com/openid/id/\d{17}$`, and `check_authentication` says valid. Each
|
||||
assertion is single-use, which the existing single-use tx cookie already enforces.
|
||||
|
||||
## 4. The build
|
||||
|
||||
**Core (`website`):**
|
||||
- `steam.provider.js`, a `BaseProvider` with `getAuthorizationUrl` and `handleCallback` doing §3. It is a built-in in
|
||||
`registry.js` (`{ id: 'steam', kind: 'steam', name: 'Steam' }`), and its config validates with no client id or secret.
|
||||
- `auth_providers.kind` gains `steam`. The schema change is additive.
|
||||
- `sso.controller.js`'s `callback` accepts Steam's `openid.*` fields where it now requires `code`, for `kind: steam`
|
||||
only, and passes them to `handleCallback`. Everything after `handleCallback` is unchanged: `finishLogin`,
|
||||
`finishLink`, `finishMobileLogin`, the TOTP gate and provisioning.
|
||||
- Admin → Auth shows Steam with an enable switch and, per Q3, an optional Web API key stored like a client secret.
|
||||
- The account page lists Steam identities with a *Link Steam* button, and each linked Steam account is shown with its
|
||||
id (or name, Q3) and an *Unlink* button.
|
||||
- **Module contract, MODULE_API 1.15.0 (additions only):** a read of a user's verified identities for one provider,
|
||||
and a hook when one is linked or unlinked. Names are settled at build. Module-uo uses neither, and runs its usual
|
||||
frozen-manifest proof against the new core.
|
||||
- OpenAPI annotations and the regenerated spec for anything new or changed; `BACKEND_DESIGN.md` §3/§4/§6.
|
||||
|
||||
**Android (`Android-app`):** the provider list comes from `/auth/providers`, so Steam appears where the app lists
|
||||
providers. The work is the button's mark and a check of the M9 bridge with Steam's callback. Confirmed in the build.
|
||||
|
||||
**Module-Rust:** a Steam identity on the account becomes a linked Rust player (Q1), beside the in-game `/link` code
|
||||
(R1). Unlinking follows Q2.
|
||||
|
||||
## 5. Security
|
||||
|
||||
- Nothing secret ships to a client. The optional Web API key is server-side and write-only in the API, like client
|
||||
secrets.
|
||||
- The `check_authentication` call is the only proof of a Steam assertion. It is made server to server, and never
|
||||
skipped.
|
||||
- **Link-only stays the rule:** a Steam id that is not linked signs nobody in, unless registration opts into SSO
|
||||
sign-up (Q4).
|
||||
- Login hardening (rate limits, bot score, honeypot) applies to the Steam routes as to the others.
|
||||
|
||||
## 6. Tests
|
||||
|
||||
- Unit: the claimed-id parser, `return_to` matching, a `check_authentication` reply of `is_valid:false`, a replayed
|
||||
assertion, a wrong `op_endpoint`.
|
||||
- Flow: login, link and mobile modes against a stubbed Steam endpoint, including the TOTP gate and provisioning.
|
||||
- Module-uo proof against the new core.
|
||||
- A live walk: sign in with a real Steam account on the local review stack, link, unlink, and the same in the app.
|
||||
|
||||
## 7. Questions for the org lead
|
||||
|
||||
- **Q1 — Does a Steam sign-in count as a linked Rust player?** For example, Alice clicks *Link Steam* on her account
|
||||
page and approves it on steamcommunity.com. Does the Rust module then treat her as owning that Steam account, so
|
||||
her kit rewards and permissions reach it, without the in-game `/link` code? **Proposed:** yes. Steam's own sign-in
|
||||
proves the account at least as well as the code does, and both routes stay.
|
||||
- **Q2 — What does unlinking remove?** For example, Alice unlinks Steam on her account page. Does her Rust link go
|
||||
too, and with it the grants D28 resolves to that Steam id? **Proposed:** the two are one link with two ways to
|
||||
make it, so removing either removes the Rust link. The site says so before it does.
|
||||
- **Q3 — Names and avatars.** Steam's sign-in returns only the Steam id. Showing *"Steam: CoolGuy99"* with an avatar
|
||||
needs a Steam Web API key, which an admin would paste into Admin → Auth like a client secret. **Proposed:** an
|
||||
optional key. Without it the account page shows the Steam id, and a provisioned username is `steam_` plus the
|
||||
last digits of the id.
|
||||
- **Q4 — Sign-up through Steam.** When registration allows SSO sign-up, a new account from Steam has **no email**.
|
||||
**Proposed:** allowed, as for any provider that sends no email. The player adds and verifies an address through
|
||||
the ordinary flow later.
|
||||
Reference in New Issue
Block a user