Skip to content

Commit

Permalink
Fixed illegal characters in random tokens generated by the TokensServ…
Browse files Browse the repository at this point in the history
…ice, so that they can be included in URLS if necessary
  • Loading branch information
jezzsantos committed Feb 12, 2024
1 parent e94c615 commit 657c9e3
Show file tree
Hide file tree
Showing 21 changed files with 130 additions and 98 deletions.
12 changes: 11 additions & 1 deletion src/Domain.Interfaces/Validations/CommonValidations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static bool Matches<TValue>(this Validation<TValue> validation, TValue va
/// </summary>
public static Validation RandomToken(int keySize = 41)
{
return new Validation($"^[a-zA-Z0-9+/]{{{keySize},{keySize + 3}}}[=]{{0,3}}$");
return new Validation($"^[a-zA-Z0-9_+-]{{{keySize},{keySize + 3}}}$");
}

private static bool IsInvalidLength<TValue>(Validation<TValue> format, TValue value)
Expand Down Expand Up @@ -228,6 +228,7 @@ public static class Password
public static class APIKeys
{
public const string ApiKeyDelimiter = "||";
public const string ApiKeyPaddingReplacement = "#";
public const string ApiKeyPrefix = "apk_";
public const int ApiKeySize = 32;
public const int ApiKeyTokenSize = 18;
Expand Down Expand Up @@ -277,6 +278,15 @@ public static class APIKeys
return true;
});

/// <summary>
/// Validation for a random token (as created by the TokensService)
/// </summary>
public static Validation RandomToken(int keySize = 41,
string paddingReplacement = ApiKeyPaddingReplacement)
{
return new Validation($"^[a-zA-Z0-9_+-]{{{keySize},{keySize + 3}}}[{paddingReplacement}]{{0,3}}$");
}

private static int CalculateBase64EncodingLength(int sizeInBytes)
{
// Base64 encoder's length formula
Expand Down
8 changes: 4 additions & 4 deletions src/Domain.Services.Shared/DomainServices/ITokensService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ namespace Domain.Services.Shared.DomainServices;

public interface ITokensService
{
APIKeyToken CreateApiKey();
APIKeyToken CreateAPIKey();

string CreateTokenForJwtRefresh();
string CreateJWTRefreshToken();

string CreateTokenForPasswordReset();
string CreatePasswordResetToken();

string CreateTokenForVerification();
string CreateRegistrationVerificationToken();

Optional<APIKeyToken> ParseApiKey(string apiKey);
}
6 changes: 3 additions & 3 deletions src/IdentityApplication.UnitTests/APIKeysApplicationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public APIKeysApplicationSpec()
_idFactory.Setup(idf => idf.Create(It.IsAny<IIdentifiableEntity>()))
.Returns("anid".ToId());
_tokensService = new Mock<ITokensService>();
_tokensService.Setup(ts => ts.CreateApiKey())
_tokensService.Setup(ts => ts.CreateAPIKey())
.Returns(new APIKeyToken
{
Key = "akey",
Expand Down Expand Up @@ -149,7 +149,7 @@ public async Task WhenCreateApiKeyWithNoInformationAsync_ThenCreates()
result.Value.ExpiresOnUtc.Should()
.BeNear(DateTime.UtcNow.ToNearestMinute().Add(APIKeysApplication.DefaultAPIKeyExpiry),
TimeSpan.FromMinutes(1));
_tokensService.Verify(ts => ts.CreateApiKey());
_tokensService.Verify(ts => ts.CreateAPIKey());
_repository.Verify(rep => rep.SaveAsync(It.Is<APIKeyRoot>(ak =>
ak.ApiKey.Value.Token == "atoken"
&& ak.ApiKey.Value.KeyHash == "akeyhash"
Expand All @@ -174,7 +174,7 @@ await _application.CreateAPIKeyAsync(_caller.Object, "auserid", "adescription",
result.Value.UserId.Should().Be("auserid");
result.Value.Description.Should().Be("adescription");
result.Value.ExpiresOnUtc.Should().Be(expiresOn);
_tokensService.Verify(ts => ts.CreateApiKey());
_tokensService.Verify(ts => ts.CreateAPIKey());
_repository.Verify(rep => rep.SaveAsync(It.Is<APIKeyRoot>(ak =>
ak.ApiKey.Value.Token == "atoken"
&& ak.ApiKey.Value.KeyHash == "akeyhash"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public PasswordCredentialsApplicationSpec()
_emailAddressService.Setup(eas => eas.EnsureUniqueAsync(It.IsAny<EmailAddress>(), It.IsAny<Identifier>()))
.Returns(Task.FromResult(true));
_tokensService = new Mock<ITokensService>();
_tokensService.Setup(ts => ts.CreateTokenForVerification())
_tokensService.Setup(ts => ts.CreateRegistrationVerificationToken())
.Returns("averificationtoken");
_passwordHasherService = new Mock<IPasswordHasherService>();
_passwordHasherService.Setup(phs => phs.ValidatePassword(It.IsAny<string>(), It.IsAny<bool>()))
Expand Down
2 changes: 1 addition & 1 deletion src/IdentityApplication/APIKeysApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public async Task<Result<APIKey, Error>> CreateAPIKeyAsync(ICallerContext contex
public async Task<Result<APIKey, Error>> CreateAPIKeyAsync(ICallerContext context, string userId,
string description, DateTime? expiresOn, CancellationToken cancellationToken)
{
var keyToken = _tokensService.CreateApiKey();
var keyToken = _tokensService.CreateAPIKey();

var created = APIKeyRoot.Create(_recorder, _identifierFactory, _apiKeyHasherService, userId.ToId(), keyToken);
if (!created.IsSuccessful)
Expand Down
47 changes: 21 additions & 26 deletions src/IdentityDomain.UnitTests/PasswordCredentialRootSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class PasswordCredentialRootSpec
private readonly Mock<IEmailAddressService> _emailAddressService;
private readonly Mock<IPasswordHasherService> _passwordHasherService;
private readonly Mock<ITokensService> _tokensService;
private const string Token = "5n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";

public PasswordCredentialRootSpec()
{
Expand All @@ -36,7 +37,7 @@ public PasswordCredentialRootSpec()
_passwordHasherService.Setup(phs => phs.ValidatePasswordHash(It.IsAny<string>()))
.Returns(true);
_tokensService = new Mock<ITokensService>();
_tokensService.Setup(ts => ts.CreateTokenForVerification())
_tokensService.Setup(ts => ts.CreateRegistrationVerificationToken())
.Returns("averificationtoken");
var settings = new Mock<IConfigurationSettings>();
settings.Setup(s => s.Platform.GetString(It.IsAny<string>(), It.IsAny<string>()))
Expand Down Expand Up @@ -246,9 +247,8 @@ public void WhenVerifyRegistration_ThenVerified()
[Fact]
public void WhenInitiatePasswordResetAndPasswordNotSet_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
_credential.InitiateRegistrationVerification();
_credential.VerifyRegistration();

Expand All @@ -260,11 +260,10 @@ public void WhenInitiatePasswordResetAndPasswordNotSet_ThenReturnsError()
[Fact]
public void WhenInitiatePasswordResetAndNotVerified_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
#if TESTINGONLY
_credential.TestingOnly_RenewVerification(token);
_credential.TestingOnly_RenewVerification(Token);
#endif
var result = _credential.InitiatePasswordReset();

Expand All @@ -275,9 +274,8 @@ public void WhenInitiatePasswordResetAndNotVerified_ThenReturnsError()
[Fact]
public void WhenInitiatePasswordReset_ThenInitiated()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
_credential.SetCredential("apassword");
_credential.SetRegistrationDetails(EmailAddress.Create("[email protected]").Value,
PersonDisplayName.Create("aname").Value);
Expand All @@ -295,11 +293,11 @@ public void WhenInitiatePasswordReset_ThenInitiated()
[Fact]
public void WhenResetPasswordWithInvalidPassword_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());

_passwordHasherService.Setup(phs => phs.ValidatePassword(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(false);

var result = _credential.ResetPassword(token, "apassword");
var result = _credential.ResetPassword(Token, "apassword");

result.Should().BeError(ErrorCode.Validation, Resources.PasswordCredentialsRoot_InvalidPassword);

Expand All @@ -309,11 +307,11 @@ public void WhenResetPasswordWithInvalidPassword_ThenReturnsError()
[Fact]
public void WhenResetPasswordAndNoExistingPassword_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());

_passwordHasherService.Setup(phs => phs.ValidatePassword(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(true);

var result = _credential.ResetPassword(token, "apassword");
var result = _credential.ResetPassword(Token, "apassword");

result.Should().BeError(ErrorCode.PreconditionViolation, Resources.PasswordCredentialsRoot_NoPassword);

Expand All @@ -323,14 +321,14 @@ public void WhenResetPasswordAndNoExistingPassword_ThenReturnsError()
[Fact]
public void WhenResetPasswordAndSamePassword_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());

_passwordHasherService.Setup(phs => phs.ValidatePassword(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(true);
_passwordHasherService.Setup(phs => phs.VerifyPassword(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);
_credential.SetCredential("apassword");

var result = _credential.ResetPassword(token, "apassword");
var result = _credential.ResetPassword(Token, "apassword");

result.Should().BeError(ErrorCode.Validation, Resources.PasswordCredentialsRoot_DuplicatePassword);

Expand Down Expand Up @@ -359,9 +357,8 @@ public void WhenResetPasswordAndExpired_ThenReturnsError()
[Fact]
public void WhenResetPasswordAndCredentialsLocked_ThenResetsPasswordAndUnlocks()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
_credential.SetCredential("apassword");
_passwordHasherService.Setup(phs => phs.VerifyPassword(It.IsAny<string>(), It.IsAny<string>()))
.Returns(false);
Expand Down Expand Up @@ -389,9 +386,8 @@ public void WhenResetPasswordAndCredentialsLocked_ThenResetsPasswordAndUnlocks()
[Fact]
public void WhenResetPassword_ThenResetsPassword()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
_passwordHasherService.Setup(phs => phs.ValidatePassword(It.IsAny<string>(), It.IsAny<bool>()))
.Returns(true);
_passwordHasherService.Setup(phs => phs.VerifyPassword(It.IsAny<string>(), It.IsAny<string>()))
Expand Down Expand Up @@ -425,9 +421,8 @@ public void WhenEnsureInvariantsAndRegisteredButEmailNotUnique_ThenReturnsErrors
[Fact]
public void WhenEnsureInvariantsAndInitiatingPasswordResetButUnRegistered_ThenReturnsErrors()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
_tokensService.Setup(ts => ts.CreateTokenForPasswordReset())
.Returns(token);
_tokensService.Setup(ts => ts.CreatePasswordResetToken())
.Returns(Token);
_credential.SetCredential("apassword");
_credential.InitiateRegistrationVerification();
_credential.VerifyRegistration();
Expand Down
43 changes: 18 additions & 25 deletions src/IdentityDomain.UnitTests/PasswordKeepSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace IdentityDomain.UnitTests;
[Trait("Category", "Unit")]
public class PasswordKeepSpec
{
private const string Token = "5n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
private readonly Mock<IPasswordHasherService> _passwordHasherService;

public PasswordKeepSpec()
Expand Down Expand Up @@ -62,32 +63,30 @@ public void WhenConstructedWithHash_ThenPropertiesAssigned()
[Fact]
public void WhenInitiatePasswordResetAndNoPasswordSet_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create().Value;

var result = password.InitiatePasswordReset(token);
var result = password.InitiatePasswordReset(Token);

result.Should().BeError(ErrorCode.RuleViolation, Resources.PasswordKeep_NoPasswordHash);
}

[Fact]
public void WhenInitiatePasswordReset_ThenCreatesResetToken()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;

password = password.InitiatePasswordReset(token).Value;
password = password.InitiatePasswordReset(Token).Value;

password.PasswordHash.Should().Be("apasswordhash");
password.Token.Should().Be(token);
password.Token.Should().Be(Token);
password.TokenExpiresUtc.Should().BeNear(DateTime.UtcNow.Add(PasswordKeep.DefaultResetExpiry));
}

[Fact]
public void WhenInitiatePasswordResetTwice_ThenCreatesNewResetToken()
{
var token1 = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var token2 = Convert.ToBase64String(Enumerable.Repeat((byte)0x02, 32).ToArray());
const string token1 = "5n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
const string token2 = "7n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;

password = password.InitiatePasswordReset(token1).Value;
Expand Down Expand Up @@ -184,10 +183,10 @@ public void WhenConfirmResetWithInvalidToken_ThenReturnsError()
[Fact]
public void WhenConfirmResetAndTokensNotMatch_ThenReturnsError()
{
var token1 = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
const string token1 = "5n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
const string token2 = "7n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;
password = password.InitiatePasswordReset(token1).Value;
var token2 = Convert.ToBase64String(Enumerable.Repeat((byte)0x02, 32).ToArray());

var result = password.ConfirmReset(token2);

Expand All @@ -197,25 +196,23 @@ public void WhenConfirmResetAndTokensNotMatch_ThenReturnsError()
[Fact]
public void WhenConfirmResetAndTokenExpired_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;
password = password.InitiatePasswordReset(token).Value;
password = password.InitiatePasswordReset(Token).Value;
#if TESTINGONLY
password = password.TestingOnly_ExpireToken();
#endif

var result = password.ConfirmReset(token);
var result = password.ConfirmReset(Token);

result.Should().BeError(ErrorCode.PreconditionViolation, Resources.PasswordKeep_TokenExpired);
}

[Fact]
public void WhenResetPasswordAndEmptyPasswordHash_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;

var result = password.ResetPassword(_passwordHasherService.Object, token, string.Empty);
var result = password.ResetPassword(_passwordHasherService.Object, Token, string.Empty);

result.Should().BeError(ErrorCode.Validation);
}
Expand All @@ -233,23 +230,22 @@ public void WhenResetPasswordAndTokenInvalid_ThenReturnsError()
[Fact]
public void WhenResetPasswordAndPasswordHashInvalid_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;
_passwordHasherService.Setup(ph => ph.ValidatePasswordHash(It.IsAny<string>()))
.Returns(false);

var result = password.ResetPassword(_passwordHasherService.Object, token, "aninvalidpasswordhash");
var result = password.ResetPassword(_passwordHasherService.Object, Token, "aninvalidpasswordhash");

result.Should().BeError(ErrorCode.Validation, Resources.PasswordKeep_InvalidPasswordHash);
}

[Fact]
public void WhenResetPasswordAndTokenNotMatch_ThenReturnsError()
{
var token1 = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
const string token1 = "5n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
const string token2 = "7n6nA42SQrsO1UIgc7lIVebR6_3CmZwcthUEx3nF2sM";
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;
password = password.InitiatePasswordReset(token1).Value;
var token2 = Convert.ToBase64String(Enumerable.Repeat((byte)0x02, 32).ToArray());

var result = password.ResetPassword(_passwordHasherService.Object, token2, "apasswordhash");

Expand All @@ -259,35 +255,32 @@ public void WhenResetPasswordAndTokenNotMatch_ThenReturnsError()
[Fact]
public void WhenResetPasswordAndTokenExpired_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value;
password = password.InitiatePasswordReset(token).Value;
password = password.InitiatePasswordReset(Token).Value;
#if TESTINGONLY
password = password.TestingOnly_ExpireToken();
#endif

var result = password.ResetPassword(_passwordHasherService.Object, token, "apasswordhash");
var result = password.ResetPassword(_passwordHasherService.Object, Token, "apasswordhash");

result.Should().BeError(ErrorCode.PreconditionViolation, Resources.PasswordKeep_TokenExpired);
}

[Fact]
public void WhenResetPasswordAndNoPasswordSet_ThenReturnsError()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create().Value;

var result = password.ResetPassword(_passwordHasherService.Object, token, "apasswordhash");
var result = password.ResetPassword(_passwordHasherService.Object, Token, "apasswordhash");

result.Should().BeError(ErrorCode.RuleViolation, Resources.PasswordKeep_NoPasswordHash);
}

[Fact]
public void WhenResetPassword_ThenReturnsNewPassword()
{
var token = Convert.ToBase64String(Enumerable.Repeat((byte)0x01, 32).ToArray());
var password = PasswordKeep.Create(_passwordHasherService.Object, "apasswordhash").Value
.InitiatePasswordReset(token).Value;
.InitiatePasswordReset(Token).Value;

password = password.ResetPassword(_passwordHasherService.Object, password.Token, "apasswordhash").Value;

Expand Down
Loading

0 comments on commit 657c9e3

Please sign in to comment.