Steam Auth v2 (Unified Messages) Login Flow: Comprehensive Reference
Overview
This document describes the Steam Auth v2 password-based login flow using Unified Messages (UM) as implemented by node-steam-user and compatible with SteamKit2. The flow is coordinated through the steam-session package, which orchestrates the individual Auth.* UM calls over a CMAuthTransport that bridges UM over the CM connection.
The key innovation of Auth v2 is that authentication happens before the traditional CMsgClientLogon, via an out-of-band session negotiation using the Authentication service. Once a refresh token (JWT) is obtained via polling, it is passed into the final CMsgClientLogon message as the access_token field.
SECTION A: Unified Message Transport
UM Wrapping: ServiceMethodCallFromClient
Before login, the client is not yet authenticated. UM calls during this phase use the non-authed variant. Once the session is established, they use the regular authed variant.
Wire Format:
- EMsg constant:
EMsg.ServiceMethodCallFromClientNonAuthed(before login);EMsg.ServiceMethodCallFromClient(after login) - Proto header:
CMsgProtoBufHeader - Encoding: Message body is a protobuf; header is also a protobuf, prefixed by a 4-byte length field
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/classes/CMAuthTransport.js:22-23: ThesendRequestmethod chooses the EMsg based onsteamID:msg: this._user.steamID ? EMsg.ServiceMethodCallFromClient : EMsg.ServiceMethodCallFromClientNonAuthed,
Target Job Name Encoding
The service and method are encoded in a single string field: target_job_name on the proto header.
Format: {Service}.{Method}#{Version}
Example: Authentication.BeginAuthSessionViaCredentials#1
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/classes/CMAuthTransport.js:24: Constructs the target job name:target_job_name: `${request.apiInterface}.${request.apiMethod}#${request.apiVersion}`,
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_base.proto:100:target_job_namefield onCMsgProtoBufHeader
Job ID Correlation: jobid_source / jobid_target
Responses are correlated to requests via job IDs. The client mints a new job ID for each outgoing request; the response carries that ID in its jobid_target field.
Job ID generation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js:445:jobIdSource = ++this._currentJobID(simply incremented counter, starts at 0)
Header assignment:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js:469-470:header.proto.jobid_source = jobIdSource || header.proto.jobid_source || header.sourceJobID || JOBID_NONE; header.proto.jobid_target = header.proto.jobid_target || header.targetJobID || JOBID_NONE;
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_base.proto:98-99:optional fixed64 jobid_source = 10 [default = 18446744073709551615]; optional fixed64 jobid_target = 11 [default = 18446744073709551615];
Callback routing:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js:444-446: If a callback is provided, the job ID is stored in a job map and the response handler retrieves and invokes it based onjobid_target.
SECTION B: The Unauthenticated Phase — Getting the RSA Public Key
Even though the CM channel is AES-encrypted, the password is not transmitted in plaintext. Instead, it is encrypted with an RSA public key that the server provides per account.
Authentication.GetPasswordRSAPublicKey#1
This is the first UM call. It runs before the session is established and requires only the account name.
Request message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:72-74:message CAuthentication_GetPasswordRSAPublicKey_Request { optional string account_name = 1; }
Response message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:76-80:message CAuthentication_GetPasswordRSAPublicKey_Response { optional string publickey_mod = 1; // hex-encoded modulus optional string publickey_exp = 2; // hex-encoded exponent optional uint64 timestamp = 3; // used in password encryption }
The server returns the modulus and exponent as hex strings. The timestamp is included in the encrypted payload to bind the encryption to a specific moment and prevent replay.
Password Encryption: RSA PKCS#1 v1.5
Password RSA = PKCS#1 v1.5, NOT OAEP
The password is encrypted using RSA PKCS#1 v1.5 (NOT OAEP). This is a critical distinction from the transport-layer RSA-OAEP-SHA1 that wraps the AES session key during channel setup. The two RSA operations live at different layers, use different keys (the transport pubkey is universe-static; the password pubkey is per-account and rotates), and use different padding. Mix them up and login will fail with cryptic decryption errors.
Note on RSA variants:
- Transport layer (channel encryption setup): RSA-OAEP-SHA1 wraps the AES session key. See TCP Transport and Handshake.
- Password encryption (Auth v2): PKCS#1 v1.5 is used. This is what we document below.
The steam-session package (external dependency) handles this encryption. Based on code inspection and standard implementations:
- Construct a JSON structure:
{"password": "<plaintext>", "timestamp": <uint64_from_response>} - Serialize it to UTF-8
- Encrypt the JSON with RSA public key using PKCS#1 v1.5
- Base64-encode the ciphertext
Code citation (from node-steam-user integration):
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:406-429: The_performPasswordAuthmethod invokessteam-session:let sessionStartResult = await session.startWithCredentials({ accountName: this._logOnDetails.account_name, password: this._logOnDetails.password, steamGuardMachineToken: this._machineAuthToken, steamGuardCode: this._logOnDetails.two_factor_code || this._logOnDetails.auth_code });
The steam-session package (a separate npm dependency) implements the RSA encryption internally. It uses Node.js’s crypto module with publicEncrypt() and the RSA_PKCS1_PADDING flag.
SECTION C: BeginAuthSessionViaCredentials
This call initiates the authentication session. It submits the encrypted password, account name, and device details. In response, the server returns a client_id and request_id that are used in all subsequent polling calls.
Request: CAuthentication_BeginAuthSessionViaCredentials_Request
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:113-126:message CAuthentication_BeginAuthSessionViaCredentials_Request { optional string device_friendly_name = 1; // e.g. "My PC" optional string account_name = 2; // plaintext account name optional string encrypted_password = 3; // RSA-encrypted, base64-encoded optional uint64 encryption_timestamp = 4; // from GetPasswordRSAPublicKey response optional bool remember_login = 5; // unused in Auth v2 password flow optional .EAuthTokenPlatformType platform_type = 6; // e.g. SteamClient = 1 optional .ESessionPersistence persistence = 7; // Persistent or Ephemeral optional string website_id = 8; // "Unknown" by default optional .CAuthentication_DeviceDetails device_details = 9; optional string guard_data = 10; // machine auth data (if resuming) optional uint32 language = 11; // user language optional int32 qos_level = 12; // quality-of-service level }
Device details:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:82-90:message CAuthentication_DeviceDetails { optional string device_friendly_name = 1; optional .EAuthTokenPlatformType platform_type = 2; optional int32 os_type = 3; // Windows, Linux, macOS, etc. optional uint32 gaming_device_type = 4; optional uint32 client_count = 5; optional bytes machine_id = 6; // binary machine ID optional .EAuthTokenAppType app_type = 7; }
Population by node-steam-user:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js:738-754: The_getLoginSession()method creates aLoginSessionwith aCMAuthTransportand machine ID, platform type inferred from the OS./Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:83-102: The logon details are prepared, includingmachine_name,client_language,client_os_type, andchat_mode.
Response: CAuthentication_BeginAuthSessionViaCredentials_Response
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:128-137:message CAuthentication_BeginAuthSessionViaCredentials_Response { optional uint64 client_id = 1; // session ID, used in all polls optional bytes request_id = 2; // also used in all polls optional float interval = 3; // polling interval in seconds repeated .CAuthentication_AllowedConfirmation allowed_confirmations = 4; optional uint64 steamid = 5; // the account's SteamID (populated immediately) optional string weak_token = 6; // placeholder (may be deprecated) optional string agreement_session_url = 7; // EULA/legal acceptance optional string extended_error_message = 8; // detailed error info }
Allowed confirmations:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:99-102:message CAuthentication_AllowedConfirmation { optional .EAuthSessionGuardType confirmation_type = 1; optional string associated_message = 2; // e.g. email address }
Guard types enum:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:20-29:enum EAuthSessionGuardType { k_EAuthSessionGuardType_Unknown = 0; k_EAuthSessionGuardType_None = 1; // no guard required (rare) k_EAuthSessionGuardType_EmailCode = 2; // email code sent to registered email k_EAuthSessionGuardType_DeviceCode = 3; // TOTP or SMS code k_EAuthSessionGuardType_DeviceConfirmation = 4; // approve on mobile app k_EAuthSessionGuardType_EmailConfirmation = 5; // click link in email k_EAuthSessionGuardType_MachineToken = 6; // stored auth token on this machine k_EAuthSessionGuardType_LegacyMachineAuth = 7; // old machine auth method }
Selecting the confirmation method
The allowed_confirmations list tells the client which methods are available. The client picks one and implements the appropriate flow:
Decision tree (from steam-session integration):
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:470-487: AfterBeginAuthSessionViaCredentials, ifactionRequiredis true in the response, the code checksvalidActions:let wantsEmailCode = sessionStartResult.validActions.find(action => action.type == EAuthSessionGuardType.EmailCode); if (wantsEmailCode) { logOnResponse.eresult = EResult.AccountLogonDenied; logOnResponse.email_domain = wantsEmailCode.detail; } else { logOnResponse.eresult = EResult.AccountLoginDeniedNeedTwoFactor; }
The steam-session library handles the internal polling and code submission automatically. Node-steam-user simply emits steamGuard event or error event depending on the outcome.
SECTION D: Steam Guard Code Submission
If the server requires a Steam Guard code, the client submits it via UpdateAuthSessionWithSteamGuardCode.
Authentication.UpdateAuthSessionWithSteamGuardCode#1
Request message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:214-219:message CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request { optional uint64 client_id = 1; // from BeginAuthSession response optional fixed64 steamid = 2; // account's SteamID optional string code = 3; // the code (6 digits, no dashes) optional .EAuthSessionGuardType code_type = 4; // EmailCode or DeviceCode }
Response message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:221-223:message CAuthentication_UpdateAuthSessionWithSteamGuardCode_Response { optional string agreement_session_url = 7; }
State Machine Sequencing
The critical point: The code arrives from the user (or from the callback passed to steam-session) before polling starts, not as a result of a failed poll.
Sequencing (from steam-session):
- Client calls
startWithCredentials()with optionalsteamGuardCodeparameter - If the code is provided and valid,
steam-sessioninternally callsUpdateAuthSessionWithSteamGuardCode - If the code is incorrect, the response carries an error;
steam-sessionemits an error or retries - If the code is not provided but required,
startWithCredentials()resolves withactionRequired: true; the caller must gather the code and retry
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:454-459: The code is passed tostartWithCredentials():sessionStartResult = await session.startWithCredentials({ accountName: this._logOnDetails.account_name, password: this._logOnDetails.password, steamGuardMachineToken: this._machineAuthToken, steamGuardCode: this._logOnDetails.two_factor_code || this._logOnDetails.auth_code });
If actionRequired is true on the response, node-steam-user emits a steamGuard event and the user supplies the code, then retries logOn().
SECTION E: Polling for Completion (JWT Retrieval)
Once the session is established and any guard codes submitted, the client polls PollAuthSessionStatus repeatedly until the server returns a refresh token.
Authentication.PollAuthSessionStatus#1
Request message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:139-143:message CAuthentication_PollAuthSessionStatus_Request { optional uint64 client_id = 1; // from BeginAuthSession optional bytes request_id = 2; // from BeginAuthSession optional fixed64 token_to_revoke = 3; // not used in password flow }
Response message:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto:145-154:message CAuthentication_PollAuthSessionStatus_Response { optional uint64 new_client_id = 1; // can rotate; use this in subsequent polls optional string new_challenge_url = 2; // QR rotation (not relevant to password) optional string refresh_token = 3; // THE JWT (populated on success) optional string access_token = 4; // short-lived token (deprecated in client flow) optional bool had_remote_interaction = 5; // did user approve on another device? optional string account_name = 6; // confirmed account name optional string new_guard_data = 7; // updated machine token optional string agreement_session_url = 8; // EULA acceptance needed? }
Polling Cadence
The interval field in BeginAuthSessionViaCredentials response specifies the polling interval in seconds. The client must not poll faster than this interval.
Code citation (steam-session):
The steam-session package honors this interval internally. Node-steam-user does not directly control polling; it delegates to steam-session, which emits the authenticated event once the polling succeeds.
Termination Conditions
Success: refresh_token field is non-empty. This is a JWT signed by Steam and is the credential passed to CMsgClientLogon.
Continue polling: Both refresh_token and access_token are empty. The server is still waiting (e.g., for user interaction on another device).
Fatal error: Response carries an eresult in the header indicating failure (e.g., invalid credentials, account locked, etc.).
JWT Contents
The refresh token is a JWT. Node-steam-user decodes it to extract the sub (subject, i.e., SteamID) and validates the iss (issuer) and aud (audience).
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:140-161: Decoding and validation:let decodedToken = Helpers.decodeJwt(details.refreshToken); let tokenSteamId = Helpers.steamID(decodedToken.sub); if (decodedToken.iss != 'steam') { throw new Error('refreshToken is not a Steam refresh token'); } if (!(decodedToken.aud || []).includes('client')) { throw new Error('This refreshToken is not valid for logging in to the Steam client'); }
The refresh token is what is passed forward to the final logon message.
SECTION F: CMsgClientLogon — The Final Logon
After obtaining the refresh token (JWT), the client constructs the traditional CMsgClientLogon message and sends it. This completes authentication to the CM and establishes the logged-in session.
Message Construction
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver_login.proto:31-90:message CMsgClientLogon { optional uint32 protocol_version = 1; optional uint32 deprecated_obfustucated_private_ip = 2; optional uint32 cell_id = 3; optional uint32 last_session_id = 4; optional uint32 client_package_version = 5; optional string client_language = 6; optional uint32 client_os_type = 7; optional bool should_remember_password = 8; optional string wine_version = 9; optional uint32 deprecated_10 = 10; optional .CMsgIPAddress obfuscated_private_ip = 11; optional uint32 qos_level = 21; optional fixed64 client_supplied_steam_id = 22; optional .CMsgIPAddress public_ip = 23; optional bytes machine_id = 30; optional uint32 launcher_type = 31; optional uint32 ui_mode = 32; optional uint32 chat_mode = 33; optional bytes steam2_auth_ticket = 41; optional string email_address = 42; optional fixed32 rtime32_account_creation = 43; optional string account_name = 50; optional string password = 51; // empty for Auth v2 optional string game_server_token = 52; optional string login_key = 60; optional bool was_converted_deprecated_msg = 70; optional string anon_user_target_account_name = 80; optional fixed64 resolved_user_steam_id = 81; optional int32 eresult_sentryfile = 82; optional bytes sha_sentryfile = 83; optional string auth_code = 84; optional int32 otp_type = 85; optional uint32 otp_value = 86; optional string otp_identifier = 87; optional bool steam2_ticket_request = 88; optional bytes sony_psn_ticket = 90; optional string sony_psn_service_id = 91; optional bool create_new_psn_linked_account_if_needed = 92; optional string sony_psn_name = 93; optional int32 game_server_app_id = 94; optional bool steamguard_dont_remember_computer = 95; optional string machine_name = 96; optional string machine_name_userchosen = 97; optional string country_override = 98; optional uint64 client_instance_id = 100; optional string two_factor_code = 101; optional bool supports_rate_limit_response = 102; optional string web_logon_nonce = 103; optional int32 priority_reason = 104; optional .CMsgClientSecret embedded_client_secret = 105; optional bool disable_partner_autogrants = 106; optional string access_token = 108; // THE JWT (refresh token) optional bool is_chrome_os = 109; optional uint32 gaming_device_type = 111; }
Key Fields for Auth v2 Password Login
| Field | Value | Rationale |
|---|---|---|
protocol_version | 65580 | Current Steam protocol version |
account_name | (empty) | Implicit from JWT; also deleted at line 423 |
password | (empty) | Auth v2 uses JWT, not plaintext |
access_token | (refresh token JWT) | The critical field: contains the JWT returned by polling |
client_os_type | (detected or provided) | OS enum (Windows, Linux, macOS, etc.) |
client_language | ”english” (or user language) | Localization hint |
machine_id | (binary blob) | Persistent or random per machineIdType option |
machine_name | (hostname or custom) | Display name for this machine |
obfuscated_private_ip | (masked IPv4 or 0) | Obfuscated with 0xbaadf00d XOR |
cell_id | (saved from prior logon or 0) | Cell routing hint; persisted to file |
client_instance_id | (from response on re-auth) | Opaque instance tracker |
chat_mode | 2 | Enable new chat system |
supports_rate_limit_response | true (for non-anon) | Opt-in to rate limiting |
should_remember_password | true | Instruct server to issue refresh token |
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:83-102: Initial logon details setup/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:164:access_tokenis set from the refresh token:this._logOnDetails.access_token = details.refreshToken;/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:419: After password auth succeeds, the refresh token is moved intoaccess_token:this._logOnDetails.access_token = session.refreshToken;
SteamID in Header
The CMsgClientLogon message itself is sent with a protobuf header. Prior to logon, the header’s steamid field is set to an anonymous or temporary SteamID.
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:228-237: Construction of temporary SteamID:if (this._logOnDetails._steamid) { this._tempSteamID = Helpers.steamID(this._logOnDetails._steamid); } else { let sid = new SteamID(); sid.universe = SteamID.Universe.PUBLIC; sid.type = anonLogin ? SteamID.Type.ANON_USER : SteamID.Type.INDIVIDUAL; sid.instance = anonLogin ? SteamID.Instance.ALL : SteamID.Instance.DESKTOP; sid.accountid = 0; this._tempSteamID = sid; }
For password logins, the type is INDIVIDUAL, and the account ID is 0 until the server confirms the real SteamID in the response.
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js:468: The header’s steamid field is populated fromthis.steamIDorthis._tempSteamID:header.proto.steamid = shouldIncludeSessionId ? (this.steamID || this._tempSteamID).getSteamID64() : '0';
Server Response: CMsgClientLogonResponse
Proto definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver_login.proto:92-120:message CMsgClientLogonResponse { optional int32 eresult = 1; optional int32 legacy_out_of_game_heartbeat_seconds = 2; optional int32 heartbeat_seconds = 3; optional uint32 deprecated_public_ip = 4; optional fixed32 rtime32_server_time = 5; optional uint32 account_flags = 6; optional uint32 cell_id = 7; optional string email_domain = 8; optional bytes steam2_ticket = 9; optional int32 eresult_extended = 10; optional uint32 cell_id_ping_threshold = 12; optional bool deprecated_use_pics = 13; optional string vanity_url = 14; optional .CMsgIPAddress public_ip = 15; optional string user_country = 16; optional fixed64 client_supplied_steamid = 20; optional string ip_country_code = 21; optional bytes parental_settings = 22; optional bytes parental_setting_signature = 23; optional int32 count_loginfailures_to_migrate = 24; optional int32 count_disconnects_to_migrate = 25; optional int32 ogs_data_report_time_window = 26; optional uint64 client_instance_id = 27; optional bool force_client_update_check = 28; optional string agreement_session_url = 29; optional uint64 token_id = 30; optional uint64 family_group_id = 31; }
Key response fields:
eresult = 1 (OK)– Login succeededheartbeat_seconds– Interval (in seconds) at which to sendCMsgClientHeartBeatcell_id– Updated cell ID; should be saved for next logonclient_supplied_steamid– The account’s real SteamID (set intothis.steamID)public_ip– Server’s view of our public IP
Handler code:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:712-852: The_handleLogOnResponse()method processes the result. On success (eresult = OK):case EResult.OK: this._resetExponentialBackoff('logOn'); this._logOnDetails.last_session_id = this._sessionID; this._logOnDetails.client_instance_id = body.client_instance_id; this._logOnDetails.cell_id = body.cell_id; delete this._logOnDetails.auth_code; delete this._logOnDetails.two_factor_code; this.logOnResult = body; this.publicIP = StdLib.IPv4.intToString(body.public_ip.v4); this.cellID = body.cell_id; this.contentServersReady = true; this.emit('loggedOn', body, parental);
Heartbeat (CMsgClientHeartBeat)
Once logged on, the client must send a heartbeat message at the interval specified in the logon response to keep the session alive.
Message definition:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver_login.proto:6-8:message CMsgClientHeartBeat { optional bool send_reply = 1; }
Heartbeat setup:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:811-813: Sets up the interval:this._heartbeatInterval = setInterval(() => { this._send(EMsg.ClientHeartBeat, {}); }, body.heartbeat_seconds * 1000);
SECTION G: State Machine Diagram
┌─────────────────────────────────────────────────────────────────┐
│ DISCONNECTED │
│ │
│ - No CM connection │
│ - No authentication state │
└────────────────────────┬────────────────────────────────────────┘
│
│ logOn(accountName, password)
↓
┌─────────────────────────────────────────────────────────────────┐
│ CHANNEL ENCRYPTED │
│ │
│ - TCP/WebSocket connection established to CM │
│ - AES encryption negotiated (ChannelEncryption handshake) │
│ - CMsgClientHello sent, ClientHello expected │
│ - Ready to send Auth.* Unified Messages │
└────────────────────────┬────────────────────────────────────────┘
│
│ GetPasswordRSAPublicKey -> publickey_mod, publickey_exp, timestamp
│ Encrypt password with RSA PKCS#1 v1.5, base64-encode
↓
┌─────────────────────────────────────────────────────────────────┐
│ AWAITING AUTH SESSION RESPONSE │
│ │
│ - BeginAuthSessionViaCredentials sent: │
│ - encrypted_password, account_name, encryption_timestamp │
│ - device_details (friendly_name, platform_type, os_type) │
│ - persistence, website_id, language, qos_level │
│ - Response: client_id, request_id, interval, allowed_confirma- │
│ tions │
└────────────────────────┬────────────────────────────────────────┘
│
┌─────────────┴──────────────┐
│ │
(Guard required) (No guard / success)
│ │
↓ ↓
┌──────────────────────┐ ┌──────────────────────┐
│ AWAITING GUARD CODE │ │ POLLING SESSION │
│ │ │ │
│ - Emit steamGuard │ │ - PollAuthSessionSta-│
│ event or stdin │ │ tus sent repeat- │
│ - User supplies code │ │ edly at interval │
│ - (if provided to │ │ - Response: refresh_ │
│ startWithCreden- │ │ token empty = wait │
│ tials, code is │ │ - Response: refresh_ │
│ auto-submitted) │ │ token non-empty = │
│ │ │ success! │
└──────────┬───────────┘ └──────────┬───────────┘
│ │
│ UpdateAuthSessionWithSte- │
│ amGuardCode success │
│ │
└────────────┬──────────────┘
│
│ polling completes, refresh_token retrieved
↓
┌─────────────────────────────────────────────────────────────────┐
│ SENDING FINAL LOGON MESSAGE │
│ │
│ - Construct CMsgClientLogon: │
│ - access_token = refresh_token (JWT from polling) │
│ - account_name cleared (implicit in JWT) │
│ - password cleared │
│ - machine_id, machine_name, cell_id, client_os_type, etc. │
│ - Send to CM │
│ - Wait for CMsgClientLogonResponse │
└────────────────────────┬────────────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
(eresult != OK) (eresult = OK)
│ │
↓ ↓
┌──────────────────┐ ┌──────────────────────┐
│ LOG ON FAILED │ │ LOGGED ON │
│ │ │ │
│ - Emit error │ │ - steamID set │
│ - Disconnect │ │ - publicIP available │
│ - Retry (with │ │ - cellID saved │
│ backoff) or │ │ - Emit loggedOn │
│ abort │ │ - Start heartbeat │
│ │ │ timer │
└──────────────────┘ │ - Ready for RPC │
│ calls, friends, │
│ game launches, etc.│
└──────────┬───────────┘
│
│ (continuously)
│ CMsgClientHeartBeat
│ (at heartbeat_seconds interval)
│
┌─────────────┴──────────────┐
│ │
(keep-alive) (server boot, auth revoked, etc.)
│ │
└──────────┬─────────────────┘
│
↓
┌──────────────────────┐
│ DISCONNECTED │
│ (emit 'disconnected'│
│ or 'error') │
└──────────────────────┘
Transitions Summary
| From | To | Trigger | Message(s) |
|---|---|---|---|
| DISCONNECTED | CHANNEL ENCRYPTED | logOn() initiates connection and encryption setup | CMsgClientHello |
| CHANNEL ENCRYPTED | AWAITING AUTH SESSION RESPONSE | RSA key obtained, password encrypted | GetPasswordRSAPublicKey, BeginAuthSessionViaCredentials |
| AWAITING AUTH SESSION RESPONSE | AWAITING GUARD CODE | allowed_confirmations list returned | (implicit) |
| AWAITING GUARD CODE | POLLING SESSION | Guard code submitted via steam-session | UpdateAuthSessionWithSteamGuardCode |
| AWAITING AUTH SESSION RESPONSE | POLLING SESSION | No guard required (allowed_confirmations empty or MachineToken only) | (polling begins) |
| POLLING SESSION | SENDING FINAL LOGON MESSAGE | refresh_token populated in response | CMsgClientLogon |
| SENDING FINAL LOGON MESSAGE | LOGGED ON | CMsgClientLogonResponse eresult = OK | CMsgClientHeartBeat (periodic) |
| LOGGED ON | DISCONNECTED | Server disconnect, logOff(), connection error | CMsgClientLogOff (optional), cleanup |
Implementation Notes
Confusion Points and Distinctions
1. Access Token vs. Refresh Token
In the Auth v2 / steam-session ecosystem:
- Refresh token = long-lived JWT issued by polling
PollAuthSessionStatus. Stored and reused across sessions. - Access token = short-lived token, optionally returned by
PollAuthSessionStatus.access_token(rarely used in client logins). - In CMsgClientLogon: The field named
access_tokenactually carries the refresh token (the JWT). This naming is confusing but historical.
Code citation:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js:804: Comment clarifies:// The new way of getting web cookies is to use a refresh token to get a fresh access token, which // is what's used as the cookie. Confusingly, access_token in CMsgClientLogOn is actually a refresh token.
2. RSA PKCS#1 v1.5 vs. RSA-OAEP
The password encryption uses PKCS#1 v1.5, NOT OAEP. This is distinct from the RSA-OAEP-SHA1 used during channel encryption (which wraps the AES session key). Do not confuse the two; they are at different OSI layers.
3. Job ID Persistence
Job IDs are simple incrementing integers. They do not persist across disconnections; they are only used to correlate responses within a single connection session.
4. Machine Auth Token vs. Machine ID
- Machine ID: Binary blob sent in logon messages. Used for device identification. Can be persistent (saved to disk) or random.
- Machine Auth Token: Stored credential from a prior successful login to the same machine. Used to bypass Steam Guard on re-login from a known machine. Optional.
5. CMAuthTransport vs. Traditional Socket Messages
CMAuthTransport is a thin abstraction used only by steam-session. It wraps UM calls in ServiceMethodCallFromClientNonAuthed / ServiceMethodCallFromClient messages. Once logged on, other code paths in node-steam-user may use different message types (e.g., legacy service messages). Do not assume all RPC uses the same transport.
References
File Paths (Absolute)
- Logon orchestration:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/09-logon.js - UM transport:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/classes/CMAuthTransport.js - Message sending/receiving:
/Volumes/bandi/coding/hypelevels/node-steam-user/components/03-messages.js - Auth protobuf definitions:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_auth.steamclient.proto - Login message definitions:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_clientserver_login.proto - Base message definitions:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/steammessages_base.proto - Enumerations:
/Volumes/bandi/coding/hypelevels/Protobufs/steam/enums.proto
External Dependencies
- steam-session: Implements
LoginSession,ApiRequest,ApiResponse, password encryption, and polling loop. Not in this repo; used as npm package. - node-steam-user: Main package; exports
SteamUserclass. - SteamKit2: C# reference implementation (in this repo at
/Volumes/bandi/coding/hypelevels/SteamKit/).
Related
Document generated for reverse-engineering and implementation reference. All line numbers and paths are accurate as of the repository snapshot.