diff --git a/README.md b/README.md index a42fdf71..71c084d6 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,16 @@ Added in 3.5.0. Defaults to `true`. +### renewRefreshTokens + +If true, then `SteamUser` will attempt to renew your refresh token every time you call [`logOn()`](#logondetails) by +passing a refresh token. If renewal succeeds, the [`refreshToken`](#refreshtoken) event will be emitted, and the refresh +token you used to log on will become invalid. + +Added in 5.0.0. + +Defaults to `false`. + # Properties [^](#contents) ### steamID @@ -508,29 +518,19 @@ Changes the value of an [option](#options-). ### setOptions(options) - `options` - An object containing zero or more [options](#options-). -### setSentry(sentry) -- `sentry` - A Buffer or string containing your machine auth token - -**THIS IS DEPRECATED AND WILL BE REMOVED IN THE NEXT MAJOR RELEASE.** - -This is retained exclusively for backwards compatibility. You should use the `machineAuthToken` property in `logOn()` -instead. - ### logOn([details]) - `details` - An object containing details for this logon - `anonymous` - Pass `true` if you want to log into an anonymous account, omit or pass `false` if not - `refreshToken` - A refresh token, [see below](#using-refresh-tokens) - `accountName` - If logging into a user account, the account's name - - `password` - If logging into an account without a login key or a web logon token, the account's password + - `password` - If logging into an account without a refresh token or web logon token, the account's password - `machineAuthToken` - If logging into an account that has email Steam Guard using the account name and password, pass a valid machine auth token to avoid needing to provide an `authCode`. This is only necessary in advanced cases, as steam-user [will take care of this for you by default](#machine-auth-tokens) - - `loginKey` - If logging into an account with a login key, this is the account's login key **\[[DEPRECATED](#login-key-deprecation)\]** - `webLogonToken` - If logging into an account with a [client logon token obtained from the web](https://github.com/DoctorMcKay/node-steamcommunity/wiki/SteamCommunity#getclientlogontokencallback), this is the token - `steamID` - If logging into an account with a client logon token obtained from the web, this is your account's SteamID, as a string or a `SteamID` object - `authCode` - If you have a Steam Guard email code, you can provide it here. You might not need to, see the [`steamGuard`](#steamguard) event. (Added in 1.9.0) - `twoFactorCode` - If you have a Steam Guard mobile two-factor authentication code, you can provide it here. You might not need to, see the [`steamGuard`](#steamguard) event. (Added in 1.9.0) - - `rememberPassword` - `true` if you want to get a login key which can be used in lieu of a password for subsequent logins. `false` or omitted otherwise. - `logonID` - A 32-bit integer to identify this login. The official Steam client derives this from your machine's private IP (it's the `obfuscated_private_ip` field in `CMsgClientLogOn`). If you try to logon twice to the same account from the same public IP with the same `logonID`, the first session will be kicked with reason `SteamUser.EResult.LogonSessionReplaced`. Defaults to `0` if not specified. - As of v4.13.0, this can also be an IPv4 address as a string, in dotted-decimal notation (e.g. `"192.168.1.5"`) - `machineName` - A string containing the name of this machine that you want to report to Steam. This will be displayed on steamcommunity.com when you view your games list (when logged in). @@ -568,11 +568,9 @@ There are five ways to log onto Steam: - `accountName` - `password` - `machineAuthToken` - - `loginKey` - `webLogonToken` - `authCode` - `twoFactorCode` - - `rememberPassword` - Individually using account name and password - These properties are required: - `accountName` @@ -581,28 +579,10 @@ There are five ways to log onto Steam: - `machineAuthToken` - Specify if you are logged into an account with email Steam Guard and you have a valid machien token - `authCode` - Specify if you are using an email Steam Guard code. - `twoFactorCode` - Specify if you are using a TOTP two-factor code (required if your account has 2FA enabled). - - `rememberPassword` - Specify if you want to get a login key for subsequent logins. - `logonID` - Defaults to 0 if not specified. - `machineName` - Defaults to empty string if not specified. - `clientOS` - Defaults to an auto-detected value if not specified. - These properties must not be provided: - - `loginKey` - - `webLogonToken` - - `steamID` -- Individually using account name and login key **[(deprecated and possibly non-functional)](#login-key-deprecation)** - - These properties are required: - - `accountName` - - `loginKey` - - These properties are optional: - - `rememberPassword` - Specify if you want to get a new login key for subsequent logins. - - `logonID` - Defaults to 0 if not specified. - - `machineName` - Defaults to empty string if not specified. - - `clientOS` - Defaults to an auto-detected value if not specified. - - These properties must not be provided: - - `password` - - `machineAuthToken` - - `authCode` - - `twoFactorCode` - `webLogonToken` - `steamID` - Individually using account name and [client logon token obtained from the web](https://github.com/DoctorMcKay/node-steamcommunity/wiki/SteamCommunity#getclientlogontokencallback) (deprecated) @@ -616,8 +596,6 @@ There are five ways to log onto Steam: - `machineAuthToken` - `authCode` - `twoFactorCode` - - `loginKey` - - `rememberPassword` - `logonID` - `machineName` - `clientOS` @@ -625,9 +603,9 @@ There are five ways to log onto Steam: #### Using Refresh Tokens The Steam client uses refresh tokens when logging on. You can obtain a refresh token using the -[steam-session module](https://www.npmjs.com/package/steam-session), or you can log on to steam-user using your account -name and password as normal, and steam-user will internally fetch a refresh token if it can -(see [legacy authentication](#legacy-authentication)). +[steam-session module](https://www.npmjs.com/package/steam-session), or you can log on to steam-user using your account name and password as normal. When +logging on using your account name and password, steam-user will internally fetch a refresh token, emit the +[`refreshToken`](#refreshtoken) event, and then use that token to log on to Steam. As of 2022-09-03, refresh tokens are JWTs that are valid for ~200 days. You can keep using the same refresh token to log on until it expires. You can find out when a token expires by [decoding it](https://www.npmjs.com/search?q=jwt) and checking @@ -643,36 +621,6 @@ provide a code every time you login. By default, steam-user will automatically s [data directory](#datadirectory), but you can also manage them yourself by listening for the [`machineAuthToken`](#machineauthtoken) event and providing the token as a `machineAuthToken` property when you log on. -#### Legacy Authentication - -steam-user will use *legacy authentication* to log onto Steam in either of these cases: - -- You are using a steam-user older 4.28.0 and are not using a refresh token -- You are using steam-user 4.28.0 or later but are running a Node.js version older than 12.22.0 - -The official Steam client no longer uses legacy authentication, so the backend may drop support for it at any time. -Additionally, legacy authentication uses login keys, [which are no longer being issued](#login-key-deprecation). - -If you are using steam-user 4.28.0 or later and Node.js 12.22.0 or later, then even if you call `logOn()` with an account -name and password, steam-user will use the modern authentication system to log on. - -Legacy authentication is deprecated, and will be removed in the next major steam-user release. - -#### Login Key Deprecation - -Steam appears to no longer issue login keys. This has implications for you if you're using [legacy authentication](#legacy-authentication): - -- The [`loginKey`](#loginkey) event will no longer be emitted -- If using mobile 2FA, steam-user can no longer reconnect to Steam following a connection drop without having a valid 2FA code - -For the sake of backward compatibility, if you call `logOn()` with an account name and password and specify -`rememberPassword: true` in an environment that supports modern authentication, then the `loginKey` event will be emitted, -but instead of containing a login key, it will contain a refresh token. You may pass this refresh token to the `loginKey` -property in `logOn()` as if it were a login key, and it will be used as a refresh token. - -**This is strictly offered for backward compatibility. Both the `loginKey` event and `loginKey` property in `logOn()` -are deprecated and will be removed in the next major steam-user release.** - ### logOff() Logs you off of Steam and closes the connection. @@ -684,7 +632,7 @@ Logs you off of Steam and closes the connection. Logs you off of Steam and then immediately back on. This can only be used if one of the following criteria are met: - You're logged into an anonymous account -- You're logged into an individual account, you logged in using an account name and password, and you didn't use [legacy authentication](#legacy-authentication) +- You're logged into an individual account and you logged in using an account name and password - You're logged into an individual account and you used a `refreshToken` to log on Attempts to call this method under any other circumstance will result in an `Error` being thrown and nothing else will happen. @@ -734,7 +682,7 @@ Properties of note in the `response` object: Finishes the process of enabling TOTP two-factor authentication for your account. You can use [`steam-totp`](https://www.npmjs.com/package/steam-totp) in the future when logging on to get a code. -**If TOTP two-factor authentication is enabled, a code will be required *on every login* unless a `loginKey` is used.** +**If TOTP two-factor authentication is enabled, a code will be required *on every login* unless a refresh token is used.** ### getSteamGuardDetails(callback) - `callback` - A function to be called when the requested data is available @@ -1864,7 +1812,8 @@ data which only the developer/publisher of the game can do anything with. ## ID Events -Events marked as **ID events** are special. They all have a `SteamID` object as their first parameter. In addition to the event itself firing, a second event comprised of `eventName + "#" + steamID.getSteamID64()` is fired. +Events marked as **ID events** are special. They all have a `SteamID` object as their first parameter. In addition to the +event itself firing, a second event comprised of `eventName + "#" + steamID.getSteamID64()` is fired. For example: @@ -1937,23 +1886,28 @@ The `SteamUser` object's `steamID` property will still be defined when this is e The `eresult` value might be 0 (Invalid), which indicates that the disconnection was due to the connection being closed directly, without Steam sending a LoggedOff message. -### sentry -- `sentry` - A Buffer containing your new machine auth token - -**THIS IS DEPRECATED AND WILL BE REMOVED IN THE NEXT MAJOR RELEASE.** - -This is retained exclusively for backwards compatibility. You should use the [machineAuthToken](#machineauthtoken) event -instead. - ### machineAuthToken - `machineAuthToken` - A string containing your new machine auth token +**v4.29.0 or later is required to use this event** + Emitted when a new machine auth token is issued. This is only relevant for accounts using email Steam Guard. Even if you are using email Steam Guard, you likely don't need to worry about this event as steam-user will [automatically manage your machine auth tokens for you](#machine-auth-tokens). This may be emitted before [`loggedOn`](#loggedon) fires. +### refreshToken +- `refreshToken` - A string containing your new refresh token + +**v5.0.0 or later is required to use this event** + +Emitted when a new refresh token is issued. This will always be emitted when logging on using an account name and password, +and when logging on using an existing refresh token, this may be emitted if a new refresh token is issued because your +provided token is nearly expired (only if [`renewRefreshTokens`](#renewrefreshtokens) is set to true). + +This may be emitted before [`loggedOn`](#loggedon) fires. + ### webSession - `sessionID` - The value of the `sessionid` cookie - `cookies` - An array of cookies, as `name=value` strings @@ -1962,19 +1916,11 @@ Emitted when a steamcommunity.com web session is successfully negotiated. This will automatically be emitted on logon (**unless** you used a `webLogonToken` to log on) and in response to [`webLogOn`](#weblogon) calls. -Some libraries require you to provide your `sessionID`, others don't. If your library doesn't, you can safely ignore it. +Some libraries require you to provide your `sessionID`, others don't. If the library you're using doesn't need you to +provide a `sessionID`, then you can safely ignore it. [Read more about how cookies work and interact with other modules.](https://dev.doctormckay.com/topic/365-cookies/#user-cookieusage) -### loginKey -- `key` - Your login key - -If you enabled `rememberPassword` in [`logOn`](#logondetails), this will be emitted when Steam sends us a new login key. This key can be passed to [`logOn`](#logondetails) as `loginKey` in lieu of a password on subsequent logins. - -At this time, I'm not sure if login keys expire, so to be safe you should record this somewhere (in a database, in a file, etc) and overwrite it every time the event is emitted. - -**[THIS IS DEPRECATED AND WILL BE REMOVED IN A FUTURE RELEASE.](#login-key-deprecation)** - ### newItems - `count` - How many new items you have (can be 0) diff --git a/components/03-messages.js b/components/03-messages.js index 80022c26..54e3e70e 100644 --- a/components/03-messages.js +++ b/components/03-messages.js @@ -9,6 +9,10 @@ const Schema = require('../protobufs/generated/_load.js'); const EMsg = require('../enums/EMsg.js'); const EResult = require('../enums/EResult.js'); +// steam-session dependencies +const {LoginSession, EAuthTokenPlatformType} = require('steam-session'); +const CMAuthTransport = require('./classes/CMAuthTransport'); + const JOBID_NONE = '18446744073709551615'; const PROTO_MASK = 0x80000000; @@ -730,6 +734,16 @@ class SteamUserMessages extends SteamUserConnection { this._send(header, SteamUserMessages._encodeProto(Proto, methodData), callback); } + + _getLoginSession() { + if (!this._loginSession) { + this._loginSession = new LoginSession(EAuthTokenPlatformType.SteamClient, { + transport: new CMAuthTransport(this) + }); + } + + return this._loginSession; + } } module.exports = SteamUserMessages; diff --git a/components/07-web.js b/components/07-web.js index 19f42559..03798ab3 100644 --- a/components/07-web.js +++ b/components/07-web.js @@ -25,7 +25,7 @@ class SteamUserWeb extends SteamUserWebAPI { throw new Error('Must not be anonymous user to use webLogOn (check to see you passed in valid credentials to logOn)') } - if (!Helpers.newAuthCapable() || !this._logOnDetails.access_token) { + if (!this._logOnDetails.access_token) { // deprecated this._send(EMsg.ClientRequestWebAPIAuthenticateUserNonce, {}); return; @@ -34,11 +34,7 @@ class SteamUserWeb extends SteamUserWebAPI { // The client uses access tokens for its session cookie now. Even though we might already technically have an // access token available from your initial auth, the client always requests a new one, so let's mimic that behavior. - const {LoginSession, EAuthTokenPlatformType} = require('steam-session'); - const CMAuthTransport = require('./classes/CMAuthTransport.js'); - - let transport = new CMAuthTransport(this); - let session = new LoginSession(EAuthTokenPlatformType.SteamClient, {transport}); + let session = this._getLoginSession(); session.refreshToken = this._logOnDetails.access_token; session.getWebCookies().then((cookies) => { if (!cookies.some(c => c.startsWith('sessionid='))) { diff --git a/components/08-machineauth.js b/components/08-machineauth.js index 7ca55882..de9bd2c8 100644 --- a/components/08-machineauth.js +++ b/components/08-machineauth.js @@ -6,15 +6,6 @@ const SteamUserBase = require('./00-base.js'); const SteamUserWeb = require('./07-web.js'); class SteamUserMachineAuth extends SteamUserWeb { - /** - * @param {Buffer|string} sentry - * @deprecated Use `machineAuthToken` property in `logOn()` instead - */ - setSentry(sentry) { - this._machineAuthTokenSetByDeprecatedSetSentry = true; - this._machineAuthToken = Buffer.isBuffer(sentry) ? sentry.toString('utf8') : sentry; - } - _getMachineAuthFilename() { let accountName = (this._logOnDetails.account_name || this._logOnDetails._newAuthAccountName).toLowerCase(); return `machineAuthToken.${accountName}.txt`; @@ -24,7 +15,6 @@ class SteamUserMachineAuth extends SteamUserWeb { this._machineAuthToken = machineToken; this.emit('machineAuthToken', machineToken); - this.emit('sentry', Buffer.from(machineToken, 'utf8')); // legacy // We should always have an account name available here since this is only possible when logging on using an // account name and password. diff --git a/components/09-logon.js b/components/09-logon.js index f45f0059..f543b610 100644 --- a/components/09-logon.js +++ b/components/09-logon.js @@ -13,13 +13,34 @@ const EMachineIDType = require('../resources/EMachineIDType.js'); const EMsg = require('../enums/EMsg.js'); const EResult = require('../enums/EResult.js'); +const {EAuthSessionGuardType} = require('steam-session'); + const SteamUserBase = require('./00-base.js'); const SteamUserMachineAuth = require('./08-machineauth.js'); const PROTOCOL_VERSION = 65580; const PRIVATE_IP_OBFUSCATION_MASK = 0xbaadf00d; +/** + * @typedef LogOnDetails + * @property {boolean} [anonymous=false] + * @property {string} [refreshToken] + * @property {string} [accountName] + * @property {string} [password] + * @property {string} [machineAuthToken] + * @property {string} [webLogonToken] + * @property {string|SteamID} [steamID] + * @property {String} [authCode] + * @property {string} [twoFactorCode] + * @property {number} [logonID] + * @property {string} [machineName] + * @property {number} [clientOS] + */ + class SteamUserLogon extends SteamUserMachineAuth { + /** + * @param {LogOnDetails} details + */ logOn(details) { // Delay the actual logon by one tick, so if users call logOn from the error event they won't get a crash because // they appear to be already logged on (the steamID property is set to null only *after* the error event is emitted) @@ -53,10 +74,9 @@ class SteamUserLogon extends SteamUserMachineAuth { this._logOnDetails = { account_name: details.accountName, password: details.password, - login_key: details.loginKey, auth_code: details.authCode, two_factor_code: details.twoFactorCode, - should_remember_password: !!(details.rememberPassword || details.refreshToken), + should_remember_password: !!details.refreshToken, obfuscated_private_ip: {v4: logonId || 0}, protocol_version: PROTOCOL_VERSION, supports_rate_limit_response: !anonLogin, @@ -68,7 +88,8 @@ class SteamUserLogon extends SteamUserMachineAuth { ui_mode: undefined, chat_mode: 2, // enable new chat web_logon_nonce: details.webLogonToken && details.steamID ? details.webLogonToken : undefined, - _steamid: details.steamID + _steamid: details.steamID, + _machineAuthToken: details.machineAuthToken }; } @@ -82,7 +103,6 @@ class SteamUserLogon extends SteamUserMachineAuth { delete this._logOnDetails.ping_ms_from_cell_search; delete this._logOnDetails.machine_id; delete this._logOnDetails.password; - delete this._logOnDetails.login_key; delete this._logOnDetails.auth_code; delete this._logOnDetails.machine_name; delete this._logOnDetails.machine_name_userchosen; @@ -90,21 +110,11 @@ class SteamUserLogon extends SteamUserMachineAuth { delete this._logOnDetails.supports_rate_limit_response; } - if ((this._logOnDetails.login_key || '').split('.').length == 3) { - // deprecated: they're using a refresh token as a login key - details.refreshToken = this._logOnDetails.login_key; - this._logOnDetails._newAuthAccountName = this._logOnDetails.account_name; - delete this._logOnDetails.account_name; - delete this._logOnDetails.password; - delete this._logOnDetails.login_key; - } - if (details.refreshToken) { // If logging in with a refresh token, we need to make sure that no conflicting properties are set let disallowedProps = [ 'account_name', 'password', - 'login_key', 'auth_code', 'two_factor_code' ]; @@ -145,6 +155,11 @@ class SteamUserLogon extends SteamUserMachineAuth { this._logOnDetails.access_token = details.refreshToken; this.emit('debug', `Provided refresh token has sub ${decodedToken.sub}, aud ${(decodedToken.aud || []).join(',')}`); + + // After we log on, we should attempt to renew this refresh token if requested + if (this.options.renewRefreshTokens) { + this._shouldAttemptRefreshTokenRenewal = true; + } } let anonLogin = !this._logOnDetails.account_name && !this._logOnDetails.access_token; @@ -220,12 +235,14 @@ class SteamUserLogon extends SteamUserMachineAuth { // Machine auth token (only necessary if logging on with account name and password) if (!anonLogin && !this._machineAuthToken && this._logOnDetails.account_name) { - let tokenContent = await this._readFile(this._getMachineAuthFilename()); + let tokenContent = this._logOnDetails._machineAuthToken || this._readFile(this._getMachineAuthFilename()); if (tokenContent) { this._machineAuthToken = tokenContent.toString('utf8').trim(); } } + delete this._logOnDetails._machineAuthToken; + // Machine ID if (!anonLogin && !this._logOnDetails.machine_id) { this._logOnDetails.machine_id = this._getMachineID(machineID); @@ -244,7 +261,7 @@ class SteamUserLogon extends SteamUserMachineAuth { } if (anonLogin) { - if (this._logOnDetails.password || this._logOnDetails.login_key) { + if (this._logOnDetails.password) { this._warn('Logging into anonymous Steam account but a password was specified... did you specify your accountName improperly?'); } else if (details !== true && !explicitlyRequestedAnonLogin) { this._warn('Logging into anonymous Steam account. If you didn\'t expect this warning, make sure that you\'re properly passing your log on details to the logOn() method. To suppress this warning, pass {anonymous: true} to logOn().'); @@ -308,20 +325,15 @@ class SteamUserLogon extends SteamUserMachineAuth { * @private */ async _sendLogOn() { - // If we're logging in with account name/password and we're running node 12.22 or later, - // go ahead and get a refresh token. if (this._logOnDetails.account_name && this._logOnDetails.password) { - if (Helpers.newAuthCapable()) { - this.emit('debug', 'Node version is new enough for steam-session; performing new auth'); - let startTime = Date.now(); - let newAuthSucceeded = await this._performNewAuth(); - if (!newAuthSucceeded) { - return; - } else { - this.emit('debug', `New auth succeeded in ${Date.now() - startTime} ms`); - } + this.emit('debug', 'Logging on with account name and password; fetching a new refresh token'); + let startTime = Date.now(); + let authSuccess = await this._performPasswordAuth(); + if (!authSuccess) { + // We would have already emitted 'error' so let's just bail now + return; } else { - this._warn('Logging onto Steam using legacy authentication. steam-user may not behave as expected. To remove this warning, log on using a refresh token or upgrade Node.js to 12.22.0 or later.'); + this.emit('debug', `Password auth succeeded in ${Date.now() - startTime} ms`); } } @@ -336,45 +348,43 @@ class SteamUserLogon extends SteamUserMachineAuth { this._send(this._logOnDetails.game_server_token ? EMsg.ClientLogonGameServer : EMsg.ClientLogon, this._logOnDetails); } - _performNewAuth() { + _performPasswordAuth() { return new Promise(async (resolve) => { this._send(EMsg.ClientHello, {protocol_version: PROTOCOL_VERSION}); - // import this here to prevent issues on older versions of node - const {LoginSession, EAuthTokenPlatformType, EAuthSessionGuardType} = require('steam-session'); - const CMAuthTransport = require('./classes/CMAuthTransport.js'); - - let transport = new CMAuthTransport(this); - let session = new LoginSession(EAuthTokenPlatformType.SteamClient, {transport}); + let session = this._getLoginSession(); - session.on('debug', (...args) => this.emit('debug', '[steam-session]', ...args)); + session.on('debug', (...args) => { + this.emit('debug', '[steam-session] ' + args.map(arg => typeof arg == 'object' ? JSON.stringify(arg) : arg).join(' ')); + }); session.on('authenticated', () => { + this.emit('refreshToken', session.refreshToken); + this._logOnDetails.access_token = session.refreshToken; this._logOnDetails._newAuthAccountName = this._logOnDetails.account_name; this._logOnDetails._steamid = session.steamID; delete this._logOnDetails.account_name; delete this._logOnDetails.password; - delete this._logOnDetails.login_key; delete this._logOnDetails.auth_code; delete this._logOnDetails.two_factor_code; this._tempSteamID = session.steamID; resolve(true); }); - session.on('error', (err) => { + session.on('error', async (err) => { // LoginSession only emits an `error` event if there's some problem with the actual interface used to // communicate with Steam. Errors for invalid credentials are handled elsewhere, so we only need to // emit ServiceUnavailable here since this should be a transient error. this.emit('debug', `steam-session error: ${err.message}`); - this._handleLogOnResponse({eresult: EResult.ServiceUnavailable}); + await this._handleLogOnResponse({eresult: EResult.ServiceUnavailable}); resolve(false); }); - session.on('timeout', () => { + session.on('timeout', async () => { this.emit('debug', 'steam-session timeout'); - this._handleLogOnResponse({eresult: EResult.ServiceUnavailable}); + await this._handleLogOnResponse({eresult: EResult.ServiceUnavailable}); resolve(false); }); @@ -397,7 +407,7 @@ class SteamUserLogon extends SteamUserMachineAuth { this.emit('debug', 'steam-session startWithCredentials exception', ex); - this._handleLogOnResponse({eresult: ex.eresult || EResult.ServiceUnavailable}); + await this._handleLogOnResponse({eresult: ex.eresult || EResult.ServiceUnavailable}); return resolve(false); } @@ -417,7 +427,7 @@ class SteamUserLogon extends SteamUserMachineAuth { logOnResponse.eresult = this._logOnDetails.two_factor_code ? EResult.TwoFactorCodeMismatch : EResult.AccountLoginDeniedNeedTwoFactor; } - this._handleLogOnResponse(logOnResponse); + await this._handleLogOnResponse(logOnResponse); } }); } @@ -555,7 +565,7 @@ class SteamUserLogon extends SteamUserMachineAuth { ); if (!relogAvailable) { - throw new Error("To use relog(), you must specify rememberPassword=true when logging on and wait for loginKey to be emitted, or log on using a refresh token"); + throw new Error('To use relog(), you must log on using a refresh token or using your account name and password'); } this._relogging = true; @@ -647,7 +657,7 @@ class SteamUserLogon extends SteamUserMachineAuth { } } - _handleLogOnResponse(body) { + async _handleLogOnResponse(body) { this.emit('debug', `Handle logon response (${EResult[body.eresult]})`); clearTimeout(this._logonMsgTimeout); @@ -675,22 +685,6 @@ class SteamUserLogon extends SteamUserMachineAuth { this._connectTime = Date.now(); this._connectTimeout = 1000; // reset exponential connect backoff - // deprecated - if (this._logOnDetails.login_key) { - // Steam doesn't send a new loginkey all the time if you're using a persistent one (remember password). Let's manually emit it on a timer to handle any edge cases. - this._loginKeyTimer = setTimeout(() => { - this.emit('loginKey', this._logOnDetails.login_key); - }, 5000); - } else if ( - (this._logOnDetails._newAuthAccountName && this._logOnDetails.should_remember_password) || - this._logOnDetails._newAuthUsedTokenAsLoginKey - ) { - // deprecated: emit the refresh token as a loginKey to support code that depends on login keys - this._loginKeyTimer = setTimeout(() => { - this.emit('loginKey', this._logOnDetails.access_token); - }, 5000); - } - this._saveFile('cellid-' + Helpers.getInternalMachineID() + '.txt', body.cell_id); let parental = body.parental_settings ? SteamUserLogon._decodeProto(Schema.ParentalSettings, body.parental_settings) : null; @@ -727,7 +721,30 @@ class SteamUserLogon extends SteamUserMachineAuth { if (this.steamID.type == SteamID.Type.INDIVIDUAL) { this._requestNotifications(); - if (Helpers.newAuthCapable() && this._logOnDetails.access_token) { + if (this._logOnDetails.access_token) { + // Even though we might have an access token available from password auth, the official client + // doesn't actually use this access token and rather immediately refreshes it. So let's delete + // our access token to match this behavior. + this._getLoginSession().accessToken = null; + + if (this._shouldAttemptRefreshTokenRenewal) { + delete this._shouldAttemptRefreshTokenRenewal; + + // Try to renew our refresh token. This will also handle the actual network request that + // fetches our web session cookie, and our subsequent call to webLogOn() will then return + // that cookie without making another request. + let session = this._getLoginSession(); + session.refreshToken = this._logOnDetails.access_token; + let renewed = await session.renewRefreshToken(); + this.emit('debug', `Attempted to renew refresh token, success = ${renewed}`); + if (renewed) { + this._logOnDetails.access_token = session.refreshToken; + this.emit('refreshToken', session.refreshToken); + } + } + + // 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. this.webLogOn(); } else if (body.webapi_authenticate_user_nonce) { this._webAuthenticate(body.webapi_authenticate_user_nonce); @@ -799,25 +816,6 @@ SteamUserBase.prototype._handlerManager.add(EMsg.ClientLoggedOff, function(body) this._handleLogOff(body.eresult, msg); }); -// deprecated: appears no longer functional -SteamUserBase.prototype._handlerManager.add(EMsg.ClientNewLoginKey, function(body) { - if (this.steamID.type == SteamID.Type.INDIVIDUAL) { - delete this._logOnDetails.password; - this._logOnDetails.login_key = body.login_key; - - if (this._loginKeyTimer) { - clearTimeout(this._loginKeyTimer); - } - - if (this._logOnDetails.should_remember_password) { - this.emit('loginKey', body.login_key); - } - - // Accept the key - this._send(EMsg.ClientNewLoginKeyAccepted, {"unique_id": body.unique_id}); - } -}); - SteamUserBase.prototype._handlerManager.add(EMsg.ClientCMList, function(body) { this.emit('debug', `Got list of ${(body.cm_websocket_addresses || []).length} WebSocket CMs, with percentage to use at ${body.percent_default_to_websocket || 0}%`); diff --git a/components/classes/CMAuthTransport.js b/components/classes/CMAuthTransport.js index a61f0a2b..daaa8227 100644 --- a/components/classes/CMAuthTransport.js +++ b/components/classes/CMAuthTransport.js @@ -6,7 +6,7 @@ class CMAuthTransport { _user; /** - * @param {SteamUserLogon} steamUser + * @param {SteamUserMessages} steamUser */ constructor(steamUser) { this._user = steamUser; diff --git a/components/helpers.js b/components/helpers.js index 02e05959..3300da5c 100644 --- a/components/helpers.js +++ b/components/helpers.js @@ -254,8 +254,3 @@ exports.decodeJwt = function(jwt) { return JSON.parse(Buffer.from(standardBase64, 'base64').toString('utf8')); } - -exports.newAuthCapable = function() { - let nodeVersion = process.versions.node.split('.'); - return nodeVersion[0] > 12 || (nodeVersion[0] == 12 && nodeVersion[1] >= 22); -}; diff --git a/index.js b/index.js index 12338135..f21ae8bf 100644 --- a/index.js +++ b/index.js @@ -132,9 +132,9 @@ class SteamUser extends SteamUserTwoFactor { this._incomingMessageQueue = []; this._useMessageQueue = false; // we only use the message queue while we're processing a multi message - if (!this._machineAuthTokenSetByDeprecatedSetSentry) { - delete this._machineAuthToken; - } + delete this._machineAuthToken; + delete this._shouldAttemptRefreshTokenRenewal; + delete this._loginSession; } get packageName() { diff --git a/package.json b/package.json index f7822ee7..8c4228f7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@bbob/parser": "^2.2.0", - "@doctormckay/stdlib": "^1.16.0", + "@doctormckay/stdlib": "^2.7.1", "@doctormckay/steam-crypto": "^1.2.0", "adm-zip": "^0.5.10", "binarykvparser": "^2.2.0", @@ -31,13 +31,13 @@ "file-manager": "^2.0.0", "kvparser": "^1.0.1", "lzma": "^2.3.2", - "protobufjs": "^6.11.3", + "protobufjs": "^7.2.4", "socks-proxy-agent": "^7.0.0", "steam-appticket": "^1.0.1", - "steam-session": "^1.3.3", + "steam-session": "^1.3.4", "steam-totp": "^2.0.1", - "steamid": "^1.1.0", - "websocket13": "^3.0.1" + "steamid": "^2.0.0", + "websocket13": "^4.0.0" }, "devDependencies": { "steamcommunity": "^3.39.0", @@ -49,6 +49,6 @@ "generate-protos": "node scripts/generate-protos.js" }, "engines": { - "node": ">=8.0.0" + "node": ">=14.0.0" } } diff --git a/resources/default_options.js b/resources/default_options.js index ce7ba077..bdd51483 100644 --- a/resources/default_options.js +++ b/resources/default_options.js @@ -11,7 +11,7 @@ const EMachineIDType = require('./EMachineIDType.js'); * @property {boolean} [picsCacheAll=false] * @property {number} [changelistUpdateInterval=60000] * @property {PackageFilter|PackageFilterFunction|null} [ownershipFilter=null] - * @property {object} [additionalHeaders={}} + * @property {object} [additionalHeaders={}] * @property {string|null} [localAddress=null] * @property {number|null} [localPort=null] * @property {string|null} [httpProxy=null] @@ -20,6 +20,7 @@ const EMachineIDType = require('./EMachineIDType.js'); * @property {string} [language='english'] * @property {boolean} [webCompatibilityMode=false] * @property {boolean} [saveAppTickets=true] + * @property {boolean} [renewRefreshTokens=false] */ module.exports = { @@ -37,5 +38,6 @@ module.exports = { protocol: EConnectionProtocol.Auto, language: 'english', webCompatibilityMode: false, - saveAppTickets: true + saveAppTickets: true, + renewRefreshTokens: false };