mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
feat(auth): enforce one password policy on every password-setting path (SH-386)
Both hosts now apply the same Identity password rule: at least 6 characters with one uppercase letter, one number and one special character. Change password requires an authenticated caller, verifies the current password before evaluating the new one, and reports a policy rejection separately from a wrong current password.
This commit is contained in:
parent
06eae2fb02
commit
66a49ab957
10 changed files with 311 additions and 28 deletions
|
|
@ -117,11 +117,11 @@ public class AuthenticationControllerTests
|
||||||
{
|
{
|
||||||
var service = new Mock<IAuthenticationService>();
|
var service = new Mock<IAuthenticationService>();
|
||||||
service.Setup(s => s.ChangePasswordAsync("42", "old", "new", It.IsAny<CancellationToken>()))
|
service.Setup(s => s.ChangePasswordAsync("42", "old", "new", It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(true);
|
.ReturnsAsync(new ChangePasswordResultDTO { Status = ChangePasswordStatus.Succeeded });
|
||||||
|
|
||||||
var controller = NewController(service, "42");
|
var controller = NewController(service, "42");
|
||||||
|
|
||||||
var result = await controller.ChangePassword(new ChangePasswords { Currentpassword = "old", Confirmpassword = "new" }, CancellationToken.None);
|
var result = await controller.ChangePassword(new ChangePasswords { Currentpassword = "old", Newpassword = "new", Confirmpassword = "new" }, CancellationToken.None);
|
||||||
|
|
||||||
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
var ok = result.Should().BeOfType<OkObjectResult>().Subject;
|
||||||
var response = ok.Value.Should().BeOfType<Response>().Subject;
|
var response = ok.Value.Should().BeOfType<Response>().Subject;
|
||||||
|
|
@ -130,11 +130,11 @@ public class AuthenticationControllerTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ChangePassword_Failure_ReturnsOldPasswordIncorrectStatus()
|
public async Task ChangePassword_WrongCurrentPassword_ReturnsOldPasswordIncorrectStatus()
|
||||||
{
|
{
|
||||||
var service = new Mock<IAuthenticationService>();
|
var service = new Mock<IAuthenticationService>();
|
||||||
service.Setup(s => s.ChangePasswordAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
service.Setup(s => s.ChangePasswordAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(false);
|
.ReturnsAsync(new ChangePasswordResultDTO { Status = ChangePasswordStatus.CurrentPasswordIncorrect });
|
||||||
|
|
||||||
var controller = NewController(service, "42");
|
var controller = NewController(service, "42");
|
||||||
|
|
||||||
|
|
@ -143,6 +143,36 @@ public class AuthenticationControllerTests
|
||||||
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||||
var response = bad.Value.Should().BeOfType<Response>().Subject;
|
var response = bad.Value.Should().BeOfType<Response>().Subject;
|
||||||
response.Status.Should().Be("Old Password is incorrect");
|
response.Status.Should().Be("Old Password is incorrect");
|
||||||
|
response.Message.Should().Be("Current password is incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangePassword_PolicyRejection_ReturnsPasswordRequirementsMessage()
|
||||||
|
{
|
||||||
|
var service = new Mock<IAuthenticationService>();
|
||||||
|
service.Setup(s => s.ChangePasswordAsync("42", "Current1!", "weak", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new ChangePasswordResultDTO { Status = ChangePasswordStatus.PasswordRejected });
|
||||||
|
|
||||||
|
var controller = NewController(service, "42");
|
||||||
|
|
||||||
|
var result = await controller.ChangePassword(
|
||||||
|
new ChangePasswords { Currentpassword = "Current1!", Newpassword = "weak", Confirmpassword = "weak" },
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
var bad = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||||
|
var response = bad.Value.Should().BeOfType<Response>().Subject;
|
||||||
|
response.Status.Should().Be("Password does not meet requirements");
|
||||||
|
response.Message.Should().Be(
|
||||||
|
"Password must be at least 6 characters and include one uppercase letter, one number, and one special character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangePassword_RequiresAuthenticatedCaller()
|
||||||
|
{
|
||||||
|
var method = typeof(AuthenticationController).GetMethod(nameof(AuthenticationController.ChangePassword))!;
|
||||||
|
|
||||||
|
method.GetCustomAttributes(typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute), inherit: true)
|
||||||
|
.Should().NotBeEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
180
Api.SeaHavenIndustries.Tests/PasswordPolicyTests.cs
Normal file
180
Api.SeaHavenIndustries.Tests/PasswordPolicyTests.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
using Api.SeaHavenIndustries.Infrastructure;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Moq;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.Configuration;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exercises the password rule through the same Identity registration the API
|
||||||
|
/// host uses, so these assertions describe the rule that runs in production.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PasswordPolicyTests : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private const string CurrentPassword = "Current1!";
|
||||||
|
private readonly ServiceProvider _provider;
|
||||||
|
private readonly AsyncServiceScope _scope;
|
||||||
|
|
||||||
|
public PasswordPolicyTests()
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
|
||||||
|
services.AddSeaHavenIdentity();
|
||||||
|
_provider = services.BuildServiceProvider();
|
||||||
|
_scope = _provider.CreateAsyncScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserManager<ApplicationUser> UserManager =>
|
||||||
|
_scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Ab1!x", "PasswordTooShort")]
|
||||||
|
[InlineData("abc12!", "PasswordRequiresUpper")]
|
||||||
|
[InlineData("Abcde!", "PasswordRequiresDigit")]
|
||||||
|
[InlineData("Abcde1", "PasswordRequiresNonAlphanumeric")]
|
||||||
|
public async Task Policy_RejectsPasswordMissingOneRule(string password, string expectedCode)
|
||||||
|
{
|
||||||
|
var user = new ApplicationUser { UserName = "policy@example.com", Email = "policy@example.com" };
|
||||||
|
|
||||||
|
var errors = await ValidateAsync(user, password);
|
||||||
|
|
||||||
|
errors.Select(error => error.Code).Should().Equal(expectedCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Abc1!x")]
|
||||||
|
[InlineData("ABC12!")]
|
||||||
|
public async Task Policy_AcceptsSixCharacterPasswordMeetingEveryRule(string password)
|
||||||
|
{
|
||||||
|
var user = new ApplicationUser { UserName = "policy@example.com", Email = "policy@example.com" };
|
||||||
|
|
||||||
|
var errors = await ValidateAsync(user, password);
|
||||||
|
|
||||||
|
errors.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Registration_AppliesSharedPolicyOptions()
|
||||||
|
{
|
||||||
|
var options = _scope.ServiceProvider.GetRequiredService<IOptions<IdentityOptions>>().Value.Password;
|
||||||
|
|
||||||
|
options.RequiredLength.Should().Be(6);
|
||||||
|
options.RequireUppercase.Should().BeTrue();
|
||||||
|
options.RequireDigit.Should().BeTrue();
|
||||||
|
options.RequireNonAlphanumeric.Should().BeTrue();
|
||||||
|
options.RequireLowercase.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangePassword_WrongCurrentPassword_IsRejectedBeforeNewPasswordIsEvaluated()
|
||||||
|
{
|
||||||
|
var user = await CreateUserAsync();
|
||||||
|
var service = NewAuthenticationService();
|
||||||
|
|
||||||
|
var result = await service.ChangePasswordAsync(user.Id, "Wrong1!", "weak", CancellationToken.None);
|
||||||
|
|
||||||
|
result.Status.Should().Be(ChangePasswordStatus.CurrentPasswordIncorrect);
|
||||||
|
(await UserManager.CheckPasswordAsync(user, CurrentPassword)).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangePassword_CorrectCurrentPasswordAndWeakNewPassword_IsRejectedByPolicy()
|
||||||
|
{
|
||||||
|
var user = await CreateUserAsync();
|
||||||
|
var service = NewAuthenticationService();
|
||||||
|
|
||||||
|
var result = await service.ChangePasswordAsync(user.Id, CurrentPassword, "abcdef", CancellationToken.None);
|
||||||
|
|
||||||
|
result.Status.Should().Be(ChangePasswordStatus.PasswordRejected);
|
||||||
|
(await UserManager.CheckPasswordAsync(user, CurrentPassword)).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangePassword_CorrectCurrentPasswordAndCompliantNewPassword_ChangesPassword()
|
||||||
|
{
|
||||||
|
var user = await CreateUserAsync();
|
||||||
|
var service = NewAuthenticationService();
|
||||||
|
|
||||||
|
var result = await service.ChangePasswordAsync(user.Id, CurrentPassword, "Next2@x", CancellationToken.None);
|
||||||
|
|
||||||
|
result.Status.Should().Be(ChangePasswordStatus.Succeeded);
|
||||||
|
var reloaded = await UserManager.FindByIdAsync(user.Id);
|
||||||
|
(await UserManager.CheckPasswordAsync(reloaded!, "Next2@x")).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChangePassword_CancelledToken_Throws()
|
||||||
|
{
|
||||||
|
var service = NewAuthenticationService();
|
||||||
|
using var cancellation = new CancellationTokenSource();
|
||||||
|
cancellation.Cancel();
|
||||||
|
|
||||||
|
var act = () => service.ChangePasswordAsync("any", CurrentPassword, "Next2@x", cancellation.Token);
|
||||||
|
|
||||||
|
await act.Should().ThrowAsync<OperationCanceledException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResetPassword_WeakPassword_IsRejectedAndKeepsCode()
|
||||||
|
{
|
||||||
|
var user = await CreateUserAsync();
|
||||||
|
var forget = new Mock<IForgetPasswordDataService>();
|
||||||
|
forget.Setup(f => f.ExistsByEmailAndCodeAsync(user.Email!, "123456", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(true);
|
||||||
|
forget.Setup(f => f.GetByEmailAsync(user.Email!, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new ForgetPasswordCode { Email = user.Email!, UserId = user.Id, Code = "123456" });
|
||||||
|
var service = NewAuthenticationService(forget);
|
||||||
|
|
||||||
|
var reset = await service.ResetPasswordAsync(user.Email!, "123456", "abcdef", CancellationToken.None);
|
||||||
|
|
||||||
|
reset.Should().BeFalse();
|
||||||
|
(await UserManager.CheckPasswordAsync(user, CurrentPassword)).Should().BeTrue();
|
||||||
|
forget.Verify(f => f.RemoveByEmailAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyList<IdentityError>> ValidateAsync(ApplicationUser user, string password)
|
||||||
|
{
|
||||||
|
var errors = new List<IdentityError>();
|
||||||
|
foreach (var validator in UserManager.PasswordValidators)
|
||||||
|
{
|
||||||
|
var result = await validator.ValidateAsync(UserManager, user, password);
|
||||||
|
errors.AddRange(result.Errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApplicationUser> CreateUserAsync()
|
||||||
|
{
|
||||||
|
var user = new ApplicationUser { UserName = "member@example.com", Email = "member@example.com" };
|
||||||
|
var created = await UserManager.CreateAsync(user, CurrentPassword);
|
||||||
|
created.Succeeded.Should().BeTrue();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationService NewAuthenticationService(Mock<IForgetPasswordDataService>? forget = null) =>
|
||||||
|
new(
|
||||||
|
UserManager,
|
||||||
|
Microsoft.Extensions.Options.Options.Create(new JwtOptions { Secret = new string('x', 64) }),
|
||||||
|
Mock.Of<IUserDataService>(),
|
||||||
|
(forget ?? new Mock<IForgetPasswordDataService>()).Object,
|
||||||
|
Mock.Of<IEmailSender>());
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await _scope.DisposeAsync();
|
||||||
|
await _provider.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,20 +55,26 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[Route("ChangePassword")]
|
[Route("ChangePassword")]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> ChangePassword(ChangePasswords usermodel, CancellationToken cancellationToken)
|
public async Task<IActionResult> ChangePassword(ChangePasswords usermodel, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
|
var userid = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "";
|
||||||
var succeeded = await _authenticationService.ChangePasswordAsync(userid, usermodel.Currentpassword, usermodel.Confirmpassword, cancellationToken);
|
var result = await _authenticationService.ChangePasswordAsync(userid, usermodel.Currentpassword, usermodel.Newpassword, cancellationToken);
|
||||||
if (succeeded)
|
return result.Status switch
|
||||||
{
|
{
|
||||||
return Ok(new Response { Status = "Success ", Message = "Password successfully changed" });
|
ChangePasswordStatus.Succeeded =>
|
||||||
}
|
Ok(new Response { Status = "Success ", Message = "Password successfully changed" }),
|
||||||
else
|
ChangePasswordStatus.PasswordRejected =>
|
||||||
return BadRequest(new Response { Status = "Old Password is incorrect" });
|
BadRequest(new Response { Status = "Password does not meet requirements", Message = PasswordRequirementsMessage }),
|
||||||
|
_ => BadRequest(new Response { Status = "Old Password is incorrect", Message = "Current password is incorrect" })
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const string PasswordRequirementsMessage =
|
||||||
|
"Password must be at least 6 characters and include one uppercase letter, one number, and one special character.";
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("UpdateProfile")]
|
[Route("UpdateProfile")]
|
||||||
public async Task<IActionResult> UserProfileUpdate([FromForm] User_DTO model, CancellationToken cancellationToken)
|
public async Task<IActionResult> UserProfileUpdate([FromForm] User_DTO model, CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Infrastructure
|
||||||
|
{
|
||||||
|
public static class IdentityRegistration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers ASP.NET Identity for the API with the shared password policy.
|
||||||
|
/// Program.cs and the behavior tests both compose Identity through this
|
||||||
|
/// method so the rule under test is the rule that runs.
|
||||||
|
/// </summary>
|
||||||
|
public static IdentityBuilder AddSeaHavenIdentity(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
||||||
|
{
|
||||||
|
options.User.RequireUniqueEmail = false;
|
||||||
|
IdentityPasswordPolicy.Apply(options.Password);
|
||||||
|
})
|
||||||
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||||
|
.AddDefaultTokenProviders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Api.SeaHavenIndustries.Helper;
|
using Api.SeaHavenIndustries.Helper;
|
||||||
using Api.SeaHavenIndustries.HostedServices;
|
using Api.SeaHavenIndustries.HostedServices;
|
||||||
|
using Api.SeaHavenIndustries.Infrastructure;
|
||||||
using Api.SeaHavenIndustries.Middleware;
|
using Api.SeaHavenIndustries.Middleware;
|
||||||
using Api.SeaHavenIndustries.Observability;
|
using Api.SeaHavenIndustries.Observability;
|
||||||
using Api.SeaHavenIndustries.Options;
|
using Api.SeaHavenIndustries.Options;
|
||||||
|
|
@ -43,12 +44,7 @@ ConfigurationManager configuration = builder.Configuration;
|
||||||
|
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
||||||
|
|
||||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
builder.Services.AddSeaHavenIdentity();
|
||||||
{
|
|
||||||
options.User.RequireUniqueEmail = false;
|
|
||||||
})
|
|
||||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
|
||||||
.AddDefaultTokenProviders();
|
|
||||||
|
|
||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
27
Data.SeaHavenIndustries/Auth/IdentityPasswordPolicy.cs
Normal file
27
Data.SeaHavenIndustries/Auth/IdentityPasswordPolicy.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The single password rule for every surface that sets a password: at least
|
||||||
|
/// six characters with one uppercase letter, one number, and one special
|
||||||
|
/// character. Lowercase letters are deliberately not required so the server
|
||||||
|
/// accepts exactly what the four-item checklist in the web app marks as met.
|
||||||
|
/// </summary>
|
||||||
|
public static class IdentityPasswordPolicy
|
||||||
|
{
|
||||||
|
public const int MinimumLength = 6;
|
||||||
|
|
||||||
|
public static void Apply(PasswordOptions options)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
|
options.RequiredLength = MinimumLength;
|
||||||
|
options.RequireUppercase = true;
|
||||||
|
options.RequireDigit = true;
|
||||||
|
options.RequireNonAlphanumeric = true;
|
||||||
|
options.RequireLowercase = false;
|
||||||
|
options.RequiredUniqueChars = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,18 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string Id { get; set; } = string.Empty;
|
public string Id { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ChangePasswordStatus
|
||||||
|
{
|
||||||
|
Succeeded,
|
||||||
|
CurrentPasswordIncorrect,
|
||||||
|
PasswordRejected
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ChangePasswordResultDTO
|
||||||
|
{
|
||||||
|
public ChangePasswordStatus Status { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateProfileRequestDTO
|
public class UpdateProfileRequestDTO
|
||||||
{
|
{
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -80,16 +80,27 @@ namespace SeaHaven.Services.Implementation
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ChangePasswordAsync(string userId, string? currentPassword, string? confirmPassword, CancellationToken cancellationToken)
|
public async Task<ChangePasswordResultDTO> ChangePasswordAsync(string userId, string? currentPassword, string? newPassword, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
var user = await _userManager.FindByIdAsync(userId);
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
if (user == null)
|
if (user == null || user.IsDeleted == true)
|
||||||
return false;
|
return ChangePasswordResult(ChangePasswordStatus.CurrentPasswordIncorrect);
|
||||||
|
|
||||||
var result = await _userManager.ChangePasswordAsync(user, currentPassword ?? "", confirmPassword ?? "");
|
// The current password is verified before the new one is evaluated, so a
|
||||||
return result.Succeeded;
|
// caller without it learns nothing about the policy outcome.
|
||||||
|
if (!await _userManager.CheckPasswordAsync(user, currentPassword ?? ""))
|
||||||
|
return ChangePasswordResult(ChangePasswordStatus.CurrentPasswordIncorrect);
|
||||||
|
|
||||||
|
var result = await _userManager.ChangePasswordAsync(user, currentPassword ?? "", newPassword ?? "");
|
||||||
|
return ChangePasswordResult(result.Succeeded
|
||||||
|
? ChangePasswordStatus.Succeeded
|
||||||
|
: ChangePasswordStatus.PasswordRejected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ChangePasswordResultDTO ChangePasswordResult(ChangePasswordStatus status) =>
|
||||||
|
new() { Status = status };
|
||||||
|
|
||||||
public async Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken)
|
public async Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var exists = await _userDataService.UpdateProfileAsync(
|
var exists = await _userDataService.UpdateProfileAsync(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ namespace SeaHaven.Services.Interfaces
|
||||||
public interface IAuthenticationService
|
public interface IAuthenticationService
|
||||||
{
|
{
|
||||||
Task<LoginResultDTO?> LoginAsync(string? username, string? password, CancellationToken cancellationToken);
|
Task<LoginResultDTO?> LoginAsync(string? username, string? password, CancellationToken cancellationToken);
|
||||||
Task<bool> ChangePasswordAsync(string userId, string? currentPassword, string? confirmPassword, CancellationToken cancellationToken);
|
Task<ChangePasswordResultDTO> ChangePasswordAsync(string userId, string? currentPassword, string? newPassword, CancellationToken cancellationToken);
|
||||||
Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken);
|
Task<UserProfileDTO?> UpdateProfileAsync(string userId, UpdateProfileRequestDTO dto, CancellationToken cancellationToken);
|
||||||
Task<bool> ForgetPasswordAsync(string email, CancellationToken cancellationToken);
|
Task<bool> ForgetPasswordAsync(string email, CancellationToken cancellationToken);
|
||||||
Task<bool> VerifyCodeAsync(string code, CancellationToken cancellationToken);
|
Task<bool> VerifyCodeAsync(string code, CancellationToken cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ builder.Services.AddAuthentication(options =>
|
||||||
.AddIdentityCookies();
|
.AddIdentityCookies();
|
||||||
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||||
builder.Services.AddDbContext<ApplicationDbContext>(options => {
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
{
|
||||||
options.UseSqlServer(connectionString);
|
options.UseSqlServer(connectionString);
|
||||||
}, ServiceLifetime.Transient);
|
}, ServiceLifetime.Transient);
|
||||||
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||||
|
|
@ -39,11 +40,7 @@ builder.Services.AddDatabaseDeveloperPageExceptionFilter();
|
||||||
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||||
{
|
{
|
||||||
options.SignIn.RequireConfirmedAccount = true;
|
options.SignIn.RequireConfirmedAccount = true;
|
||||||
options.Password.RequireDigit = true;
|
IdentityPasswordPolicy.Apply(options.Password);
|
||||||
options.Password.RequireLowercase = false;
|
|
||||||
options.Password.RequireUppercase = false;
|
|
||||||
options.Password.RequireNonAlphanumeric = true;
|
|
||||||
options.Password.RequiredLength = 8;
|
|
||||||
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddSignInManager()
|
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddSignInManager()
|
||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue