145 lines
6.4 KiB
C#
145 lines
6.4 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Tiku.Application.Auth;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.Infrastructure.Auth;
|
|
|
|
public sealed partial class AuthService
|
|
{
|
|
public async Task<AuthenticationResult> ChangeRequiredPasswordAsync(
|
|
PasswordChangeChallengeRequest request,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var challenge = await FindChallengeAsync(
|
|
request.ChallengeToken, AuthChallengePurpose.PasswordChange, cancellationToken);
|
|
var user = await userManager.FindByIdAsync(challenge.UserId.ToString())
|
|
?? throw new InvalidAuthChallengeException();
|
|
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
|
|
var reset = await userManager.ResetPasswordAsync(user, resetToken, 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 clear the password-change requirement.");
|
|
|
|
await sessionStore.RevokeAllAsync(user.Id, "password_changed", cancellationToken);
|
|
await ConsumeChallengeAsync(challenge, cancellationToken);
|
|
await AddSecurityAuditAsync(
|
|
user.Id, challenge.TenantId, "auth.password.changed", null,
|
|
request.IpAddress, request.UserAgent, cancellationToken);
|
|
return await CompleteSuccessfulLoginAsync(
|
|
challenge.Realm, challenge.TenantId, user, challenge.Provider,
|
|
user.Email ?? user.Phone ?? user.Id.ToString(),
|
|
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);
|
|
}
|
|
} |