forked from xiongyuxing/tiku-backend.net
feat(auth): replace TOTP with phone-first login
This commit is contained in:
@@ -31,6 +31,23 @@ public sealed class AuthServiceTests
|
||||
Assert.Equal(PasswordVerificationResult.Failed, hasher.VerifyHashedPassword(user, hash, "wrong"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("abc12345", true)]
|
||||
[InlineData("ABC12345", true)]
|
||||
[InlineData("abcdefgh", false)]
|
||||
[InlineData("12345678", false)]
|
||||
[InlineData("abc1234", false)]
|
||||
public async Task Default_password_policy_requires_eight_characters_letters_and_digits(
|
||||
string password,
|
||||
bool expectedSuccess)
|
||||
{
|
||||
var validator = new LetterAndDigitPasswordValidator<User>();
|
||||
|
||||
var result = await validator.ValidateAsync(null!, new User(), password);
|
||||
|
||||
Assert.Equal(expectedSuccess, result.Succeeded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Password_login_issues_tenant_session_for_regular_member()
|
||||
{
|
||||
@@ -70,48 +87,17 @@ public sealed class AuthServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Backend_permission_requires_one_time_mfa_enrollment_challenge()
|
||||
public async Task Backend_permission_user_authenticates_without_an_additional_challenge()
|
||||
{
|
||||
await using var fixture = await AuthFixture.CreateAsync(includeBackendPermission: true);
|
||||
|
||||
var result = await fixture.AuthService.LoginWithPasswordAsync(new PasswordLoginRequest(
|
||||
AuthRealm.Tenant, fixture.TenantId, AuthFixture.Phone, AuthFixture.Password, null, null));
|
||||
|
||||
Assert.Equal(AuthenticationStatus.MfaEnrollmentRequired, result.Status);
|
||||
Assert.Null(result.User);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.ChallengeToken));
|
||||
Assert.Empty(await fixture.DbContext.AuthSessions.ToArrayAsync());
|
||||
Assert.Single(await fixture.DbContext.AuthChallenges.ToArrayAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Incomplete_authenticator_setup_still_requires_enrollment()
|
||||
{
|
||||
await using var fixture = await AuthFixture.CreateAsync(includeBackendPermission: true);
|
||||
var user = await fixture.UserManager.FindByIdAsync(fixture.UserId.ToString());
|
||||
Assert.True((await fixture.UserManager.ResetAuthenticatorKeyAsync(user!)).Succeeded);
|
||||
Assert.False(user!.TwoFactorEnabled);
|
||||
|
||||
var result = await fixture.AuthService.LoginWithPasswordAsync(new PasswordLoginRequest(
|
||||
AuthRealm.Tenant, fixture.TenantId, AuthFixture.Phone, AuthFixture.Password, null, null));
|
||||
|
||||
Assert.Equal(AuthenticationStatus.MfaEnrollmentRequired, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mfa_setup_audit_captures_request_origin()
|
||||
{
|
||||
await using var fixture = await AuthFixture.CreateAsync(includeBackendPermission: true);
|
||||
var login = await fixture.AuthService.LoginWithPasswordAsync(new PasswordLoginRequest(
|
||||
AuthRealm.Tenant, fixture.TenantId, AuthFixture.Phone, AuthFixture.Password, null, null));
|
||||
|
||||
await fixture.AuthService.SetupTotpAsync(new MfaChallengeRequest(
|
||||
login.ChallengeToken!, null, "127.0.0.9", "mfa-audit-test"));
|
||||
|
||||
var audit = await fixture.DbContext.AuditLogs.SingleAsync(item =>
|
||||
item.Action == "auth.mfa.enrollment_setup");
|
||||
Assert.Equal("127.0.0.9", audit.IpAddress);
|
||||
Assert.Equal("mfa-audit-test", audit.UserAgent);
|
||||
Assert.Equal(AuthenticationStatus.Authenticated, result.Status);
|
||||
Assert.NotNull(result.User);
|
||||
Assert.Single(await fixture.DbContext.AuthSessions.ToArrayAsync());
|
||||
Assert.Empty(await fixture.DbContext.AuthChallenges.ToArrayAsync());
|
||||
}
|
||||
|
||||
private sealed class AuthFixture : IAsyncDisposable
|
||||
@@ -146,9 +132,9 @@ public sealed class AuthServiceTests
|
||||
options.UseInMemoryDatabase(Guid.NewGuid().ToString("N")));
|
||||
services.AddIdentityCore<User>(options =>
|
||||
{
|
||||
options.Password.RequiredLength = 10;
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
@@ -156,7 +142,8 @@ public sealed class AuthServiceTests
|
||||
})
|
||||
.AddEntityFrameworkStores<TikuDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
.AddDefaultTokenProviders()
|
||||
.AddPasswordValidator<LetterAndDigitPasswordValidator<User>>();
|
||||
services.Configure<PasswordHasherOptions>(options => options.IterationCount = 210_000);
|
||||
services.Configure<JwtOptions>(options =>
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Infrastructure.Bootstrap;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.UnitTests.Bootstrap;
|
||||
@@ -28,7 +29,6 @@ public sealed class PlatformAdminBootstrapperTests
|
||||
var context = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
||||
var user = await context.Users.SingleAsync(item => item.Id == result.UserId);
|
||||
Assert.True(user.ForcePasswordChange);
|
||||
Assert.False(user.TwoFactorEnabled);
|
||||
Assert.True(user.EmailConfirmed);
|
||||
Assert.Equal(UserStatus.Active, user.Status);
|
||||
var role = await context.PlatformBackendRoles.SingleAsync(item => item.Id == result.RoleId);
|
||||
@@ -97,7 +97,6 @@ public sealed class PlatformAdminBootstrapperTests
|
||||
var user = await context.Users.SingleAsync();
|
||||
Assert.Equal(DevelopmentPlatformAdminSeeder.Email, user.Email);
|
||||
Assert.True(user.ForcePasswordChange);
|
||||
Assert.False(user.TwoFactorEnabled);
|
||||
Assert.Equal(BackendPermissions.Platform.Count, await context.PlatformBackendRolePermissions.CountAsync());
|
||||
Assert.Single(context.PlatformBackendUserRoles);
|
||||
Assert.Single(context.AuditLogs);
|
||||
@@ -127,14 +126,15 @@ public sealed class PlatformAdminBootstrapperTests
|
||||
options.UseInMemoryDatabase(Guid.NewGuid().ToString()));
|
||||
services.AddIdentityCore<User>(options =>
|
||||
{
|
||||
options.Password.RequiredLength = 10;
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
})
|
||||
.AddEntityFrameworkStores<TikuDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
.AddDefaultTokenProviders()
|
||||
.AddPasswordValidator<LetterAndDigitPasswordValidator<User>>();
|
||||
services.AddDataProtection().UseEphemeralDataProtectionProvider();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user