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
108 lines
7.9 KiB
Markdown
108 lines
7.9 KiB
Markdown
# 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.
|