From cce7a09838a796d143a3443434edebf92f8fa5c9 Mon Sep 17 00:00:00 2001 From: Adrian Clark Date: Thu, 13 Oct 2022 00:29:41 +1000 Subject: [PATCH] Allow Supply of Encoded Passwords Give the user of the library an option to supply a pre-encoded password value suitable for passing directly to iRacing's API. This opens the possibility of running the library without the system it operates on ever knowing the plain-text password. Fixes: #130 --- .../LoginViaOptionsTests.cs | 90 ++++++++++++++++++- .../CompatibilitySuppressions.xml | 14 +++ src/Aydsko.iRacingData/DataClient.cs | 26 ++++-- src/Aydsko.iRacingData/IDataClient.cs | 7 ++ .../Package Release Notes.txt | 9 +- .../iRacingDataClientOptions.cs | 4 + 6 files changed, 136 insertions(+), 14 deletions(-) diff --git a/src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs b/src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs index c8e2e9c..dad505c 100644 --- a/src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs +++ b/src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs @@ -6,7 +6,7 @@ namespace Aydsko.iRacingData.UnitTests; -public class LoginViaOptionsTests : MockedHttpTestBase +public class PasswordEncodingTests : MockedHttpTestBase { [SetUp] public void SetUp() @@ -15,12 +15,13 @@ public void SetUp() } [TestCaseSource(nameof(GetTestCases))] - public async Task ValidateLoginRequestViaOptions(string username, string password, string expectedEncodedPassword) + public async Task ValidateLoginRequestViaOptions(string username, string password, bool passwordIsEncoded, string expectedEncodedPassword) { var options = new iRacingDataClientOptions { Username = username, Password = password, + PasswordIsEncoded = passwordIsEncoded, RestoreCookies = null, SaveCookies = null, }; @@ -51,7 +52,92 @@ public async Task ValidateLoginRequestViaOptions(string username, string passwor Assert.That(lookups.Data, Is.Not.Null.Or.Empty); } + [TestCaseSource(nameof(GetTestCases))] + public async Task ValidateLoginRequestViaMethodWithPasswordIsEncodedParam(string username, string password, bool passwordIsEncoded, string expectedEncodedPassword) + { + var options = new iRacingDataClientOptions + { + RestoreCookies = null, + SaveCookies = null, + }; + + await MessageHandler.QueueResponsesAsync(nameof(CapturedResponseValidationTests.GetLookupsSuccessfulAsync)).ConfigureAwait(false); + + var sut = new DataClient(HttpClient, + new TestLogger(), + options, + CookieContainer); + + sut.UseUsernameAndPassword(username, password, passwordIsEncoded); + + var lookups = await sut.GetLookupsAsync(CancellationToken.None).ConfigureAwait(false); + + var loginRequest = MessageHandler.Requests.Peek(); + Assert.That(loginRequest, Is.Not.Null); + + var contentStreamTask = loginRequest.Content?.ReadAsStreamAsync() ?? Task.FromResult(Stream.Null); + using var requestContentStream = await contentStreamTask.ConfigureAwait(false); + Assert.That(requestContentStream, Is.Not.Null.Or.Empty); + + var loginDto = await JsonSerializer.DeserializeAsync(requestContentStream).ConfigureAwait(false); + Assert.That(loginDto, Is.Not.Null); + + Assert.That(loginDto!.Email, Is.EqualTo(username)); + Assert.That(loginDto!.Password, Is.EqualTo(expectedEncodedPassword)); + + Assert.That(sut.IsLoggedIn, Is.True); + Assert.That(lookups, Is.Not.Null); + Assert.That(lookups.Data, Is.Not.Null.Or.Empty); + } + + [TestCaseSource(nameof(GetTestCasesWithUnencodedPasswords))] + public async Task ValidateLoginRequestViaMethod(string username, string password, string expectedEncodedPassword) + { + var options = new iRacingDataClientOptions + { + RestoreCookies = null, + SaveCookies = null, + }; + + await MessageHandler.QueueResponsesAsync(nameof(CapturedResponseValidationTests.GetLookupsSuccessfulAsync)).ConfigureAwait(false); + + var sut = new DataClient(HttpClient, + new TestLogger(), + options, + CookieContainer); + + sut.UseUsernameAndPassword(username, password); + + var lookups = await sut.GetLookupsAsync(CancellationToken.None).ConfigureAwait(false); + + var loginRequest = MessageHandler.Requests.Peek(); + Assert.That(loginRequest, Is.Not.Null); + + var contentStreamTask = loginRequest.Content?.ReadAsStreamAsync() ?? Task.FromResult(Stream.Null); + using var requestContentStream = await contentStreamTask.ConfigureAwait(false); + Assert.That(requestContentStream, Is.Not.Null.Or.Empty); + + var loginDto = await JsonSerializer.DeserializeAsync(requestContentStream).ConfigureAwait(false); + Assert.That(loginDto, Is.Not.Null); + + Assert.That(loginDto!.Email, Is.EqualTo(username)); + Assert.That(loginDto!.Password, Is.EqualTo(expectedEncodedPassword)); + + Assert.That(sut.IsLoggedIn, Is.True); + Assert.That(lookups, Is.Not.Null); + Assert.That(lookups.Data, Is.Not.Null.Or.Empty); + } + public static IEnumerable GetTestCases() + { + yield return new("test.user@example.com", "SuperSecretPassword", false, "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso="); + yield return new("CLunky@iracing.Com", "MyPassWord", false, "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q="); + + yield return new("test.user@example.com", "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso=", true, "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso="); + yield return new("CLunky@iracing.Com", "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q=", true, "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q="); + } + + public static IEnumerable GetTestCasesWithUnencodedPasswords() { yield return new("test.user@example.com", "SuperSecretPassword", "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso="); yield return new("CLunky@iracing.Com", "MyPassWord", "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q="); diff --git a/src/Aydsko.iRacingData/CompatibilitySuppressions.xml b/src/Aydsko.iRacingData/CompatibilitySuppressions.xml index 1b1836b..fba00c4 100644 --- a/src/Aydsko.iRacingData/CompatibilitySuppressions.xml +++ b/src/Aydsko.iRacingData/CompatibilitySuppressions.xml @@ -6,4 +6,18 @@ lib/netstandard2.0/Aydsko.iRacingData.dll lib/net6.0/Aydsko.iRacingData.dll + + CP0006 + M:Aydsko.iRacingData.IDataClient.UseUsernameAndPassword(System.String,System.String,System.Boolean) + lib/net6.0/Aydsko.iRacingData.dll + lib/net6.0/Aydsko.iRacingData.dll + true + + + CP0006 + M:Aydsko.iRacingData.IDataClient.UseUsernameAndPassword(System.String,System.String,System.Boolean) + lib/netstandard2.0/Aydsko.iRacingData.dll + lib/netstandard2.0/Aydsko.iRacingData.dll + true + \ No newline at end of file diff --git a/src/Aydsko.iRacingData/DataClient.cs b/src/Aydsko.iRacingData/DataClient.cs index e9521a6..cf432c5 100644 --- a/src/Aydsko.iRacingData/DataClient.cs +++ b/src/Aydsko.iRacingData/DataClient.cs @@ -46,7 +46,7 @@ public DataClient(HttpClient httpClient, } /// - public void UseUsernameAndPassword(string username, string password) + public void UseUsernameAndPassword(string username, string password, bool passwordIsEncoded) { if (string.IsNullOrWhiteSpace(username)) { @@ -60,11 +60,18 @@ public void UseUsernameAndPassword(string username, string password) options.Username = username; options.Password = password; + options.PasswordIsEncoded = passwordIsEncoded; // If the username & password has been updated likely the authentication needs to run again. IsLoggedIn = false; } + /// + public void UseUsernameAndPassword(string username, string password) + { + UseUsernameAndPassword(username, password, false); + } + /// public async Task>> GetCarAssetDetailsAsync(CancellationToken cancellationToken = default) { @@ -1501,11 +1508,20 @@ private async Task LoginInternalAsync(CancellationToken cancellationToken) cookieContainer.Add(savedCookies); } - using var sha256 = SHA256.Create(); + string? encodedHash = null; - var passwordAndEmail = options.Password + (options.Username?.ToLowerInvariant()); - var hashedPasswordAndEmailBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(passwordAndEmail)); - var encodedHash = Convert.ToBase64String(hashedPasswordAndEmailBytes); + if (options.PasswordIsEncoded) + { + encodedHash = options.Password; + } + else + { + using var sha256 = SHA256.Create(); + + var passwordAndEmail = options.Password + (options.Username?.ToLowerInvariant()); + var hashedPasswordAndEmailBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(passwordAndEmail)); + encodedHash = Convert.ToBase64String(hashedPasswordAndEmailBytes); + } var loginResponse = await httpClient.PostAsJsonAsync("https://members-ng.iracing.com/auth", new diff --git a/src/Aydsko.iRacingData/IDataClient.cs b/src/Aydsko.iRacingData/IDataClient.cs index aa86b22..ba8402a 100644 --- a/src/Aydsko.iRacingData/IDataClient.cs +++ b/src/Aydsko.iRacingData/IDataClient.cs @@ -24,6 +24,13 @@ public interface IDataClient /// Either or were or white space. void UseUsernameAndPassword(string username, string password); + /// Supply the username and password if they weren't supplied through the object. + /// iRacing user name to use for authentication. + /// Password associated with the iRacing user name used to authenticate. + /// If indicates the value is already encoded ready for submission to the iRacing API. + /// Either or were or white space. + void UseUsernameAndPassword(string username, string password, bool passwordIsEncoded); + /// Retrieves details about the car assets, including image paths and descriptions. /// A token to allow the operation to be cancelled. /// A containing a dictionary which maps the car identifier to a object for each car. diff --git a/src/Aydsko.iRacingData/Package Release Notes.txt b/src/Aydsko.iRacingData/Package Release Notes.txt index 4622c85..9fa850e 100644 --- a/src/Aydsko.iRacingData/Package Release Notes.txt +++ b/src/Aydsko.iRacingData/Package Release Notes.txt @@ -1,11 +1,6 @@ Fixes / Changes: - - TODO - - - -Contributions: - - - TODO + - Accept Pre-Hashed Password (Issue #130) + - Allow a user of the library the ability of using a pre-encoded password rather than the plain text. Thanks for helping out with pull requests to the library! \ No newline at end of file diff --git a/src/Aydsko.iRacingData/iRacingDataClientOptions.cs b/src/Aydsko.iRacingData/iRacingDataClientOptions.cs index ca36136..3370eae 100644 --- a/src/Aydsko.iRacingData/iRacingDataClientOptions.cs +++ b/src/Aydsko.iRacingData/iRacingDataClientOptions.cs @@ -20,6 +20,10 @@ public class iRacingDataClientOptions /// Password associated with the iRacing user name used to authenticate. public string? Password { get; set; } + /// If indicates the property value is already encoded ready for submission to the iRacing API. + /// + public bool PasswordIsEncoded { get; set; } + /// Called to retrieve cookie values stored from a previous authentication. public Func? RestoreCookies { get; set; }