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.
This commit is contained in:
Łukasz Domeradzki
2025-12-08 23:25:15 +01:00
parent 90fe1a9448
commit 4eee2e2ac7
3 changed files with 82 additions and 22 deletions

View File

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

View File

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

View File

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