Skip to content

Commit

Permalink
Merge pull request #131 from AdrianJSClark/130-accept-pre-hashed-pass…
Browse files Browse the repository at this point in the history
…word

Accept Pre-Hashed Password
  • Loading branch information
AdrianJSClark authored Oct 12, 2022
2 parents a03aee9 + cce7a09 commit f1cc595
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 26 deletions.
83 changes: 69 additions & 14 deletions src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@

namespace Aydsko.iRacingData.UnitTests;

public class LoginViaOptionsTests : MockedHttpTestBase
public class PasswordEncodingTests : MockedHttpTestBase
{
[SetUp]
public void SetUp()
{
BaseSetUp();
}

[Test]
public async Task GivenOptionsWithUsernameAndPasswordWhenAMethodIsCalledThenItWillSucceedAsync()
[TestCaseSource(nameof(GetTestCases))]
public async Task ValidateLoginRequestViaOptions(string username, string password, bool passwordIsEncoded, string expectedEncodedPassword)
{
var options = new iRacingDataClientOptions
{
Username = "[email protected]",
Password = "SuperSecretPassword",
Username = username,
Password = password,
PasswordIsEncoded = passwordIsEncoded,
RestoreCookies = null,
SaveCookies = null,
};
Expand All @@ -43,51 +44,105 @@ public async Task GivenOptionsWithUsernameAndPasswordWhenAMethodIsCalledThenItWi
var loginDto = await JsonSerializer.DeserializeAsync<TestLoginDto>(requestContentStream).ConfigureAwait(false);
Assert.That(loginDto, Is.Not.Null);

Assert.That(loginDto!.Email, Is.EqualTo("[email protected]"));
Assert.That(loginDto!.Password, Is.EqualTo("nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso="));
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);
}

[Test]
public async Task GivenOptionsWithUsernameAndPasswordAndGiven22S3ModeWhenAMethodIsCalledThenItWillSucceedAsync()
[TestCaseSource(nameof(GetTestCases))]
public async Task ValidateLoginRequestViaMethodWithPasswordIsEncodedParam(string username, string password, bool passwordIsEncoded, string expectedEncodedPassword)
{
var options = new iRacingDataClientOptions
{
Username = "[email protected]",
Password = "MyPassWord",
RestoreCookies = null,
SaveCookies = null,
};

await MessageHandler.QueueResponsesAsync(nameof(CapturedResponseValidationTests.GetLookupsSuccessfulAsync)).ConfigureAwait(false);

var sut = new DataClient(HttpClient,
new TestLogger<DataClient>(),
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<TestLoginDto>(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<DataClient>(),
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);
Assert.That(requestContentStream, Is.Not.Null.Or.Empty);

var loginDto = await JsonSerializer.DeserializeAsync<TestLoginDto>(requestContentStream).ConfigureAwait(false);
Assert.That(loginDto, Is.Not.Null);

Assert.That(loginDto!.Email, Is.EqualTo("[email protected]"));
Assert.That(loginDto!.Password, Is.EqualTo("xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q="));
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<TestCaseData> GetTestCases()
{
yield return new("[email protected]", "SuperSecretPassword", false, "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso=");
yield return new("[email protected]", "MyPassWord", false, "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q=");

yield return new("[email protected]", "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso=", true, "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso=");
yield return new("[email protected]", "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q=", true, "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q=");
}

public static IEnumerable<TestCaseData> GetTestCasesWithUnencodedPasswords()
{
yield return new("[email protected]", "SuperSecretPassword", "nXmEFCdpHheD1R3XBVkm6VQavR7ZLbW7SRmzo/MfFso=");
yield return new("[email protected]", "MyPassWord", "xGKecAR27ALXNuMLsGaG0v5Q9pSs2tZTZRKNgmHMg+Q=");
}

private class TestLoginDto
{
[JsonPropertyName("email")]
Expand Down
14 changes: 14 additions & 0 deletions src/Aydsko.iRacingData/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,18 @@
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aydsko.iRacingData.IDataClient.UseUsernameAndPassword(System.String,System.String,System.Boolean)</Target>
<Left>lib/net6.0/Aydsko.iRacingData.dll</Left>
<Right>lib/net6.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Aydsko.iRacingData.IDataClient.UseUsernameAndPassword(System.String,System.String,System.Boolean)</Target>
<Left>lib/netstandard2.0/Aydsko.iRacingData.dll</Left>
<Right>lib/netstandard2.0/Aydsko.iRacingData.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>
26 changes: 21 additions & 5 deletions src/Aydsko.iRacingData/DataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public DataClient(HttpClient httpClient,
}

/// <inheritdoc/>
public void UseUsernameAndPassword(string username, string password)
public void UseUsernameAndPassword(string username, string password, bool passwordIsEncoded)
{
if (string.IsNullOrWhiteSpace(username))
{
Expand All @@ -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;
}

/// <inheritdoc/>
public void UseUsernameAndPassword(string username, string password)
{
UseUsernameAndPassword(username, password, false);
}

/// <inheritdoc />
public async Task<DataResponse<IReadOnlyDictionary<string, CarAssetDetail>>> GetCarAssetDetailsAsync(CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/Aydsko.iRacingData/IDataClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ public interface IDataClient
/// <exception cref="iRacingClientOptionsValueMissingException">Either <paramref name="username"/> or <paramref name="password"/> were <see langword="null"/> or white space.</exception>
void UseUsernameAndPassword(string username, string password);

/// <summary>Supply the username and password if they weren't supplied through the <see cref="iRacingDataClientOptions"/> object.</summary>
/// <param name="username">iRacing user name to use for authentication.</param>
/// <param name="password">Password associated with the iRacing user name used to authenticate.</param>
/// <param name="passwordIsEncoded">If <see langword="true" /> indicates the <paramref name="password"/> value is already encoded ready for submission to the iRacing API.</param>
/// <exception cref="iRacingClientOptionsValueMissingException">Either <paramref name="username"/> or <paramref name="password"/> were <see langword="null"/> or white space.</exception>
void UseUsernameAndPassword(string username, string password, bool passwordIsEncoded);

/// <summary>Retrieves details about the car assets, including image paths and descriptions.</summary>
/// <param name="cancellationToken">A token to allow the operation to be cancelled.</param>
/// <returns>A <see cref="DataResponse{TData}"/> containing a dictionary which maps the car identifier to a <see cref="CarAssetDetail"/> object for each car.</returns>
Expand Down
9 changes: 2 additions & 7 deletions src/Aydsko.iRacingData/Package Release Notes.txt
Original file line number Diff line number Diff line change
@@ -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!
4 changes: 4 additions & 0 deletions src/Aydsko.iRacingData/iRacingDataClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class iRacingDataClientOptions
/// <summary>Password associated with the iRacing user name used to authenticate.</summary>
public string? Password { get; set; }

/// <summary>If <see langword="true" /> indicates the <see cref="Password"/> property value is already encoded ready for submission to the iRacing API.</summary>
/// <seealso href="https://forums.iracing.com/discussion/22109/login-form-changes/p1" />
public bool PasswordIsEncoded { get; set; }

/// <summary>Called to retrieve cookie values stored from a previous authentication.</summary>
public Func<CookieCollection>? RestoreCookies { get; set; }

Expand Down

0 comments on commit f1cc595

Please sign in to comment.