* 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
This commit is contained in:
Łukasz Domeradzki 2023-10-19 13:38:39 +02:00 committed by GitHub
parent 4106c5f41a
commit d571cd9580
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 244 additions and 118 deletions

View file

@ -21,6 +21,7 @@
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" />
<PackageReference Include="System.Composition" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" />
<PackageReference Include="System.Linq.Async" />
<PackageReference Include="zxcvbn-core" />
</ItemGroup>

View file

@ -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<string> 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<INode> SelectNodes(this IDocument document, string xpath) {
ArgumentNullException.ThrowIfNull(document);

View file

@ -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<bool> RefreshSession() {
internal async Task<bool> 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);

View file

@ -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<IAuthentication> UnifiedAuthenticationService;
private readonly SteamUnifiedMessages.UnifiedService<IChatRoom> UnifiedChatRoomService;
private readonly SteamUnifiedMessages.UnifiedService<IClanChatRooms> UnifiedClanChatRoomsService;
private readonly SteamUnifiedMessages.UnifiedService<ICredentials> UnifiedCredentialsService;
@ -53,6 +54,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
ArgumentNullException.ThrowIfNull(steamUnifiedMessages);
ArchiLogger = archiLogger ?? throw new ArgumentNullException(nameof(archiLogger));
UnifiedAuthenticationService = steamUnifiedMessages.CreateService<IAuthentication>();
UnifiedChatRoomService = steamUnifiedMessages.CreateService<IChatRoom>();
UnifiedClanChatRoomsService = steamUnifiedMessages.CreateService<IClanChatRooms>();
UnifiedCredentialsService = steamUnifiedMessages.CreateService<ICredentials>();
@ -358,6 +360,37 @@ public sealed class ArchiHandler : ClientMsgHandler {
Client.Send(request);
}
internal async Task<CAuthentication_AccessToken_GenerateForApp_Response?> 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<CAuthentication_AccessToken_GenerateForApp_Response>() : null;
}
internal async Task<ulong> GetClanChatGroupID(ulong steamID) {
if ((steamID == 0) || !new SteamID(steamID).IsClanAccount) {
throw new ArgumentOutOfRangeException(nameof(steamID));

View file

@ -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<bool> Init(ulong steamID, EUniverse universe, string webAPIUserNonce, string? parentalCode = null) {
internal async Task<bool> 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<string, object?> 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;

View file

@ -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;

View file

@ -18,6 +18,7 @@
<PackageVersion Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
<PackageVersion Include="System.Composition" Version="7.0.0" />
<PackageVersion Include="System.Composition.AttributedModel" Version="7.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="7.0.3" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="zxcvbn-core" Version="7.0.92" />
</ItemGroup>