Skip to content

feat(client): password login via IAuthenticationService over WebSocket CM#25

Open
fabieu wants to merge 7 commits into
masterfrom
23-login-with-password-for-steam-client
Open

feat(client): password login via IAuthenticationService over WebSocket CM#25
fabieu wants to merge 7 commits into
masterfrom
23-login-with-password-for-steam-client

Conversation

@fabieu

@fabieu fabieu commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Steam no longer accepts plaintext passwords at the CM, which broke SteamClient.login(username, password) and the old login_key "remember password" flow. Steam also only serves the pre-logon IAuthenticationService credential flow over its WebSocket CM endpoints, not the raw TCP CMs. This PR rebuilds password login on the modern credential auth session and adds a WebSocket CM transport to carry it.

What changed

Password login via IAuthenticationService

  • login(): when a password is supplied, run the credential flow to obtain a JWT refresh token, then log on with it. A previously obtained token can be passed directly via access_token. Dropped the login_key parameter; auth codes are now handled pre-logon rather than on CMsgClientLogon.
  • Auth session flow (_get_refresh_token / _begin_auth_session): GetPasswordRSAPublicKeyBeginAuthSessionViaCredentials → optional UpdateAuthSessionWithSteamGuardCode → poll PollAuthSessionStatus, all sent as NonAuthed unified messages before logon.
  • Out-of-band confirmation: wait up to confirmation_timeout for either a Steam Guard code or a mobile-app approval — ask for a code and poll for a mobile confirmation in the background, whichever completes first.
  • Session reuse: the auth session (client_id/request_id) is cached on the client and reused across login() retries while a guard code is collected, so an email code isn't invalidated by a fresh session on each retry. An accepted code whose token mints slowly is no longer misreported as a mismatch.
  • relogin / relogin_available: now backed by the stored refresh_token instead of login_key. Removed EVENT_NEW_LOGIN_KEY and _handle_login_key.
  • send_um / send_um_and_wait: accept an emsg override so pre-logon calls can use ServiceMethodCallFromClientNonAuthed (new EMsg 9804).

WebSocket CM transport

  • WebsocketConnection (wss://<cm>/cmsocket/): frames each Steam message as one binary WebSocket frame (no VT01 prefix, no AES — TLS secures the channel) and plugs into the base Connection via _frame / _close_socket hooks.
  • CMClient: gains PROTOCOL_WEBSOCKET, sets channel_secured immediately, sends the required ClientHello, stamps realm/steamid/client_sessionid on the WS headers, and bootstraps from serverlist_websockets. SteamClient now defaults to the WebSocket transport.
  • Requires the websocket-client dependency (client extra) and cooperative sockets — login recipes now call steam.monkey.patch_minimal().

Cleanup

  • rsa_encrypt_password: new helper in steam.core.crypto; webauth reuses it (UTF-8 password encoding).
  • Removed the deprecated MobileWebAuth.

Tests

  • tests/test_client_login.py — refresh-token flow (success, invalid password, 2FA/email code required and accepted/rejected, mobile confirmation, session reuse, slow token mint, unknown confirmation type), _send_logon / NonAuthed unified message behavior, and a unicode round-trip for rsa_encrypt_password.
  • tests/test_websocket_transport.py — WebSocket framing and connection behavior.
  • tests/test_core_cm.py — updated for the WebSocket protocol path.

Closes #23.

Steam no longer accepts plaintext passwords at the CM. Replace the
removed login_key flow with the credential auth session flow: encrypt
the password with the account RSA key, run BeginAuthSessionViaCredentials
over the CM as a NonAuthed unified message, handle Steam Guard codes /
mobile confirmation, poll for a refresh token, and log on with it.

The acquired refresh token is stored on `refresh_token` and reused by
`relogin`. The auth session (client_id/request_id) is cached on the
client and reused across `login()` calls while a guard code is being
collected, so an email code is not invalidated by a fresh session on
each retry, and an accepted code that mints its token slowly is not
misreported as a mismatch.
@fabieu fabieu linked an issue Jul 16, 2026 that may be closed by this pull request
- Split _get_refresh_token's guard-code submission and token polling into
  _submit_guard_code / _poll_for_refresh_token to cut cognitive complexity.
- Drop the redundant tuple() constructor in webauth._encrypt_password.
- Use `_` for unpacked return values the login tests do not assert.
- Test rsa_encrypt_password against a 2048-bit key and rename the password
  variable; the round-trip must use PKCS#1 v1.5, the scheme Steam mandates.
@fabieu fabieu self-assigned this Jul 17, 2026
@fabieu fabieu added the bug Something isn't working label Jul 17, 2026
Steam only serves the pre-logon IAuthenticationService credential flow over
the WebSocket CM endpoints, so the raw TCP CMs drop the non-authed auth call.
Add a WebsocketConnection (wss://<cm>/cmsocket/) that frames each Steam message
as one binary frame (no VT01 prefix, no AES -- TLS secures the channel) and
plugs into the base Connection via _frame/_close_socket hooks. CMClient gains
PROTOCOL_WEBSOCKET, sets channel_secured immediately, sends the required
ClientHello, stamps realm/steamid/client_sessionid on WS headers, and
bootstraps from serverlist_websockets. SteamClient now defaults to WebSocket.

Also wait up to confirmation_timeout for an out-of-band approval: ask for a
code and poll for a mobile-app confirmation in the background, whichever
completes first. Removes the deprecated MobileWebAuth and reuses
rsa_encrypt_password in webauth.

Requires the websocket-client dependency (client extra) and cooperative
sockets -- login recipes now call steam.monkey.patch_minimal().
@fabieu fabieu added breaking change Backward-incompatible changes and removed bug Something isn't working labels Jul 17, 2026
@fabieu fabieu changed the title feat(client): login with password via IAuthenticationService feat(client): password login via IAuthenticationService over WebSocket CM Jul 17, 2026
fabieu added 4 commits July 17, 2026 21:39
- Extract _request_guard_code to drop _get_refresh_token cognitive complexity below 15.
- Rename TCPChannelEncrypt_Scenario to satisfy the class-name convention.
- Explain the empty FakeWS.settimeout stub and use a documentation-range IP in the bootstrap test.
- Tolerate unknown logon eresults via _eresult() instead of raising
- Prevent a finishing background poll from orphaning a newer one
- Pace failed CM discovery rounds so the websocket loop cannot busy-spin
- Add wait_for_confirmation to cli_login so out-of-band approval works,
  and skip the code prompt when a background poll already logged us on
- Drop the impossible-scenario guard around socket.gethostname()
Extract the Steam Guard code prompt and transient-error handling from
cli_login into _cli_prompt_guard_code and _cli_handle_unavailable to
bring the function under SonarCloud's cognitive complexity limit.
Scope the already-logged-on short-circuit to the same account so a login
for a different user no longer silently returns OK, and drop a stored
refresh_token / pending auth session on an account switch to avoid a
mismatched (username, refresh_token) pair and stale session reuse.

Tolerate unknown logon eresults in the base handler instead of raising
out of the message greenlet, reject empty input at the "keep retrying"
CLI prompt, and fail gracefully when GetCMList omits the WebSocket
serverlist instead of raising KeyError.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Backward-incompatible changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Can't login with password despite it being correct

1 participant