feat(auth): replace TOTP with phone-first login

This commit is contained in:
2026-07-28 17:39:29 +08:00
parent e7d350ec3d
commit c7f9a4e3c9
43 changed files with 18386 additions and 882 deletions

View File

@@ -109,7 +109,7 @@ public sealed class AuthSessionStore(
try
{
await AssertRealmAccessAsync(
current.Realm, current.TenantId, current.UserId, current.MfaSatisfied, cancellationToken);
current.Realm, current.TenantId, current.UserId, cancellationToken);
}
catch (TenantAccessDeniedException)
{
@@ -133,7 +133,7 @@ public sealed class AuthSessionStore(
var request = new AuthSessionIssueRequest(
user.Id, user.Phone, user.Email, user.SecurityStamp ?? string.Empty,
current.Realm, current.TenantId, "refresh", current.MfaSatisfied,
current.Realm, current.TenantId, "refresh",
ipAddress, userAgent, current.TokenFamilyId, current.Id);
var next = CreateSession(request, nextId);
var nextToken = GenerateRefreshToken(next.Realm, next.TenantId, next.Id);
@@ -171,14 +171,14 @@ public sealed class AuthSessionStore(
try
{
await AssertRealmAccessAsync(
realm, tenantId, userId, session.MfaSatisfied, cancellationToken);
realm, tenantId, userId, cancellationToken);
}
catch (TenantAccessDeniedException)
{
return null;
}
return new AuthSessionValidationResult(userId, realm, tenantId, session.MfaSatisfied);
return new AuthSessionValidationResult(userId, realm, tenantId);
}
public async Task RevokeFamilyAsync(string refreshToken, string reason, CancellationToken cancellationToken = default)
@@ -256,7 +256,6 @@ public sealed class AuthSessionStore(
TokenFamilyId = request.TokenFamilyId ?? sessionId,
ParentSessionId = request.ParentSessionId,
SecurityStamp = request.SecurityStamp,
MfaSatisfied = request.MfaSatisfied,
Provider = request.Provider,
ExpiresAt = DateTimeOffset.UtcNow.AddDays(options.RefreshTokenDays),
IpAddress = request.IpAddress,
@@ -267,7 +266,7 @@ public sealed class AuthSessionStore(
{
var access = tokenService.CreateAccessToken(
request.UserId, session.Id, request.Phone, request.Email,
request.Realm, request.TenantId, request.MfaSatisfied);
request.Realm, request.TenantId);
return new AuthTokenPair(access.Token, refreshToken, access.ExpiresAt, session.ExpiresAt);
}
@@ -275,14 +274,13 @@ public sealed class AuthSessionStore(
AuthRealm realm,
Guid? tenantId,
Guid userId,
bool mfaSatisfied,
CancellationToken cancellationToken)
{
if (realm == AuthRealm.Tenant && tenantId.HasValue)
{
var active = await dbContext.Tenants.AnyAsync(item => item.Id == tenantId && item.Status == TenantStatus.Active, cancellationToken) &&
await dbContext.TenantMemberships.AnyAsync(item => item.TenantId == tenantId && item.UserId == userId && item.Status == MembershipStatus.Active, cancellationToken);
if (active && (!mfaSatisfied || await HasTenantBackendPermissionAsync(tenantId.Value, userId, cancellationToken)))
if (active)
{
return;
}
@@ -303,19 +301,6 @@ public sealed class AuthSessionStore(
throw new TenantAccessDeniedException();
}
private Task<bool> HasTenantBackendPermissionAsync(
Guid tenantId,
Guid userId,
CancellationToken cancellationToken) =>
(from userRole in dbContext.TenantBackendUserRoles
join role in dbContext.TenantBackendRoles on userRole.RoleId equals role.Id
join binding in dbContext.TenantBackendRolePermissions on role.Id equals binding.RoleId
join permission in dbContext.BackendPermissions on binding.PermissionCode equals permission.Code
where userRole.TenantId == tenantId && userRole.UserId == userId &&
binding.TenantId == tenantId && role.Status == BackendRoleStatus.Active &&
(permission.Area == BackendPermissionArea.Tenant || permission.Area == BackendPermissionArea.Both)
select permission.Id).AnyAsync(cancellationToken);
private async Task<int> RevokeFamilyCoreAsync(Guid familyId, string reason, DateTimeOffset now, CancellationToken cancellationToken)
{
var owner = await dbContext.AuthSessions.AsNoTracking()