Skip to content

Commit

Permalink
Fix steam authentication with valid credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
affederaffe committed Aug 23, 2024
1 parent 28664bf commit 52e2f88
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 117 deletions.
2 changes: 1 addition & 1 deletion BeatSaberModManager/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ internal static class AssetProvidersModule;
[Register<ModsViewModel>(Scope.SingleInstance)]
[Register<LegacyGameVersionsViewModel>(Scope.SingleInstance)]
[Register<SettingsViewModel>(Scope.SingleInstance)]
[Register(typeof(SteamAuthenticationViewModel), Scope.SingleInstance, typeof(SteamAuthenticationViewModel), typeof(ISteamAuthenticator))]
[Register<SteamAuthenticationViewModel>(Scope.SingleInstance)]
[Register<AssetInstallWindowViewModel>(Scope.SingleInstance)]
internal static class ViewModelModule;

Expand Down
3 changes: 3 additions & 0 deletions BeatSaberModManager/Resources/Localization/de.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
<sys:String x:Key="SteamGuardCodeView:SubmitButton">Absenden</sys:String>
<sys:String x:Key="SteamGuardCodeView:CancelButton">Cancel</sys:String>

<sys:String x:Key="SteamDeviceConfirmationView:ConfirmLogin">Benutze die Steam App, um deine Anmeldung zu bestätigen</sys:String>
<sys:String x:Key="SteamDeviceConfirmationView:Title">Anmeldung Bestätigen</sys:String>

<sys:String x:Key="Status:Installing">Installiere:</sys:String>
<sys:String x:Key="Status:Uninstalling">Deinstalliere:</sys:String>
<sys:String x:Key="Status:Completed">Installation abgeschlossen</sys:String>
Expand Down
3 changes: 3 additions & 0 deletions BeatSaberModManager/Resources/Localization/en.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
<sys:String x:Key="SteamGuardCodeView:SubmitButton">Submit</sys:String>
<sys:String x:Key="SteamGuardCodeView:CancelButton">Cancel</sys:String>

<sys:String x:Key="SteamDeviceConfirmationView:ConfirmLogin">Use the Steam mobile app to confirm your sign in</sys:String>
<sys:String x:Key="SteamDeviceConfirmationView:Title">Confirm Login</sys:String>

<sys:String x:Key="Status:Installing">Installing:</sys:String>
<sys:String x:Key="Status:Uninstalling">Uninstalling:</sys:String>
<sys:String x:Key="Status:Completed">Installation completed</sys:String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,26 @@
namespace BeatSaberModManager.Services.Implementations.GameVersions.Steam
{
/// <inheritdoc />
public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ISteamAuthenticator steamAuthenticator, ILogger logger) : ILegacyGameVersionInstaller
public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ILogger logger) : ILegacyGameVersionInstaller
{
/// <inheritdoc />
public async Task<string?> InstallLegacyGameVersionAsync(IGameVersion gameVersion, CancellationToken cancellationToken, IProgress<double>? progress = null)
public async Task<string?> InstallLegacyGameVersionAsync(IGameVersion gameVersion, ILegacyGameVersionAuthenticator authenticator, CancellationToken cancellationToken, IProgress<double>? progress = null)
{
ArgumentNullException.ThrowIfNull(gameVersion);
if (gameVersion is not SteamGameVersion steamLegacyGameVersion)
if (gameVersion is not SteamGameVersion steamLegacyGameVersion || authenticator is not ISteamAuthenticator steamAuthenticator)
return null;
string appDataDirPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string legacyGameVersionsDirPath = Path.Join(appDataDirPath, ThisAssembly.Info.Product, "LegacyGameVersions", gameVersion.GameVersion);
DownloadConfig downloadConfig = new() { InstallDirectory = legacyGameVersionsDirPath, MaxDownloads = 3 };
await steam3Session.LoginAsync(steamAuthenticator, cancellationToken).ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested)
try
{
await steam3Session.LoginAsync(steamAuthenticator, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
return null;
}

ContentDownloader contentDownloader = new(downloadConfig, steam3Session);
List<(uint DepotId, ulong ManifestId)> depotManifestIds = [(620981, steamLegacyGameVersion.ManifestId)];
try
Expand All @@ -51,7 +57,6 @@ public class SteamLegacyGameVersionInstaller(Steam3Session steam3Session, ISteam
/// <inheritdoc />
public Task<bool> UninstallLegacyGameVersionAsync(IGameVersion gameVersion)
{
// TODO: doesn't work
ArgumentNullException.ThrowIfNull(gameVersion);
if (gameVersion is not SteamGameVersion)
return Task.FromResult(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace BeatSaberModManager.Services.Interfaces
{
/// <summary>
/// TODO
/// </summary>
#pragma warning disable CA1040
public interface ILegacyGameVersionAuthenticator;
#pragma warning restore CA1040
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ public interface ILegacyGameVersionInstaller
///
/// </summary>
/// <param name="gameVersion"></param>
/// <param name="authenticator"></param>
/// <param name="cancellationToken"></param>
/// <param name="progress"></param>
/// <returns></returns>
Task<string?> InstallLegacyGameVersionAsync(IGameVersion gameVersion, CancellationToken cancellationToken, IProgress<double>? progress = null);
Task<string?> InstallLegacyGameVersionAsync(IGameVersion gameVersion, ILegacyGameVersionAuthenticator authenticator, CancellationToken cancellationToken, IProgress<double>? progress = null);

/// <summary>
///
Expand Down
27 changes: 20 additions & 7 deletions BeatSaberModManager/ViewModels/LegacyGameVersionsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace BeatSaberModManager.ViewModels
/// <summary>
/// TODO
/// </summary>
public sealed class LegacyGameVersionsViewModel : ViewModelBase
public sealed class LegacyGameVersionsViewModel : ViewModelBase, IDisposable
{
private readonly IGameInstallLocator _gameInstallLocator;
private readonly IInstallDirValidator _installDirValidator;
Expand All @@ -32,6 +32,8 @@ public sealed class LegacyGameVersionsViewModel : ViewModelBase
private readonly ObservableAsPropertyHelper<bool> _canUninstall;
private readonly ObservableAsPropertyHelper<LegacyGameVersionsTab[]> _legacyGameVersions;

private CancellationTokenSource? _installLegacyVersionCts;

/// <summary>
/// TODO
/// </summary>
Expand Down Expand Up @@ -145,9 +147,12 @@ public GameVersionViewModel? SelectedGameVersion

private async Task<IReadOnlyList<GameVersionViewModel>?> GetLegacyGameVersionsAsync()
{
IReadOnlyList<IGameVersion>? availableGameVersions = await _legacyGameVersionProvider.GetAvailableGameVersionsAsync().ConfigureAwait(false);
IReadOnlyList<IGameVersion>? installedLegacyGameVersions = await _legacyGameVersionProvider.GetInstalledLegacyGameVersionsAsync().ConfigureAwait(false);
IGameVersion? installedStoreVersion = await _gameInstallLocator.LocateGameInstallAsync().ConfigureAwait(false);
Task<IReadOnlyList<IGameVersion>?> t1 = _legacyGameVersionProvider.GetAvailableGameVersionsAsync();
Task<IReadOnlyList<IGameVersion>?> t2 = _legacyGameVersionProvider.GetInstalledLegacyGameVersionsAsync();
Task<IGameVersion?> t3 = _gameInstallLocator.LocateGameInstallAsync();
IReadOnlyList<IGameVersion>? availableGameVersions = await t1.ConfigureAwait(false);
IReadOnlyList<IGameVersion>? installedLegacyGameVersions = await t2.ConfigureAwait(false);
IGameVersion? installedStoreVersion = await t3.ConfigureAwait(false);
List<IGameVersion> allInstalledGameVersions = installedLegacyGameVersions?.ToList() ?? [];
if (installedStoreVersion is not null)
allInstalledGameVersions.Insert(0, installedStoreVersion);
Expand All @@ -166,11 +171,12 @@ public GameVersionViewModel? SelectedGameVersion

private async Task<bool> InstallSelectedLegacyGameVersionAsync()
{
_installLegacyVersionCts?.Dispose();
_installLegacyVersionCts = new CancellationTokenSource();
GameVersionViewModel selectedGameVersion = SelectedGameVersion!;
StatusProgress.Report(new ProgressInfo(StatusType.Installing, selectedGameVersion.GameVersion.GameVersion));
using CancellationTokenSource cts = new();
using IDisposable subscription = SteamAuthenticationViewModel.CancelCommand.Subscribe(_ => cts.Cancel());
string? installDir = await _legacyGameVersionInstaller.InstallLegacyGameVersionAsync(selectedGameVersion.GameVersion, cts.Token, StatusProgress).ConfigureAwait(false);
using IDisposable subscription = SteamAuthenticationViewModel.CancelCommand.Subscribe(_ => _installLegacyVersionCts.Cancel());
string? installDir = await Task.Run(() => _legacyGameVersionInstaller.InstallLegacyGameVersionAsync(selectedGameVersion.GameVersion, SteamAuthenticationViewModel, _installLegacyVersionCts.Token, StatusProgress)).ConfigureAwait(false);
if (installDir is null)
{
StatusProgress.Report(new ProgressInfo(StatusType.Failed, null));
Expand Down Expand Up @@ -205,5 +211,12 @@ private void MarkStoreVersionAsInstalled(IGameVersion storeVersion, IEnumerable<
break;
}
}

/// <inheritdoc />
public void Dispose()
{
_installLegacyVersionCts?.Cancel();
_installLegacyVersionCts?.Dispose();
}
}
}
130 changes: 71 additions & 59 deletions BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Threading;
using System.Threading.Tasks;

using BeatSaberModManager.Services.Interfaces;

using LibDepotDownloader;

using ReactiveUI;
Expand All @@ -17,11 +19,13 @@ namespace BeatSaberModManager.ViewModels
/// <summary>
///
/// </summary>
public sealed class SteamAuthenticationViewModel : ViewModelBase, ISteamAuthenticator, IDisposable
public sealed class SteamAuthenticationViewModel : ViewModelBase, ILegacyGameVersionAuthenticator, ISteamAuthenticator, IDisposable

Check failure on line 22 in BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

'SteamAuthenticationViewModel' does not implement interface member 'ISteamAuthenticator.DisplayQrCode(string)'

Check failure on line 22 in BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Windows

'SteamAuthenticationViewModel' does not implement interface member 'ISteamAuthenticator.ProvideLoginDetailsAsync()'

Check failure on line 22 in BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

'SteamAuthenticationViewModel' does not implement interface member 'ISteamAuthenticator.DisplayQrCode(string)'

Check failure on line 22 in BeatSaberModManager/ViewModels/SteamAuthenticationViewModel.cs

View workflow job for this annotation

GitHub Actions / Build Linux

'SteamAuthenticationViewModel' does not implement interface member 'ISteamAuthenticator.ProvideLoginDetailsAsync()'
{
private readonly Steam3Session _session;

private CancellationTokenSource? _cancellationTokenSource;
private CancellationTokenSource? _credentialsAuthSessionCts;
private CancellationTokenSource? _qrAuthSessionCts;
private CancellationTokenSource? _deviceConfirmationCts;

/// <summary>
///
Expand All @@ -32,27 +36,32 @@ public SteamAuthenticationViewModel(Steam3Session session)
_session = session;
IObservable<bool> canLogin = this.WhenAnyValue(static x => x.Username, static x => x.Password)
.Select(static x => !string.IsNullOrWhiteSpace(x.Item1) && !string.IsNullOrWhiteSpace(x.Item2));
LoginCommand = ReactiveCommand.CreateFromTask(LoginWithCredentialsAsync, canLogin);
CancelCommand = ReactiveCommand.Create(() => _cancellationTokenSource?.Cancel());
LoginCommand = ReactiveCommand.Create(() => { _qrAuthSessionCts?.Cancel(); }, canLogin);
CancelCommand = ReactiveCommand.Create(() => { _credentialsAuthSessionCts?.Cancel(); _qrAuthSessionCts?.Cancel(); });
IObservable<bool> canSubmitSteamGuardCode = this.WhenAnyValue(static x => x.SteamGuardCode)
.Select(static code => code is not null && code.Length == 5);
SubmitSteamGuardCodeCommand = ReactiveCommand.Create(() => SteamGuardCode!, canSubmitSteamGuardCode);
}

/// <summary>
/// TODO
///
/// </summary>
public Interaction<CancellationToken, Unit> AuthenticateInteraction { get; } = new(RxApp.MainThreadScheduler);

/// <summary>
///
/// </summary>
public Interaction<Unit, AuthPollResult?> AuthenticateSteamInteraction { get; } = new(RxApp.MainThreadScheduler);
public Interaction<CancellationToken, Unit> DeviceConfirmationInteraction { get; } = new(RxApp.MainThreadScheduler);

/// <summary>
///
/// </summary>
public Interaction<bool, string> GetSteamGuardCodeInteraction { get; } = new(RxApp.MainThreadScheduler);
public Interaction<Unit, string?> GetDeviceCodeInteraction { get; } = new(RxApp.MainThreadScheduler);

/// <summary>
///
/// </summary>
public ReactiveCommand<Unit, AuthPollResult?> LoginCommand { get; }
public ReactiveCommand<Unit, Unit> LoginCommand { get; }

/// <summary>
///
Expand Down Expand Up @@ -108,6 +117,17 @@ public bool RememberPassword

private bool _rememberPassword;

/// <summary>
///
/// </summary>
public bool IsPasswordInvalid
{
get => _isPasswordInvalid;
set => this.RaiseAndSetIfChanged(ref _isPasswordInvalid, value);
}

private bool _isPasswordInvalid;

/// <summary>
///
/// </summary>
Expand All @@ -120,82 +140,74 @@ public string? SteamGuardCode
private string? _steamGuardCode;

/// <inheritdoc />
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect) => GetSteamGuardCodeInteraction.Handle(previousCodeWasIncorrect).ToTask();
public void Dispose()
{
_credentialsAuthSessionCts?.Dispose();
_qrAuthSessionCts?.Dispose();
_deviceConfirmationCts?.Dispose();
}

/// <inheritdoc />
public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect) => throw new System.NotImplementedException();
public Task<string> GetDeviceCodeAsync(bool previousCodeWasIncorrect) => GetDeviceCodeInteraction.Handle(Unit.Default).ToTask()!;

/// <inheritdoc />
public Task<bool> AcceptDeviceConfirmationAsync() => Task.FromResult(false);

/// <summary>
///
/// </summary>
/// <returns></returns>
public Task<AuthPollResult?> AuthenticateAsync() => AuthenticateSteamInteraction.Handle(Unit.Default).ToTask();
public Task<string> GetEmailCodeAsync(string email, bool previousCodeWasIncorrect) => throw new NotImplementedException();

/// <summary>
///
/// </summary>
/// <returns></returns>
public Task<AuthPollResult?> StartAuthenticationAsync()
/// <inheritdoc />
public Task<bool> AcceptDeviceConfirmationAsync()
{
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
if (_qrAuthSessionCts!.IsCancellationRequested)
DeviceConfirmationInteraction.Handle(_deviceConfirmationCts!.Token).Subscribe();

return Observable.StartAsync(LoginWithQrCodeAsync)
.Merge(LoginCommand)
.FirstAsync()
.ToTask();
return Task.FromResult(true);
}

/// <inheritdoc />
public void Dispose()
public async Task<AuthPollResult?> AuthenticateAsync()
{
_cancellationTokenSource?.Dispose();
}
_credentialsAuthSessionCts?.Dispose();
_qrAuthSessionCts?.Dispose();
_deviceConfirmationCts?.Dispose();
_credentialsAuthSessionCts = new CancellationTokenSource();
_qrAuthSessionCts = new CancellationTokenSource();
_deviceConfirmationCts = new CancellationTokenSource();

private async Task<AuthPollResult?> LoginWithCredentialsAsync()
{
AuthSessionDetails details = new()
{
Authenticator = this,
Username = Username,
Password = Password,
IsPersistentSession = RememberPassword
};
IsPasswordInvalid = false;

CredentialsAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaCredentialsAsync(details).ConfigureAwait(false);
AuthenticateInteraction.Handle(_deviceConfirmationCts.Token).Subscribe();

AuthSessionDetails details = new() { Authenticator = this };

try
{
AuthPollResult result = await authSession.PollingWaitForResultAsync(_cancellationTokenSource!.Token).ConfigureAwait(false);
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
QrAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaQRAsync(details).ConfigureAwait(false);
LoginChallenge = authSession.ChallengeURL;
authSession.ChallengeURLChanged += () => LoginChallenge = authSession.ChallengeURL;
AuthPollResult result = await authSession.PollingWaitForResultAsync(_qrAuthSessionCts.Token).ConfigureAwait(false);
await _deviceConfirmationCts.CancelAsync().ConfigureAwait(false);
return result;
}
catch (TaskCanceledException)
{
return null;
}
}
catch (TaskCanceledException) { }

private async Task<AuthPollResult?> LoginWithQrCodeAsync()
{
AuthSessionDetails details = new()
{
Authenticator = this
};
if (_credentialsAuthSessionCts.IsCancellationRequested)
return null;

QrAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaQRAsync(details).ConfigureAwait(false);
LoginChallenge = authSession.ChallengeURL;
authSession.ChallengeURLChanged += () => LoginChallenge = authSession.ChallengeURL;
details.Username = Username;
details.Password = Password;
details.IsPersistentSession = RememberPassword;

try
{
AuthPollResult result = await authSession.PollingWaitForResultAsync(_cancellationTokenSource!.Token).ConfigureAwait(false);
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
CredentialsAuthSession authSession = await _session.SteamClient.Authentication.BeginAuthSessionViaCredentialsAsync(details).ConfigureAwait(false);
AuthPollResult result = await authSession.PollingWaitForResultAsync(_credentialsAuthSessionCts.Token).ConfigureAwait(false);
await _deviceConfirmationCts.CancelAsync().ConfigureAwait(false);
return result;
}
catch (AuthenticationException)
{
IsPasswordInvalid = true;
return null;
}
catch (TaskCanceledException)
{
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Globalization;

using Avalonia.Data.Converters;
using Avalonia.Media;


namespace BeatSaberModManager.Views.Converters
{
/// <summary>
///
/// </summary>
public class IsPasswordInvalidColorConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not bool isPasswordInvalid)
throw new InvalidCastException();
return isPasswordInvalid ? Brushes.Red : Brushes.Transparent;
}

/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new InvalidOperationException();
}
}
Loading

0 comments on commit 52e2f88

Please sign in to comment.