From 4eee2e2ac7aa50406d2287297dff50adc2a1b5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Mon, 8 Dec 2025 23:25:15 +0100 Subject: [PATCH] Fix authentication flow when reaching max failures in credentials provider This one is tricky, previously we've properly handled max failures, and told SK2 to abort the request with operation canceled exception, which we even handled back in bot flow, so it looked OK at first, but the bot didn't do anything with that, which resulted in forced TryAnotherCM disconnection after 1 minute of inactivity. Since we need to be informed what to do in such case, simple cancellation of flow is not enough, we require a custom exception to handle, which will tell us the precise reason for failure + possible count of them, and that will in result allow us to react accordingly in the bot flow, e.g. by stopping the bot if needed. --- ArchiSteamFarm/Steam/Bot.cs | 31 +++++++++++--- .../Integration/BotCredentialsProvider.cs | 33 ++++++++------- .../Security/BotAuthenticationException.cs | 40 +++++++++++++++++++ 3 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 ArchiSteamFarm/Steam/Security/BotAuthenticationException.cs diff --git a/ArchiSteamFarm/Steam/Bot.cs b/ArchiSteamFarm/Steam/Bot.cs index 0613dd058..3ff7c0588 100644 --- a/ArchiSteamFarm/Steam/Bot.cs +++ b/ArchiSteamFarm/Steam/Bot.cs @@ -2778,14 +2778,14 @@ public sealed class Bot : IAsyncDisposable, IDisposable { InitConnectionFailureTimer(); if (string.IsNullOrEmpty(RefreshToken)) { + BotCredentialsProvider botCredentialsProvider = new(this); + AuthPollResult pollResult; try { - using CancellationTokenSource authCancellationTokenSource = new(); - CredentialsAuthSession authSession = await SteamClient.Authentication.BeginAuthSessionViaCredentialsAsync( new AuthSessionDetails { - Authenticator = new BotCredentialsProvider(this, authCancellationTokenSource), + Authenticator = botCredentialsProvider, DeviceFriendlyName = machineName, GuardData = BotConfig.UseLoginKeys ? BotDatabase.SteamGuardData : null, IsPersistentSession = true, @@ -2794,10 +2794,12 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } ).ConfigureAwait(false); - pollResult = await authSession.PollingWaitForResultAsync(authCancellationTokenSource.Token).ConfigureAwait(false); + pollResult = await authSession.PollingWaitForResultAsync().ConfigureAwait(false); } catch (AsyncJobFailedException e) { ArchiLogger.LogGenericWarningException(e); + LoginFailures += botCredentialsProvider.LoginFailures; + await HandleLoginResult(EResult.Timeout, EResult.Timeout).ConfigureAwait(false); ReconnectOnUserInitiated = true; @@ -2807,6 +2809,17 @@ public sealed class Bot : IAsyncDisposable, IDisposable { } catch (AuthenticationException e) { ArchiLogger.LogGenericWarningException(e); + LoginFailures += botCredentialsProvider.LoginFailures; + + await HandleLoginResult(e.Result, e.Result).ConfigureAwait(false); + + ReconnectOnUserInitiated = true; + SteamClient.Disconnect(); + + return; + } catch (BotAuthenticationException e) { + LoginFailures += botCredentialsProvider.LoginFailures; + await HandleLoginResult(e.Result, e.Result).ConfigureAwait(false); ReconnectOnUserInitiated = true; @@ -2814,10 +2827,18 @@ public sealed class Bot : IAsyncDisposable, IDisposable { return; } catch (OperationCanceledException) { - // This is okay, we already took care of that and can ignore it here + LoginFailures += botCredentialsProvider.LoginFailures; + + await HandleLoginResult(EResult.Timeout, EResult.Timeout).ConfigureAwait(false); + + ReconnectOnUserInitiated = true; + SteamClient.Disconnect(); + return; } + LoginFailures += botCredentialsProvider.LoginFailures; + if (!string.IsNullOrEmpty(pollResult.NewGuardData) && BotConfig.UseLoginKeys) { BotDatabase.SteamGuardData = pollResult.NewGuardData; } diff --git a/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs index a11c3f4bd..b46f650d6 100644 --- a/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs +++ b/ArchiSteamFarm/Steam/Integration/BotCredentialsProvider.cs @@ -23,10 +23,10 @@ using System; using System.ComponentModel; -using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Core; using ArchiSteamFarm.Localization; +using ArchiSteamFarm.Steam.Security; using ArchiSteamFarm.Storage; using SteamKit2; using SteamKit2.Authentication; @@ -34,19 +34,16 @@ using SteamKit2.Authentication; namespace ArchiSteamFarm.Steam.Integration; internal sealed class BotCredentialsProvider : IAuthenticator { - private const byte MaxLoginFailures = 5; + private const byte MaxLoginFailures = 3; private readonly Bot Bot; - private readonly CancellationTokenSource CancellationTokenSource; - private byte LoginFailures; + internal byte LoginFailures { get; private set; } - internal BotCredentialsProvider(Bot bot, CancellationTokenSource cancellationTokenSource) { + internal BotCredentialsProvider(Bot bot) { ArgumentNullException.ThrowIfNull(bot); - ArgumentNullException.ThrowIfNull(cancellationTokenSource); Bot = bot; - CancellationTokenSource = cancellationTokenSource; } public async Task AcceptDeviceConfirmationAsync() { @@ -75,24 +72,26 @@ internal sealed class BotCredentialsProvider : IAuthenticator { throw new InvalidEnumArgumentException(nameof(inputType), (int) inputType, typeof(ASF.EUserInputType)); } - if (previousCodeWasIncorrect && (++LoginFailures >= MaxLoginFailures)) { - EResult reason = inputType == ASF.EUserInputType.TwoFactorAuthentication ? EResult.TwoFactorCodeMismatch : EResult.InvalidLoginAuthCode; + EResult reason = inputType == ASF.EUserInputType.TwoFactorAuthentication ? EResult.TwoFactorCodeMismatch : EResult.InvalidLoginAuthCode; + if (previousCodeWasIncorrect) { Bot.ArchiLogger.LogGenericWarning(Strings.FormatBotUnableToLogin(reason, reason)); - await CancellationTokenSource.CancelAsync().ConfigureAwait(false); - - return ""; + if (++LoginFailures >= MaxLoginFailures) { + throw new BotAuthenticationException(reason); + } } - string? result = await Bot.RequestInput(inputType, previousCodeWasIncorrect).ConfigureAwait(false); + string? input = await Bot.RequestInput(inputType, previousCodeWasIncorrect).ConfigureAwait(false); - if (string.IsNullOrEmpty(result)) { - await CancellationTokenSource.CancelAsync().ConfigureAwait(false); + if (string.IsNullOrEmpty(input)) { + Bot.ArchiLogger.LogGenericWarning(Strings.FormatErrorIsEmpty(nameof(input))); - return ""; + LoginFailures = MaxLoginFailures; + + throw new BotAuthenticationException(reason); } - return result; + return input; } } diff --git a/ArchiSteamFarm/Steam/Security/BotAuthenticationException.cs b/ArchiSteamFarm/Steam/Security/BotAuthenticationException.cs new file mode 100644 index 000000000..5568b3f9c --- /dev/null +++ b/ArchiSteamFarm/Steam/Security/BotAuthenticationException.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------------------- +// _ _ _ ____ _ _____ +// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___ +// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \ +// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | | +// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_| +// ---------------------------------------------------------------------------------------------- +// | +// Copyright 2015-2025 Ɓukasz "JustArchi" Domeradzki +// Contact: JustArchi@JustArchi.net +// | +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// | +// http://www.apache.org/licenses/LICENSE-2.0 +// | +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.ComponentModel; +using SteamKit2; + +namespace ArchiSteamFarm.Steam.Security; + +internal sealed class BotAuthenticationException : Exception { + internal readonly EResult Result; + + internal BotAuthenticationException(EResult result) { + if (!Enum.IsDefined(result)) { + throw new InvalidEnumArgumentException(nameof(result), (int) result, typeof(EResult)); + } + + Result = result; + } +}