feat: strengthen P0 security and operations
This commit is contained in:
@@ -226,6 +226,127 @@ public sealed class AuthService(
|
||||
request.IpAddress, request.UserAgent, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<SmsSendResult> RequestPasswordResetAsync(
|
||||
PasswordResetCodeRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var phone = SmsCodeHashing.NormalizePhone(request.Phone);
|
||||
var userId = await dbContext.Users.AsNoTracking()
|
||||
.Where(user => user.Phone == phone && user.Status == UserStatus.Active)
|
||||
.Select(user => (Guid?)user.Id)
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
var eligible = userId.HasValue && await dbContext.TenantMemberships.AsNoTracking().AnyAsync(
|
||||
membership => membership.TenantId == request.TenantId && membership.UserId == userId.Value &&
|
||||
membership.Status == MembershipStatus.Active,
|
||||
cancellationToken);
|
||||
if (!eligible)
|
||||
{
|
||||
return new SmsSendResult(Guid.NewGuid(), DateTimeOffset.UtcNow.AddMinutes(5));
|
||||
}
|
||||
|
||||
return await smsVerificationService.CreateCodeAsync(
|
||||
new SendSmsCodeRequest(
|
||||
request.TenantId,
|
||||
phone,
|
||||
SmsPurpose.ResetPassword,
|
||||
request.IpAddress,
|
||||
request.UserAgent,
|
||||
request.DeviceId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ResetPasswordAsync(
|
||||
PasswordResetRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var phone = SmsCodeHashing.NormalizePhone(request.Phone);
|
||||
var user = await dbContext.Users.SingleOrDefaultAsync(
|
||||
item => item.Phone == phone && item.Status == UserStatus.Active,
|
||||
cancellationToken);
|
||||
if (user is null || !await dbContext.TenantMemberships.AnyAsync(
|
||||
membership => membership.TenantId == request.TenantId && membership.UserId == user.Id &&
|
||||
membership.Status == MembershipStatus.Active,
|
||||
cancellationToken))
|
||||
{
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
await smsVerificationService.VerifyCodeAsync(
|
||||
request.TenantId,
|
||||
phone,
|
||||
SmsPurpose.ResetPassword,
|
||||
request.Code,
|
||||
cancellationToken);
|
||||
var token = await userManager.GeneratePasswordResetTokenAsync(user);
|
||||
var reset = await userManager.ResetPasswordAsync(user, token, request.NewPassword);
|
||||
if (!reset.Succeeded)
|
||||
{
|
||||
throw new InvalidCredentialsException("invalid_new_password");
|
||||
}
|
||||
|
||||
user.ForcePasswordChange = false;
|
||||
var updated = await userManager.UpdateAsync(user);
|
||||
if (!updated.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to finalize the password reset.");
|
||||
}
|
||||
|
||||
await sessionStore.RevokeAllAsync(user.Id, "password_reset", cancellationToken);
|
||||
await AddSecurityAuditAsync(
|
||||
user.Id,
|
||||
request.TenantId,
|
||||
"auth.password.reset",
|
||||
null,
|
||||
request.IpAddress,
|
||||
request.UserAgent,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<AuthenticationResult> ChangePasswordAsync(
|
||||
AuthenticatedPasswordChangeRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var session = await sessionStore.ResolveActiveSessionAsync(
|
||||
request.SessionId,
|
||||
request.UserId,
|
||||
cancellationToken) ?? throw new SessionRevokedException();
|
||||
var user = await userManager.FindByIdAsync(request.UserId.ToString())
|
||||
?? throw new InvalidCredentialsException();
|
||||
var changed = await userManager.ChangePasswordAsync(user, request.CurrentPassword, request.NewPassword);
|
||||
if (!changed.Succeeded)
|
||||
{
|
||||
var currentPasswordInvalid = changed.Errors.Any(error =>
|
||||
string.Equals(error.Code, "PasswordMismatch", StringComparison.OrdinalIgnoreCase));
|
||||
throw new InvalidCredentialsException(currentPasswordInvalid ? "invalid_credentials" : "invalid_new_password");
|
||||
}
|
||||
|
||||
user.ForcePasswordChange = false;
|
||||
var updated = await userManager.UpdateAsync(user);
|
||||
if (!updated.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to finalize the password change.");
|
||||
}
|
||||
|
||||
await sessionStore.RevokeAllAsync(user.Id, "password_changed", cancellationToken);
|
||||
await AddSecurityAuditAsync(
|
||||
user.Id,
|
||||
session.TenantId,
|
||||
"auth.password.changed_authenticated",
|
||||
null,
|
||||
request.IpAddress,
|
||||
request.UserAgent,
|
||||
cancellationToken);
|
||||
return await CompleteSuccessfulLoginAsync(
|
||||
session.Realm,
|
||||
session.TenantId,
|
||||
user,
|
||||
PasswordProvider,
|
||||
user.Email ?? user.Phone ?? user.Id.ToString(),
|
||||
request.IpAddress,
|
||||
request.UserAgent,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<AuthChallenge> FindChallengeAsync(
|
||||
string token,
|
||||
AuthChallengePurpose purpose,
|
||||
|
||||
Reference in New Issue
Block a user