From 0084f8e2e21952627ab12be16d3659378b287220 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 29 Aug 2022 11:09:20 +0300 Subject: [PATCH 01/29] Create SteamAuthentication --- .../SteamAuthentication.cs | 156 ++++++++++++++++++ .../Steam/SteamClient/SteamClient.cs | 1 + 2 files changed, 157 insertions(+) create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs new file mode 100644 index 000000000..d0ecd3a16 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -0,0 +1,156 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using SteamKit2.Internal; + +namespace SteamKit2 +{ + /// + /// This handler is used for authenticating on Steam. + /// + public sealed class SteamAuthentication : ClientMsgHandler + { + /// + /// Represents the details required to authenticate on Steam. + /// + public sealed class AuthSessionDetails + { + /// + /// Gets or sets the username. + /// + /// The username. + public string? Username { get; set; } + + /// + /// Gets or sets the password. + /// + /// The password. + public string? Password { get; set; } + + /// + /// Gets or sets the device name (or user agent). + /// + /// The device name. + public string? DeviceFriendlyName { get; set; } + + /// + /// Gets or sets the platform type that the login will be performed for. + /// + public EAuthTokenPlatformType PlatformType { get; set; } = EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient; + + /// + /// Gets or sets the session persistence. + /// + /// The persistence. + public ESessionPersistence Persistence { get; set; } = ESessionPersistence.k_ESessionPersistence_Persistent; + + /// + /// Gets or sets the website id that the login will be performed for. (EMachineAuthWebDomain) + /// + /// The website id. + public string? WebsiteID { get; set; } + } + + /// + /// Gets public key for the provided account name which can be used to encrypt the account password. + /// + /// The account name to get RSA public key for. + public async Task GetPasswordRSAPublicKey( string accountName ) + { + var request = new CAuthentication_GetPasswordRSAPublicKey_Request + { + account_name = accountName + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); + var response = message.GetDeserializedResponse(); + + return response; + } + + /// + /// + /// + /// The details to use for logging on. + public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) + { + var request = new CAuthentication_BeginAuthSessionViaQR_Request + { + platform_type = details.PlatformType, + device_friendly_name = details.DeviceFriendlyName, + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); + var response = message.GetDeserializedResponse(); + } + + /// + /// + /// + /// The details to use for logging on. + /// No auth details were provided. + /// Username or password are not set within . + public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) + { + if ( details == null ) + { + throw new ArgumentNullException( nameof( details ) ); + } + + if ( string.IsNullOrEmpty( details.Username ) || string.IsNullOrEmpty( details.Password ) ) + { + throw new ArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in 'details'." ); + } + + // Encrypt the password + var publicKey = await GetPasswordRSAPublicKey( details.Username! ); + var rsaParameters = new RSAParameters + { + Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), + Exponent = Utils.DecodeHexString( publicKey.publickey_exp ), + }; + + using var rsa = RSA.Create(); + rsa.ImportParameters( rsaParameters ); + var encryptedPassword = rsa.Encrypt( Encoding.UTF8.GetBytes( details.Password ), RSAEncryptionPadding.Pkcs1 ); + + // Create request + var request = new CAuthentication_BeginAuthSessionViaCredentials_Request + { + platform_type = details.PlatformType, + device_friendly_name = details.DeviceFriendlyName, + account_name = details.Username, + persistence = details.Persistence, + website_id = details.WebsiteID, + encrypted_password = Convert.ToBase64String( encryptedPassword ), + encryption_timestamp = publicKey.timestamp, + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); + var response = message.GetDeserializedResponse(); + + var t = 1; + } + + /// + /// Handles a client message. This should not be called directly. + /// + /// The packet message that contains the data. + public override void HandleMsg( IPacketMsg packetMsg ) + { + // not used + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index f640256cd..7bd490f21 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -83,6 +83,7 @@ public SteamClient( SteamConfiguration configuration, string identifier ) // notice: SteamFriends should be added before SteamUser due to AccountInfoCallback this.AddHandler( new SteamFriends() ); this.AddHandler( new SteamUser() ); + this.AddHandler( new SteamAuthentication() ); this.AddHandler( new SteamApps() ); this.AddHandler( new SteamGameCoordinator() ); this.AddHandler( new SteamGameServer() ); From 3c8472d4f8d93674ea1fe357fcc2bbe28da52f9c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 29 Aug 2022 18:05:42 +0300 Subject: [PATCH 02/29] Add some polling support --- .../SteamAuthentication.cs | 171 +++++++++++++++++- 1 file changed, 168 insertions(+), 3 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index d0ecd3a16..dfee3b3f5 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -4,6 +4,7 @@ */ using System; +using System.Collections.Generic; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -16,6 +17,26 @@ namespace SteamKit2 /// public sealed class SteamAuthentication : ClientMsgHandler { + /* EPasswordLoginSessionStatus + Unstarted: 0, + Starting: 1, + InvalidCredentials: 2, + WaitingForEmailCode: 3, + WaitingForEmailConfirmation: 4, + WaitingForDeviceCode: 5, + WaitingForDeviceConfirmation: 6, + StartMoveAuthenticator: 7, + WaitingForMoveCode: 8, + AuthenticatorMoved: 9, + InvalidEmailCode: 10, + InvalidDeviceCode: 11, + InvalidMoveCode: 12, + WaitingForToken: 13, + Success: 14, + Failure: 15, + Stopped: 16, + */ + /// /// Represents the details required to authenticate on Steam. /// @@ -57,6 +78,111 @@ public sealed class AuthSessionDetails public string? WebsiteID { get; set; } } + public class BeginAuthSessionResponse + { + public ulong ClientID { get; set; } + public byte[] RequestID { get; set; } + public List AllowedConfirmations { get; set; } + public TimeSpan PollingInterval { get; set; } + } + + public sealed class QrBeginAuthSessionResponse : BeginAuthSessionResponse + { + public string ChallengeURL { get; set; } + } + + public sealed class CredentialsBeginAuthSessionResponse : BeginAuthSessionResponse + { + public SteamID SteamID { get; set; } + } + + public async Task StartPolling( BeginAuthSessionResponse baseResponse ) + { + // TODO: Sort by preferred methods? + foreach ( var allowedConfirmation in baseResponse.AllowedConfirmations ) + { + switch ( allowedConfirmation.confirmation_type ) + { + case EAuthSessionGuardType.k_EAuthSessionGuardType_None: + // no steam guard + // if we poll now we will get access token in response and send login to the cm + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: + // sent steam guard email at allowedConfirmation.associated_message + // use SendSteamGuardCode + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: + // totp code from mobile app + // use SendSteamGuardCode + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: + // TODO: is this accept prompt that automatically appears in the mobile app? + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: + // TODO: what is this? + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: + // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set + break; + + } + } + + while ( true ) + { + // TODO: For guard type none we don't need delay + await Task.Delay( baseResponse.PollingInterval ); + + var pollResponse = await PollAuthSessionStatus( baseResponse ); + var k = 1; + } + } + + public async Task PollAuthSessionStatus( BeginAuthSessionResponse baseResponse ) + { + var request = new CAuthentication_PollAuthSessionStatus_Request + { + client_id = baseResponse.ClientID, + request_id = baseResponse.RequestID, + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.PollAuthSessionStatus( request ) ); + var response = message.GetDeserializedResponse(); + + // eresult can be Expired, FileNotFound, Fail + + return response; + } + + public async Task SendSteamGuardCode( CredentialsBeginAuthSessionResponse baseResponse, string code, EAuthSessionGuardType codeType ) + { + var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request + { + client_id = baseResponse.ClientID, + steamid = baseResponse.SteamID, + code = code, + code_type = codeType, + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); + var response = message.GetDeserializedResponse(); + + // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Failed to send steam guard code with result {message.Result}" ); + } + } + /// /// Gets public key for the provided account name which can be used to encrypt the account password. /// @@ -71,6 +197,12 @@ public async Task GetPasswordR var unifiedMessages = Client.GetHandler()!; var contentService = unifiedMessages.CreateService(); var message = await contentService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); + + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Failed to get password public key with result {message.Result}" ); + } + var response = message.GetDeserializedResponse(); return response; @@ -80,7 +212,7 @@ public async Task GetPasswordR /// /// /// The details to use for logging on. - public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) + public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) { var request = new CAuthentication_BeginAuthSessionViaQR_Request { @@ -91,7 +223,24 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) var unifiedMessages = Client.GetHandler()!; var contentService = unifiedMessages.CreateService(); var message = await contentService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); + + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Failed to begin QR auth session with result {message.Result}" ); + } + var response = message.GetDeserializedResponse(); + + var authResponse = new QrBeginAuthSessionResponse + { + ClientID = response.client_id, + RequestID = response.request_id, + AllowedConfirmations = response.allowed_confirmations, + PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), + ChallengeURL = response.challenge_url, + }; + + return authResponse; } /// @@ -100,7 +249,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) /// The details to use for logging on. /// No auth details were provided. /// Username or password are not set within . - public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) + public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) { if ( details == null ) { @@ -139,9 +288,25 @@ public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) var unifiedMessages = Client.GetHandler()!; var contentService = unifiedMessages.CreateService(); var message = await contentService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); + + // eresult can be InvalidPassword, ServiceUnavailable + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Authentication failed with result {message.Result}" ); + } + var response = message.GetDeserializedResponse(); - var t = 1; + var authResponse = new CredentialsBeginAuthSessionResponse + { + ClientID = response.client_id, + RequestID = response.request_id, + AllowedConfirmations = response.allowed_confirmations, + PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), + SteamID = new SteamID( response.steamid ), + }; + + return authResponse; } /// From cf5635aa3790e05e63e90b2fe9e8abc73d626837 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 29 Aug 2022 21:04:09 +0300 Subject: [PATCH 03/29] Add AccessToken to LogOnDetails --- SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs index f04658f21..42cae7bea 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs @@ -74,6 +74,11 @@ public sealed class LogOnDetails /// /// The sentry file hash. public byte[]? SentryFileHash { get; set; } + /// + /// Gets or sets the access token used to login. This a token that has been provided after a successful login using . + /// + /// The access token. + public string? AccessToken { get; set; } /// /// Gets or sets the account instance. 1 for the PC instance or 2 for the Console (PS3) instance. @@ -295,7 +300,7 @@ public void LogOn( LogOnDetails details ) { throw new ArgumentNullException( nameof( details ) ); } - if ( string.IsNullOrEmpty( details.Username ) || ( string.IsNullOrEmpty( details.Password ) && string.IsNullOrEmpty( details.LoginKey ) ) ) + if ( string.IsNullOrEmpty( details.Username ) || ( string.IsNullOrEmpty( details.Password ) && string.IsNullOrEmpty( details.LoginKey ) && string.IsNullOrEmpty( details.AccessToken ) ) ) { throw new ArgumentException( "LogOn requires a username and password to be set in 'details'." ); } @@ -360,6 +365,7 @@ public void LogOn( LogOnDetails details ) logon.Body.two_factor_code = details.TwoFactorCode; logon.Body.login_key = details.LoginKey; + logon.Body.access_token = details.AccessToken; logon.Body.sha_sentryfile = details.SentryFileHash; logon.Body.eresult_sentryfile = ( int )( details.SentryFileHash != null ? EResult.OK : EResult.FileNotFound ); From a127e684c1ee6d0ab3a57e24905dcf45d0c99d31 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 29 Aug 2022 21:04:26 +0300 Subject: [PATCH 04/29] Return tokens when poll returns data --- .../SteamAuthentication.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index dfee3b3f5..bc19ca962 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -78,6 +78,13 @@ public sealed class AuthSessionDetails public string? WebsiteID { get; set; } } + public class AuthComplete + { + public string AccountName { get; set; } + public string RefreshToken { get; set; } + public string AccessToken { get; set; } + } + public class BeginAuthSessionResponse { public ulong ClientID { get; set; } @@ -96,7 +103,7 @@ public sealed class CredentialsBeginAuthSessionResponse : BeginAuthSessionRespon public SteamID SteamID { get; set; } } - public async Task StartPolling( BeginAuthSessionResponse baseResponse ) + public async Task StartPolling( BeginAuthSessionResponse baseResponse ) { // TODO: Sort by preferred methods? foreach ( var allowedConfirmation in baseResponse.AllowedConfirmations ) @@ -139,7 +146,16 @@ public async Task StartPolling( BeginAuthSessionResponse baseResponse ) await Task.Delay( baseResponse.PollingInterval ); var pollResponse = await PollAuthSessionStatus( baseResponse ); - var k = 1; + + if ( pollResponse.refresh_token.Length > 0 ) + { + return new AuthComplete + { + AccessToken = pollResponse.access_token, + RefreshToken = pollResponse.refresh_token, + AccountName = pollResponse.account_name, + }; + } } } @@ -158,6 +174,16 @@ public async Task PollAuthSessio // eresult can be Expired, FileNotFound, Fail + if ( response.new_client_id > 0 ) + { + baseResponse.ClientID = response.new_client_id; + } + + if ( baseResponse is QrBeginAuthSessionResponse qrResponse && response.new_challenge_url.Length > 0 ) + { + qrResponse.ChallengeURL = response.new_challenge_url; + } + return response; } From b6170564acfe27172023917130a2a490b46625fc Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 30 Aug 2022 14:18:12 +0300 Subject: [PATCH 05/29] Put send/poll methods on the session object --- .../SteamAuthentication.cs | 197 +++++++++--------- 1 file changed, 100 insertions(+), 97 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index bc19ca962..0fbdac4bd 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -85,127 +85,128 @@ public class AuthComplete public string AccessToken { get; set; } } - public class BeginAuthSessionResponse + public class AuthSession { + public SteamClient Client { get; internal set; } public ulong ClientID { get; set; } public byte[] RequestID { get; set; } public List AllowedConfirmations { get; set; } public TimeSpan PollingInterval { get; set; } - } - public sealed class QrBeginAuthSessionResponse : BeginAuthSessionResponse - { - public string ChallengeURL { get; set; } - } - - public sealed class CredentialsBeginAuthSessionResponse : BeginAuthSessionResponse - { - public SteamID SteamID { get; set; } - } - - public async Task StartPolling( BeginAuthSessionResponse baseResponse ) - { - // TODO: Sort by preferred methods? - foreach ( var allowedConfirmation in baseResponse.AllowedConfirmations ) + public async Task StartPolling() { - switch ( allowedConfirmation.confirmation_type ) + // TODO: Sort by preferred methods? + foreach ( var allowedConfirmation in AllowedConfirmations ) { - case EAuthSessionGuardType.k_EAuthSessionGuardType_None: - // no steam guard - // if we poll now we will get access token in response and send login to the cm - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: - // sent steam guard email at allowedConfirmation.associated_message - // use SendSteamGuardCode - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: - // totp code from mobile app - // use SendSteamGuardCode - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: - // TODO: is this accept prompt that automatically appears in the mobile app? - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: - // TODO: what is this? - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: - // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set - break; - + switch ( allowedConfirmation.confirmation_type ) + { + case EAuthSessionGuardType.k_EAuthSessionGuardType_None: + // no steam guard + // if we poll now we will get access token in response and send login to the cm + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: + // sent steam guard email at allowedConfirmation.associated_message + // use SendSteamGuardCode + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: + // totp code from mobile app + // use SendSteamGuardCode + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: + // TODO: is this accept prompt that automatically appears in the mobile app? + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: + // TODO: what is this? + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: + // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set + break; + + } } - } - while ( true ) - { - // TODO: For guard type none we don't need delay - await Task.Delay( baseResponse.PollingInterval ); + while ( true ) + { + // TODO: For guard type none we don't need delay + await Task.Delay( PollingInterval ); - var pollResponse = await PollAuthSessionStatus( baseResponse ); + var pollResponse = await PollAuthSessionStatus(); - if ( pollResponse.refresh_token.Length > 0 ) - { - return new AuthComplete + if ( pollResponse.refresh_token.Length > 0 ) { - AccessToken = pollResponse.access_token, - RefreshToken = pollResponse.refresh_token, - AccountName = pollResponse.account_name, - }; + return new AuthComplete + { + AccessToken = pollResponse.access_token, + RefreshToken = pollResponse.refresh_token, + AccountName = pollResponse.account_name, + }; + } } } - } - public async Task PollAuthSessionStatus( BeginAuthSessionResponse baseResponse ) - { - var request = new CAuthentication_PollAuthSessionStatus_Request + public async Task PollAuthSessionStatus() { - client_id = baseResponse.ClientID, - request_id = baseResponse.RequestID, - }; + var request = new CAuthentication_PollAuthSessionStatus_Request + { + client_id = ClientID, + request_id = RequestID, + }; - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.PollAuthSessionStatus( request ) ); - var response = message.GetDeserializedResponse(); + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.PollAuthSessionStatus( request ) ); + var response = message.GetDeserializedResponse(); - // eresult can be Expired, FileNotFound, Fail + // eresult can be Expired, FileNotFound, Fail - if ( response.new_client_id > 0 ) - { - baseResponse.ClientID = response.new_client_id; - } + if ( response.new_client_id > 0 ) + { + ClientID = response.new_client_id; + } - if ( baseResponse is QrBeginAuthSessionResponse qrResponse && response.new_challenge_url.Length > 0 ) - { - qrResponse.ChallengeURL = response.new_challenge_url; - } + if ( this is QrAuthSession qrResponse && response.new_challenge_url.Length > 0 ) + { + qrResponse.ChallengeURL = response.new_challenge_url; + } - return response; + return response; + } } - public async Task SendSteamGuardCode( CredentialsBeginAuthSessionResponse baseResponse, string code, EAuthSessionGuardType codeType ) + public sealed class QrAuthSession : AuthSession { - var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request - { - client_id = baseResponse.ClientID, - steamid = baseResponse.SteamID, - code = code, - code_type = codeType, - }; + public string ChallengeURL { get; set; } + } - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); - var response = message.GetDeserializedResponse(); + public sealed class CredentialsAuthSession : AuthSession + { + public SteamID SteamID { get; set; } - // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired - if ( message.Result != EResult.OK ) + public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeType ) { - throw new Exception( $"Failed to send steam guard code with result {message.Result}" ); + var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request + { + client_id = ClientID, + steamid = SteamID, + code = code, + code_type = codeType, + }; + + var unifiedMessages = Client.GetHandler()!; + var contentService = unifiedMessages.CreateService(); + var message = await contentService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); + var response = message.GetDeserializedResponse(); + + // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Failed to send steam guard code with result {message.Result}" ); + } } } @@ -238,7 +239,7 @@ public async Task GetPasswordR /// /// /// The details to use for logging on. - public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) + public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) { var request = new CAuthentication_BeginAuthSessionViaQR_Request { @@ -257,8 +258,9 @@ public async Task BeginAuthSessionViaQR( AuthSession var response = message.GetDeserializedResponse(); - var authResponse = new QrBeginAuthSessionResponse + var authResponse = new QrAuthSession { + Client = Client, ClientID = response.client_id, RequestID = response.request_id, AllowedConfirmations = response.allowed_confirmations, @@ -275,7 +277,7 @@ public async Task BeginAuthSessionViaQR( AuthSession /// The details to use for logging on. /// No auth details were provided. /// Username or password are not set within . - public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) + public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) { if ( details == null ) { @@ -323,8 +325,9 @@ public async Task BeginAuthSessionViaCreden var response = message.GetDeserializedResponse(); - var authResponse = new CredentialsBeginAuthSessionResponse + var authResponse = new CredentialsAuthSession { + Client = Client, ClientID = response.client_id, RequestID = response.request_id, AllowedConfirmations = response.allowed_confirmations, From d37363025d0774a91af410e2e2dc775f7cb1e46e Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 30 Aug 2022 14:54:50 +0300 Subject: [PATCH 06/29] Add authenticator object and implement 2FA auth --- .../SteamAuthentication/IAuthenticator.cs | 13 ++++++ .../SteamAuthentication.cs | 40 ++++++++++++++++--- .../UserConsoleAuthenticator.cs | 36 +++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs new file mode 100644 index 000000000..c8f849b57 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace SteamKit2 +{ + public interface IAuthenticator + { + public Task ProvideDeviceCode(); + public Task ProvideEmailCode(string email); + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 0fbdac4bd..92e049ff8 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -76,6 +76,11 @@ public sealed class AuthSessionDetails /// /// The website id. public string? WebsiteID { get; set; } + + /// + /// + /// + public IAuthenticator? Authenticator { get; set; } } public class AuthComplete @@ -88,6 +93,7 @@ public class AuthComplete public class AuthSession { public SteamClient Client { get; internal set; } + public IAuthenticator? Authenticator { get; set; } public ulong ClientID { get; set; } public byte[] RequestID { get; set; } public List AllowedConfirmations { get; set; } @@ -106,13 +112,33 @@ public async Task StartPolling() break; case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: - // sent steam guard email at allowedConfirmation.associated_message - // use SendSteamGuardCode - break; - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: - // totp code from mobile app - // use SendSteamGuardCode + if( !( this is CredentialsAuthSession credentialsAuthSession ) ) + { + throw new InvalidOperationException( $"Got {allowedConfirmation.confirmation_type} confirmation type in a session that is not {nameof(CredentialsAuthSession)}." ); + } + + if ( Authenticator == null ) + { + throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); + } + + var task = allowedConfirmation.confirmation_type switch + { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( allowedConfirmation.associated_message ), + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode(), + _ => throw new NotImplementedException(), + }; + + var code = await task; + + if ( string.IsNullOrEmpty( code ) ) + { + throw new InvalidOperationException( "No code was provided by the authenticator." ); + } + + await credentialsAuthSession.SendSteamGuardCode( code, allowedConfirmation.confirmation_type ); + break; case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: @@ -132,6 +158,7 @@ public async Task StartPolling() while ( true ) { + // TODO: Realistically we only need to poll for confirmation-based (like qr, or device confirm) types // TODO: For guard type none we don't need delay await Task.Delay( PollingInterval ); @@ -328,6 +355,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe var authResponse = new CredentialsAuthSession { Client = Client, + Authenticator = details.Authenticator, ClientID = response.client_id, RequestID = response.request_id, AllowedConfirmations = response.allowed_confirmations, diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs new file mode 100644 index 000000000..a7a210b06 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace SteamKit2 +{ + public class UserConsoleAuthenticator : IAuthenticator + { + public Task ProvideDeviceCode() + { + string? code; + + do + { + Console.Write( "STEAM GUARD! Please enter your 2 factor auth code from your authenticator app: " ); + code = Console.ReadLine()?.Trim(); + } + while ( string.IsNullOrEmpty( code ) ); + + return Task.FromResult( code! ); + } + + public Task ProvideEmailCode( string email ) + { + string? code; + + do + { + Console.Write( $"STEAM GUARD! Please enter the auth code sent to the email at {email}: " ); + code = Console.ReadLine()?.Trim(); + } + while ( string.IsNullOrEmpty( code ) ); + + return Task.FromResult( code! ); + } + } +} From a8add894cf00c4d018b2a3ed9df0b3ccb6509e11 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Tue, 30 Aug 2022 22:01:10 +0300 Subject: [PATCH 07/29] Sort by preferred auth confirmation --- .../SteamAuthentication.cs | 173 ++++++++++++------ 1 file changed, 119 insertions(+), 54 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 92e049ff8..1856d6106 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -83,7 +84,7 @@ public sealed class AuthSessionDetails public IAuthenticator? Authenticator { get; set; } } - public class AuthComplete + public class AuthPollResult { public string AccountName { get; set; } public string RefreshToken { get; set; } @@ -99,61 +100,80 @@ public class AuthSession public List AllowedConfirmations { get; set; } public TimeSpan PollingInterval { get; set; } - public async Task StartPolling() + public async Task StartPolling() { - // TODO: Sort by preferred methods? - foreach ( var allowedConfirmation in AllowedConfirmations ) + var pollLoop = false; + var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); + + if ( preferredConfirmation == null || preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown ) { - switch ( allowedConfirmation.confirmation_type ) - { - case EAuthSessionGuardType.k_EAuthSessionGuardType_None: - // no steam guard - // if we poll now we will get access token in response and send login to the cm - break; + throw new InvalidOperationException( "There are no allowed confirmations" ); + } + + switch ( preferredConfirmation.confirmation_type ) + { + case EAuthSessionGuardType.k_EAuthSessionGuardType_None: + // no steam guard + break; - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: - if( !( this is CredentialsAuthSession credentialsAuthSession ) ) - { - throw new InvalidOperationException( $"Got {allowedConfirmation.confirmation_type} confirmation type in a session that is not {nameof(CredentialsAuthSession)}." ); - } + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: + if ( !( this is CredentialsAuthSession credentialsAuthSession ) ) + { + throw new InvalidOperationException( $"Got {preferredConfirmation.confirmation_type} confirmation type in a session that is not {nameof( CredentialsAuthSession )}." ); + } + + if ( Authenticator == null ) + { + throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); + } + + var task = preferredConfirmation.confirmation_type switch + { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( preferredConfirmation.associated_message ), + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode(), + _ => throw new NotImplementedException(), + }; - if ( Authenticator == null ) - { - throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); - } + var code = await task; - var task = allowedConfirmation.confirmation_type switch - { - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( allowedConfirmation.associated_message ), - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode(), - _ => throw new NotImplementedException(), - }; + if ( string.IsNullOrEmpty( code ) ) + { + throw new InvalidOperationException( "No code was provided by the authenticator." ); + } - var code = await task; + await credentialsAuthSession.SendSteamGuardCode( code, preferredConfirmation.confirmation_type ); - if ( string.IsNullOrEmpty( code ) ) - { - throw new InvalidOperationException( "No code was provided by the authenticator." ); - } + break; - await credentialsAuthSession.SendSteamGuardCode( code, allowedConfirmation.confirmation_type ); + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: + // TODO: is this accept prompt that automatically appears in the mobile app? + pollLoop = true; + break; - break; + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: + // TODO: what is this? + pollLoop = true; + break; - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: - // TODO: is this accept prompt that automatically appears in the mobile app? - break; + case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: + // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set + throw new NotImplementedException( $"Machine token confirmation is not supported by SteamKit at the moment." ); - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: - // TODO: what is this? - break; + default: + throw new NotImplementedException( $"Unsupported confirmation type {preferredConfirmation.confirmation_type}." ); + } - case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: - // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set - break; + if ( !pollLoop ) + { + var pollResponse = await PollAuthSessionStatus(); + if ( pollResponse == null ) + { + throw new Exception( "Auth failed" ); } + + return pollResponse; } while ( true ) @@ -164,19 +184,14 @@ public async Task StartPolling() var pollResponse = await PollAuthSessionStatus(); - if ( pollResponse.refresh_token.Length > 0 ) + if( pollResponse != null ) { - return new AuthComplete - { - AccessToken = pollResponse.access_token, - RefreshToken = pollResponse.refresh_token, - AccountName = pollResponse.account_name, - }; + return pollResponse; } } } - public async Task PollAuthSessionStatus() + public async Task PollAuthSessionStatus() { var request = new CAuthentication_PollAuthSessionStatus_Request { @@ -187,9 +202,14 @@ public async Task PollAuthSessio var unifiedMessages = Client.GetHandler()!; var contentService = unifiedMessages.CreateService(); var message = await contentService.SendMessage( api => api.PollAuthSessionStatus( request ) ); - var response = message.GetDeserializedResponse(); // eresult can be Expired, FileNotFound, Fail + if ( message.Result != EResult.OK ) + { + throw new Exception( $"Failed to poll with result {message.Result}" ); + } + + var response = message.GetDeserializedResponse(); if ( response.new_client_id > 0 ) { @@ -201,7 +221,17 @@ public async Task PollAuthSessio qrResponse.ChallengeURL = response.new_challenge_url; } - return response; + if ( response.refresh_token.Length > 0 ) + { + return new AuthPollResult + { + AccessToken = response.access_token, + RefreshToken = response.refresh_token, + AccountName = response.account_name, + }; + } + + return null; } } @@ -290,7 +320,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai Client = Client, ClientID = response.client_id, RequestID = response.request_id, - AllowedConfirmations = response.allowed_confirmations, + AllowedConfirmations = SortConfirmations( response.allowed_confirmations ), PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), ChallengeURL = response.challenge_url, }; @@ -358,7 +388,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe Authenticator = details.Authenticator, ClientID = response.client_id, RequestID = response.request_id, - AllowedConfirmations = response.allowed_confirmations, + AllowedConfirmations = SortConfirmations( response.allowed_confirmations ), PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), SteamID = new SteamID( response.steamid ), }; @@ -374,5 +404,40 @@ public override void HandleMsg( IPacketMsg packetMsg ) { // not used } + + private static List SortConfirmations( List confirmations ) + { + /* + valve's preferred order: + 0. k_EAuthSessionGuardType_DeviceConfirmation = 4, poll + 1. k_EAuthSessionGuardType_DeviceCode = 3, no poll + 2. k_EAuthSessionGuardType_EmailCode = 2, no poll + 3. k_EAuthSessionGuardType_None = 1, instant poll + 4. k_EAuthSessionGuardType_Unknown = 0, + 5. k_EAuthSessionGuardType_EmailConfirmation = 5, poll + k_EAuthSessionGuardType_MachineToken = 6, checkdevice then instant poll + */ + var preferredConfirmationTypes = new EAuthSessionGuardType[] + { + EAuthSessionGuardType.k_EAuthSessionGuardType_None, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken, + EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown, + }; + var sortOrder = Enumerable.Range( 0, preferredConfirmationTypes.Length ).ToDictionary( x => preferredConfirmationTypes[ x ], x => x ); + + return confirmations.OrderBy( x => + { + if( sortOrder.TryGetValue( x.confirmation_type, out var sortIndex ) ) + { + return sortIndex; + } + + return int.MaxValue; + } ).ToList(); + } } } From 7603aa8c1438ad840025c27fe42509ef57889b6f Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 31 Aug 2022 21:32:40 +0300 Subject: [PATCH 08/29] Add AuthenticationException --- .../AuthenticationException.cs | 20 +++++++++++++++++++ .../SteamAuthentication.cs | 16 +++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs new file mode 100644 index 000000000..8bf57cef3 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SteamKit2 +{ + internal class AuthenticationException : Exception + { + /// + /// + /// + public EResult Result { get; private set; } + + public AuthenticationException( string message, EResult result ) + : base( $"{message} with result {result}." ) + { + Result = result; + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 1856d6106..23824422c 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -170,7 +170,7 @@ public async Task StartPolling() if ( pollResponse == null ) { - throw new Exception( "Auth failed" ); + throw new AuthenticationException( "Auth failed", EResult.Fail ); } return pollResponse; @@ -184,7 +184,7 @@ public async Task StartPolling() var pollResponse = await PollAuthSessionStatus(); - if( pollResponse != null ) + if ( pollResponse != null ) { return pollResponse; } @@ -206,7 +206,7 @@ public async Task StartPolling() // eresult can be Expired, FileNotFound, Fail if ( message.Result != EResult.OK ) { - throw new Exception( $"Failed to poll with result {message.Result}" ); + throw new AuthenticationException( "Failed to poll status", message.Result ); } var response = message.GetDeserializedResponse(); @@ -262,7 +262,7 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired if ( message.Result != EResult.OK ) { - throw new Exception( $"Failed to send steam guard code with result {message.Result}" ); + throw new AuthenticationException( "Failed to send steam guard code", message.Result ); } } } @@ -284,7 +284,7 @@ public async Task GetPasswordR if ( message.Result != EResult.OK ) { - throw new Exception( $"Failed to get password public key with result {message.Result}" ); + throw new AuthenticationException( "Failed to get password public key", message.Result ); } var response = message.GetDeserializedResponse(); @@ -310,7 +310,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai if ( message.Result != EResult.OK ) { - throw new Exception( $"Failed to begin QR auth session with result {message.Result}" ); + throw new AuthenticationException( "Failed to begin QR auth session", message.Result ); } var response = message.GetDeserializedResponse(); @@ -377,7 +377,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe // eresult can be InvalidPassword, ServiceUnavailable if ( message.Result != EResult.OK ) { - throw new Exception( $"Authentication failed with result {message.Result}" ); + throw new AuthenticationException( "Authentication failed", message.Result ); } var response = message.GetDeserializedResponse(); @@ -431,7 +431,7 @@ private static List SortConfirmations( List return confirmations.OrderBy( x => { - if( sortOrder.TryGetValue( x.confirmation_type, out var sortIndex ) ) + if ( sortOrder.TryGetValue( x.confirmation_type, out var sortIndex ) ) { return sortIndex; } From 6322eb34bf129debb9fcc323a3c5dfdbf051e0ea Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 3 Mar 2023 12:07:03 +0200 Subject: [PATCH 09/29] Add guard_data; add some comments --- .../SteamAuthentication.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 23824422c..54578808d 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -78,6 +78,12 @@ public sealed class AuthSessionDetails /// The website id. public string? WebsiteID { get; set; } + /// + /// Steam guard data for client login. Provide if available. + /// + /// The guard data. + public string? GuardData { get; set; } + /// /// /// @@ -86,18 +92,43 @@ public sealed class AuthSessionDetails public class AuthPollResult { + /// + /// Account name of authenticating account. + /// public string AccountName { get; set; } + /// + /// New refresh token. + /// public string RefreshToken { get; set; } + /// + /// New token subordinate to refresh_token. + /// public string AccessToken { get; set; } + /// + /// May contain remembered machine ID for future login. + /// + public string? NewGuardData { get; set; } } public class AuthSession { public SteamClient Client { get; internal set; } public IAuthenticator? Authenticator { get; set; } + /// + /// Unique identifier of requestor, also used for routing, portion of QR code. + /// public ulong ClientID { get; set; } + /// + /// Unique request ID to be presented by requestor at poll time. + /// public byte[] RequestID { get; set; } + /// + /// Confirmation types that will be able to confirm the request. + /// public List AllowedConfirmations { get; set; } + /// + /// Refresh interval with which requestor should call PollAuthSessionStatus. + /// public TimeSpan PollingInterval { get; set; } public async Task StartPolling() @@ -228,6 +259,7 @@ public async Task StartPolling() AccessToken = response.access_token, RefreshToken = response.refresh_token, AccountName = response.account_name, + NewGuardData = response.new_guard_data, }; } @@ -237,11 +269,17 @@ public async Task StartPolling() public sealed class QrAuthSession : AuthSession { + /// + /// URL based on client ID, which can be rendered as QR code. + /// public string ChallengeURL { get; set; } } public sealed class CredentialsAuthSession : AuthSession { + /// + /// SteamID of the account logging in, will only be included if the credentials were correct. + /// public SteamID SteamID { get; set; } public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeType ) @@ -366,6 +404,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe account_name = details.Username, persistence = details.Persistence, website_id = details.WebsiteID, + guard_data = details.GuardData, encrypted_password = Convert.ToBase64String( encryptedPassword ), encryption_timestamp = publicKey.timestamp, }; From 8c7b10f2490e831b58c3598b1b36a5ef081b95b7 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sat, 11 Mar 2023 12:27:48 +0200 Subject: [PATCH 10/29] Add authentication sample --- Samples/1a.Authentication/Program.cs | 108 ++++++++++++++++++ .../Sample1a_Authentication.csproj | 18 +++ Samples/Samples.sln | 44 ++++--- .../SteamAuthentication/IAuthenticator.cs | 17 ++- .../SteamAuthentication.cs | 37 ++++++ .../UserConsoleAuthenticator.cs | 12 ++ 6 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 Samples/1a.Authentication/Program.cs create mode 100644 Samples/1a.Authentication/Sample1a_Authentication.csproj diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs new file mode 100644 index 000000000..d154a3799 --- /dev/null +++ b/Samples/1a.Authentication/Program.cs @@ -0,0 +1,108 @@ +using System; + +using SteamKit2; + +if ( args.Length < 2 ) +{ + Console.WriteLine( "Sample1: No username and password specified!" ); + return; +} + +// save our logon details +var user = args[ 0 ]; +var pass = args[ 1 ]; + +// create our steamclient instance +var steamClient = new SteamClient(); +// create the callback manager which will route callbacks to function calls +var manager = new CallbackManager( steamClient ); + +// get the authentication handler, which used for authenticating with Steam +var auth = steamClient.GetHandler(); + +// get the steamuser handler, which is used for logging on after successfully connecting +var steamUser = steamClient.GetHandler(); + +// register a few callbacks we're interested in +// these are registered upon creation to a callback manager, which will then route the callbacks +// to the functions specified +manager.Subscribe( OnConnected ); +manager.Subscribe( OnDisconnected ); + +manager.Subscribe( OnLoggedOn ); +manager.Subscribe( OnLoggedOff ); + +var isRunning = true; + +Console.WriteLine( "Connecting to Steam..." ); + +// initiate the connection +steamClient.Connect(); + +// create our callback handling loop +while ( isRunning ) +{ + // in order for the callbacks to get routed, they need to be handled by the manager + manager.RunWaitCallbacks( TimeSpan.FromSeconds( 1 ) ); +} + +async void OnConnected( SteamClient.ConnectedCallback callback ) +{ + Console.WriteLine( "Connected to Steam! Logging in '{0}'...", user ); + + /* + var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails() ); + + Console.WriteLine( $"QR Link: {authSession.ChallengeURL}" ); + */ + + // Begin authenticating via credentials + var authSession = await auth.BeginAuthSessionViaCredentials( new SteamAuthentication.AuthSessionDetails + { + Username = user, + Password = pass, + Persistence = SteamKit2.Internal.ESessionPersistence.k_ESessionPersistence_Ephemeral, + WebsiteID = "Client", + Authenticator = new UserConsoleAuthenticator(), + } ); + + // Starting polling Steam for authentication response + var pollResponse = await authSession.StartPolling(); + + // Logon to Steam with the access token we have received + steamUser.LogOn( new SteamUser.LogOnDetails + { + Username = pollResponse.AccountName, + AccessToken = pollResponse.RefreshToken, + } ); +} + +void OnDisconnected( SteamClient.DisconnectedCallback callback ) +{ + Console.WriteLine( "Disconnected from Steam" ); + + isRunning = false; +} + +void OnLoggedOn( SteamUser.LoggedOnCallback callback ) +{ + if ( callback.Result != EResult.OK ) + { + Console.WriteLine( "Unable to logon to Steam: {0} / {1}", callback.Result, callback.ExtendedResult ); + + isRunning = false; + return; + } + + Console.WriteLine( "Successfully logged on!" ); + + // at this point, we'd be able to perform actions on Steam + + // for this sample we'll just log off + steamUser.LogOff(); +} + +void OnLoggedOff( SteamUser.LoggedOffCallback callback ) +{ + Console.WriteLine( "Logged off of Steam: {0}", callback.Result ); +} diff --git a/Samples/1a.Authentication/Sample1a_Authentication.csproj b/Samples/1a.Authentication/Sample1a_Authentication.csproj new file mode 100644 index 000000000..4f3903cc5 --- /dev/null +++ b/Samples/1a.Authentication/Sample1a_Authentication.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + SteamRE + Sample1a_Authentication.csproj + + SteamKit Sample 1a: Authentication + Copyright © Pavel Djundik 2023 + Sample1a_Authentication.csproj + + + + + + + diff --git a/Samples/Samples.sln b/Samples/Samples.sln index 64b17b112..a3fefc603 100644 --- a/Samples/Samples.sln +++ b/Samples/Samples.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2003 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1_Logon", "1.Logon\Sample1_Logon.csproj", "{CEF39496-576D-4A70-9A06-16112B84B79F}" EndProject @@ -23,7 +23,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample9_AsyncJobs", "9.Asyn EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample10_DotaMatchRequest", "10.DotaMatchRequest\Sample10_DotaMatchRequest.csproj", "{734863D3-4EED-4758-B1E9-7B324C2D8D72}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamKit2", "..\SteamKit2\SteamKit2\SteamKit2.csproj", "{D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamKit2", "..\SteamKit2\SteamKit2\SteamKit2.csproj", "{4B2B0365-DE37-4B65-B614-3E4E7C05147D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1a_Authentication", "1a.Authentication\Sample1a_Authentication.csproj", "{C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -155,18 +157,30 @@ Global {734863D3-4EED-4758-B1E9-7B324C2D8D72}.Release|Mixed Platforms.Build.0 = Release|Any CPU {734863D3-4EED-4758-B1E9-7B324C2D8D72}.Release|x86.ActiveCfg = Release|Any CPU {734863D3-4EED-4758-B1E9-7B324C2D8D72}.Release|x86.Build.0 = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|x86.ActiveCfg = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Debug|x86.Build.0 = Debug|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|Any CPU.Build.0 = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|x86.ActiveCfg = Release|Any CPU - {D3E2F2D3-A663-4232-B9C5-A4F81DB665A2}.Release|x86.Build.0 = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|x86.ActiveCfg = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Debug|x86.Build.0 = Debug|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|Any CPU.Build.0 = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|x86.ActiveCfg = Release|Any CPU + {4B2B0365-DE37-4B65-B614-3E4E7C05147D}.Release|x86.Build.0 = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Debug|x86.Build.0 = Debug|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|Any CPU.Build.0 = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|x86.ActiveCfg = Release|Any CPU + {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs index c8f849b57..a386f9d26 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs @@ -1,13 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; namespace SteamKit2 { + /// + /// + /// public interface IAuthenticator { + /// + /// + /// + /// public Task ProvideDeviceCode(); + /// + /// + /// + /// + /// public Task ProvideEmailCode(string email); } } diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 54578808d..39e5212a4 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -90,6 +90,9 @@ public sealed class AuthSessionDetails public IAuthenticator? Authenticator { get; set; } } + /// + /// + /// public class AuthPollResult { /// @@ -110,9 +113,18 @@ public class AuthPollResult public string? NewGuardData { get; set; } } + /// + /// + /// public class AuthSession { + /// + /// + /// public SteamClient Client { get; internal set; } + /// + /// + /// public IAuthenticator? Authenticator { get; set; } /// /// Unique identifier of requestor, also used for routing, portion of QR code. @@ -131,6 +143,13 @@ public class AuthSession /// public TimeSpan PollingInterval { get; set; } + /// + /// + /// + /// + /// + /// + /// public async Task StartPolling() { var pollLoop = false; @@ -222,6 +241,11 @@ public async Task StartPolling() } } + /// + /// + /// + /// + /// public async Task PollAuthSessionStatus() { var request = new CAuthentication_PollAuthSessionStatus_Request @@ -267,6 +291,9 @@ public async Task StartPolling() } } + /// + /// + /// public sealed class QrAuthSession : AuthSession { /// @@ -275,6 +302,9 @@ public sealed class QrAuthSession : AuthSession public string ChallengeURL { get; set; } } + /// + /// + /// public sealed class CredentialsAuthSession : AuthSession { /// @@ -282,6 +312,13 @@ public sealed class CredentialsAuthSession : AuthSession /// public SteamID SteamID { get; set; } + /// + /// + /// + /// + /// + /// + /// public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeType ) { var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs index a7a210b06..61f9b3267 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs @@ -3,8 +3,15 @@ namespace SteamKit2 { + /// + /// + /// public class UserConsoleAuthenticator : IAuthenticator { + /// + /// + /// + /// public Task ProvideDeviceCode() { string? code; @@ -19,6 +26,11 @@ public Task ProvideDeviceCode() return Task.FromResult( code! ); } + /// + /// + /// + /// + /// public Task ProvideEmailCode( string email ) { string? code; From f92bac394efdd572e9638fb3acfd82d0002299cc Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Sun, 12 Mar 2023 11:17:08 +0200 Subject: [PATCH 11/29] Change enum to IsPersistentSession bool --- Samples/1a.Authentication/Program.cs | 2 +- .../Sample1a_Authentication.csproj | 4 ++-- .../SteamAuthentication/SteamAuthentication.cs | 17 +++++------------ 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index d154a3799..d1e9eacd0 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -61,7 +61,7 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { Username = user, Password = pass, - Persistence = SteamKit2.Internal.ESessionPersistence.k_ESessionPersistence_Ephemeral, + IsPersistentSession = true, WebsiteID = "Client", Authenticator = new UserConsoleAuthenticator(), } ); diff --git a/Samples/1a.Authentication/Sample1a_Authentication.csproj b/Samples/1a.Authentication/Sample1a_Authentication.csproj index 4f3903cc5..e5aa29cd0 100644 --- a/Samples/1a.Authentication/Sample1a_Authentication.csproj +++ b/Samples/1a.Authentication/Sample1a_Authentication.csproj @@ -4,11 +4,11 @@ Exe net6.0 SteamRE - Sample1a_Authentication.csproj + Sample1a_Authentication SteamKit Sample 1a: Authentication Copyright © Pavel Djundik 2023 - Sample1a_Authentication.csproj + Sample1a_Authentication diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 39e5212a4..1173fdd4b 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -70,7 +70,7 @@ public sealed class AuthSessionDetails /// Gets or sets the session persistence. /// /// The persistence. - public ESessionPersistence Persistence { get; set; } = ESessionPersistence.k_ESessionPersistence_Persistent; + public bool IsPersistentSession { get; set; } = true; /// /// Gets or sets the website id that the login will be performed for. (EMachineAuthWebDomain) @@ -439,7 +439,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe platform_type = details.PlatformType, device_friendly_name = details.DeviceFriendlyName, account_name = details.Username, - persistence = details.Persistence, + persistence = details.IsPersistentSession ? ESessionPersistence.k_ESessionPersistence_Persistent : ESessionPersistence.k_ESessionPersistence_Ephemeral, website_id = details.WebsiteID, guard_data = details.GuardData, encrypted_password = Convert.ToBase64String( encryptedPassword ), @@ -481,18 +481,11 @@ public override void HandleMsg( IPacketMsg packetMsg ) // not used } + /// + /// Sort available guard confirmation methods by an order that we prefer to handle them in + /// private static List SortConfirmations( List confirmations ) { - /* - valve's preferred order: - 0. k_EAuthSessionGuardType_DeviceConfirmation = 4, poll - 1. k_EAuthSessionGuardType_DeviceCode = 3, no poll - 2. k_EAuthSessionGuardType_EmailCode = 2, no poll - 3. k_EAuthSessionGuardType_None = 1, instant poll - 4. k_EAuthSessionGuardType_Unknown = 0, - 5. k_EAuthSessionGuardType_EmailConfirmation = 5, poll - k_EAuthSessionGuardType_MachineToken = 6, checkdevice then instant poll - */ var preferredConfirmationTypes = new EAuthSessionGuardType[] { EAuthSessionGuardType.k_EAuthSessionGuardType_None, From b7d393f64af806183a7585dd568d93b5a93f7f29 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Mon, 13 Mar 2023 11:06:37 +0200 Subject: [PATCH 12/29] Add cancellation token to polling --- Samples/1a.Authentication/Program.cs | 2 +- .../Handlers/SteamAuthentication/SteamAuthentication.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index d1e9eacd0..690e3a8b7 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -61,7 +61,7 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { Username = user, Password = pass, - IsPersistentSession = true, + IsPersistentSession = false, WebsiteID = "Client", Authenticator = new UserConsoleAuthenticator(), } ); diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 1173fdd4b..6f7a04c93 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; using SteamKit2.Internal; @@ -150,7 +151,7 @@ public class AuthSession /// /// /// - public async Task StartPolling() + public async Task StartPolling( CancellationToken? cancellationToken = null) { var pollLoop = false; var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); @@ -228,6 +229,8 @@ public async Task StartPolling() while ( true ) { + cancellationToken?.ThrowIfCancellationRequested(); + // TODO: Realistically we only need to poll for confirmation-based (like qr, or device confirm) types // TODO: For guard type none we don't need delay await Task.Delay( PollingInterval ); From ec96a7334c7343be02199176e9d49935adc4e92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Domeradzki?= Date: Wed, 15 Mar 2023 17:49:55 +0100 Subject: [PATCH 13/29] Make `AuthenticationException` public `StartPolling()` can throw `AuthenticationException ` e.g. in rate limited case, if we want to allow callers to handle this (e.g. by adding delay before next try), we must make it public. --- .../Handlers/SteamAuthentication/AuthenticationException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs index 8bf57cef3..657bf29ed 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs @@ -4,7 +4,7 @@ namespace SteamKit2 { - internal class AuthenticationException : Exception + public class AuthenticationException : Exception { /// /// From 36872184517c5017cffc0a601eac0b983cc1593b Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 15 Mar 2023 20:03:22 +0200 Subject: [PATCH 14/29] Allow consumers to decide whether they want to poll for device confirmation --- .../SteamAuthentication/IAuthenticator.cs | 22 +++++++++---- .../SteamAuthentication.cs | 32 ++++++++++++++++--- .../UserConsoleAuthenticator.cs | 23 +++++++------ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs index a386f9d26..b0ca95bd3 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace SteamKit2 { @@ -8,15 +9,24 @@ namespace SteamKit2 public interface IAuthenticator { /// - /// + /// This method is called when the account being logged into requires 2-factor authentication using the authenticator app. /// - /// + /// The 2-factor auth code used to login. This is the code that can be received from the authenticator app. public Task ProvideDeviceCode(); + /// - /// + /// This method is called when the account being logged into uses Steam Guard email authentication. This code is sent to the user's email. /// - /// - /// + /// The email address that the Steam Guard email was sent to. + /// The Steam Guard auth code used to login. public Task ProvideEmailCode(string email); + + /// + /// This method is called when the account being logged has the Steam Mobile App and accepts authentication notification prompts. + /// + /// Return false if you want to fallback to entering a code instead. + /// + /// Return true to poll until the authentication is accepted, return false to fallback to entering a code. + public Task AcceptDeviceConfirmation(); } } diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 6f7a04c93..781a2b1e2 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -151,7 +151,7 @@ public class AuthSession /// /// /// - public async Task StartPolling( CancellationToken? cancellationToken = null) + public async Task StartPolling( CancellationToken? cancellationToken = null ) { var pollLoop = false; var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); @@ -161,12 +161,30 @@ public async Task StartPolling( CancellationToken? cancellationT throw new InvalidOperationException( "There are no allowed confirmations" ); } + // If an authenticator is provided and we device confirmation is available, allow consumers to choose whether they want to + // simply poll until confirmation is accepted, or whether they want to fallback to the next preferred confirmation type. + if ( Authenticator != null && preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation ) + { + var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmation(); + + if ( !prefersToPollForConfirmation ) + { + if ( AllowedConfirmations.Count <= 1 ) + { + throw new InvalidOperationException( "AcceptDeviceConfirmation returned false which indicates a fallback to another confirmation type, but there are no other confirmation types available." ); + } + + preferredConfirmation = AllowedConfirmations[ 1 ]; + } + } + switch ( preferredConfirmation.confirmation_type ) { + // No steam guard case EAuthSessionGuardType.k_EAuthSessionGuardType_None: - // no steam guard break; + // 2-factor code from the authenticator app or sent to an email case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: if ( !( this is CredentialsAuthSession credentialsAuthSession ) ) @@ -197,11 +215,12 @@ public async Task StartPolling( CancellationToken? cancellationT break; + // This is a prompt that appears in the Steam mobile app case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: - // TODO: is this accept prompt that automatically appears in the mobile app? pollLoop = true; break; + /* case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: // TODO: what is this? pollLoop = true; @@ -210,6 +229,7 @@ public async Task StartPolling( CancellationToken? cancellationT case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set throw new NotImplementedException( $"Machine token confirmation is not supported by SteamKit at the moment." ); + */ default: throw new NotImplementedException( $"Unsupported confirmation type {preferredConfirmation.confirmation_type}." ); @@ -217,6 +237,8 @@ public async Task StartPolling( CancellationToken? cancellationT if ( !pollLoop ) { + cancellationToken?.ThrowIfCancellationRequested(); + var pollResponse = await PollAuthSessionStatus(); if ( pollResponse == null ) @@ -231,10 +253,10 @@ public async Task StartPolling( CancellationToken? cancellationT { cancellationToken?.ThrowIfCancellationRequested(); - // TODO: Realistically we only need to poll for confirmation-based (like qr, or device confirm) types - // TODO: For guard type none we don't need delay await Task.Delay( PollingInterval ); + cancellationToken?.ThrowIfCancellationRequested(); + var pollResponse = await PollAuthSessionStatus(); if ( pollResponse != null ) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs index 61f9b3267..f67fafb88 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs @@ -4,14 +4,13 @@ namespace SteamKit2 { /// - /// + /// This is a default implementation of to ease of use. + /// + /// This implementation will prompt user to enter 2-factor authentication codes in the console. /// public class UserConsoleAuthenticator : IAuthenticator { - /// - /// - /// - /// + /// public Task ProvideDeviceCode() { string? code; @@ -26,11 +25,7 @@ public Task ProvideDeviceCode() return Task.FromResult( code! ); } - /// - /// - /// - /// - /// + /// public Task ProvideEmailCode( string email ) { string? code; @@ -44,5 +39,13 @@ public Task ProvideEmailCode( string email ) return Task.FromResult( code! ); } + + /// + public Task AcceptDeviceConfirmation() + { + Console.WriteLine( "STEAM GUARD! Use the Steam Mobile App to confirm your sign in..." ); + + return Task.FromResult( true ); + } } } From 5767a157aafa46d973242d2b7e4dbee9d7af24b1 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 15 Mar 2023 20:23:35 +0200 Subject: [PATCH 15/29] Add a sample for authenticating using qr codes --- Samples/1a.Authentication/Program.cs | 6 -- Samples/1b.QrCodeAuthentication/Program.cs | 100 ++++++++++++++++++ .../Sample1b_QrCodeAuthentication.csproj | 22 ++++ Samples/Samples.sln | 18 +++- 4 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 Samples/1b.QrCodeAuthentication/Program.cs create mode 100644 Samples/1b.QrCodeAuthentication/Sample1b_QrCodeAuthentication.csproj diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index 690e3a8b7..00b475c9a 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -50,12 +50,6 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { Console.WriteLine( "Connected to Steam! Logging in '{0}'...", user ); - /* - var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails() ); - - Console.WriteLine( $"QR Link: {authSession.ChallengeURL}" ); - */ - // Begin authenticating via credentials var authSession = await auth.BeginAuthSessionViaCredentials( new SteamAuthentication.AuthSessionDetails { diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs new file mode 100644 index 000000000..14f0bee5d --- /dev/null +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -0,0 +1,100 @@ +using System; +using QRCoder; +using SteamKit2; + +// create our steamclient instance +var steamClient = new SteamClient(); +// create the callback manager which will route callbacks to function calls +var manager = new CallbackManager( steamClient ); + +// get the authentication handler, which used for authenticating with Steam +var auth = steamClient.GetHandler(); + +// get the steamuser handler, which is used for logging on after successfully connecting +var steamUser = steamClient.GetHandler(); + +// register a few callbacks we're interested in +// these are registered upon creation to a callback manager, which will then route the callbacks +// to the functions specified +manager.Subscribe( OnConnected ); +manager.Subscribe( OnDisconnected ); + +manager.Subscribe( OnLoggedOn ); +manager.Subscribe( OnLoggedOff ); + +var isRunning = true; + +Console.WriteLine( "Connecting to Steam..." ); + +// initiate the connection +steamClient.Connect(); + +// create our callback handling loop +while ( isRunning ) +{ + // in order for the callbacks to get routed, they need to be handled by the manager + manager.RunWaitCallbacks( TimeSpan.FromSeconds( 1 ) ); +} + +async void OnConnected( SteamClient.ConnectedCallback callback ) +{ + // Start an authentication session by requesting a link + var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails + { + DeviceFriendlyName = "SteamKit Sample" + } ); + + Console.WriteLine( $"QR Link: {authSession.ChallengeURL}" ); + Console.WriteLine(); + + // Encode the link as a QR code + var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode( authSession.ChallengeURL, QRCodeGenerator.ECCLevel.L ); + var qrCode = new AsciiQRCode( qrCodeData ); + var qrCodeAsAsciiArt = qrCode.GetGraphic( 1, drawQuietZones: false ); + + Console.WriteLine( "Use the Steam Mobile App to sign in via QR code:" ); + Console.WriteLine( qrCodeAsAsciiArt ); + + // Starting polling Steam for authentication response + var pollResponse = await authSession.StartPolling(); + + Console.WriteLine( $"Logging in as '{pollResponse.AccountName}'..." ); + + // Logon to Steam with the access token we have received + steamUser.LogOn( new SteamUser.LogOnDetails + { + Username = pollResponse.AccountName, + AccessToken = pollResponse.RefreshToken, + } ); +} + +void OnDisconnected( SteamClient.DisconnectedCallback callback ) +{ + Console.WriteLine( "Disconnected from Steam" ); + + isRunning = false; +} + +void OnLoggedOn( SteamUser.LoggedOnCallback callback ) +{ + if ( callback.Result != EResult.OK ) + { + Console.WriteLine( "Unable to logon to Steam: {0} / {1}", callback.Result, callback.ExtendedResult ); + + isRunning = false; + return; + } + + Console.WriteLine( "Successfully logged on!" ); + + // at this point, we'd be able to perform actions on Steam + + // for this sample we'll just log off + steamUser.LogOff(); +} + +void OnLoggedOff( SteamUser.LoggedOffCallback callback ) +{ + Console.WriteLine( "Logged off of Steam: {0}", callback.Result ); +} diff --git a/Samples/1b.QrCodeAuthentication/Sample1b_QrCodeAuthentication.csproj b/Samples/1b.QrCodeAuthentication/Sample1b_QrCodeAuthentication.csproj new file mode 100644 index 000000000..4aef5b5af --- /dev/null +++ b/Samples/1b.QrCodeAuthentication/Sample1b_QrCodeAuthentication.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + SteamRE + Sample1b_QrCodeAuthentication + + SteamKit Sample 1b: Authentication using QR codes + Copyright © Pavel Djundik 2023 + Sample1b_QrCodeAuthentication + + + + + + + + + + + diff --git a/Samples/Samples.sln b/Samples/Samples.sln index a3fefc603..fddf1ae7c 100644 --- a/Samples/Samples.sln +++ b/Samples/Samples.sln @@ -5,6 +5,10 @@ VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1_Logon", "1.Logon\Sample1_Logon.csproj", "{CEF39496-576D-4A70-9A06-16112B84B79F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1a_Authentication", "1a.Authentication\Sample1a_Authentication.csproj", "{C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1b_QrCodeAuthentication", "1b.QrCodeAuthentication\Sample1b_QrCodeAuthentication.csproj", "{EFC8F224-9441-48D0-8FEE-2FC9F948837C}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample2_Extending", "2.Extending\Sample2_Extending.csproj", "{B8D7F87B-DBAA-4FBE-8254-E1FE07D6C7DC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample3_DebugLog", "3.DebugLog\Sample3_DebugLog.csproj", "{808EAE9B-B9F6-4692-8F5A-9E2A703BF8CE}" @@ -25,8 +29,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample10_DotaMatchRequest", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SteamKit2", "..\SteamKit2\SteamKit2\SteamKit2.csproj", "{4B2B0365-DE37-4B65-B614-3E4E7C05147D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample1a_Authentication", "1a.Authentication\Sample1a_Authentication.csproj", "{C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -181,6 +183,18 @@ Global {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|x86.ActiveCfg = Release|Any CPU {C6FC3BF7-BF99-46FE-98F3-56C7C123BDC5}.Release|x86.Build.0 = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|x86.ActiveCfg = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Debug|x86.Build.0 = Debug|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|Any CPU.Build.0 = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|x86.ActiveCfg = Release|Any CPU + {EFC8F224-9441-48D0-8FEE-2FC9F948837C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 1c2cc7a88077d5d7b179de4a3a6d040223ecfdb0 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 15 Mar 2023 20:48:07 +0200 Subject: [PATCH 16/29] Add more comments --- .../AuthenticationException.cs | 12 +++-- .../SteamAuthentication.cs | 49 ++++++------------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs index 657bf29ed..0343ace3f 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs @@ -1,16 +1,22 @@ using System; -using System.Collections.Generic; -using System.Text; namespace SteamKit2 { + /// + /// Thrown when fails to authenticate. + /// public class AuthenticationException : Exception { /// - /// + /// Gets the result of the authentication request. /// public EResult Result { get; private set; } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The result code that describes the error. public AuthenticationException( string message, EResult result ) : base( $"{message} with result {result}." ) { diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 781a2b1e2..d24826eec 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -19,26 +19,6 @@ namespace SteamKit2 /// public sealed class SteamAuthentication : ClientMsgHandler { - /* EPasswordLoginSessionStatus - Unstarted: 0, - Starting: 1, - InvalidCredentials: 2, - WaitingForEmailCode: 3, - WaitingForEmailConfirmation: 4, - WaitingForDeviceCode: 5, - WaitingForDeviceConfirmation: 6, - StartMoveAuthenticator: 7, - WaitingForMoveCode: 8, - AuthenticatorMoved: 9, - InvalidEmailCode: 10, - InvalidDeviceCode: 11, - InvalidMoveCode: 12, - WaitingForToken: 13, - Success: 14, - Failure: 15, - Stopped: 16, - */ - /// /// Represents the details required to authenticate on Steam. /// @@ -86,8 +66,10 @@ public sealed class AuthSessionDetails public string? GuardData { get; set; } /// - /// + /// Authenticator object which will be used to handle 2-factor authentication if necessary. + /// Use for a default implementation. /// + /// The authenticator object. public IAuthenticator? Authenticator { get; set; } } @@ -115,7 +97,7 @@ public class AuthPollResult } /// - /// + /// Represents an authentication sesssion which can be used to finish the authentication and get access tokens. /// public class AuthSession { @@ -124,7 +106,7 @@ public class AuthSession /// public SteamClient Client { get; internal set; } /// - /// + /// Authenticator object which will be used to handle 2-factor authentication if necessary. /// public IAuthenticator? Authenticator { get; set; } /// @@ -145,12 +127,11 @@ public class AuthSession public TimeSpan PollingInterval { get; set; } /// - /// + /// Handle any 2-factor authentication, and if necessary poll for updates until authentication succeeds. /// - /// - /// - /// - /// + /// An object containing tokens which can be used to login to Steam. + /// Thrown when an invalid state occurs, such as no supported confirmation methods are available. + /// Thrown when polling fails. public async Task StartPolling( CancellationToken? cancellationToken = null ) { var pollLoop = false; @@ -243,7 +224,7 @@ public async Task StartPolling( CancellationToken? cancellationT if ( pollResponse == null ) { - throw new AuthenticationException( "Auth failed", EResult.Fail ); + throw new AuthenticationException( "Authentication failed", EResult.Fail ); } return pollResponse; @@ -267,10 +248,10 @@ public async Task StartPolling( CancellationToken? cancellationT } /// - /// + /// Polls for authentication status once. Prefer using instead. /// - /// - /// + /// An object containing tokens which can be used to login to Steam, or null if not yet authenticated. + /// Thrown when polling fails. public async Task PollAuthSessionStatus() { var request = new CAuthentication_PollAuthSessionStatus_Request @@ -393,7 +374,7 @@ public async Task GetPasswordR } /// - /// + /// Start the authentication process using QR codes. /// /// The details to use for logging on. public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) @@ -429,7 +410,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai } /// - /// + /// Start the authentication process by providing username and password. /// /// The details to use for logging on. /// No auth details were provided. From 8d10d5a253c3813194300c76cdcb8b01c06651b4 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 14:13:27 +0200 Subject: [PATCH 17/29] Cleanup, add qr challenge url change callback, add os type --- Samples/1b.QrCodeAuthentication/Program.cs | 39 +++-- .../SteamAuthentication.cs | 144 ++++++++++-------- 2 files changed, 110 insertions(+), 73 deletions(-) diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs index 14f0bee5d..6cf63e80d 100644 --- a/Samples/1b.QrCodeAuthentication/Program.cs +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -39,24 +39,22 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { // Start an authentication session by requesting a link - var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails - { - DeviceFriendlyName = "SteamKit Sample" - } ); + var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails() ); - Console.WriteLine( $"QR Link: {authSession.ChallengeURL}" ); - Console.WriteLine(); + // Steam will periodically refresh the challenge url, this callback allows you to draw a new qr code + authSession.ChallengeURLChanged = () => + { + Console.WriteLine(); + Console.WriteLine( "Steam has refreshed the challenge url" ); - // Encode the link as a QR code - var qrGenerator = new QRCodeGenerator(); - var qrCodeData = qrGenerator.CreateQrCode( authSession.ChallengeURL, QRCodeGenerator.ECCLevel.L ); - var qrCode = new AsciiQRCode( qrCodeData ); - var qrCodeAsAsciiArt = qrCode.GetGraphic( 1, drawQuietZones: false ); + DrawQRCode( authSession ); + }; - Console.WriteLine( "Use the Steam Mobile App to sign in via QR code:" ); - Console.WriteLine( qrCodeAsAsciiArt ); + // Draw current qr right away + DrawQRCode( authSession ); // Starting polling Steam for authentication response + // This response is later used to logon to Steam after connecting var pollResponse = await authSession.StartPolling(); Console.WriteLine( $"Logging in as '{pollResponse.AccountName}'..." ); @@ -98,3 +96,18 @@ void OnLoggedOff( SteamUser.LoggedOffCallback callback ) { Console.WriteLine( "Logged off of Steam: {0}", callback.Result ); } + +void DrawQRCode( SteamAuthentication.QrAuthSession authSession ) +{ + Console.WriteLine( $"Challenge URL: {authSession.ChallengeURL}" ); + Console.WriteLine(); + + // Encode the link as a QR code + var qrGenerator = new QRCodeGenerator(); + var qrCodeData = qrGenerator.CreateQrCode( authSession.ChallengeURL, QRCodeGenerator.ECCLevel.L ); + var qrCode = new AsciiQRCode( qrCodeData ); + var qrCodeAsAsciiArt = qrCode.GetGraphic( 1, drawQuietZones: false ); + + Console.WriteLine( "Use the Steam Mobile App to sign in via QR code:" ); + Console.WriteLine( qrCodeAsAsciiArt ); +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index d24826eec..86a791a6f 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -40,24 +40,31 @@ public sealed class AuthSessionDetails /// Gets or sets the device name (or user agent). /// /// The device name. - public string? DeviceFriendlyName { get; set; } + public string? DeviceFriendlyName { get; set; } = $"{Environment.MachineName} (SteamKit2)"; /// /// Gets or sets the platform type that the login will be performed for. /// public EAuthTokenPlatformType PlatformType { get; set; } = EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient; + /// + /// Gets or sets the client operating system type. + /// + /// The client operating system type. + public EOSType ClientOSType { get; set; } = Utils.GetOSType(); + /// /// Gets or sets the session persistence. /// /// The persistence. - public bool IsPersistentSession { get; set; } = true; + public bool IsPersistentSession { get; set; } = false; /// - /// Gets or sets the website id that the login will be performed for. (EMachineAuthWebDomain) + /// Gets or sets the website id that the login will be performed for. + /// Can be "Unknown", "Client", "Website", "Store", "Community" /// /// The website id. - public string? WebsiteID { get; set; } + public string? WebsiteID { get; set; } = "Client"; /// /// Steam guard data for client login. Provide if available. @@ -94,6 +101,14 @@ public class AuthPollResult /// May contain remembered machine ID for future login. /// public string? NewGuardData { get; set; } + + internal AuthPollResult( CAuthentication_PollAuthSessionStatus_Response response ) + { + AccessToken = response.access_token; + RefreshToken = response.refresh_token; + AccountName = response.account_name; + NewGuardData = response.new_guard_data; + } } /// @@ -101,10 +116,7 @@ public class AuthPollResult /// public class AuthSession { - /// - /// - /// - public SteamClient Client { get; internal set; } + internal SteamUnifiedMessages.UnifiedService AuthenticationService { get; private set; } /// /// Authenticator object which will be used to handle 2-factor authentication if necessary. /// @@ -126,6 +138,16 @@ public class AuthSession /// public TimeSpan PollingInterval { get; set; } + internal AuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, ulong clientId, byte[] requestId, List allowedConfirmations, float pollingInterval ) + { + AuthenticationService = authenticationService; + Authenticator = authenticator; + ClientID = clientId; + RequestID = requestId; + AllowedConfirmations = SortConfirmations( allowedConfirmations ); + PollingInterval = TimeSpan.FromSeconds( ( double )pollingInterval ); + } + /// /// Handle any 2-factor authentication, and if necessary poll for updates until authentication succeeds. /// @@ -260,9 +282,7 @@ public async Task StartPolling( CancellationToken? cancellationT request_id = RequestID, }; - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.PollAuthSessionStatus( request ) ); + var message = await AuthenticationService.SendMessage( api => api.PollAuthSessionStatus( request ) ); // eresult can be Expired, FileNotFound, Fail if ( message.Result != EResult.OK ) @@ -280,17 +300,12 @@ public async Task StartPolling( CancellationToken? cancellationT if ( this is QrAuthSession qrResponse && response.new_challenge_url.Length > 0 ) { qrResponse.ChallengeURL = response.new_challenge_url; + qrResponse.ChallengeURLChanged?.Invoke(); } if ( response.refresh_token.Length > 0 ) { - return new AuthPollResult - { - AccessToken = response.access_token, - RefreshToken = response.refresh_token, - AccountName = response.account_name, - NewGuardData = response.new_guard_data, - }; + return new AuthPollResult( response ); } return null; @@ -298,7 +313,7 @@ public async Task StartPolling( CancellationToken? cancellationT } /// - /// + /// QR code based authentication session. /// public sealed class QrAuthSession : AuthSession { @@ -306,10 +321,21 @@ public sealed class QrAuthSession : AuthSession /// URL based on client ID, which can be rendered as QR code. /// public string ChallengeURL { get; set; } + + /// + /// Called whenever the challenge url is refreshed by Steam. + /// + public Action? ChallengeURLChanged { get; set; } + + internal QrAuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaQR_Response response ) + : base( authenticationService, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + { + ChallengeURL = response.challenge_url; + } } /// - /// + /// Credentials based authentication session. /// public sealed class CredentialsAuthSession : AuthSession { @@ -318,11 +344,17 @@ public sealed class CredentialsAuthSession : AuthSession /// public SteamID SteamID { get; set; } + internal CredentialsAuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaCredentials_Response response ) + : base( authenticationService, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + { + SteamID = new SteamID( response.steamid ); + } + /// - /// + /// Send Steam Guard code for this authentication session. /// - /// - /// + /// The code. + /// Type of code. /// /// public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeType ) @@ -335,9 +367,7 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp code_type = codeType, }; - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); + var message = await AuthenticationService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); var response = message.GetDeserializedResponse(); // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired @@ -345,6 +375,8 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp { throw new AuthenticationException( "Failed to send steam guard code", message.Result ); } + + // response may contain agreement_session_url } } @@ -352,16 +384,15 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp /// Gets public key for the provided account name which can be used to encrypt the account password. /// /// The account name to get RSA public key for. - public async Task GetPasswordRSAPublicKey( string accountName ) + /// IAuthentication unified service. + private async Task GetPasswordRSAPublicKey( string accountName, SteamUnifiedMessages.UnifiedService authenticationService ) { var request = new CAuthentication_GetPasswordRSAPublicKey_Request { account_name = accountName }; - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); + var message = await authenticationService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); if ( message.Result != EResult.OK ) { @@ -381,13 +412,18 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai { var request = new CAuthentication_BeginAuthSessionViaQR_Request { - platform_type = details.PlatformType, - device_friendly_name = details.DeviceFriendlyName, + website_id = details.WebsiteID, + device_details = new CAuthentication_DeviceDetails + { + device_friendly_name = details.DeviceFriendlyName, + platform_type = details.PlatformType, + os_type = ( int )details.ClientOSType, + } }; var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); + var authenticationService = unifiedMessages.CreateService(); + var message = await authenticationService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); if ( message.Result != EResult.OK ) { @@ -396,15 +432,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai var response = message.GetDeserializedResponse(); - var authResponse = new QrAuthSession - { - Client = Client, - ClientID = response.client_id, - RequestID = response.request_id, - AllowedConfirmations = SortConfirmations( response.allowed_confirmations ), - PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), - ChallengeURL = response.challenge_url, - }; + var authResponse = new QrAuthSession( authenticationService, details.Authenticator, response ); return authResponse; } @@ -427,8 +455,11 @@ public async Task BeginAuthSessionViaCredentials( AuthSe throw new ArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in 'details'." ); } + var unifiedMessages = Client.GetHandler()!; + var authenticationService = unifiedMessages.CreateService(); + // Encrypt the password - var publicKey = await GetPasswordRSAPublicKey( details.Username! ); + var publicKey = await GetPasswordRSAPublicKey( details.Username!, authenticationService ); var rsaParameters = new RSAParameters { Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), @@ -442,21 +473,23 @@ public async Task BeginAuthSessionViaCredentials( AuthSe // Create request var request = new CAuthentication_BeginAuthSessionViaCredentials_Request { - platform_type = details.PlatformType, - device_friendly_name = details.DeviceFriendlyName, account_name = details.Username, persistence = details.IsPersistentSession ? ESessionPersistence.k_ESessionPersistence_Persistent : ESessionPersistence.k_ESessionPersistence_Ephemeral, website_id = details.WebsiteID, guard_data = details.GuardData, encrypted_password = Convert.ToBase64String( encryptedPassword ), encryption_timestamp = publicKey.timestamp, + device_details = new CAuthentication_DeviceDetails + { + device_friendly_name = details.DeviceFriendlyName, + platform_type = details.PlatformType, + os_type = ( int )details.ClientOSType, + } }; - var unifiedMessages = Client.GetHandler()!; - var contentService = unifiedMessages.CreateService(); - var message = await contentService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); + var message = await authenticationService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); - // eresult can be InvalidPassword, ServiceUnavailable + // eresult can be InvalidPassword, ServiceUnavailable, InvalidParam, RateLimitExceeded if ( message.Result != EResult.OK ) { throw new AuthenticationException( "Authentication failed", message.Result ); @@ -464,16 +497,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe var response = message.GetDeserializedResponse(); - var authResponse = new CredentialsAuthSession - { - Client = Client, - Authenticator = details.Authenticator, - ClientID = response.client_id, - RequestID = response.request_id, - AllowedConfirmations = SortConfirmations( response.allowed_confirmations ), - PollingInterval = TimeSpan.FromSeconds( ( double )response.interval ), - SteamID = new SteamID( response.steamid ), - }; + var authResponse = new CredentialsAuthSession( authenticationService, details.Authenticator, response ); return authResponse; } From 43b634aa1954dc295fe64fca3c38a4600b61c8ac Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 14:18:05 +0200 Subject: [PATCH 18/29] Use default WebsiteID in sample --- Samples/1a.Authentication/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index 00b475c9a..f121124b7 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -56,7 +56,6 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) Username = user, Password = pass, IsPersistentSession = false, - WebsiteID = "Client", Authenticator = new UserConsoleAuthenticator(), } ); From f8ec8a920f76cd8963e1cf0c47ffa2d58a288f41 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 16:39:47 +0200 Subject: [PATCH 19/29] Add a sample parsing jwt payload --- Samples/1a.Authentication/Program.cs | 37 +++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index f121124b7..f8cbd6502 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -1,5 +1,5 @@ using System; - +using System.Text.Json; using SteamKit2; if ( args.Length < 2 ) @@ -63,11 +63,16 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) var pollResponse = await authSession.StartPolling(); // Logon to Steam with the access token we have received + // Note that we are using RefreshToken for logging on here steamUser.LogOn( new SteamUser.LogOnDetails { Username = pollResponse.AccountName, AccessToken = pollResponse.RefreshToken, } ); + + // This is not required, but it is possible to parse the JWT access token to see the scope and expiration date. + ParseJsonWebToken( pollResponse.AccessToken, nameof( pollResponse.AccessToken ) ); + ParseJsonWebToken( pollResponse.RefreshToken, nameof( pollResponse.RefreshToken ) ); } void OnDisconnected( SteamClient.DisconnectedCallback callback ) @@ -99,3 +104,33 @@ void OnLoggedOff( SteamUser.LoggedOffCallback callback ) { Console.WriteLine( "Logged off of Steam: {0}", callback.Result ); } + + + +// This is simply showing how to parse JWT, this is not required to login to Steam +void ParseJsonWebToken( string token, string name ) +{ + // You can use a JWT library to do the parsing for you + var tokenComponents = token.Split( '.' ); + + // Fix up base64url to normal base64 + var base64 = tokenComponents[ 1 ].Replace( '-', '+' ).Replace( '_', '/' ); + + if ( base64.Length % 4 != 0 ) + { + base64 += new string( '=', 4 - base64.Length % 4 ); + } + + var payloadBytes = Convert.FromBase64String( base64 ); + + // Payload can be parsed as JSON, and then fields such expiration date, scope, etc can be accessed + var payload = JsonDocument.Parse( payloadBytes ); + + // For brevity we will simply output formatted json to console + var formatted = JsonSerializer.Serialize( payload, new JsonSerializerOptions + { + WriteIndented = true, + } ); + Console.WriteLine( $"{name}: {formatted}" ); + Console.WriteLine(); +} From 44cf6b42aa40221030f95f3561da9658d8e22b23 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 16:44:34 +0200 Subject: [PATCH 20/29] Deprecate login keys --- SteamKit2/SteamKit2/Steam/Handlers/SteamUser/Callbacks.cs | 1 + SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/Callbacks.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/Callbacks.cs index 34cd7a445..beb885993 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/Callbacks.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/Callbacks.cs @@ -198,6 +198,7 @@ internal LoggedOffCallback( EResult result ) /// /// This callback is returned some time after logging onto the network. /// + [Obsolete("Steam no longer sends new login keys as of March 2023, use SteamAuthentication.")] public sealed class LoginKeyCallback : CallbackMsg { /// diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs index 42cae7bea..8c4ee1502 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs @@ -63,6 +63,7 @@ public sealed class LogOnDetails /// Gets or sets the login key used to login. This is a key that has been recieved in a previous Steam sesson by a . /// /// The login key. + [Obsolete( "Steam no longer sends new login keys as of March 2023, use SteamAuthentication." )] public string? LoginKey { get; set; } /// /// Gets or sets the 'Should Remember Password' flag. This is used in combination with the login key and for password-less login. @@ -490,6 +491,7 @@ public AsyncJob RequestWebAPIUserNonce() /// Accepts the new Login Key provided by a . /// /// The callback containing the new Login Key. + [Obsolete( "Steam no longer sends new login keys as of March 2023, use SteamAuthentication." )] public void AcceptNewLoginKey( LoginKeyCallback callback ) { if ( callback == null ) From b006cd2c713e484cc611ed378002dcc144b780d2 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 17:29:40 +0200 Subject: [PATCH 21/29] Document more website ids [skip ci] --- .../Steam/Handlers/SteamAuthentication/SteamAuthentication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 86a791a6f..fffb6f500 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -61,7 +61,7 @@ public sealed class AuthSessionDetails /// /// Gets or sets the website id that the login will be performed for. - /// Can be "Unknown", "Client", "Website", "Store", "Community" + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner". /// /// The website id. public string? WebsiteID { get; set; } = "Client"; From 2fc013ec97ee8908761575f22fbb4e887967d76d Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Fri, 17 Mar 2023 19:48:05 +0200 Subject: [PATCH 22/29] Handle incorrect 2FA codes and call the authenticator again --- .../SteamAuthentication/IAuthenticator.cs | 6 ++- .../SteamAuthentication.cs | 44 +++++++++++++++---- .../UserConsoleAuthenticator.cs | 20 ++++++--- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs index b0ca95bd3..0f9989d7a 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs @@ -11,15 +11,17 @@ public interface IAuthenticator /// /// This method is called when the account being logged into requires 2-factor authentication using the authenticator app. /// + /// True when previously provided code was incorrect. /// The 2-factor auth code used to login. This is the code that can be received from the authenticator app. - public Task ProvideDeviceCode(); + public Task ProvideDeviceCode( bool previousCodeWasIncorrect ); /// /// This method is called when the account being logged into uses Steam Guard email authentication. This code is sent to the user's email. /// /// The email address that the Steam Guard email was sent to. + /// True when previously provided code was incorrect. /// The Steam Guard auth code used to login. - public Task ProvideEmailCode(string email); + public Task ProvideEmailCode( string email, bool previousCodeWasIncorrect ); /// /// This method is called when the account being logged has the Steam Mobile App and accepts authentication notification prompts. diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index fffb6f500..63796b67b 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -200,21 +200,47 @@ public async Task StartPolling( CancellationToken? cancellationT throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); } - var task = preferredConfirmation.confirmation_type switch + var expectedInvalidCodeResult = preferredConfirmation.confirmation_type switch { - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( preferredConfirmation.associated_message ), - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode(), + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => EResult.InvalidLoginAuthCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => EResult.TwoFactorCodeMismatch, _ => throw new NotImplementedException(), }; + var previousCodeWasIncorrect = false; + var waitingForValidCode = true; - var code = await task; - - if ( string.IsNullOrEmpty( code ) ) + do { - throw new InvalidOperationException( "No code was provided by the authenticator." ); + cancellationToken?.ThrowIfCancellationRequested(); + + try + { + var task = preferredConfirmation.confirmation_type switch + { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( preferredConfirmation.associated_message, previousCodeWasIncorrect ), + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode( previousCodeWasIncorrect ), + _ => throw new NotImplementedException(), + }; + + var code = await task; + + cancellationToken?.ThrowIfCancellationRequested(); + + if ( string.IsNullOrEmpty( code ) ) + { + throw new InvalidOperationException( "No code was provided by the authenticator." ); + } + + await credentialsAuthSession.SendSteamGuardCode( code, preferredConfirmation.confirmation_type ); + + waitingForValidCode = false; + } + catch ( AuthenticationException e ) when ( e.Result == expectedInvalidCodeResult ) + { + previousCodeWasIncorrect = true; + } } - - await credentialsAuthSession.SendSteamGuardCode( code, preferredConfirmation.confirmation_type ); + while ( waitingForValidCode ); break; diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs index f67fafb88..7af4cb973 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs @@ -11,13 +11,18 @@ namespace SteamKit2 public class UserConsoleAuthenticator : IAuthenticator { /// - public Task ProvideDeviceCode() + public Task ProvideDeviceCode( bool previousCodeWasIncorrect ) { + if ( previousCodeWasIncorrect ) + { + Console.Error.WriteLine( "The previous 2-factor auth code you have provided is incorrect." ); + } + string? code; do { - Console.Write( "STEAM GUARD! Please enter your 2 factor auth code from your authenticator app: " ); + Console.Error.Write( "STEAM GUARD! Please enter your 2-factor auth code from your authenticator app: " ); code = Console.ReadLine()?.Trim(); } while ( string.IsNullOrEmpty( code ) ); @@ -26,13 +31,18 @@ public Task ProvideDeviceCode() } /// - public Task ProvideEmailCode( string email ) + public Task ProvideEmailCode( string email, bool previousCodeWasIncorrect ) { + if ( previousCodeWasIncorrect ) + { + Console.Error.WriteLine( "The previous 2-factor auth code you have provided is incorrect." ); + } + string? code; do { - Console.Write( $"STEAM GUARD! Please enter the auth code sent to the email at {email}: " ); + Console.Error.Write( $"STEAM GUARD! Please enter the auth code sent to the email at {email}: " ); code = Console.ReadLine()?.Trim(); } while ( string.IsNullOrEmpty( code ) ); @@ -43,7 +53,7 @@ public Task ProvideEmailCode( string email ) /// public Task AcceptDeviceConfirmation() { - Console.WriteLine( "STEAM GUARD! Use the Steam Mobile App to confirm your sign in..." ); + Console.Error.WriteLine( "STEAM GUARD! Use the Steam Mobile App to confirm your sign in..." ); return Task.FromResult( true ); } From 20c0b84bed4c7d2aab8f02ad683addd01fa0e0aa Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 22 Mar 2023 11:58:46 +0200 Subject: [PATCH 23/29] Update guard data comment --- .../Handlers/SteamAuthentication/SteamAuthentication.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index 63796b67b..cd9969fc1 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -61,7 +61,7 @@ public sealed class AuthSessionDetails /// /// Gets or sets the website id that the login will be performed for. - /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner". + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". /// /// The website id. public string? WebsiteID { get; set; } = "Client"; @@ -98,7 +98,8 @@ public class AuthPollResult /// public string AccessToken { get; set; } /// - /// May contain remembered machine ID for future login. + /// May contain remembered machine ID for future login, usually when account uses email based Steam Guard. + /// Supply it in for future logins to avoid resending an email. This value should be stored per account. /// public string? NewGuardData { get; set; } From 7b266f3c26da1c8e57ba650ae245e81e782a20d3 Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 22 Mar 2023 15:38:22 +0200 Subject: [PATCH 24/29] Review --- Samples/1a.Authentication/Program.cs | 4 +- Samples/1b.QrCodeAuthentication/Program.cs | 2 +- .../AuthenticationException.cs | 12 +- .../SteamAuthentication/IAuthenticator.cs | 8 +- .../SteamAuthentication.cs | 143 ++++++++++-------- .../UserConsoleAuthenticator.cs | 16 +- .../Steam/SteamClient/SteamClient.cs | 2 +- 7 files changed, 111 insertions(+), 76 deletions(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index f8cbd6502..5c4958af7 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -4,7 +4,7 @@ if ( args.Length < 2 ) { - Console.WriteLine( "Sample1: No username and password specified!" ); + Console.Error.WriteLine( "Sample1a: No username and password specified!" ); return; } @@ -60,7 +60,7 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) } ); // Starting polling Steam for authentication response - var pollResponse = await authSession.StartPolling(); + var pollResponse = await authSession.PollingWaitForResultAsync(); // Logon to Steam with the access token we have received // Note that we are using RefreshToken for logging on here diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs index 6cf63e80d..6ed00d5a5 100644 --- a/Samples/1b.QrCodeAuthentication/Program.cs +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -55,7 +55,7 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) // Starting polling Steam for authentication response // This response is later used to logon to Steam after connecting - var pollResponse = await authSession.StartPolling(); + var pollResponse = await authSession.PollingWaitForResultAsync(); Console.WriteLine( $"Logging in as '{pollResponse.AccountName}'..." ); diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs index 0343ace3f..cd9a08a89 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs @@ -5,12 +5,20 @@ namespace SteamKit2 /// /// Thrown when fails to authenticate. /// - public class AuthenticationException : Exception + [Serializable] + public sealed class AuthenticationException : Exception { /// /// Gets the result of the authentication request. /// - public EResult Result { get; private set; } + public EResult Result { get; } + + /// + /// Initializes a new instance of the class. + /// + public AuthenticationException() + { + } /// /// Initializes a new instance of the class. diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs index 0f9989d7a..43d70d4f6 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs @@ -4,7 +4,7 @@ namespace SteamKit2 { /// - /// + /// Represents an authenticator to be used with . /// public interface IAuthenticator { @@ -13,7 +13,7 @@ public interface IAuthenticator /// /// True when previously provided code was incorrect. /// The 2-factor auth code used to login. This is the code that can be received from the authenticator app. - public Task ProvideDeviceCode( bool previousCodeWasIncorrect ); + public Task GetDeviceCodeAsync( bool previousCodeWasIncorrect ); /// /// This method is called when the account being logged into uses Steam Guard email authentication. This code is sent to the user's email. @@ -21,7 +21,7 @@ public interface IAuthenticator /// The email address that the Steam Guard email was sent to. /// True when previously provided code was incorrect. /// The Steam Guard auth code used to login. - public Task ProvideEmailCode( string email, bool previousCodeWasIncorrect ); + public Task GetEmailCodeAsync( string email, bool previousCodeWasIncorrect ); /// /// This method is called when the account being logged has the Steam Mobile App and accepts authentication notification prompts. @@ -29,6 +29,6 @@ public interface IAuthenticator /// Return false if you want to fallback to entering a code instead. /// /// Return true to poll until the authentication is accepted, return false to fallback to entering a code. - public Task AcceptDeviceConfirmation(); + public Task AcceptDeviceConfirmationAsync(); } } diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs index cd9969fc1..a307278c4 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs @@ -88,20 +88,20 @@ public class AuthPollResult /// /// Account name of authenticating account. /// - public string AccountName { get; set; } + public string AccountName { get; } /// /// New refresh token. /// - public string RefreshToken { get; set; } + public string RefreshToken { get; } /// - /// New token subordinate to refresh_token. + /// New token subordinate to . /// - public string AccessToken { get; set; } + public string AccessToken { get; } /// /// May contain remembered machine ID for future login, usually when account uses email based Steam Guard. /// Supply it in for future logins to avoid resending an email. This value should be stored per account. /// - public string? NewGuardData { get; set; } + public string? NewGuardData { get; } internal AuthPollResult( CAuthentication_PollAuthSessionStatus_Response response ) { @@ -117,31 +117,33 @@ internal AuthPollResult( CAuthentication_PollAuthSessionStatus_Response response /// public class AuthSession { - internal SteamUnifiedMessages.UnifiedService AuthenticationService { get; private set; } + internal SteamAuthentication Authentication { get; } + + /// + /// Confirmation types that will be able to confirm the request. + /// + internal List AllowedConfirmations { get; } + /// /// Authenticator object which will be used to handle 2-factor authentication if necessary. /// - public IAuthenticator? Authenticator { get; set; } + public IAuthenticator? Authenticator { get; } /// /// Unique identifier of requestor, also used for routing, portion of QR code. /// - public ulong ClientID { get; set; } + public ulong ClientID { get; internal set; } /// /// Unique request ID to be presented by requestor at poll time. /// - public byte[] RequestID { get; set; } - /// - /// Confirmation types that will be able to confirm the request. - /// - public List AllowedConfirmations { get; set; } + public byte[] RequestID { get; } /// /// Refresh interval with which requestor should call PollAuthSessionStatus. /// - public TimeSpan PollingInterval { get; set; } + public TimeSpan PollingInterval { get; } - internal AuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, ulong clientId, byte[] requestId, List allowedConfirmations, float pollingInterval ) + internal AuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, ulong clientId, byte[] requestId, List allowedConfirmations, float pollingInterval ) { - AuthenticationService = authenticationService; + Authentication = authentication; Authenticator = authenticator; ClientID = clientId; RequestID = requestId; @@ -155,7 +157,7 @@ internal AuthSession( SteamUnifiedMessages.UnifiedService authe /// An object containing tokens which can be used to login to Steam. /// Thrown when an invalid state occurs, such as no supported confirmation methods are available. /// Thrown when polling fails. - public async Task StartPolling( CancellationToken? cancellationToken = null ) + public async Task PollingWaitForResultAsync( CancellationToken? cancellationToken = null ) { var pollLoop = false; var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); @@ -169,7 +171,7 @@ public async Task StartPolling( CancellationToken? cancellationT // simply poll until confirmation is accepted, or whether they want to fallback to the next preferred confirmation type. if ( Authenticator != null && preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation ) { - var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmation(); + var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmationAsync(); if ( !prefersToPollForConfirmation ) { @@ -218,8 +220,8 @@ public async Task StartPolling( CancellationToken? cancellationT { var task = preferredConfirmation.confirmation_type switch { - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.ProvideEmailCode( preferredConfirmation.associated_message, previousCodeWasIncorrect ), - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.ProvideDeviceCode( previousCodeWasIncorrect ), + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.GetEmailCodeAsync( preferredConfirmation.associated_message, previousCodeWasIncorrect ), + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.GetDeviceCodeAsync( previousCodeWasIncorrect ), _ => throw new NotImplementedException(), }; @@ -232,7 +234,7 @@ public async Task StartPolling( CancellationToken? cancellationT throw new InvalidOperationException( "No code was provided by the authenticator." ); } - await credentialsAuthSession.SendSteamGuardCode( code, preferredConfirmation.confirmation_type ); + await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ); waitingForValidCode = false; } @@ -269,7 +271,7 @@ public async Task StartPolling( CancellationToken? cancellationT { cancellationToken?.ThrowIfCancellationRequested(); - var pollResponse = await PollAuthSessionStatus(); + var pollResponse = await PollAuthSessionStatusAsync(); if ( pollResponse == null ) { @@ -281,13 +283,16 @@ public async Task StartPolling( CancellationToken? cancellationT while ( true ) { - cancellationToken?.ThrowIfCancellationRequested(); - - await Task.Delay( PollingInterval ); - - cancellationToken?.ThrowIfCancellationRequested(); + if( cancellationToken is CancellationToken nonNullCancellationToken ) + { + await Task.Delay( PollingInterval, nonNullCancellationToken ); + } + else + { + await Task.Delay( PollingInterval ); + } - var pollResponse = await PollAuthSessionStatus(); + var pollResponse = await PollAuthSessionStatusAsync(); if ( pollResponse != null ) { @@ -297,11 +302,11 @@ public async Task StartPolling( CancellationToken? cancellationT } /// - /// Polls for authentication status once. Prefer using instead. + /// Polls for authentication status once. Prefer using instead. /// /// An object containing tokens which can be used to login to Steam, or null if not yet authenticated. /// Thrown when polling fails. - public async Task PollAuthSessionStatus() + public async Task PollAuthSessionStatusAsync() { var request = new CAuthentication_PollAuthSessionStatus_Request { @@ -309,7 +314,7 @@ public async Task StartPolling( CancellationToken? cancellationT request_id = RequestID, }; - var message = await AuthenticationService.SendMessage( api => api.PollAuthSessionStatus( request ) ); + var message = await Authentication.AuthenticationService!.SendMessage( api => api.PollAuthSessionStatus( request ) ); // eresult can be Expired, FileNotFound, Fail if ( message.Result != EResult.OK ) @@ -319,16 +324,7 @@ public async Task StartPolling( CancellationToken? cancellationT var response = message.GetDeserializedResponse(); - if ( response.new_client_id > 0 ) - { - ClientID = response.new_client_id; - } - - if ( this is QrAuthSession qrResponse && response.new_challenge_url.Length > 0 ) - { - qrResponse.ChallengeURL = response.new_challenge_url; - qrResponse.ChallengeURLChanged?.Invoke(); - } + HandlePollAuthSessionStatusResponse( response ); if ( response.refresh_token.Length > 0 ) { @@ -337,6 +333,14 @@ public async Task StartPolling( CancellationToken? cancellationT return null; } + + internal virtual void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response) + { + if ( response.new_client_id != default ) + { + ClientID = response.new_client_id; + } + } } /// @@ -347,18 +351,29 @@ public sealed class QrAuthSession : AuthSession /// /// URL based on client ID, which can be rendered as QR code. /// - public string ChallengeURL { get; set; } + public string ChallengeURL { get; internal set; } /// /// Called whenever the challenge url is refreshed by Steam. /// public Action? ChallengeURLChanged { get; set; } - internal QrAuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaQR_Response response ) - : base( authenticationService, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + internal QrAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaQR_Response response ) + : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) { ChallengeURL = response.challenge_url; } + + internal override void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) + { + base.HandlePollAuthSessionStatusResponse( response ); + + if ( response.new_challenge_url.Length > 0 ) + { + ChallengeURL = response.new_challenge_url; + ChallengeURLChanged?.Invoke(); + } + } } /// @@ -369,10 +384,10 @@ public sealed class CredentialsAuthSession : AuthSession /// /// SteamID of the account logging in, will only be included if the credentials were correct. /// - public SteamID SteamID { get; set; } + public SteamID SteamID { get; } - internal CredentialsAuthSession( SteamUnifiedMessages.UnifiedService authenticationService, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaCredentials_Response response ) - : base( authenticationService, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + internal CredentialsAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaCredentials_Response response ) + : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) { SteamID = new SteamID( response.steamid ); } @@ -384,7 +399,7 @@ internal CredentialsAuthSession( SteamUnifiedMessages.UnifiedServiceType of code. /// /// - public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeType ) + public async Task SendSteamGuardCodeAsync( string code, EAuthSessionGuardType codeType ) { var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request { @@ -394,7 +409,7 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp code_type = codeType, }; - var message = await AuthenticationService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); + var message = await Authentication.AuthenticationService!.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); var response = message.GetDeserializedResponse(); // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired @@ -407,19 +422,20 @@ public async Task SendSteamGuardCode( string code, EAuthSessionGuardType codeTyp } } + internal SteamUnifiedMessages.UnifiedService? AuthenticationService { get; private set; } + /// /// Gets public key for the provided account name which can be used to encrypt the account password. /// /// The account name to get RSA public key for. - /// IAuthentication unified service. - private async Task GetPasswordRSAPublicKey( string accountName, SteamUnifiedMessages.UnifiedService authenticationService ) + async Task GetPasswordRSAPublicKeyAsync( string accountName ) { var request = new CAuthentication_GetPasswordRSAPublicKey_Request { account_name = accountName }; - var message = await authenticationService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); + var message = await AuthenticationService!.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); if ( message.Result != EResult.OK ) { @@ -437,6 +453,9 @@ private async Task GetPassword /// The details to use for logging on. public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) { + var unifiedMessages = Client.GetHandler()!; + AuthenticationService = unifiedMessages.CreateService(); + var request = new CAuthentication_BeginAuthSessionViaQR_Request { website_id = details.WebsiteID, @@ -448,9 +467,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai } }; - var unifiedMessages = Client.GetHandler()!; - var authenticationService = unifiedMessages.CreateService(); - var message = await authenticationService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); + var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); if ( message.Result != EResult.OK ) { @@ -459,7 +476,7 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai var response = message.GetDeserializedResponse(); - var authResponse = new QrAuthSession( authenticationService, details.Authenticator, response ); + var authResponse = new QrAuthSession( this, details.Authenticator, response ); return authResponse; } @@ -472,6 +489,9 @@ public async Task BeginAuthSessionViaQR( AuthSessionDetails detai /// Username or password are not set within . public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) { + var unifiedMessages = Client.GetHandler()!; + AuthenticationService = unifiedMessages.CreateService(); + if ( details == null ) { throw new ArgumentNullException( nameof( details ) ); @@ -482,11 +502,8 @@ public async Task BeginAuthSessionViaCredentials( AuthSe throw new ArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in 'details'." ); } - var unifiedMessages = Client.GetHandler()!; - var authenticationService = unifiedMessages.CreateService(); - // Encrypt the password - var publicKey = await GetPasswordRSAPublicKey( details.Username!, authenticationService ); + var publicKey = await GetPasswordRSAPublicKeyAsync( details.Username! ); var rsaParameters = new RSAParameters { Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), @@ -514,7 +531,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe } }; - var message = await authenticationService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); + var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); // eresult can be InvalidPassword, ServiceUnavailable, InvalidParam, RateLimitExceeded if ( message.Result != EResult.OK ) @@ -524,7 +541,7 @@ public async Task BeginAuthSessionViaCredentials( AuthSe var response = message.GetDeserializedResponse(); - var authResponse = new CredentialsAuthSession( authenticationService, details.Authenticator, response ); + var authResponse = new CredentialsAuthSession( this, details.Authenticator, response ); return authResponse; } @@ -541,7 +558,7 @@ public override void HandleMsg( IPacketMsg packetMsg ) /// /// Sort available guard confirmation methods by an order that we prefer to handle them in /// - private static List SortConfirmations( List confirmations ) + static List SortConfirmations( List confirmations ) { var preferredConfirmationTypes = new EAuthSessionGuardType[] { diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs index 7af4cb973..5e064700c 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs @@ -11,7 +11,7 @@ namespace SteamKit2 public class UserConsoleAuthenticator : IAuthenticator { /// - public Task ProvideDeviceCode( bool previousCodeWasIncorrect ) + public Task GetDeviceCodeAsync( bool previousCodeWasIncorrect ) { if ( previousCodeWasIncorrect ) { @@ -24,6 +24,11 @@ public Task ProvideDeviceCode( bool previousCodeWasIncorrect ) { Console.Error.Write( "STEAM GUARD! Please enter your 2-factor auth code from your authenticator app: " ); code = Console.ReadLine()?.Trim(); + + if( code == null ) + { + break; + } } while ( string.IsNullOrEmpty( code ) ); @@ -31,7 +36,7 @@ public Task ProvideDeviceCode( bool previousCodeWasIncorrect ) } /// - public Task ProvideEmailCode( string email, bool previousCodeWasIncorrect ) + public Task GetEmailCodeAsync( string email, bool previousCodeWasIncorrect ) { if ( previousCodeWasIncorrect ) { @@ -44,6 +49,11 @@ public Task ProvideEmailCode( string email, bool previousCodeWasIncorrec { Console.Error.Write( $"STEAM GUARD! Please enter the auth code sent to the email at {email}: " ); code = Console.ReadLine()?.Trim(); + + if ( code == null ) + { + break; + } } while ( string.IsNullOrEmpty( code ) ); @@ -51,7 +61,7 @@ public Task ProvideEmailCode( string email, bool previousCodeWasIncorrec } /// - public Task AcceptDeviceConfirmation() + public Task AcceptDeviceConfirmationAsync() { Console.Error.WriteLine( "STEAM GUARD! Use the Steam Mobile App to confirm your sign in..." ); diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index 7bd490f21..01d0f9345 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -83,7 +83,6 @@ public SteamClient( SteamConfiguration configuration, string identifier ) // notice: SteamFriends should be added before SteamUser due to AccountInfoCallback this.AddHandler( new SteamFriends() ); this.AddHandler( new SteamUser() ); - this.AddHandler( new SteamAuthentication() ); this.AddHandler( new SteamApps() ); this.AddHandler( new SteamGameCoordinator() ); this.AddHandler( new SteamGameServer() ); @@ -93,6 +92,7 @@ public SteamClient( SteamConfiguration configuration, string identifier ) this.AddHandler( new SteamWorkshop() ); this.AddHandler( new SteamTrading() ); this.AddHandler( new SteamUnifiedMessages() ); + this.AddHandler( new SteamAuthentication() ); this.AddHandler( new SteamScreenshots() ); this.AddHandler( new SteamMatchmaking() ); this.AddHandler( new SteamNetworking() ); From 2cb4a75ebf1902ff14d005eb12eded812ebe936c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 22 Mar 2023 16:06:57 +0200 Subject: [PATCH 25/29] Move authentication stuff to its own namespace --- Samples/1a.Authentication/Program.cs | 9 +- Samples/1b.QrCodeAuthentication/Program.cs | 11 +- .../Steam/Authentication/AuthPollResult.cs | 41 ++ .../Steam/Authentication/AuthSession.cs | 273 ++++++++ .../Authentication/AuthSessionDetails.cs | 71 +++ .../AuthenticationException.cs | 2 +- .../Authentication/CredentialsAuthSession.cs | 56 ++ .../IAuthenticator.cs | 2 +- .../Steam/Authentication/QrAuthSession.cs | 43 ++ .../Authentication/SteamAuthentication.cs | 165 +++++ .../UserConsoleAuthenticator.cs | 2 +- .../SteamAuthentication.cs | 586 ------------------ .../Steam/Handlers/SteamUser/SteamUser.cs | 2 +- .../Steam/SteamClient/SteamClient.cs | 1 - 14 files changed, 664 insertions(+), 600 deletions(-) create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/AuthSessionDetails.cs rename SteamKit2/SteamKit2/Steam/{Handlers/SteamAuthentication => Authentication}/AuthenticationException.cs (96%) create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/CredentialsAuthSession.cs rename SteamKit2/SteamKit2/Steam/{Handlers/SteamAuthentication => Authentication}/IAuthenticator.cs (98%) create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs create mode 100644 SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs rename SteamKit2/SteamKit2/Steam/{Handlers/SteamAuthentication => Authentication}/UserConsoleAuthenticator.cs (98%) delete mode 100644 SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index 5c4958af7..42aa6deb0 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -1,6 +1,7 @@ using System; using System.Text.Json; using SteamKit2; +using SteamKit2.Authentication; if ( args.Length < 2 ) { @@ -17,9 +18,6 @@ // create the callback manager which will route callbacks to function calls var manager = new CallbackManager( steamClient ); -// get the authentication handler, which used for authenticating with Steam -var auth = steamClient.GetHandler(); - // get the steamuser handler, which is used for logging on after successfully connecting var steamUser = steamClient.GetHandler(); @@ -50,8 +48,11 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { Console.WriteLine( "Connected to Steam! Logging in '{0}'...", user ); + // get the authentication handler, which used for authenticating with Steam + var auth = new SteamAuthentication( steamClient ); + // Begin authenticating via credentials - var authSession = await auth.BeginAuthSessionViaCredentials( new SteamAuthentication.AuthSessionDetails + var authSession = await auth.BeginAuthSessionViaCredentialsAsync( new AuthSessionDetails { Username = user, Password = pass, diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs index 6ed00d5a5..1722b6e84 100644 --- a/Samples/1b.QrCodeAuthentication/Program.cs +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -1,15 +1,13 @@ using System; using QRCoder; using SteamKit2; +using SteamKit2.Authentication; // create our steamclient instance var steamClient = new SteamClient(); // create the callback manager which will route callbacks to function calls var manager = new CallbackManager( steamClient ); -// get the authentication handler, which used for authenticating with Steam -var auth = steamClient.GetHandler(); - // get the steamuser handler, which is used for logging on after successfully connecting var steamUser = steamClient.GetHandler(); @@ -38,8 +36,11 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { + // get the authentication handler, which used for authenticating with Steam + var auth = new SteamAuthentication( steamClient ); + // Start an authentication session by requesting a link - var authSession = await auth.BeginAuthSessionViaQR( new SteamAuthentication.AuthSessionDetails() ); + var authSession = await auth.BeginAuthSessionViaQRAsync( new AuthSessionDetails() ); // Steam will periodically refresh the challenge url, this callback allows you to draw a new qr code authSession.ChallengeURLChanged = () => @@ -97,7 +98,7 @@ void OnLoggedOff( SteamUser.LoggedOffCallback callback ) Console.WriteLine( "Logged off of Steam: {0}", callback.Result ); } -void DrawQRCode( SteamAuthentication.QrAuthSession authSession ) +void DrawQRCode( QrAuthSession authSession ) { Console.WriteLine( $"Challenge URL: {authSession.ChallengeURL}" ); Console.WriteLine(); diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs new file mode 100644 index 000000000..405961ab9 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs @@ -0,0 +1,41 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// Represents authentication poll result. + /// + public sealed class AuthPollResult + { + /// + /// Account name of authenticating account. + /// + public string AccountName { get; } + /// + /// New refresh token. + /// + public string RefreshToken { get; } + /// + /// New token subordinate to . + /// + public string AccessToken { get; } + /// + /// May contain remembered machine ID for future login, usually when account uses email based Steam Guard. + /// Supply it in for future logins to avoid resending an email. This value should be stored per account. + /// + public string? NewGuardData { get; } + + internal AuthPollResult( CAuthentication_PollAuthSessionStatus_Response response ) + { + AccessToken = response.access_token; + RefreshToken = response.refresh_token; + AccountName = response.account_name; + NewGuardData = response.new_guard_data; + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs new file mode 100644 index 000000000..379224262 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs @@ -0,0 +1,273 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// Represents an authentication sesssion which can be used to finish the authentication and get access tokens. + /// + public class AuthSession + { + internal SteamAuthentication Authentication { get; } + + /// + /// Confirmation types that will be able to confirm the request. + /// + internal List AllowedConfirmations { get; } + + /// + /// Authenticator object which will be used to handle 2-factor authentication if necessary. + /// + public IAuthenticator? Authenticator { get; } + /// + /// Unique identifier of requestor, also used for routing, portion of QR code. + /// + public ulong ClientID { get; internal set; } + /// + /// Unique request ID to be presented by requestor at poll time. + /// + public byte[] RequestID { get; } + /// + /// Refresh interval with which requestor should call PollAuthSessionStatus. + /// + public TimeSpan PollingInterval { get; } + + internal AuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, ulong clientId, byte[] requestId, List allowedConfirmations, float pollingInterval ) + { + Authentication = authentication; + Authenticator = authenticator; + ClientID = clientId; + RequestID = requestId; + AllowedConfirmations = SortConfirmations( allowedConfirmations ); + PollingInterval = TimeSpan.FromSeconds( ( double )pollingInterval ); + } + + /// + /// Handle any 2-factor authentication, and if necessary poll for updates until authentication succeeds. + /// + /// An object containing tokens which can be used to login to Steam. + /// Thrown when an invalid state occurs, such as no supported confirmation methods are available. + /// Thrown when polling fails. + public async Task PollingWaitForResultAsync( CancellationToken? cancellationToken = null ) + { + var pollLoop = false; + var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); + + if ( preferredConfirmation == null || preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown ) + { + throw new InvalidOperationException( "There are no allowed confirmations" ); + } + + // If an authenticator is provided and we device confirmation is available, allow consumers to choose whether they want to + // simply poll until confirmation is accepted, or whether they want to fallback to the next preferred confirmation type. + if ( Authenticator != null && preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation ) + { + var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmationAsync(); + + if ( !prefersToPollForConfirmation ) + { + if ( AllowedConfirmations.Count <= 1 ) + { + throw new InvalidOperationException( "AcceptDeviceConfirmation returned false which indicates a fallback to another confirmation type, but there are no other confirmation types available." ); + } + + preferredConfirmation = AllowedConfirmations[ 1 ]; + } + } + + switch ( preferredConfirmation.confirmation_type ) + { + // No steam guard + case EAuthSessionGuardType.k_EAuthSessionGuardType_None: + break; + + // 2-factor code from the authenticator app or sent to an email + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: + if ( !( this is CredentialsAuthSession credentialsAuthSession ) ) + { + throw new InvalidOperationException( $"Got {preferredConfirmation.confirmation_type} confirmation type in a session that is not {nameof( CredentialsAuthSession )}." ); + } + + if ( Authenticator == null ) + { + throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); + } + + var expectedInvalidCodeResult = preferredConfirmation.confirmation_type switch + { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => EResult.InvalidLoginAuthCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => EResult.TwoFactorCodeMismatch, + _ => throw new NotImplementedException(), + }; + var previousCodeWasIncorrect = false; + var waitingForValidCode = true; + + do + { + cancellationToken?.ThrowIfCancellationRequested(); + + try + { + var task = preferredConfirmation.confirmation_type switch + { + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.GetEmailCodeAsync( preferredConfirmation.associated_message, previousCodeWasIncorrect ), + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.GetDeviceCodeAsync( previousCodeWasIncorrect ), + _ => throw new NotImplementedException(), + }; + + var code = await task; + + cancellationToken?.ThrowIfCancellationRequested(); + + if ( string.IsNullOrEmpty( code ) ) + { + throw new InvalidOperationException( "No code was provided by the authenticator." ); + } + + await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ); + + waitingForValidCode = false; + } + catch ( AuthenticationException e ) when ( e.Result == expectedInvalidCodeResult ) + { + previousCodeWasIncorrect = true; + } + } + while ( waitingForValidCode ); + + break; + + // This is a prompt that appears in the Steam mobile app + case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: + pollLoop = true; + break; + + /* + case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: + // TODO: what is this? + pollLoop = true; + break; + + case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: + // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set + throw new NotImplementedException( $"Machine token confirmation is not supported by SteamKit at the moment." ); + */ + + default: + throw new NotImplementedException( $"Unsupported confirmation type {preferredConfirmation.confirmation_type}." ); + } + + if ( !pollLoop ) + { + cancellationToken?.ThrowIfCancellationRequested(); + + var pollResponse = await PollAuthSessionStatusAsync(); + + if ( pollResponse == null ) + { + throw new AuthenticationException( "Authentication failed", EResult.Fail ); + } + + return pollResponse; + } + + while ( true ) + { + if ( cancellationToken is CancellationToken nonNullCancellationToken ) + { + await Task.Delay( PollingInterval, nonNullCancellationToken ); + } + else + { + await Task.Delay( PollingInterval ); + } + + var pollResponse = await PollAuthSessionStatusAsync(); + + if ( pollResponse != null ) + { + return pollResponse; + } + } + } + + /// + /// Polls for authentication status once. Prefer using instead. + /// + /// An object containing tokens which can be used to login to Steam, or null if not yet authenticated. + /// Thrown when polling fails. + public async Task PollAuthSessionStatusAsync() + { + var request = new CAuthentication_PollAuthSessionStatus_Request + { + client_id = ClientID, + request_id = RequestID, + }; + + var message = await Authentication.AuthenticationService.SendMessage( api => api.PollAuthSessionStatus( request ) ); + + // eresult can be Expired, FileNotFound, Fail + if ( message.Result != EResult.OK ) + { + throw new AuthenticationException( "Failed to poll status", message.Result ); + } + + var response = message.GetDeserializedResponse(); + + HandlePollAuthSessionStatusResponse( response ); + + if ( response.refresh_token.Length > 0 ) + { + return new AuthPollResult( response ); + } + + return null; + } + + internal virtual void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) + { + if ( response.new_client_id != default ) + { + ClientID = response.new_client_id; + } + } + + /// + /// Sort available guard confirmation methods by an order that we prefer to handle them in. + /// + static List SortConfirmations( List confirmations ) + { + var preferredConfirmationTypes = new EAuthSessionGuardType[] + { + EAuthSessionGuardType.k_EAuthSessionGuardType_None, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode, + EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation, + EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken, + EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown, + }; + var sortOrder = Enumerable.Range( 0, preferredConfirmationTypes.Length ).ToDictionary( x => preferredConfirmationTypes[ x ], x => x ); + + return confirmations.OrderBy( x => + { + if ( sortOrder.TryGetValue( x.confirmation_type, out var sortIndex ) ) + { + return sortIndex; + } + + return int.MaxValue; + } ).ToList(); + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthSessionDetails.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthSessionDetails.cs new file mode 100644 index 000000000..8fd90d4b2 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthSessionDetails.cs @@ -0,0 +1,71 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// Represents the details required to authenticate on Steam. + /// + public sealed class AuthSessionDetails + { + /// + /// Gets or sets the username. + /// + /// The username. + public string? Username { get; set; } + + /// + /// Gets or sets the password. + /// + /// The password. + public string? Password { get; set; } + + /// + /// Gets or sets the device name (or user agent). + /// + /// The device name. + public string? DeviceFriendlyName { get; set; } = $"{Environment.MachineName} (SteamKit2)"; + + /// + /// Gets or sets the platform type that the login will be performed for. + /// + public EAuthTokenPlatformType PlatformType { get; set; } = EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient; + + /// + /// Gets or sets the client operating system type. + /// + /// The client operating system type. + public EOSType ClientOSType { get; set; } = Utils.GetOSType(); + + /// + /// Gets or sets the session persistence. + /// + /// The persistence. + public bool IsPersistentSession { get; set; } = false; + + /// + /// Gets or sets the website id that the login will be performed for. + /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". + /// + /// The website id. + public string? WebsiteID { get; set; } = "Client"; + + /// + /// Steam guard data for client login. Provide if available. + /// + /// The guard data. + public string? GuardData { get; set; } + + /// + /// Authenticator object which will be used to handle 2-factor authentication if necessary. + /// Use for a default implementation. + /// + /// The authenticator object. + public IAuthenticator? Authenticator { get; set; } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthenticationException.cs similarity index 96% rename from SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs rename to SteamKit2/SteamKit2/Steam/Authentication/AuthenticationException.cs index cd9a08a89..881a31480 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/AuthenticationException.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthenticationException.cs @@ -1,6 +1,6 @@ using System; -namespace SteamKit2 +namespace SteamKit2.Authentication { /// /// Thrown when fails to authenticate. diff --git a/SteamKit2/SteamKit2/Steam/Authentication/CredentialsAuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/CredentialsAuthSession.cs new file mode 100644 index 000000000..62b87d78b --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/CredentialsAuthSession.cs @@ -0,0 +1,56 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System.Threading.Tasks; +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// Credentials based authentication session. + /// + public sealed class CredentialsAuthSession : AuthSession + { + /// + /// SteamID of the account logging in, will only be included if the credentials were correct. + /// + public SteamID SteamID { get; } + + internal CredentialsAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaCredentials_Response response ) + : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + { + SteamID = new SteamID( response.steamid ); + } + + /// + /// Send Steam Guard code for this authentication session. + /// + /// The code. + /// Type of code. + /// + /// + public async Task SendSteamGuardCodeAsync( string code, EAuthSessionGuardType codeType ) + { + var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request + { + client_id = ClientID, + steamid = SteamID, + code = code, + code_type = codeType, + }; + + var message = await Authentication.AuthenticationService.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); + var response = message.GetDeserializedResponse(); + + // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired + if ( message.Result != EResult.OK ) + { + throw new AuthenticationException( "Failed to send steam guard code", message.Result ); + } + + // response may contain agreement_session_url + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Authentication/IAuthenticator.cs similarity index 98% rename from SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs rename to SteamKit2/SteamKit2/Steam/Authentication/IAuthenticator.cs index 43d70d4f6..06b521020 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/IAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/IAuthenticator.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace SteamKit2 +namespace SteamKit2.Authentication { /// /// Represents an authenticator to be used with . diff --git a/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs new file mode 100644 index 000000000..a58a71cae --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs @@ -0,0 +1,43 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// QR code based authentication session. + /// + public sealed class QrAuthSession : AuthSession + { + /// + /// URL based on client ID, which can be rendered as QR code. + /// + public string ChallengeURL { get; internal set; } + + /// + /// Called whenever the challenge url is refreshed by Steam. + /// + public Action? ChallengeURLChanged { get; set; } + + internal QrAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaQR_Response response ) + : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) + { + ChallengeURL = response.challenge_url; + } + + internal override void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) + { + base.HandlePollAuthSessionStatusResponse( response ); + + if ( response.new_challenge_url.Length > 0 ) + { + ChallengeURL = response.new_challenge_url; + ChallengeURLChanged?.Invoke(); + } + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs new file mode 100644 index 000000000..545e6dd4f --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs @@ -0,0 +1,165 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'license.txt', which is part of this source code package. + */ + +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using SteamKit2.Internal; + +namespace SteamKit2.Authentication +{ + /// + /// This handler is used for authenticating on Steam. + /// + public sealed class SteamAuthentication + { + SteamClient Client; + internal SteamUnifiedMessages.UnifiedService AuthenticationService { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The this instance will be associated with. + public SteamAuthentication( SteamClient steamClient ) + { + if ( steamClient == null ) + { + throw new ArgumentNullException( nameof( steamClient ) ); + } + + Client = steamClient; + + var unifiedMessages = steamClient.GetHandler()!; + AuthenticationService = unifiedMessages.CreateService(); + } + + /// + /// Gets public key for the provided account name which can be used to encrypt the account password. + /// + /// The account name to get RSA public key for. + async Task GetPasswordRSAPublicKeyAsync( string accountName ) + { + var request = new CAuthentication_GetPasswordRSAPublicKey_Request + { + account_name = accountName + }; + + var message = await AuthenticationService.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); + + if ( message.Result != EResult.OK ) + { + throw new AuthenticationException( "Failed to get password public key", message.Result ); + } + + var response = message.GetDeserializedResponse(); + + return response; + } + + /// + /// Start the authentication process using QR codes. + /// + /// The details to use for logging on. + public async Task BeginAuthSessionViaQRAsync( AuthSessionDetails details ) + { + if ( !Client.IsConnected ) + { + throw new InvalidOperationException( "The SteamClient instance must be connected." ); + } + + var request = new CAuthentication_BeginAuthSessionViaQR_Request + { + website_id = details.WebsiteID, + device_details = new CAuthentication_DeviceDetails + { + device_friendly_name = details.DeviceFriendlyName, + platform_type = details.PlatformType, + os_type = ( int )details.ClientOSType, + } + }; + + var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); + + if ( message.Result != EResult.OK ) + { + throw new AuthenticationException( "Failed to begin QR auth session", message.Result ); + } + + var response = message.GetDeserializedResponse(); + + var authResponse = new QrAuthSession( this, details.Authenticator, response ); + + return authResponse; + } + + /// + /// Start the authentication process by providing username and password. + /// + /// The details to use for logging on. + /// No auth details were provided. + /// Username or password are not set within . + public async Task BeginAuthSessionViaCredentialsAsync( AuthSessionDetails details ) + { + if ( details == null ) + { + throw new ArgumentNullException( nameof( details ) ); + } + + if ( string.IsNullOrEmpty( details.Username ) || string.IsNullOrEmpty( details.Password ) ) + { + throw new ArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in 'details'." ); + } + + if ( !Client.IsConnected ) + { + throw new InvalidOperationException( "The SteamClient instance must be connected." ); + } + + // Encrypt the password + var publicKey = await GetPasswordRSAPublicKeyAsync( details.Username! ); + var rsaParameters = new RSAParameters + { + Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), + Exponent = Utils.DecodeHexString( publicKey.publickey_exp ), + }; + + using var rsa = RSA.Create(); + rsa.ImportParameters( rsaParameters ); + var encryptedPassword = rsa.Encrypt( Encoding.UTF8.GetBytes( details.Password ), RSAEncryptionPadding.Pkcs1 ); + + // Create request + var request = new CAuthentication_BeginAuthSessionViaCredentials_Request + { + account_name = details.Username, + persistence = details.IsPersistentSession ? ESessionPersistence.k_ESessionPersistence_Persistent : ESessionPersistence.k_ESessionPersistence_Ephemeral, + website_id = details.WebsiteID, + guard_data = details.GuardData, + encrypted_password = Convert.ToBase64String( encryptedPassword ), + encryption_timestamp = publicKey.timestamp, + device_details = new CAuthentication_DeviceDetails + { + device_friendly_name = details.DeviceFriendlyName, + platform_type = details.PlatformType, + os_type = ( int )details.ClientOSType, + } + }; + + var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); + + // eresult can be InvalidPassword, ServiceUnavailable, InvalidParam, RateLimitExceeded + if ( message.Result != EResult.OK ) + { + throw new AuthenticationException( "Authentication failed", message.Result ); + } + + var response = message.GetDeserializedResponse(); + + var authResponse = new CredentialsAuthSession( this, details.Authenticator, response ); + + return authResponse; + } + } +} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs similarity index 98% rename from SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs rename to SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs index 5e064700c..095481955 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/UserConsoleAuthenticator.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace SteamKit2 +namespace SteamKit2.Authentication { /// /// This is a default implementation of to ease of use. diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs deleted file mode 100644 index a307278c4..000000000 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamAuthentication/SteamAuthentication.cs +++ /dev/null @@ -1,586 +0,0 @@ -/* - * This file is subject to the terms and conditions defined in - * file 'license.txt', which is part of this source code package. - */ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using SteamKit2.Internal; - -namespace SteamKit2 -{ - /// - /// This handler is used for authenticating on Steam. - /// - public sealed class SteamAuthentication : ClientMsgHandler - { - /// - /// Represents the details required to authenticate on Steam. - /// - public sealed class AuthSessionDetails - { - /// - /// Gets or sets the username. - /// - /// The username. - public string? Username { get; set; } - - /// - /// Gets or sets the password. - /// - /// The password. - public string? Password { get; set; } - - /// - /// Gets or sets the device name (or user agent). - /// - /// The device name. - public string? DeviceFriendlyName { get; set; } = $"{Environment.MachineName} (SteamKit2)"; - - /// - /// Gets or sets the platform type that the login will be performed for. - /// - public EAuthTokenPlatformType PlatformType { get; set; } = EAuthTokenPlatformType.k_EAuthTokenPlatformType_SteamClient; - - /// - /// Gets or sets the client operating system type. - /// - /// The client operating system type. - public EOSType ClientOSType { get; set; } = Utils.GetOSType(); - - /// - /// Gets or sets the session persistence. - /// - /// The persistence. - public bool IsPersistentSession { get; set; } = false; - - /// - /// Gets or sets the website id that the login will be performed for. - /// Known values are "Unknown", "Client", "Mobile", "Website", "Store", "Community", "Partner", "SteamStats". - /// - /// The website id. - public string? WebsiteID { get; set; } = "Client"; - - /// - /// Steam guard data for client login. Provide if available. - /// - /// The guard data. - public string? GuardData { get; set; } - - /// - /// Authenticator object which will be used to handle 2-factor authentication if necessary. - /// Use for a default implementation. - /// - /// The authenticator object. - public IAuthenticator? Authenticator { get; set; } - } - - /// - /// - /// - public class AuthPollResult - { - /// - /// Account name of authenticating account. - /// - public string AccountName { get; } - /// - /// New refresh token. - /// - public string RefreshToken { get; } - /// - /// New token subordinate to . - /// - public string AccessToken { get; } - /// - /// May contain remembered machine ID for future login, usually when account uses email based Steam Guard. - /// Supply it in for future logins to avoid resending an email. This value should be stored per account. - /// - public string? NewGuardData { get; } - - internal AuthPollResult( CAuthentication_PollAuthSessionStatus_Response response ) - { - AccessToken = response.access_token; - RefreshToken = response.refresh_token; - AccountName = response.account_name; - NewGuardData = response.new_guard_data; - } - } - - /// - /// Represents an authentication sesssion which can be used to finish the authentication and get access tokens. - /// - public class AuthSession - { - internal SteamAuthentication Authentication { get; } - - /// - /// Confirmation types that will be able to confirm the request. - /// - internal List AllowedConfirmations { get; } - - /// - /// Authenticator object which will be used to handle 2-factor authentication if necessary. - /// - public IAuthenticator? Authenticator { get; } - /// - /// Unique identifier of requestor, also used for routing, portion of QR code. - /// - public ulong ClientID { get; internal set; } - /// - /// Unique request ID to be presented by requestor at poll time. - /// - public byte[] RequestID { get; } - /// - /// Refresh interval with which requestor should call PollAuthSessionStatus. - /// - public TimeSpan PollingInterval { get; } - - internal AuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, ulong clientId, byte[] requestId, List allowedConfirmations, float pollingInterval ) - { - Authentication = authentication; - Authenticator = authenticator; - ClientID = clientId; - RequestID = requestId; - AllowedConfirmations = SortConfirmations( allowedConfirmations ); - PollingInterval = TimeSpan.FromSeconds( ( double )pollingInterval ); - } - - /// - /// Handle any 2-factor authentication, and if necessary poll for updates until authentication succeeds. - /// - /// An object containing tokens which can be used to login to Steam. - /// Thrown when an invalid state occurs, such as no supported confirmation methods are available. - /// Thrown when polling fails. - public async Task PollingWaitForResultAsync( CancellationToken? cancellationToken = null ) - { - var pollLoop = false; - var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); - - if ( preferredConfirmation == null || preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown ) - { - throw new InvalidOperationException( "There are no allowed confirmations" ); - } - - // If an authenticator is provided and we device confirmation is available, allow consumers to choose whether they want to - // simply poll until confirmation is accepted, or whether they want to fallback to the next preferred confirmation type. - if ( Authenticator != null && preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation ) - { - var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmationAsync(); - - if ( !prefersToPollForConfirmation ) - { - if ( AllowedConfirmations.Count <= 1 ) - { - throw new InvalidOperationException( "AcceptDeviceConfirmation returned false which indicates a fallback to another confirmation type, but there are no other confirmation types available." ); - } - - preferredConfirmation = AllowedConfirmations[ 1 ]; - } - } - - switch ( preferredConfirmation.confirmation_type ) - { - // No steam guard - case EAuthSessionGuardType.k_EAuthSessionGuardType_None: - break; - - // 2-factor code from the authenticator app or sent to an email - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode: - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode: - if ( !( this is CredentialsAuthSession credentialsAuthSession ) ) - { - throw new InvalidOperationException( $"Got {preferredConfirmation.confirmation_type} confirmation type in a session that is not {nameof( CredentialsAuthSession )}." ); - } - - if ( Authenticator == null ) - { - throw new InvalidOperationException( $"This account requires an authenticator for login, but none was provided in {nameof( AuthSessionDetails )}." ); - } - - var expectedInvalidCodeResult = preferredConfirmation.confirmation_type switch - { - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => EResult.InvalidLoginAuthCode, - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => EResult.TwoFactorCodeMismatch, - _ => throw new NotImplementedException(), - }; - var previousCodeWasIncorrect = false; - var waitingForValidCode = true; - - do - { - cancellationToken?.ThrowIfCancellationRequested(); - - try - { - var task = preferredConfirmation.confirmation_type switch - { - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode => Authenticator.GetEmailCodeAsync( preferredConfirmation.associated_message, previousCodeWasIncorrect ), - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode => Authenticator.GetDeviceCodeAsync( previousCodeWasIncorrect ), - _ => throw new NotImplementedException(), - }; - - var code = await task; - - cancellationToken?.ThrowIfCancellationRequested(); - - if ( string.IsNullOrEmpty( code ) ) - { - throw new InvalidOperationException( "No code was provided by the authenticator." ); - } - - await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ); - - waitingForValidCode = false; - } - catch ( AuthenticationException e ) when ( e.Result == expectedInvalidCodeResult ) - { - previousCodeWasIncorrect = true; - } - } - while ( waitingForValidCode ); - - break; - - // This is a prompt that appears in the Steam mobile app - case EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation: - pollLoop = true; - break; - - /* - case EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation: - // TODO: what is this? - pollLoop = true; - break; - - case EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken: - // ${u.De.LOGIN_BASE_URL}jwt/checkdevice - with steam machine guard cookie set - throw new NotImplementedException( $"Machine token confirmation is not supported by SteamKit at the moment." ); - */ - - default: - throw new NotImplementedException( $"Unsupported confirmation type {preferredConfirmation.confirmation_type}." ); - } - - if ( !pollLoop ) - { - cancellationToken?.ThrowIfCancellationRequested(); - - var pollResponse = await PollAuthSessionStatusAsync(); - - if ( pollResponse == null ) - { - throw new AuthenticationException( "Authentication failed", EResult.Fail ); - } - - return pollResponse; - } - - while ( true ) - { - if( cancellationToken is CancellationToken nonNullCancellationToken ) - { - await Task.Delay( PollingInterval, nonNullCancellationToken ); - } - else - { - await Task.Delay( PollingInterval ); - } - - var pollResponse = await PollAuthSessionStatusAsync(); - - if ( pollResponse != null ) - { - return pollResponse; - } - } - } - - /// - /// Polls for authentication status once. Prefer using instead. - /// - /// An object containing tokens which can be used to login to Steam, or null if not yet authenticated. - /// Thrown when polling fails. - public async Task PollAuthSessionStatusAsync() - { - var request = new CAuthentication_PollAuthSessionStatus_Request - { - client_id = ClientID, - request_id = RequestID, - }; - - var message = await Authentication.AuthenticationService!.SendMessage( api => api.PollAuthSessionStatus( request ) ); - - // eresult can be Expired, FileNotFound, Fail - if ( message.Result != EResult.OK ) - { - throw new AuthenticationException( "Failed to poll status", message.Result ); - } - - var response = message.GetDeserializedResponse(); - - HandlePollAuthSessionStatusResponse( response ); - - if ( response.refresh_token.Length > 0 ) - { - return new AuthPollResult( response ); - } - - return null; - } - - internal virtual void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response) - { - if ( response.new_client_id != default ) - { - ClientID = response.new_client_id; - } - } - } - - /// - /// QR code based authentication session. - /// - public sealed class QrAuthSession : AuthSession - { - /// - /// URL based on client ID, which can be rendered as QR code. - /// - public string ChallengeURL { get; internal set; } - - /// - /// Called whenever the challenge url is refreshed by Steam. - /// - public Action? ChallengeURLChanged { get; set; } - - internal QrAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaQR_Response response ) - : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) - { - ChallengeURL = response.challenge_url; - } - - internal override void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) - { - base.HandlePollAuthSessionStatusResponse( response ); - - if ( response.new_challenge_url.Length > 0 ) - { - ChallengeURL = response.new_challenge_url; - ChallengeURLChanged?.Invoke(); - } - } - } - - /// - /// Credentials based authentication session. - /// - public sealed class CredentialsAuthSession : AuthSession - { - /// - /// SteamID of the account logging in, will only be included if the credentials were correct. - /// - public SteamID SteamID { get; } - - internal CredentialsAuthSession( SteamAuthentication authentication, IAuthenticator? authenticator, CAuthentication_BeginAuthSessionViaCredentials_Response response ) - : base( authentication, authenticator, response.client_id, response.request_id, response.allowed_confirmations, response.interval ) - { - SteamID = new SteamID( response.steamid ); - } - - /// - /// Send Steam Guard code for this authentication session. - /// - /// The code. - /// Type of code. - /// - /// - public async Task SendSteamGuardCodeAsync( string code, EAuthSessionGuardType codeType ) - { - var request = new CAuthentication_UpdateAuthSessionWithSteamGuardCode_Request - { - client_id = ClientID, - steamid = SteamID, - code = code, - code_type = codeType, - }; - - var message = await Authentication.AuthenticationService!.SendMessage( api => api.UpdateAuthSessionWithSteamGuardCode( request ) ); - var response = message.GetDeserializedResponse(); - - // can be InvalidLoginAuthCode, TwoFactorCodeMismatch, Expired - if ( message.Result != EResult.OK ) - { - throw new AuthenticationException( "Failed to send steam guard code", message.Result ); - } - - // response may contain agreement_session_url - } - } - - internal SteamUnifiedMessages.UnifiedService? AuthenticationService { get; private set; } - - /// - /// Gets public key for the provided account name which can be used to encrypt the account password. - /// - /// The account name to get RSA public key for. - async Task GetPasswordRSAPublicKeyAsync( string accountName ) - { - var request = new CAuthentication_GetPasswordRSAPublicKey_Request - { - account_name = accountName - }; - - var message = await AuthenticationService!.SendMessage( api => api.GetPasswordRSAPublicKey( request ) ); - - if ( message.Result != EResult.OK ) - { - throw new AuthenticationException( "Failed to get password public key", message.Result ); - } - - var response = message.GetDeserializedResponse(); - - return response; - } - - /// - /// Start the authentication process using QR codes. - /// - /// The details to use for logging on. - public async Task BeginAuthSessionViaQR( AuthSessionDetails details ) - { - var unifiedMessages = Client.GetHandler()!; - AuthenticationService = unifiedMessages.CreateService(); - - var request = new CAuthentication_BeginAuthSessionViaQR_Request - { - website_id = details.WebsiteID, - device_details = new CAuthentication_DeviceDetails - { - device_friendly_name = details.DeviceFriendlyName, - platform_type = details.PlatformType, - os_type = ( int )details.ClientOSType, - } - }; - - var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaQR( request ) ); - - if ( message.Result != EResult.OK ) - { - throw new AuthenticationException( "Failed to begin QR auth session", message.Result ); - } - - var response = message.GetDeserializedResponse(); - - var authResponse = new QrAuthSession( this, details.Authenticator, response ); - - return authResponse; - } - - /// - /// Start the authentication process by providing username and password. - /// - /// The details to use for logging on. - /// No auth details were provided. - /// Username or password are not set within . - public async Task BeginAuthSessionViaCredentials( AuthSessionDetails details ) - { - var unifiedMessages = Client.GetHandler()!; - AuthenticationService = unifiedMessages.CreateService(); - - if ( details == null ) - { - throw new ArgumentNullException( nameof( details ) ); - } - - if ( string.IsNullOrEmpty( details.Username ) || string.IsNullOrEmpty( details.Password ) ) - { - throw new ArgumentException( "BeginAuthSessionViaCredentials requires a username and password to be set in 'details'." ); - } - - // Encrypt the password - var publicKey = await GetPasswordRSAPublicKeyAsync( details.Username! ); - var rsaParameters = new RSAParameters - { - Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), - Exponent = Utils.DecodeHexString( publicKey.publickey_exp ), - }; - - using var rsa = RSA.Create(); - rsa.ImportParameters( rsaParameters ); - var encryptedPassword = rsa.Encrypt( Encoding.UTF8.GetBytes( details.Password ), RSAEncryptionPadding.Pkcs1 ); - - // Create request - var request = new CAuthentication_BeginAuthSessionViaCredentials_Request - { - account_name = details.Username, - persistence = details.IsPersistentSession ? ESessionPersistence.k_ESessionPersistence_Persistent : ESessionPersistence.k_ESessionPersistence_Ephemeral, - website_id = details.WebsiteID, - guard_data = details.GuardData, - encrypted_password = Convert.ToBase64String( encryptedPassword ), - encryption_timestamp = publicKey.timestamp, - device_details = new CAuthentication_DeviceDetails - { - device_friendly_name = details.DeviceFriendlyName, - platform_type = details.PlatformType, - os_type = ( int )details.ClientOSType, - } - }; - - var message = await AuthenticationService.SendMessage( api => api.BeginAuthSessionViaCredentials( request ) ); - - // eresult can be InvalidPassword, ServiceUnavailable, InvalidParam, RateLimitExceeded - if ( message.Result != EResult.OK ) - { - throw new AuthenticationException( "Authentication failed", message.Result ); - } - - var response = message.GetDeserializedResponse(); - - var authResponse = new CredentialsAuthSession( this, details.Authenticator, response ); - - return authResponse; - } - - /// - /// Handles a client message. This should not be called directly. - /// - /// The packet message that contains the data. - public override void HandleMsg( IPacketMsg packetMsg ) - { - // not used - } - - /// - /// Sort available guard confirmation methods by an order that we prefer to handle them in - /// - static List SortConfirmations( List confirmations ) - { - var preferredConfirmationTypes = new EAuthSessionGuardType[] - { - EAuthSessionGuardType.k_EAuthSessionGuardType_None, - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation, - EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceCode, - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailCode, - EAuthSessionGuardType.k_EAuthSessionGuardType_EmailConfirmation, - EAuthSessionGuardType.k_EAuthSessionGuardType_MachineToken, - EAuthSessionGuardType.k_EAuthSessionGuardType_Unknown, - }; - var sortOrder = Enumerable.Range( 0, preferredConfirmationTypes.Length ).ToDictionary( x => preferredConfirmationTypes[ x ], x => x ); - - return confirmations.OrderBy( x => - { - if ( sortOrder.TryGetValue( x.confirmation_type, out var sortIndex ) ) - { - return sortIndex; - } - - return int.MaxValue; - } ).ToList(); - } - } -} diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs index 8c4ee1502..4a0ba0277 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs @@ -76,7 +76,7 @@ public sealed class LogOnDetails /// The sentry file hash. public byte[]? SentryFileHash { get; set; } /// - /// Gets or sets the access token used to login. This a token that has been provided after a successful login using . + /// Gets or sets the access token used to login. This a token that has been provided after a successful login using . /// /// The access token. public string? AccessToken { get; set; } diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index 01d0f9345..f640256cd 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -92,7 +92,6 @@ public SteamClient( SteamConfiguration configuration, string identifier ) this.AddHandler( new SteamWorkshop() ); this.AddHandler( new SteamTrading() ); this.AddHandler( new SteamUnifiedMessages() ); - this.AddHandler( new SteamAuthentication() ); this.AddHandler( new SteamScreenshots() ); this.AddHandler( new SteamMatchmaking() ); this.AddHandler( new SteamNetworking() ); From 2496b06bc57eb4bdb6e5b2bf088f2ca2b8ac813c Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Wed, 22 Mar 2023 16:11:13 +0200 Subject: [PATCH 26/29] Suppress LoginKey obsolete warnings in SK codebase --- .../SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs index 4a0ba0277..f42c2a47e 100644 --- a/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs +++ b/SteamKit2/SteamKit2/Steam/Handlers/SteamUser/SteamUser.cs @@ -301,10 +301,13 @@ public void LogOn( LogOnDetails details ) { throw new ArgumentNullException( nameof( details ) ); } + +#pragma warning disable CS0618 // LoginKey is obsolete if ( string.IsNullOrEmpty( details.Username ) || ( string.IsNullOrEmpty( details.Password ) && string.IsNullOrEmpty( details.LoginKey ) && string.IsNullOrEmpty( details.AccessToken ) ) ) { throw new ArgumentException( "LogOn requires a username and password to be set in 'details'." ); } + if ( !string.IsNullOrEmpty( details.LoginKey ) && !details.ShouldRememberPassword ) { // Prevent consumers from screwing this up. @@ -312,6 +315,8 @@ public void LogOn( LogOnDetails details ) // The inverse is not applicable (you can log in with should_remember_password and no login_key). throw new ArgumentException( "ShouldRememberPassword is required to be set to true in order to use LoginKey." ); } +#pragma warning restore CS0618 // LoginKey is obsolete + if ( !this.Client.IsConnected ) { this.Client.PostCallback( new LoggedOnCallback( EResult.NoConnection ) ); @@ -365,7 +370,10 @@ public void LogOn( LogOnDetails details ) logon.Body.auth_code = details.AuthCode; logon.Body.two_factor_code = details.TwoFactorCode; +#pragma warning disable CS0618 // LoginKey is obsolete logon.Body.login_key = details.LoginKey; +#pragma warning restore CS0618 // LoginKey is obsolete + logon.Body.access_token = details.AccessToken; logon.Body.sha_sentryfile = details.SentryFileHash; @@ -562,7 +570,9 @@ void HandleLoginKey( IPacketMsg packetMsg ) { var loginKey = new ClientMsgProtobuf( packetMsg ); +#pragma warning disable CS0618 // LoginKey is obsolete var callback = new LoginKeyCallback( loginKey.Body ); +#pragma warning restore CS0618 // LoginKey is obsolete this.Client.PostCallback( callback ); } void HandleLogOnResponse( IPacketMsg packetMsg ) From 9aa61d066729e3958594b5835c89e139c006ab7f Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Thu, 23 Mar 2023 10:16:24 +0200 Subject: [PATCH 27/29] Review --- .../Steam/Authentication/AuthSession.cs | 33 +++++++++---------- .../Steam/Authentication/QrAuthSession.cs | 3 +- .../Authentication/SteamAuthentication.cs | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs index 379224262..d6ae9ee63 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs @@ -57,7 +57,7 @@ internal AuthSession( SteamAuthentication authentication, IAuthenticator? authen /// An object containing tokens which can be used to login to Steam. /// Thrown when an invalid state occurs, such as no supported confirmation methods are available. /// Thrown when polling fails. - public async Task PollingWaitForResultAsync( CancellationToken? cancellationToken = null ) + public async Task PollingWaitForResultAsync( CancellationToken cancellationToken = default ) { var pollLoop = false; var preferredConfirmation = AllowedConfirmations.FirstOrDefault(); @@ -71,7 +71,7 @@ public async Task PollingWaitForResultAsync( CancellationToken? // simply poll until confirmation is accepted, or whether they want to fallback to the next preferred confirmation type. if ( Authenticator != null && preferredConfirmation.confirmation_type == EAuthSessionGuardType.k_EAuthSessionGuardType_DeviceConfirmation ) { - var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmationAsync(); + var prefersToPollForConfirmation = await Authenticator.AcceptDeviceConfirmationAsync().ConfigureAwait( false ); if ( !prefersToPollForConfirmation ) { @@ -114,7 +114,7 @@ public async Task PollingWaitForResultAsync( CancellationToken? do { - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); try { @@ -125,16 +125,16 @@ public async Task PollingWaitForResultAsync( CancellationToken? _ => throw new NotImplementedException(), }; - var code = await task; + var code = await task.ConfigureAwait( false ); - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); if ( string.IsNullOrEmpty( code ) ) { throw new InvalidOperationException( "No code was provided by the authenticator." ); } - await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ); + await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ).ConfigureAwait( false ); waitingForValidCode = false; } @@ -169,9 +169,9 @@ public async Task PollingWaitForResultAsync( CancellationToken? if ( !pollLoop ) { - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - var pollResponse = await PollAuthSessionStatusAsync(); + var pollResponse = await PollAuthSessionStatusAsync().ConfigureAwait( false ); if ( pollResponse == null ) { @@ -183,16 +183,9 @@ public async Task PollingWaitForResultAsync( CancellationToken? while ( true ) { - if ( cancellationToken is CancellationToken nonNullCancellationToken ) - { - await Task.Delay( PollingInterval, nonNullCancellationToken ); - } - else - { - await Task.Delay( PollingInterval ); - } + await Task.Delay( PollingInterval, cancellationToken ).ConfigureAwait( false ); - var pollResponse = await PollAuthSessionStatusAsync(); + var pollResponse = await PollAuthSessionStatusAsync().ConfigureAwait( false ); if ( pollResponse != null ) { @@ -234,7 +227,11 @@ public async Task PollingWaitForResultAsync( CancellationToken? return null; } - internal virtual void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) + /// + /// Handles poll authentication session status response. + /// + /// The response. + protected virtual void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) { if ( response.new_client_id != default ) { diff --git a/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs index a58a71cae..d77974684 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs @@ -29,7 +29,8 @@ internal QrAuthSession( SteamAuthentication authentication, IAuthenticator? auth ChallengeURL = response.challenge_url; } - internal override void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) + /// + protected override void HandlePollAuthSessionStatusResponse( CAuthentication_PollAuthSessionStatus_Response response ) { base.HandlePollAuthSessionStatusResponse( response ); diff --git a/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs index 545e6dd4f..ce11c07ef 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs @@ -119,7 +119,7 @@ public async Task BeginAuthSessionViaCredentialsAsync( A } // Encrypt the password - var publicKey = await GetPasswordRSAPublicKeyAsync( details.Username! ); + var publicKey = await GetPasswordRSAPublicKeyAsync( details.Username! ).ConfigureAwait( false ); var rsaParameters = new RSAParameters { Modulus = Utils.DecodeHexString( publicKey.publickey_mod ), From 79931d7b358e25428c0e076b7a7875200a04f9fc Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Thu, 23 Mar 2023 11:24:29 +0200 Subject: [PATCH 28/29] Change visibility --- SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs index d6ae9ee63..d0eda52f2 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthSession.cs @@ -17,12 +17,15 @@ namespace SteamKit2.Authentication /// public class AuthSession { - internal SteamAuthentication Authentication { get; } + /// + /// Instance of that created this authentication session. + /// + private protected SteamAuthentication Authentication { get; } /// /// Confirmation types that will be able to confirm the request. /// - internal List AllowedConfirmations { get; } + List AllowedConfirmations; /// /// Authenticator object which will be used to handle 2-factor authentication if necessary. From 1d41f5fcdb932d4df5c1663b3ea2e83cce9fa28d Mon Sep 17 00:00:00 2001 From: Pavel Djundik Date: Thu, 23 Mar 2023 11:33:03 +0200 Subject: [PATCH 29/29] Added `SteamClient.Authentication` --- Samples/1a.Authentication/Program.cs | 5 +---- Samples/1b.QrCodeAuthentication/Program.cs | 5 +---- .../SteamKit2/Steam/Authentication/AuthPollResult.cs | 1 + .../SteamKit2/Steam/Authentication/SteamAuthentication.cs | 2 +- SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs | 8 ++++++++ 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs index 42aa6deb0..9cb5caf4a 100644 --- a/Samples/1a.Authentication/Program.cs +++ b/Samples/1a.Authentication/Program.cs @@ -48,11 +48,8 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { Console.WriteLine( "Connected to Steam! Logging in '{0}'...", user ); - // get the authentication handler, which used for authenticating with Steam - var auth = new SteamAuthentication( steamClient ); - // Begin authenticating via credentials - var authSession = await auth.BeginAuthSessionViaCredentialsAsync( new AuthSessionDetails + var authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync( new AuthSessionDetails { Username = user, Password = pass, diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs index 1722b6e84..a894467b8 100644 --- a/Samples/1b.QrCodeAuthentication/Program.cs +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -36,11 +36,8 @@ async void OnConnected( SteamClient.ConnectedCallback callback ) { - // get the authentication handler, which used for authenticating with Steam - var auth = new SteamAuthentication( steamClient ); - // Start an authentication session by requesting a link - var authSession = await auth.BeginAuthSessionViaQRAsync( new AuthSessionDetails() ); + var authSession = await steamClient.Authentication.BeginAuthSessionViaQRAsync( new AuthSessionDetails() ); // Steam will periodically refresh the challenge url, this callback allows you to draw a new qr code authSession.ChallengeURLChanged = () => diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs index 405961ab9..37f3594a0 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs @@ -18,6 +18,7 @@ public sealed class AuthPollResult public string AccountName { get; } /// /// New refresh token. + /// This can be provided to . /// public string RefreshToken { get; } /// diff --git a/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs index ce11c07ef..1674cadc8 100644 --- a/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs +++ b/SteamKit2/SteamKit2/Steam/Authentication/SteamAuthentication.cs @@ -23,7 +23,7 @@ public sealed class SteamAuthentication /// Initializes a new instance of the class. /// /// The this instance will be associated with. - public SteamAuthentication( SteamClient steamClient ) + internal SteamAuthentication( SteamClient steamClient ) { if ( steamClient == null ) { diff --git a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs index f640256cd..6337bdabb 100644 --- a/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs +++ b/SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Threading; using ProtoBuf; +using SteamKit2.Authentication; using SteamKit2.Internal; namespace SteamKit2 @@ -35,6 +36,13 @@ public sealed partial class SteamClient : CMClient internal AsyncJobManager jobManager; + SteamAuthentication? _authentication = null; + + /// + /// Handler used for authenticating on Steam. + /// + public SteamAuthentication Authentication => _authentication ??= new SteamAuthentication( this ); + /// /// Initializes a new instance of the class with the default configuration. ///