feat(auth): sign in with Steam (MODULE_API 1.17.0) #218

Merged
whitlocktech merged 2 commits from feat/steam-signin into edge 2026-10-20 15:26:02 +00:00
Member

What & why

Sign in with Steam as a built-in provider beside Google and Discord (docs/website/STEAM_SIGNIN.md, decisions D392–D396, docs#363). Closes #214. Goes on edge for the cutover in progress. Before branching I fast-forwarded edge to main (6ba2ed9); it had nothing of its own.

  • OpenID 2.0 (auth/providers/steam.provider.js): checkid_setup redirect with our state inside return_to. The callback checks op_endpoint, an exact return_to (so a captured assertion can't be replayed into another flow), a 17-digit claimed id, identity = claimed id, and then the server-to-server check_authentication. That check is never skipped, and a transport failure refuses the sign-in.
  • D394: Steam's only config is a Steam Web API key (stored in client_secret_enc, write-only). Admin → Auth refuses to enable Steam without one. The persona name and avatar (Steam CDN only) are stored on the identity and refreshed at each sign-in. A failed lookup never blocks a sign-in.
  • D395: Steam never provisions an account, on the web or the mobile bridge, whatever player_registration says.
  • Several Steam accounts per person: the account pages keep Link Steam offered. DELETE /auth/me/account/identities/:provider?id= unlinks one identity; without ?id= it behaves as before.
  • MODULE_API 1.17.0: ctx.identities.list(userId, provider), plus api.onIdentityLinking (refuse-only, fails closed, 5 s; a module's text is logged and never put in the redirect, only link_error=in_use|refused) and api.onIdentityLinked (bounded; a failure never undoes the link). There is no unlink hook (D393).
  • Schema (additive): auth_providers.kind gains steam; user_identities gains display_name and avatar_url.
  • Spec: OpenAPI, routes.manifest.json / routes.guards.json and engagement-triggers.json regenerated. Generate them with MODULES_DIR pointing at an empty directory: a local modules/ leaks module routes into core's manifests.

Docs: docs docs/steam-signin-build (STEAM_SIGNIN.md §8, MODULE_API.md 1.17.0, BACKEND_DESIGN.md §3/§4/§6). Companions: Module-Rust feat/steam-signin-link and Android-app feat/steam-signin, both into edge.

How it was tested

  • npm test (server, MODULES_DIR empty): 2,161 pass, 0 fail. That includes the new test/steamSignin.test.js (32 tests): §3's checks, a replayed assertion, is_valid:false, Steam unreachable, a failed Web API lookup, a non-CDN avatar, enabling with no key, no provisioning on web and mobile, a mobile sign-in, cancel, a module refusal writing nothing, fail-closed, a linked hook failing, multi-account unlink.
  • Client: npm run build plus 430 tests pass.
  • Local walk (DB steamwalk, a copy of rustwalk, with Module-Rust from its branch):
    • The schema upgrade and a fresh install both apply.
    • Admin → Auth Steam tab: the no-key refusal, and the key saved encrypted.
    • Continue with Steam appears on the player login, and its 302 to steamcommunity.com carries the right fields.
    • Through the real finishLink: a free id becomes a Rust link; another account's Rust link is refused with nothing written; a second Steam account links; an id linked elsewhere is refused by core.
    • In the browser: both accounts on the account page, one unlinked, and its Rust link stays.
  • Owed: the live walk with a real Steam account and key (org lead).
  • routeManifest.test.js fails on this machine with the local modules/ installed on main too. It passes with MODULES_DIR empty, which is what CI sees.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Claude Opus 5.5). I have reviewed and understand
    every change, and take responsibility for it. AI-authored commits are
    marked with a Co-Authored-By / Assisted-By trailer.

License

  • I agree that my contribution is licensed under this project's license
    (GNU GPL v3.0 or later), and I have the right to contribute it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

## What & why Sign in with Steam as a built-in provider beside Google and Discord (`docs/website/STEAM_SIGNIN.md`, decisions D392–D396, docs#363). Closes #214. Goes on `edge` for the cutover in progress. Before branching I fast-forwarded `edge` to `main` (6ba2ed9); it had nothing of its own. - **OpenID 2.0** (`auth/providers/steam.provider.js`): `checkid_setup` redirect with our `state` inside `return_to`. The callback checks `op_endpoint`, an exact `return_to` (so a captured assertion can't be replayed into another flow), a 17-digit claimed id, identity = claimed id, and then the **server-to-server `check_authentication`**. That check is never skipped, and a transport failure refuses the sign-in. - **D394:** Steam's only config is a **Steam Web API key** (stored in `client_secret_enc`, write-only). Admin → Auth refuses to enable Steam without one. The persona name and avatar (Steam CDN only) are stored on the identity and refreshed at each sign-in. A failed lookup never blocks a sign-in. - **D395:** Steam **never provisions** an account, on the web or the mobile bridge, whatever `player_registration` says. - **Several Steam accounts per person:** the account pages keep *Link Steam* offered. `DELETE /auth/me/account/identities/:provider?id=` unlinks one identity; without `?id=` it behaves as before. - **MODULE_API 1.17.0:** `ctx.identities.list(userId, provider)`, plus `api.onIdentityLinking` (refuse-only, **fails closed**, 5 s; a module's text is logged and never put in the redirect, only `link_error=in_use|refused`) and `api.onIdentityLinked` (bounded; a failure never undoes the link). There is no unlink hook (D393). - **Schema** (additive): `auth_providers.kind` gains `steam`; `user_identities` gains `display_name` and `avatar_url`. - **Spec:** OpenAPI, `routes.manifest.json` / `routes.guards.json` and `engagement-triggers.json` regenerated. **Generate them with `MODULES_DIR` pointing at an empty directory**: a local `modules/` leaks module routes into core's manifests. Docs: docs `docs/steam-signin-build` (`STEAM_SIGNIN.md` §8, `MODULE_API.md` 1.17.0, `BACKEND_DESIGN.md` §3/§4/§6). Companions: Module-Rust `feat/steam-signin-link` and Android-app `feat/steam-signin`, both into `edge`. ## How it was tested - `npm test` (server, `MODULES_DIR` empty): **2,161 pass, 0 fail**. That includes the new `test/steamSignin.test.js` (32 tests): §3's checks, a replayed assertion, `is_valid:false`, Steam unreachable, a failed Web API lookup, a non-CDN avatar, enabling with no key, no provisioning on web and mobile, a mobile sign-in, cancel, a module refusal writing nothing, fail-closed, a linked hook failing, multi-account unlink. - Client: `npm run build` plus **430 tests pass**. - **Local walk** (DB `steamwalk`, a copy of `rustwalk`, with Module-Rust from its branch): - The schema upgrade and a fresh install both apply. - Admin → Auth Steam tab: the no-key refusal, and the key saved encrypted. - *Continue with Steam* appears on the player login, and its 302 to steamcommunity.com carries the right fields. - Through the real `finishLink`: a free id becomes a Rust link; another account's Rust link is refused with nothing written; a second Steam account links; an id linked elsewhere is refused by core. - In the browser: both accounts on the account page, one unlinked, and its Rust link stays. - **Owed:** the live walk with a real Steam account and key (org lead). - `routeManifest.test.js` fails on this machine with the local `modules/` installed on `main` too. It passes with `MODULES_DIR` empty, which is what CI sees. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Claude Opus 5.5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` / `Assisted-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 2 commits 2026-10-20 01:34:21 +00:00
Steam is a built-in provider beside Google and Discord (STEAM_SIGNIN.md,
D392-D396). OpenID 2.0: the assertion is verified server to server with
check_authentication, return_to must match byte for byte, and the claimed id
must be a 17-digit SteamID64. A Steam Web API key is required to enable it
(D394); persona name and avatar are stored on the identity and refreshed at
each sign-in, and a failed lookup never blocks one. Steam never signs anybody
up (D395), on the web or the mobile bridge. Several Steam accounts may be
linked; DELETE /auth/me/account/identities/:provider takes ?id= to unlink one.

MODULE_API 1.17.0: ctx.identities.list, api.onIdentityLinking (refuse-only,
fails closed, module text never shown) and api.onIdentityLinked. No unlink
hook (D393). Schema additive; OpenAPI, routes and engagement manifests
regenerated with no modules installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
test(auth): a linked Steam account signs in through the mobile bridge
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 39s
PR Checks / client-build (pull_request) Successful in 54s
PR Checks / server-tests (pull_request) Successful in 12m40s
02897030e5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit 9a1654fc54 into edge 2026-10-20 15:26:02 +00:00
whitlocktech deleted branch feat/steam-signin 2026-10-20 15:26:04 +00:00
Sign in to join this conversation.
No description provided.