Skip to content

Commit

Permalink
Allow Supply of Encoded Passwords
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AdrianJSClark committed Oct 12, 2022
1 parent 614ab38 commit cce7a09
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 14 deletions.
90 changes: 88 additions & 2 deletions src/Aydsko.iRacingData.UnitTests/LoginViaOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace Aydsko.iRacingData.UnitTests;

public class LoginViaOptionsTests : MockedHttpTestBase
public class PasswordEncodingTests : MockedHttpTestBase
{
[SetUp]
public void SetUp()
Expand All @@ -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,
};
Expand Down Expand Up @@ -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<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.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);
}

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=");
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 cce7a09

Please sign in to comment.