mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 19:32:10 +00:00
fix(auth): queue the reset email only after its code is stored
This commit is contained in:
parent
2f23f6fadc
commit
ca9322fa60
2 changed files with 38 additions and 17 deletions
|
|
@ -151,7 +151,7 @@ public class AuthenticationServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ForgetPassword_EmailThatCannotBeQueued_DropsTheCodeAndDoesNotCountTheRequest()
|
public async Task ForgetPassword_EmailThatCannotBeQueued_DoesNotCountTheRequest()
|
||||||
{
|
{
|
||||||
var user = IdentityTestHelpers.User();
|
var user = IdentityTestHelpers.User();
|
||||||
var userData = new Mock<IUserDataService>();
|
var userData = new Mock<IUserDataService>();
|
||||||
|
|
@ -175,19 +175,38 @@ public class AuthenticationServiceTests
|
||||||
for (var request = 0; request < 4; request++)
|
for (var request = 0; request < 4; request++)
|
||||||
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
await service.ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
||||||
|
|
||||||
// The three undelivered requests did not use up the hourly limit of three.
|
// The three undelivered requests did not use up the hourly limit of three, and every
|
||||||
|
// email carries the code stored just before it was queued.
|
||||||
email.Verify(e => e.TryEnqueue(user.Email!, It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(4));
|
email.Verify(e => e.TryEnqueue(user.Email!, It.IsAny<string>(), It.IsAny<string>()), Times.Exactly(4));
|
||||||
stored.Should().HaveCount(4);
|
stored.Should().HaveCount(4);
|
||||||
for (var request = 0; request < 3; request++)
|
for (var request = 0; request < 4; request++)
|
||||||
{
|
{
|
||||||
var code = System.Text.RegularExpressions.Regex.Match(bodies[request], @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
var code = System.Text.RegularExpressions.Regex.Match(bodies[request], @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
||||||
PasswordResetCodeSecrets.Matches(ResetKey, stored[request].Salt, code, stored[request].Hash).Should().BeFalse();
|
PasswordResetCodeSecrets.Matches(ResetKey, stored[request].Salt, code, stored[request].Hash).Should().BeTrue();
|
||||||
}
|
}
|
||||||
var delivered = System.Text.RegularExpressions.Regex.Match(bodies[3], @"Your Password Reset Code is: (\d{6})").Groups[1].Value;
|
|
||||||
PasswordResetCodeSecrets.Matches(ResetKey, stored[3].Salt, delivered, stored[3].Hash).Should().BeTrue();
|
|
||||||
forget.Verify(f => f.RemoveByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
forget.Verify(f => f.RemoveByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public async Task ForgetPassword_WriteThatFailsOrIsCancelled_QueuesNoEmail(bool cancelled)
|
||||||
|
{
|
||||||
|
var user = IdentityTestHelpers.User();
|
||||||
|
var userData = new Mock<IUserDataService>();
|
||||||
|
userData.Setup(u => u.GetByEmailNormalizedAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync(user);
|
||||||
|
var forget = new Mock<IForgetPasswordDataService>();
|
||||||
|
forget.Setup(f => f.ReplaceCodeAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(Task.FromException(cancelled ? new OperationCanceledException() : new InvalidOperationException("database unavailable")));
|
||||||
|
var email = new Mock<IPasswordResetEmailQueue>();
|
||||||
|
email.Setup(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>())).Returns(true);
|
||||||
|
|
||||||
|
var act = () => NewService(userData, forget, email, out _, out _).ForgetPasswordAsync("alice@example.com", CancellationToken.None);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<Exception>();
|
||||||
|
email.Verify(e => e.TryEnqueue(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ForgetPassword_EmailThatCannotBeQueued_MakesTheSameDataCallsAsAnUnknownEmail()
|
public async Task ForgetPassword_EmailThatCannotBeQueued_MakesTheSameDataCallsAsAnUnknownEmail()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -170,12 +170,22 @@ namespace SeaHaven.Services.Implementation
|
||||||
var hash = PasswordResetCodeSecrets.Hash(ResetCodeKey, salt, code);
|
var hash = PasswordResetCodeSecrets.Hash(ResetCodeKey, salt, code);
|
||||||
var active = user != null && user.IsDeleted != true && !string.IsNullOrWhiteSpace(user.Email);
|
var active = user != null && user.IsDeleted != true && !string.IsNullOrWhiteSpace(user.Email);
|
||||||
|
|
||||||
// The request stays counted only once its row is stored. A code that cannot be
|
// The email is queued only after its code is stored, so a failed or cancelled
|
||||||
// emailed is stored unmatchable and not counted, and a failed or cancelled
|
// write never sends a code that cannot be used. The request stays counted only
|
||||||
// write is not counted either.
|
// when the row is stored and, for an account, its email was queued; when the
|
||||||
|
// queue is full the stored code is left unsent and the user can ask again.
|
||||||
var counted = false;
|
var counted = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await _forgetPasswordDataService.ReplaceCodeAsync(
|
||||||
|
active ? user!.Email! : requested,
|
||||||
|
active ? user!.Id : string.Empty,
|
||||||
|
active ? hash : PasswordResetCodeSecrets.NewUnmatchableHash(),
|
||||||
|
salt,
|
||||||
|
nowUtc.Add(ResetCodeLifetime),
|
||||||
|
nowUtc,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
var queued = false;
|
var queued = false;
|
||||||
if (active)
|
if (active)
|
||||||
{
|
{
|
||||||
|
|
@ -183,14 +193,6 @@ namespace SeaHaven.Services.Implementation
|
||||||
queued = _resetEmails.TryEnqueue(user!.Email!, "Forget Password Request.", body);
|
queued = _resetEmails.TryEnqueue(user!.Email!, "Forget Password Request.", body);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _forgetPasswordDataService.ReplaceCodeAsync(
|
|
||||||
active ? user!.Email! : requested,
|
|
||||||
active ? user!.Id : string.Empty,
|
|
||||||
queued ? hash : PasswordResetCodeSecrets.NewUnmatchableHash(),
|
|
||||||
salt,
|
|
||||||
nowUtc.Add(ResetCodeLifetime),
|
|
||||||
nowUtc,
|
|
||||||
cancellationToken);
|
|
||||||
counted = queued || !active;
|
counted = queued || !active;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue