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: The sendRequest method chooses the EMsg based on steamID:
    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_name field on CMsgProtoBufHeader

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 on jobid_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:

  1. Construct a JSON structure: {"password": "<plaintext>", "timestamp": <uint64_from_response>}
  2. Serialize it to UTF-8
  3. Encrypt the JSON with RSA public key using PKCS#1 v1.5
  4. 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 _performPasswordAuth method invokes steam-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 a LoginSession with a CMAuthTransport and 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, including machine_name, client_language, client_os_type, and chat_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: After BeginAuthSessionViaCredentials, if actionRequired is true in the response, the code checks validActions:
    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):

  1. Client calls startWithCredentials() with optional steamGuardCode parameter
  2. If the code is provided and valid, steam-session internally calls UpdateAuthSessionWithSteamGuardCode
  3. If the code is incorrect, the response carries an error; steam-session emits an error or retries
  4. If the code is not provided but required, startWithCredentials() resolves with actionRequired: 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 to startWithCredentials():
    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

FieldValueRationale
protocol_version65580Current 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_mode2Enable new chat system
supports_rate_limit_responsetrue (for non-anon)Opt-in to rate limiting
should_remember_passwordtrueInstruct 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_token is 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 into access_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 from this.steamID or this._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 succeeded
  • heartbeat_seconds – Interval (in seconds) at which to send CMsgClientHeartBeat
  • cell_id – Updated cell ID; should be saved for next logon
  • client_supplied_steamid – The account’s real SteamID (set into this.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

FromToTriggerMessage(s)
DISCONNECTEDCHANNEL ENCRYPTEDlogOn() initiates connection and encryption setupCMsgClientHello
CHANNEL ENCRYPTEDAWAITING AUTH SESSION RESPONSERSA key obtained, password encryptedGetPasswordRSAPublicKey, BeginAuthSessionViaCredentials
AWAITING AUTH SESSION RESPONSEAWAITING GUARD CODEallowed_confirmations list returned(implicit)
AWAITING GUARD CODEPOLLING SESSIONGuard code submitted via steam-sessionUpdateAuthSessionWithSteamGuardCode
AWAITING AUTH SESSION RESPONSEPOLLING SESSIONNo guard required (allowed_confirmations empty or MachineToken only)(polling begins)
POLLING SESSIONSENDING FINAL LOGON MESSAGErefresh_token populated in responseCMsgClientLogon
SENDING FINAL LOGON MESSAGELOGGED ONCMsgClientLogonResponse eresult = OKCMsgClientHeartBeat (periodic)
LOGGED ONDISCONNECTEDServer disconnect, logOff(), connection errorCMsgClientLogOff (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_token actually 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 SteamUser class.
  • SteamKit2: C# reference implementation (in this repo at /Volumes/bandi/coding/hypelevels/SteamKit/).


Document generated for reverse-engineering and implementation reference. All line numbers and paths are accurate as of the repository snapshot.