docs(website): Steam sign-in's questions answered (D392-D396)
The org lead answered STEAM_SIGNIN.md's Q1-Q4 on 2026-10-19 and ruled on the conflict they raise together: - D392 a Steam account linked on the site is a linked Rust player (Q1) - D393 unlinking Steam removes only the sign-in identity (Q2) - D394 Steam needs a Steam Web API key to be enabled (Q3) - D395 Steam never signs anybody up (Q4) - D396 a Steam link whose id is another account's Rust link is refused The build section follows: the key is required, the identity row keeps the persona name and avatar, and MODULE_API 1.17.0 adds a pre-link check (which can only refuse) and a linked hook, with no unlink hook. PLAN_REDESIGNS tables the three decisions Module-Rust builds to and points §15 at them. The build goes on the website's edge and ships in the cutover in progress. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 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.
|
||||
> **Status: plan, written 2026-10-14; its questions answered 2026-10-19 (§7, D392–D396). Nothing is built.** It is
|
||||
> built on the website's `edge` and ships in the cutover in progress (the org lead, 2026-10-19).
|
||||
> 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
|
||||
@@ -12,7 +13,8 @@
|
||||
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 follows the **same link-only policy**, more strictly: **Steam never signs anybody up**, whatever
|
||||
`player_registration` says (D395).
|
||||
- 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.
|
||||
@@ -42,7 +44,7 @@ core that no module can weaken (MODULE_SYSTEM.md).
|
||||
| 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) |
|
||||
| Email and name from the IdP | **No email.** The name and avatar come from the Steam Web API, with the key the site must hold (D394) |
|
||||
|
||||
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
|
||||
@@ -52,57 +54,91 @@ assertion is single-use, which the existing single-use tx cookie already enforce
|
||||
|
||||
**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.
|
||||
`registry.js` (`{ id: 'steam', kind: 'steam', name: 'Steam' }`). Its config needs no client id or secret, and
|
||||
**needs a Steam Web API key to be enabled** (D394): saving Steam as enabled with no key is refused with a message.
|
||||
The key is stored in `client_secret_enc`, AES-256-GCM through `secretBox`, and is write-only like a client secret.
|
||||
- After the assertion is verified, `handleCallback` calls `ISteamUser/GetPlayerSummaries` for the persona name and
|
||||
avatar. **That call never decides a sign-in:** if it fails, the sign-in goes ahead with the bare id and a warning
|
||||
in the log. The proof is `check_authentication` alone (§5).
|
||||
- `auth_providers.kind` gains `steam`, and `user_identities` gains nullable `display_name` and `avatar_url`, written at
|
||||
link and refreshed at each Steam sign-in. Both schema changes are additive; other providers leave the columns
|
||||
null.
|
||||
- `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.17.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs` and 1.16.0 by a dry run's advice, Rust
|
||||
PLAN_REDESIGNS §14.3.13):** 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.
|
||||
only, and passes them to `handleCallback`. After that, `finishLogin`, `finishLink`, `finishMobileLogin` and the
|
||||
TOTP gate are unchanged, with two differences for Steam: **an unlinked Steam id never reaches
|
||||
`provisionSsoPlayer`** (D395) and gets the ordinary *"no account is linked"* answer, and **`finishLink` asks the
|
||||
modules first** (below).
|
||||
- Admin → Auth shows Steam with an enable switch and the Web API key field.
|
||||
- The account page lists Steam identities with a *Link Steam* button. Each linked Steam account shows its persona
|
||||
name and avatar, its id, and an *Unlink* button. Unlinking removes the sign-in identity and nothing else (D393).
|
||||
- **Module contract, MODULE_API 1.17.0 (additions only; 1.15.0 was taken by a step's `retryAfterMs` and 1.16.0 by a
|
||||
dry run's advice, Rust PLAN_REDESIGNS §14.3.13 and §11.4.6):**
|
||||
- a **read** of a user's verified identities for one provider;
|
||||
- a **pre-link check**: before an identity is written, each module that registered one may refuse it with a
|
||||
message the player sees, and nothing is written (D396). A refusal only ever stops a link; it cannot let one
|
||||
through that core would refuse, and it never touches sign-in or session creation;
|
||||
- a **linked hook** after the identity is written. No unlink hook: D393 leaves nothing for a module to undo.
|
||||
|
||||
Names are settled at build. Module-uo uses none of them, 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;
|
||||
`MODULE_API.md` for 1.17.0.
|
||||
|
||||
**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.
|
||||
**Module-Rust:** a Steam identity linked on the site **is** a linked Rust player (D392), beside the in-game `/link`
|
||||
code (R1): the linked hook writes the Rust link, and the pre-link check refuses a Steam id that is already another
|
||||
website account's Rust link (D396). Unlinking Steam leaves the Rust link in place (D393); it is removed on the Rust
|
||||
page as today.
|
||||
|
||||
## 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.
|
||||
- Nothing secret ships to a client. The 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).
|
||||
skipped. The Web API lookup only decorates an identity; it is never proof.
|
||||
- **Link-only, with no exception:** a Steam id that is not linked signs nobody in, and SSO sign-up never applies to
|
||||
Steam (D395).
|
||||
- A module's pre-link check can only refuse. It runs inside `finishLink` for an already signed-in user, so it has no
|
||||
path to a session.
|
||||
- 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.
|
||||
assertion, a wrong `op_endpoint`, a failed Web API lookup that still signs in, enabling with no key refused.
|
||||
- Flow: login, link and mobile modes against a stubbed Steam endpoint, including the TOTP gate, an unlinked Steam id
|
||||
refused with SSO sign-up on, a module's pre-link refusal writing nothing, and the linked hook firing once.
|
||||
- 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.
|
||||
It needs the org lead (a real Steam account, and a Web API key for the review stack), so it joins the end walk's
|
||||
in-game rows rather than holding the build.
|
||||
|
||||
## 7. Questions for the org lead
|
||||
## 7. Questions, answered 2026-10-19
|
||||
|
||||
The org lead answered Q1–Q4 and ruled on the case Q1 and Q2 raised together. The D-numbers continue Rust's
|
||||
[`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §10, where D360 was recorded.
|
||||
|
||||
- **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.
|
||||
her kit rewards and permissions reach it, without the in-game `/link` code? *Proposed:* yes. **Answered: yes
|
||||
(D392).**
|
||||
- **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.
|
||||
too, and with it the grants D28 resolves to that Steam id? *Proposed:* both go. **Answered: only the sign-in goes
|
||||
(D393).**
|
||||
- **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.
|
||||
needs a Steam Web API key, which an admin would paste into Admin → Auth like a client secret. *Proposed:* an
|
||||
optional key. **Answered: the key is required (D394).**
|
||||
- **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.
|
||||
*Proposed:* allowed. **Answered: never (D395).**
|
||||
- **The conflict.** Bob linked Steam id 7656…001 with the in-game `/link` code months ago. Alice, a different
|
||||
website account, clicks *Link Steam* and signs in to steamcommunity.com as 7656…001. **Answered: Alice's link is
|
||||
refused (D396).**
|
||||
|
||||
| # | Decision | Rejected |
|
||||
|---|---|---|
|
||||
| **D392** | **A Steam account linked on the site is a linked Rust player**, beside the in-game `/link` code (R1). Steam's sign-in proves the account at least as well as the code does, and both routes stay. | Steam sign-in as sign-in only. |
|
||||
| **D393** | **Unlinking Steam removes only the sign-in identity.** A Rust link it made stays, with its grants, until it is removed on the Rust page. | One link with two ways to make it, gone with either. |
|
||||
| **D394** | **Steam needs a Steam Web API key to be enabled.** Admin → Auth refuses to enable Steam without one; with it, a linked Steam account shows its persona name and avatar. The key is stored like a client secret and is never returned. A failed lookup never stops a sign-in. | An optional key with bare ids without it; never calling the Web API. |
|
||||
| **D395** | **Steam never signs anybody up.** An unlinked Steam id signs nobody in, even when `player_registration` allows SSO sign-up; a player links Steam to an account they already have. | Sign-up through Steam with no email. |
|
||||
| **D396** | **A Steam link whose id is already another website account's Rust link is refused.** Alice sees *"This Steam account is already linked to another account on this site"*, and nothing changes. If her claim is real, staff remove Bob's Rust link and she links again. Core makes this possible through a module pre-link check (§4). | Steam wins and moves the Rust link to Alice; Alice gets the sign-in but not the Rust link. |
|
||||
|
||||
Reference in New Issue
Block a user