add passkey & totp

This commit is contained in:
biss committed 2026-08-27 21:45:39 +08:00
1 parent 70f6b52577
commit 1d06bbd482
13 files changed
+9114 -4

No files matched your search

+52 -1
View File
@@ -19,6 +19,7 @@ public sealed class AuthController(
AppDbContext db,
UserManager<ApplicationUser> userManager,
IAuthSessionService authSessionService,
ITwoFactorLoginTicketService twoFactorTickets,
IAppCache cache) : ControllerBase
{
[AllowAnonymous]
@@ -137,7 +138,7 @@ public sealed class AuthController(
[AllowAnonymous]
[EnableRateLimiting("public-auth")]
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login(
public async Task<ActionResult> Login(
LoginRequest request,
CancellationToken cancellationToken)
{
@@ -164,6 +165,11 @@ public sealed class AuthController(
});
}
if (user.TwoFactorEnabled)
{
return Ok(new TwoFactorRequiredResponse(twoFactorTickets.Create(user.Id, request.IsNativeApp)));
}
await userManager.ResetAccessFailedCountAsync(user);
user.LastLoginAt = DateTime.UtcNow;
await userManager.UpdateAsync(user);
@@ -176,6 +182,36 @@ public sealed class AuthController(
? AuthenticationClientType.App
: AuthenticationClientType.Web,
cancellationToken);
return Ok(CreateLoginResponse(session));
}
[AllowAnonymous]
[EnableRateLimiting("public-auth")]
[HttpPost("login/totp")]
public async Task<ActionResult<LoginResponse>> CompleteTotpLogin(
TotpLoginRequest request,
CancellationToken cancellationToken)
{
if (!twoFactorTickets.TryRead(request.TwoFactorTicket, out var userId, out var isNativeApp))
return Unauthorized(LoginProblem());
var user = await userManager.FindByIdAsync(userId.ToString("D"));
var code = request.Code.Replace(" ", string.Empty);
var isValid = user is not null && (await userManager.VerifyTwoFactorTokenAsync(
user, TokenOptions.DefaultAuthenticatorProvider, code) ||
(await userManager.RedeemTwoFactorRecoveryCodeAsync(user, code)).Succeeded);
if (user is null || !user.IsEnabled || await userManager.IsLockedOutAsync(user) || !isValid)
{
if (user is not null) await userManager.AccessFailedAsync(user);
return Unauthorized(LoginProblem());
}
await userManager.ResetAccessFailedCountAsync(user);
user.LastLoginAt = DateTime.UtcNow;
await userManager.UpdateAsync(user);
var roles = await userManager.GetRolesAsync(user);
var session = await authSessionService.CreateAsync(user, roles,
isNativeApp ? AuthenticationClientType.App : AuthenticationClientType.Web,
cancellationToken);
return CreateLoginResponse(session);
}
@@ -248,6 +284,12 @@ public sealed class AuthController(
Status = status
});
private static ProblemDetails LoginProblem() => new()
{
Title = "登录失败", Detail = "验证码无效、已过期或账号已停用。",
Status = StatusCodes.Status401Unauthorized
};
internal static LoginResponse CreateLoginResponse(AuthSessionResult session) =>
new(
session.AccessToken,
@@ -268,6 +310,15 @@ public sealed record LoginRequest(
[Required, MaxLength(100)] string Password,
bool IsNativeApp = false);
public sealed record TotpLoginRequest(
[Required, MinLength(20), MaxLength(2048)] string TwoFactorTicket,
[Required, MinLength(6), MaxLength(12)] string Code);
public sealed record TwoFactorRequiredResponse(string TwoFactorTicket)
{
public bool RequiresTotp => true;
}
public sealed record RefreshTokenRequest(
[Required, MinLength(40), MaxLength(200)] string RefreshToken);