diff --git a/ArchiSteamFarm/ArchiSteamFarm.csproj b/ArchiSteamFarm/ArchiSteamFarm.csproj
index e2717031a294e..18f51791e0064 100644
--- a/ArchiSteamFarm/ArchiSteamFarm.csproj
+++ b/ArchiSteamFarm/ArchiSteamFarm.csproj
@@ -21,6 +21,7 @@
+
diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs
index 5bb6adedbfaf0..5d3acadf1d690 100644
--- a/ArchiSteamFarm/Steam/Bot.cs
+++ b/ArchiSteamFarm/Steam/Bot.cs
@@ -27,6 +27,7 @@
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;
@@ -229,6 +230,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
internal bool PlayingBlocked { get; private set; }
internal bool PlayingWasBlocked { get; private set; }
+ private string? AccessToken;
+ private DateTime? AccessTokenValidUntil;
private string? AuthCode;
[JsonProperty]
@@ -244,6 +247,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;
@@ -357,6 +362,7 @@ public void Dispose() {
ConnectionFailureTimer?.Dispose();
GamesRedeemerInBackgroundTimer?.Dispose();
PlayingWasBlockedTimer?.Dispose();
+ RefreshTokensTimer?.Dispose();
SendItemsTimer?.Dispose();
SteamSaleEvent?.Dispose();
TradeCheckTimer?.Dispose();
@@ -390,6 +396,10 @@ public async ValueTask DisposeAsync() {
await PlayingWasBlockedTimer.DisposeAsync().ConfigureAwait(false);
}
+ if (RefreshTokensTimer != null) {
+ await RefreshTokensTimer.DisposeAsync().ConfigureAwait(false);
+ }
+
if (SendItemsTimer != null) {
await SendItemsTimer.DisposeAsync().ConfigureAwait(false);
}
@@ -1492,32 +1502,57 @@ internal async Task OnFarmingStopped() {
await PluginsCore.OnBotFarmingStopped(this).ConfigureAwait(false);
}
- internal async Task RefreshSession() {
+ internal async Task RefreshWebSession() {
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 (!string.IsNullOrEmpty(AccessToken) && AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > now.AddMinutes(5))) {
+ // 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;
+ BotDatabase.AccessTokenValidUntil = AccessTokenValidUntil = 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;
+ BotDatabase.AccessTokenValidUntil = AccessTokenValidUntil = null;
+
await Connect(true).ConfigureAwait(false);
return false;
@@ -2274,6 +2309,20 @@ private async Task InitModules() {
WalletBalance = 0;
WalletCurrency = ECurrencyCode.Invalid;
+ AccessToken = BotDatabase.AccessToken;
+ AccessTokenValidUntil = BotDatabase.AccessTokenValidUntil;
+ 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 +2393,40 @@ private void InitPlayingWasBlockedTimer() {
);
}
+ private void InitRefreshTokensTimer(DateTime validUntil) {
+ if (validUntil <= DateTime.UnixEpoch) {
+ 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 10 minutes before it's invalid
+ if (delay.TotalMinutes > 10) {
+ delay -= TimeSpan.FromMinutes(10);
+ }
+
+ // 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 +2565,7 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
}
}
- 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 +2610,7 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
InitConnectionFailureTimer();
- if (string.IsNullOrEmpty(refreshToken)) {
+ if (string.IsNullOrEmpty(RefreshToken)) {
AuthPollResult pollResult;
try {
@@ -2569,19 +2642,15 @@ private async void OnConnected(SteamClient.ConnectedCallback callback) {
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 +2675,7 @@ private async void OnDisconnected(SteamClient.DisconnectedCallback callback) {
HeartBeatFailures = 0;
StopConnectionFailureTimer();
StopPlayingWasBlockedTimer();
+ StopRefreshTokensTimer();
ArchiLogger.LogGenericInfo(Strings.BotDisconnected);
@@ -3087,11 +3157,9 @@ private async void OnLoggedOn(SteamUser.LoggedOnCallback callback) {
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 +3304,15 @@ private async void OnPlayingSessionState(SteamUser.PlayingSessionStateCallback c
await CheckOccupationStatus().ConfigureAwait(false);
}
+ private async void OnRefreshTokensTimer(object? state = null) {
+ if (AccessTokenValidUntil.HasValue && (AccessTokenValidUntil.Value > DateTime.UtcNow.AddMinutes(15))) {
+ // 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 +3785,46 @@ private void StopPlayingWasBlockedTimer() {
PlayingWasBlockedTimer = null;
}
+ private void StopRefreshTokensTimer() {
+ if (RefreshTokensTimer == null) {
+ return;
+ }
+
+ RefreshTokensTimer.Dispose();
+ RefreshTokensTimer = null;
+ }
+
+ private void UpdateTokens(string accessToken, string refreshToken) {
+ ArgumentException.ThrowIfNullOrEmpty(accessToken);
+ ArgumentException.ThrowIfNullOrEmpty(refreshToken);
+
+ AccessToken = accessToken;
+ AccessTokenValidUntil = null;
+ RefreshToken = refreshToken;
+
+ JwtSecurityTokenHandler jwtSecurityTokenHandler = new();
+
+ try {
+ JwtSecurityToken? accessTokenJwt = jwtSecurityTokenHandler.ReadJwtToken(accessToken);
+
+ AccessTokenValidUntil = accessTokenJwt.ValidTo > DateTime.UnixEpoch ? accessTokenJwt.ValidTo : DateTime.MaxValue;
+ } catch (Exception e) {
+ ArchiLogger.LogGenericException(e);
+ }
+
+ 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;
+ }
+
+ BotDatabase.AccessTokenValidUntil = AccessTokenValidUntil;
+ }
+ }
+
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 201a7d4de950c..db4e06027b6a1 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 @@ internal ArchiHandler(ArchiLogger archiLogger, SteamUnifiedMessages steamUnified
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,35 @@ internal void AcknowledgeClanInvite(ulong steamID, bool acceptInvite) {
Client.Send(request);
}
+ internal async Task GenerateAccessTokens(string refreshToken) {
+ ArgumentException.ThrowIfNullOrEmpty(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 2d50ecb71a324..98fa10f7d60b6 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 @@ internal async Task GetServerTime() {
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 @@ internal async Task Init(ulong steamID, EUniverse universe, string webAPIU
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 @@ internal async Task Init(ulong steamID, EUniverse universe, string webAPIU
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 @@ private async Task RefreshSession() {
}
Bot.ArchiLogger.LogGenericInfo(Strings.RefreshingOurSession);
- bool result = await Bot.RefreshSession().ConfigureAwait(false);
+ bool result = await Bot.RefreshWebSession().ConfigureAwait(false);
DateTime now = DateTime.UtcNow;
diff --git a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
index 9056f41941f59..98f927c12e93f 100644
--- a/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
+++ b/ArchiSteamFarm/Steam/Storage/BotDatabase.cs
@@ -68,6 +68,32 @@ internal uint GamesToRedeemInBackgroundCount {
[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 DateTime? AccessTokenValidUntil {
+ get => BackingAccessTokenValidUntil;
+
+ set {
+ if (BackingAccessTokenValidUntil == value) {
+ return;
+ }
+
+ BackingAccessTokenValidUntil = value;
+ Utilities.InBackground(Save);
+ }
+ }
+
internal MobileAuthenticator? MobileAuthenticator {
get => BackingMobileAuthenticator;
@@ -107,6 +133,12 @@ internal string? SteamGuardData {
}
}
+ [JsonProperty]
+ private string? BackingAccessToken;
+
+ [JsonProperty(Required = Required.DisallowNull)]
+ private DateTime? BackingAccessTokenValidUntil;
+
[JsonProperty($"_{nameof(MobileAuthenticator)}")]
private MobileAuthenticator? BackingMobileAuthenticator;
@@ -134,6 +166,12 @@ private BotDatabase() {
TradingBlacklistSteamIDs.OnModified += OnObjectModified;
}
+ [UsedImplicitly]
+ public bool ShouldSerializeBackingAccessToken() => !string.IsNullOrEmpty(BackingAccessToken);
+
+ [UsedImplicitly]
+ public bool ShouldSerializeBackingAccessTokenValidUntil() => BackingAccessTokenValidUntil.HasValue;
+
[UsedImplicitly]
public bool ShouldSerializeBackingMobileAuthenticator() => BackingMobileAuthenticator != null;
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 313053e3797c8..57786bc23d6a7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -18,6 +18,7 @@
+