diff --git a/Samples/1a.Authentication/Program.cs b/Samples/1a.Authentication/Program.cs new file mode 100644 index 000000000..9cb5caf4a --- /dev/null +++ b/Samples/1a.Authentication/Program.cs @@ -0,0 +1,134 @@ +using System; +using System.Text.Json; +using SteamKit2; +using SteamKit2.Authentication; + +if ( args.Length < 2 ) +{ + Console.Error.WriteLine( "Sample1a: 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 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 ); + + // Begin authenticating via credentials + var authSession = await steamClient.Authentication.BeginAuthSessionViaCredentialsAsync( new AuthSessionDetails + { + Username = user, + Password = pass, + IsPersistentSession = false, + Authenticator = new UserConsoleAuthenticator(), + } ); + + // Starting polling Steam for authentication response + 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 + 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 ) +{ + 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 ); +} + + + +// 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(); +} diff --git a/Samples/1a.Authentication/Sample1a_Authentication.csproj b/Samples/1a.Authentication/Sample1a_Authentication.csproj new file mode 100644 index 000000000..e5aa29cd0 --- /dev/null +++ b/Samples/1a.Authentication/Sample1a_Authentication.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + SteamRE + Sample1a_Authentication + + SteamKit Sample 1a: Authentication + Copyright © Pavel Djundik 2023 + Sample1a_Authentication + + + + + + + diff --git a/Samples/1b.QrCodeAuthentication/Program.cs b/Samples/1b.QrCodeAuthentication/Program.cs new file mode 100644 index 000000000..a894467b8 --- /dev/null +++ b/Samples/1b.QrCodeAuthentication/Program.cs @@ -0,0 +1,111 @@ +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 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 steamClient.Authentication.BeginAuthSessionViaQRAsync( new AuthSessionDetails() ); + + // 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" ); + + DrawQRCode( authSession ); + }; + + // 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.PollingWaitForResultAsync(); + + 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 ); +} + +void DrawQRCode( 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/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 64b17b112..fddf1ae7c 100644 --- a/Samples/Samples.sln +++ b/Samples/Samples.sln @@ -1,10 +1,14 @@  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 +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}" @@ -23,7 +27,7 @@ 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -155,18 +159,42 @@ 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 + {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 diff --git a/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs new file mode 100644 index 000000000..37f3594a0 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthPollResult.cs @@ -0,0 +1,42 @@ +/* + * 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. + /// This can be provided to . + /// + 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..d0eda52f2 --- /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 + { + /// + /// Instance of that created this authentication session. + /// + private protected SteamAuthentication Authentication { get; } + + /// + /// Confirmation types that will be able to confirm the request. + /// + List AllowedConfirmations; + + /// + /// 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 = default ) + { + 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().ConfigureAwait( false ); + + 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.ConfigureAwait( false ); + + cancellationToken.ThrowIfCancellationRequested(); + + if ( string.IsNullOrEmpty( code ) ) + { + throw new InvalidOperationException( "No code was provided by the authenticator." ); + } + + await credentialsAuthSession.SendSteamGuardCodeAsync( code, preferredConfirmation.confirmation_type ).ConfigureAwait( false ); + + 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().ConfigureAwait( false ); + + if ( pollResponse == null ) + { + throw new AuthenticationException( "Authentication failed", EResult.Fail ); + } + + return pollResponse; + } + + while ( true ) + { + await Task.Delay( PollingInterval, cancellationToken ).ConfigureAwait( false ); + + var pollResponse = await PollAuthSessionStatusAsync().ConfigureAwait( false ); + + 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; + } + + /// + /// Handles poll authentication session status response. + /// + /// The response. + protected 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/Authentication/AuthenticationException.cs b/SteamKit2/SteamKit2/Steam/Authentication/AuthenticationException.cs new file mode 100644 index 000000000..881a31480 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/AuthenticationException.cs @@ -0,0 +1,34 @@ +using System; + +namespace SteamKit2.Authentication +{ + /// + /// Thrown when fails to authenticate. + /// + [Serializable] + public sealed class AuthenticationException : Exception + { + /// + /// Gets the result of the authentication request. + /// + public EResult Result { get; } + + /// + /// Initializes a new instance of the class. + /// + public AuthenticationException() + { + } + + /// + /// 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}." ) + { + Result = result; + } + } +} 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/Authentication/IAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Authentication/IAuthenticator.cs new file mode 100644 index 000000000..06b521020 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/IAuthenticator.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; + +namespace SteamKit2.Authentication +{ + /// + /// Represents an authenticator to be used with . + /// + 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 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. + /// + /// 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 GetEmailCodeAsync( string email, bool previousCodeWasIncorrect ); + + /// + /// 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 AcceptDeviceConfirmationAsync(); + } +} diff --git a/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs new file mode 100644 index 000000000..d77974684 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/QrAuthSession.cs @@ -0,0 +1,44 @@ +/* + * 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; + } + + /// + protected 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..1674cadc8 --- /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. + internal 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! ).ConfigureAwait( false ); + 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/Authentication/UserConsoleAuthenticator.cs b/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs new file mode 100644 index 000000000..095481955 --- /dev/null +++ b/SteamKit2/SteamKit2/Steam/Authentication/UserConsoleAuthenticator.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading.Tasks; + +namespace SteamKit2.Authentication +{ + /// + /// 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 GetDeviceCodeAsync( bool previousCodeWasIncorrect ) + { + if ( previousCodeWasIncorrect ) + { + Console.Error.WriteLine( "The previous 2-factor auth code you have provided is incorrect." ); + } + + string? code; + + do + { + 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 ) ); + + return Task.FromResult( code! ); + } + + /// + public Task GetEmailCodeAsync( string email, bool previousCodeWasIncorrect ) + { + if ( previousCodeWasIncorrect ) + { + Console.Error.WriteLine( "The previous 2-factor auth code you have provided is incorrect." ); + } + + string? code; + + do + { + 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 ) ); + + return Task.FromResult( code! ); + } + + /// + public Task AcceptDeviceConfirmationAsync() + { + Console.Error.WriteLine( "STEAM GUARD! Use the Steam Mobile App to confirm your sign in..." ); + + return Task.FromResult( true ); + } + } +} 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 f04658f21..f42c2a47e 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. @@ -74,6 +75,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,10 +301,13 @@ public void LogOn( LogOnDetails details ) { throw new ArgumentNullException( nameof( details ) ); } - if ( string.IsNullOrEmpty( details.Username ) || ( string.IsNullOrEmpty( details.Password ) && string.IsNullOrEmpty( details.LoginKey ) ) ) + +#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. @@ -306,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 ) ); @@ -359,7 +370,11 @@ 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; logon.Body.eresult_sentryfile = ( int )( details.SentryFileHash != null ? EResult.OK : EResult.FileNotFound ); @@ -484,6 +499,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 ) @@ -554,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 ) 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. ///