From d571cd9580f6ac1f2cdb43b96af1e33430801bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Thu, 19 Oct 2023 13:38:39 +0200 Subject: [PATCH] Closes #3043 (#3044) * Implement support for access tokens A bit more work and testing is needed * Make ValidUntil computed, fix netf, among others * netf fixes as always * Allow AWH to forcefully refresh session * Unify access token lifetime --- ArchiSteamFarm/ArchiSteamFarm.csproj | 1 + ArchiSteamFarm/Core/Utilities.cs | 18 ++ ArchiSteamFarm/Steam/Bot.cs | 201 +++++++++++++++--- .../Steam/Integration/ArchiHandler.cs | 33 +++ .../Steam/Integration/ArchiWebHandler.cs | 89 +------- ArchiSteamFarm/Steam/Storage/BotDatabase.cs | 19 ++ Directory.Packages.props | 1 + 7 files changed, 244 insertions(+), 118 deletions(-) diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj index e2717031a..18f51791e 100644 --- a/ArchiSteamFarm/ArchiSteamFarm.csproj +++ b/ArchiSteamFarm/ArchiSteamFarm.csproj @@ -21,6 +21,7 @@ + diff --git a/ArchiSteamFarm/Core/Utilities.cs b/ArchiSteamFarm/Core/Utilities.cs index b0078ff94..8f8725112 100644 --- a/ArchiSteamFarm/Core/Utilities.cs +++ b/ArchiSteamFarm/Core/Utilities.cs @@ -24,6 +24,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Net; @@ -48,6 +49,8 @@ public static class Utilities { // normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm" private static readonly ImmutableHashSet ForbiddenPasswordPhrases = ImmutableHashSet.Create(StringComparer.InvariantCultureIgnoreCase, "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password"); + private static readonly JwtSecurityTokenHandler JwtSecurityTokenHandler = new(); + [PublicAPI] public static string GetArgsAsText(string[] args, byte argsToSkip, string delimiter) { ArgumentNullException.ThrowIfNull(args); @@ -187,6 +190,21 @@ public static class Utilities { return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit); } + [PublicAPI] + public static JwtSecurityToken? ReadJwtToken(string token) { + if (string.IsNullOrEmpty(token)) { + throw new ArgumentNullException(nameof(token)); + } + + try { + return JwtSecurityTokenHandler.ReadJwtToken(token); + } catch (Exception e) { + ASF.ArchiLogger.LogGenericException(e); + + return null; + } + } + [PublicAPI] public static IList SelectNodes(this IDocument document, string xpath) { ArgumentNullException.ThrowIfNull(document); diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 5bb6adedb..725238e00 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -27,6 +27,7 @@ using System.Collections.Immutable; using System.Collections.Specialized; using System.ComponentModel; using System.Globalization; +using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Net.Http; @@ -67,6 +68,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private const byte LoginCooldownInMinutes = 25; // Captcha disappears after around 20 minutes, so we make it 25 private const uint LoginID = 1242; // This must be the same for all ASF bots and all ASF processes private const byte MaxLoginFailures = WebBrowser.MaxTries; // Max login failures in a row before we determine that our credentials are invalid (because Steam wrongly returns those, of course)course) + private const byte MinimumAccessTokenValidityMinutes = 10; private const byte RedeemCooldownInHours = 1; // 1 hour since first redeem attempt, this is a limitation enforced by Steam [PublicAPI] @@ -229,11 +231,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable { internal bool PlayingBlocked { get; private set; } internal bool PlayingWasBlocked { get; private set; } + private DateTime? AccessTokenValidUntil; private string? AuthCode; [JsonProperty] private string? AvatarHash; + private string? BackingAccessToken; private Timer? ConnectionFailureTimer; private bool FirstTradeSent; private Timer? GamesRedeemerInBackgroundTimer; @@ -244,6 +248,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private ulong MasterChatGroupID; private Timer? PlayingWasBlockedTimer; private bool ReconnectOnUserInitiated; + private string? RefreshToken; + private Timer? RefreshTokensTimer; private bool SendCompleteTypesScheduled; private Timer? SendItemsTimer; private bool SteamParentalActive; @@ -251,6 +257,30 @@ public sealed class Bot : IAsyncDisposable, IDisposable { private Timer? TradeCheckTimer; private string? TwoFactorCode; + private string? AccessToken { + get => BackingAccessToken; + + set { + AccessTokenValidUntil = null; + BackingAccessToken = value; + + if (string.IsNullOrEmpty(value)) { + return; + } + + // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework + JwtSecurityToken? jwtToken = Utilities.ReadJwtToken(value!); + + if (jwtToken == null) { + return; + } + + if (jwtToken.ValidTo > DateTime.MinValue) { + AccessTokenValidUntil = jwtToken.ValidTo; + } + } + } + private Bot(string botName, BotConfig botConfig, BotDatabase botDatabase) { BotName = !string.IsNullOrEmpty(botName) ? botName : throw new ArgumentNullException(nameof(botName)); BotConfig = botConfig ?? throw new ArgumentNullException(nameof(botConfig)); @@ -357,6 +387,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { ConnectionFailureTimer?.Dispose(); GamesRedeemerInBackgroundTimer?.Dispose(); PlayingWasBlockedTimer?.Dispose(); + RefreshTokensTimer?.Dispose(); SendItemsTimer?.Dispose(); SteamSaleEvent?.Dispose(); TradeCheckTimer?.Dispose(); @@ -390,6 +421,10 @@ public sealed class Bot : IAsyncDisposable, IDisposable { await PlayingWasBlockedTimer.DisposeAsync().ConfigureAwait(false); } + if (RefreshTokensTimer != null) { + await RefreshTokensTimer.DisposeAsync().ConfigureAwait(false); + } + if (SendItemsTimer != null) { await SendItemsTimer.DisposeAsync().ConfigureAwait(false); } @@ -1492,32 +1527,55 @@ public sealed class Bot : IAsyncDisposable, IDisposable { await PluginsCore.OnBotFarmingStopped(this).ConfigureAwait(false); } - internal async Task RefreshSession() { + internal async Task RefreshWebSession(bool force = false) { if (!IsConnectedAndLoggedOn) { return false; } - SteamUser.WebAPIUserNonceCallback callback; + DateTime now = DateTime.UtcNow; - try { - callback = await SteamUser.RequestWebAPIUserNonce().ToLongRunningTask().ConfigureAwait(false); - } catch (Exception e) { - ArchiLogger.LogGenericWarningException(e); + if (!force && !string.IsNullOrEmpty(AccessToken) && AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > now.AddMinutes(MinimumAccessTokenValidityMinutes))) { + // We can use the tokens we already have + if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, AccessToken!, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { + InitRefreshTokensTimer(AccessTokenValidUntil.Value); + + return true; + } + } + + // We need to refresh our session, access token is no longer valid + BotDatabase.AccessToken = AccessToken = null; + + if (string.IsNullOrEmpty(RefreshToken)) { + // Without refresh token we can't get fresh access tokens, relog needed await Connect(true).ConfigureAwait(false); return false; } - if (string.IsNullOrEmpty(callback.Nonce)) { + CAuthentication_AccessToken_GenerateForApp_Response? response = await ArchiHandler.GenerateAccessTokens(RefreshToken!).ConfigureAwait(false); + + if (response == null) { + // The request has failed, in almost all cases this means our refresh token is no longer valid, relog needed + BotDatabase.RefreshToken = RefreshToken = null; + await Connect(true).ConfigureAwait(false); return false; } - if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.Nonce, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { + // TODO: Handle update of refresh token with next SK2 release + UpdateTokens(response.access_token, RefreshToken!); + + if (await ArchiWebHandler.Init(SteamID, SteamClient.Universe, response.access_token, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { + InitRefreshTokensTimer(AccessTokenValidUntil ?? now.AddDays(1)); + return true; } + // We got the tokens, but failed to authorize? Purge them just to be sure and reconnect + BotDatabase.AccessToken = AccessToken = null; + await Connect(true).ConfigureAwait(false); return false; @@ -2274,6 +2332,19 @@ public sealed class Bot : IAsyncDisposable, IDisposable { WalletBalance = 0; WalletCurrency = ECurrencyCode.Invalid; + AccessToken = BotDatabase.AccessToken; + RefreshToken = BotDatabase.RefreshToken; + + if (BotConfig.PasswordFormat.HasTransformation()) { + if (!string.IsNullOrEmpty(AccessToken)) { + AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, AccessToken!).ConfigureAwait(false); + } + + if (!string.IsNullOrEmpty(RefreshToken)) { + AccessToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, RefreshToken!).ConfigureAwait(false); + } + } + CardsFarmer.SetInitialState(BotConfig.Paused); if (SendItemsTimer != null) { @@ -2344,6 +2415,42 @@ public sealed class Bot : IAsyncDisposable, IDisposable { ); } + private void InitRefreshTokensTimer(DateTime validUntil) { + if (validUntil == DateTime.MinValue) { + throw new ArgumentOutOfRangeException(nameof(validUntil)); + } + + if (validUntil == DateTime.MaxValue) { + // OK, tokens do not require refreshing + StopRefreshTokensTimer(); + + return; + } + + TimeSpan delay = validUntil - DateTime.UtcNow; + + // Start refreshing token before it's invalid + if (delay.TotalMinutes > MinimumAccessTokenValidityMinutes) { + delay -= TimeSpan.FromMinutes(MinimumAccessTokenValidityMinutes); + } else { + delay = TimeSpan.Zero; + } + + // Timer can accept only dueTimes up to 2^32 - 2 + uint dueTime = (uint) Math.Min(uint.MaxValue - 1, (ulong) delay.TotalMilliseconds); + + if (RefreshTokensTimer == null) { + RefreshTokensTimer = new Timer( + OnRefreshTokensTimer, + null, + TimeSpan.FromMilliseconds(dueTime), // Delay + TimeSpan.FromMinutes(1) // Period + ); + } else { + RefreshTokensTimer.Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMinutes(1)); + } + } + private void InitStart() { if (!BotConfig.Enabled) { ArchiLogger.LogGenericWarning(Strings.BotInstanceNotStartingBecauseDisabled); @@ -2482,17 +2589,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } } - string? refreshToken = BotDatabase.RefreshToken; - - if (!string.IsNullOrEmpty(refreshToken)) { - // Decrypt refreshToken if needed - if (BotConfig.PasswordFormat.HasTransformation()) { - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - refreshToken = await ArchiCryptoHelper.Decrypt(BotConfig.PasswordFormat, refreshToken!).ConfigureAwait(false); - } - } - - if (!await InitLoginAndPassword(string.IsNullOrEmpty(refreshToken)).ConfigureAwait(false)) { + if (!await InitLoginAndPassword(string.IsNullOrEmpty(RefreshToken)).ConfigureAwait(false)) { Stop(); return; @@ -2537,7 +2634,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { InitConnectionFailureTimer(); - if (string.IsNullOrEmpty(refreshToken)) { + if (string.IsNullOrEmpty(RefreshToken)) { AuthPollResult pollResult; try { @@ -2569,19 +2666,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable { return; } - refreshToken = pollResult.RefreshToken; - - if (BotConfig.UseLoginKeys) { - BotDatabase.RefreshToken = BotConfig.PasswordFormat.HasTransformation() ? ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken) : refreshToken; - - if (!string.IsNullOrEmpty(pollResult.NewGuardData)) { - BotDatabase.SteamGuardData = pollResult.NewGuardData; - } + if (!string.IsNullOrEmpty(pollResult.NewGuardData) && BotConfig.UseLoginKeys) { + BotDatabase.SteamGuardData = pollResult.NewGuardData; } + + UpdateTokens(pollResult.AccessToken, pollResult.RefreshToken); } SteamUser.LogOnDetails logOnDetails = new() { - AccessToken = refreshToken, + AccessToken = RefreshToken, CellID = ASF.GlobalDatabase?.CellID, LoginID = LoginID, SentryFileHash = sentryFileHash, @@ -2606,6 +2699,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable { HeartBeatFailures = 0; StopConnectionFailureTimer(); StopPlayingWasBlockedTimer(); + StopRefreshTokensTimer(); ArchiLogger.LogGenericInfo(Strings.BotDisconnected); @@ -3087,11 +3181,9 @@ public sealed class Bot : IAsyncDisposable, IDisposable { ArchiWebHandler.OnVanityURLChanged(callback.VanityURL); - // ReSharper disable once RedundantSuppressNullableWarningExpression - required for .NET Framework - if (string.IsNullOrEmpty(callback.WebAPIUserNonce) || !await ArchiWebHandler.Init(SteamID, SteamClient.Universe, callback.WebAPIUserNonce!, SteamParentalActive ? BotConfig.SteamParentalCode : null).ConfigureAwait(false)) { - if (!await RefreshSession().ConfigureAwait(false)) { - return; - } + // Establish web session + if (!await RefreshWebSession().ConfigureAwait(false)) { + return; } // Pre-fetch API key for future usage if possible @@ -3236,6 +3328,15 @@ public sealed class Bot : IAsyncDisposable, IDisposable { await CheckOccupationStatus().ConfigureAwait(false); } + private async void OnRefreshTokensTimer(object? state = null) { + if (AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > DateTime.UtcNow.AddMinutes(MinimumAccessTokenValidityMinutes))) { + // We don't need to refresh just yet + InitRefreshTokensTimer(AccessTokenValidUntil.Value); + } + + await RefreshWebSession().ConfigureAwait(false); + } + private async void OnSendItemsTimer(object? state = null) => await Actions.SendInventory(filterFunction: item => BotConfig.LootableTypes.Contains(item.Type)).ConfigureAwait(false); private async void OnServiceMethod(SteamUnifiedMessages.ServiceMethodNotification notification) { @@ -3708,6 +3809,38 @@ public sealed class Bot : IAsyncDisposable, IDisposable { PlayingWasBlockedTimer = null; } + private void StopRefreshTokensTimer() { + if (RefreshTokensTimer == null) { + return; + } + + RefreshTokensTimer.Dispose(); + RefreshTokensTimer = null; + } + + private void UpdateTokens(string accessToken, string refreshToken) { + if (string.IsNullOrEmpty(accessToken)) { + throw new ArgumentNullException(nameof(accessToken)); + } + + if (string.IsNullOrEmpty(refreshToken)) { + throw new ArgumentNullException(nameof(refreshToken)); + } + + AccessToken = accessToken; + RefreshToken = refreshToken; + + if (BotConfig.UseLoginKeys) { + if (BotConfig.PasswordFormat.HasTransformation()) { + BotDatabase.AccessToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, accessToken); + BotDatabase.RefreshToken = ArchiCryptoHelper.Encrypt(BotConfig.PasswordFormat, refreshToken); + } else { + BotDatabase.AccessToken = accessToken; + BotDatabase.RefreshToken = refreshToken; + } + } + } + private (bool IsSteamParentalEnabled, string? SteamParentalCode) ValidateSteamParental(ParentalSettings settings, string? steamParentalCode = null, bool allowGeneration = true) { ArgumentNullException.ThrowIfNull(settings); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs index 201a7d4de..d2d4f6601 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiHandler.cs @@ -39,6 +39,7 @@ public sealed class ArchiHandler : ClientMsgHandler { internal const byte MaxGamesPlayedConcurrently = 32; // This is limit introduced by Steam Network private readonly ArchiLogger ArchiLogger; + private readonly SteamUnifiedMessages.UnifiedService UnifiedAuthenticationService; private readonly SteamUnifiedMessages.UnifiedService UnifiedChatRoomService; private readonly SteamUnifiedMessages.UnifiedService UnifiedClanChatRoomsService; private readonly SteamUnifiedMessages.UnifiedService UnifiedCredentialsService; @@ -53,6 +54,7 @@ public sealed class ArchiHandler : ClientMsgHandler { ArgumentNullException.ThrowIfNull(steamUnifiedMessages); ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger)); + UnifiedAuthenticationService = steamUnifiedMessages.CreateService(); UnifiedChatRoomService = steamUnifiedMessages.CreateService(); UnifiedClanChatRoomsService = steamUnifiedMessages.CreateService(); UnifiedCredentialsService = steamUnifiedMessages.CreateService(); @@ -358,6 +360,37 @@ public sealed class ArchiHandler : ClientMsgHandler { Client.Send(request); } + internal async Task GenerateAccessTokens(string refreshToken) { + if (string.IsNullOrEmpty(refreshToken)) { + throw new ArgumentNullException(nameof(refreshToken)); + } + + if (Client == null) { + throw new InvalidOperationException(nameof(Client)); + } + + if (!Client.IsConnected || (Client.SteamID == null)) { + return null; + } + + CAuthentication_AccessToken_GenerateForApp_Request request = new() { + refresh_token = refreshToken, + steamid = Client.SteamID + }; + + SteamUnifiedMessages.ServiceMethodResponse response; + + try { + response = await UnifiedAuthenticationService.SendMessage(x => x.GenerateAccessTokenForApp(request)).ToLongRunningTask().ConfigureAwait(false); + } catch (Exception e) { + ArchiLogger.LogGenericWarningException(e); + + return null; + } + + return response.Result == EResult.OK ? response.GetDeserializedResponse() : null; + } + internal async Task GetClanChatGroupID(ulong steamID) { if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) { throw new ArgumentOutOfRangeException(nameof(steamID)); diff --git a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs index 2d50ecb71..95ed33f6e 100644 --- a/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs +++ b/ArchiSteamFarm/Steam/Integration/ArchiWebHandler.cs @@ -52,7 +52,6 @@ public sealed class ArchiWebHandler : IDisposable { private const ushort MaxItemsInSingleInventoryRequest = 5000; private const byte MinimumSessionValidityInSeconds = 10; private const string SteamAppsService = "ISteamApps"; - private const string SteamUserAuthService = "ISteamUserAuth"; private const string SteamUserService = "ISteamUser"; private const string TwoFactorService = "ITwoFactorService"; @@ -2290,7 +2289,7 @@ public sealed class ArchiWebHandler : IDisposable { return response?.Content?.Success; } - internal async Task Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string? parentalCode = null) { + internal async Task Init(ulong steamID, EUniverse universe, string accessToken, string? parentalCode = null) { if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) { throw new ArgumentOutOfRangeException(nameof(steamID)); } @@ -2299,83 +2298,8 @@ public sealed class ArchiWebHandler : IDisposable { throw new InvalidEnumArgumentException(nameof(universe), (int) universe, typeof(EUniverse)); } - if (string.IsNullOrEmpty(webAPIUserNonce)) { - throw new ArgumentNullException(nameof(webAPIUserNonce)); - } - - byte[]? publicKey = KeyDictionary.GetPublicKey(universe); - - if ((publicKey == null) || (publicKey.Length == 0)) { - Bot.ArchiLogger.LogNullError(publicKey); - - return false; - } - - // Generate a random 32-byte session key - byte[] sessionKey = CryptoHelper.GenerateRandomBlock(32); - - // RSA encrypt our session key with the public key for the universe we're on - byte[] encryptedSessionKey; - - using (RSACrypto rsa = new(publicKey)) { - encryptedSessionKey = rsa.Encrypt(sessionKey); - } - - // Generate login key from the user nonce that we've received from Steam network - byte[] loginKey = Encoding.UTF8.GetBytes(webAPIUserNonce); - - // AES encrypt our login key with our session key - byte[] encryptedLoginKey = CryptoHelper.SymmetricEncrypt(loginKey, sessionKey); - - Dictionary arguments = new(3, StringComparer.Ordinal) { - { "encrypted_loginkey", encryptedLoginKey }, - { "sessionkey", encryptedSessionKey }, - { "steamid", steamID } - }; - - // We're now ready to send the data to Steam API - Bot.ArchiLogger.LogGenericInfo(string.Format(CultureInfo.CurrentCulture, Strings.LoggingIn, SteamUserAuthService)); - - KeyValue? response; - - // We do not use usual retry pattern here as webAPIUserNonce is valid only for a single request - // Even during timeout, webAPIUserNonce is most likely already invalid - // Instead, the caller is supposed to ask for new webAPIUserNonce and call Init() again on failure - using (WebAPI.AsyncInterface steamUserAuthService = Bot.SteamConfiguration.GetAsyncWebAPIInterface(SteamUserAuthService)) { - steamUserAuthService.Timeout = WebBrowser.Timeout; - - try { - response = await WebLimitRequest( - WebAPI.DefaultBaseAddress, - - // ReSharper disable once AccessToDisposedClosure - async () => await steamUserAuthService.CallAsync(HttpMethod.Post, "AuthenticateUser", args: arguments).ConfigureAwait(false) - ).ConfigureAwait(false); - } catch (TaskCanceledException e) { - Bot.ArchiLogger.LogGenericDebuggingException(e); - - return false; - } catch (Exception e) { - Bot.ArchiLogger.LogGenericWarningException(e); - - return false; - } - } - - string? steamLogin = response["token"].AsString(); - - if (string.IsNullOrEmpty(steamLogin)) { - Bot.ArchiLogger.LogNullError(steamLogin); - - return false; - } - - string? steamLoginSecure = response["tokensecure"].AsString(); - - if (string.IsNullOrEmpty(steamLoginSecure)) { - Bot.ArchiLogger.LogNullError(steamLoginSecure); - - return false; + if (string.IsNullOrEmpty(accessToken)) { + throw new ArgumentNullException(nameof(accessToken)); } string sessionID = Convert.ToBase64String(Encoding.UTF8.GetBytes(steamID.ToString(CultureInfo.InvariantCulture))); @@ -2385,10 +2309,7 @@ public sealed class ArchiWebHandler : IDisposable { WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamHelpURL.Host}")); WebBrowser.CookieContainer.Add(new Cookie("sessionid", sessionID, "/", $".{SteamStoreURL.Host}")); - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCheckoutURL.Host}")); - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamCommunityURL.Host}")); - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamHelpURL.Host}")); - WebBrowser.CookieContainer.Add(new Cookie("steamLogin", steamLogin, "/", $".{SteamStoreURL.Host}")); + string steamLoginSecure = $"{steamID}||{accessToken}"; WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCheckoutURL.Host}")); WebBrowser.CookieContainer.Add(new Cookie("steamLoginSecure", steamLoginSecure, "/", $".{SteamCommunityURL.Host}")); @@ -2782,7 +2703,7 @@ public sealed class ArchiWebHandler : IDisposable { } Bot.ArchiLogger.LogGenericInfo(Strings.RefreshingOurSession); - bool result = await Bot.RefreshSession().ConfigureAwait(false); + bool result = await Bot.RefreshWebSession(true).ConfigureAwait(false); DateTime now = DateTime.UtcNow; diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs index 9056f4194..570622504 100644 --- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs +++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs @@ -68,6 +68,19 @@ public sealed class BotDatabase : GenericDatabase { [JsonProperty(Required = Required.DisallowNull)] private readonly OrderedDictionary GamesToRedeemInBackground = new(); + internal string? AccessToken { + get => BackingAccessToken; + + set { + if (BackingAccessToken == value) { + return; + } + + BackingAccessToken = value; + Utilities.InBackground(Save); + } + } + internal MobileAuthenticator? MobileAuthenticator { get => BackingMobileAuthenticator; @@ -107,6 +120,9 @@ public sealed class BotDatabase : GenericDatabase { } } + [JsonProperty] + private string? BackingAccessToken; + [JsonProperty($"_{nameof(MobileAuthenticator)}")] private MobileAuthenticator? BackingMobileAuthenticator; @@ -134,6 +150,9 @@ public sealed class BotDatabase : GenericDatabase { TradingBlacklistSteamIDs.OnModified += OnObjectModified; } + [UsedImplicitly] + public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken); + [UsedImplicitly] public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null; diff --git a/Directory.Packages.props b/Directory.Packages.props index 313053e37..57786bc23 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,6 +18,7 @@ +