using System.Text;
using Dpz.Core.Authenticator;
using Dpz.Core.Public.Entity.Auth;
using Dpz.Core.Public.ViewModel.WebAuthn;
using Dpz.Core.Service.Mediator.Features.Security.Commands;
using Microsoft.AspNetCore.Authentication;
using OpenIddict.Abstractions;
using OpenIddict.Core;

namespace Dpz.Core.Auth.Controllers;

[ApiController]
[Route("api/auth")]
public class AuthApiController(
    ILogger<AuthApiController> logger,
    IAccountLoginHistoryService accountLoginHistoryService,
    IUserTwoFactorService userTwoFactorService,
    IUserMfaVerificationService mfaVerificationService,
    IUserPasskeySessionService passkeySessionService,
    SignInManager<VmUserInfo> signInManager,
    IMediator mediator,
    OpenIddictAuthorizationManager<DpzAuthorization> authorizationManager,
    IOpenIddictTokenManager tokenManager
) : ControllerBase
{
    [Authorize]
    [HttpGet("security-bindings")]
    public async Task<ResponseResult<UserSecurityBindingStatusResponse>> SecurityBindings(
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
        }

        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        return ResponseResult<UserSecurityBindingStatusResponse>.Ok(status);
    }

    [Authorize]
    [HttpGet("two-factor/setup")]
    public async Task<ResponseResult<UserTwoFactorSetupResponse>> TwoFactorSetup(
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<UserTwoFactorSetupResponse>.Fail("用户未登录");
        }

        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        if (status.HasTwoFactor)
        {
            return ResponseResult<UserTwoFactorSetupResponse>.Fail("您已绑定双因素认证");
        }

        var challenge = await userTwoFactorService.BeginSetupAsync(account, cancellationToken);
        if (challenge == null)
        {
            logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
            return ResponseResult<UserTwoFactorSetupResponse>.Fail("获取双因素密钥失败");
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
            issuer: "叫我阿胖",
            accountTitleNoSpaces: account,
            accountSecretKey: challenge.Key,
            secretIsBase32: false
        );
        return ResponseResult<UserTwoFactorSetupResponse>.Ok(
            new UserTwoFactorSetupResponse
            {
                SetupToken = challenge.SetupToken,
                Account = setupInfo.Account,
                ManualEntryKey = setupInfo.ManualEntryKey,
                QrCodeSetupImageUrl = setupInfo.QrCodeSetupImageUrl,
                SetupUrl = setupInfo.SetupUrl,
            }
        );
    }

    [Authorize]
    [ValidateAntiForgeryToken]
    [HttpPost("two-factor/bind")]
    public async Task<ResponseResult<UserSecurityBindingStatusResponse>> BindTwoFactor(
        [FromBody] BindTwoFactorRequest? request,
        CancellationToken cancellationToken = default
    )
    {
        if (request == null || string.IsNullOrWhiteSpace(request.Value.PinCode))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("请输入双因素验证码");
        }
        if (string.IsNullOrWhiteSpace(request.Value.SetupToken))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("双因素绑定请求已过期");
        }

        var account = User.NameIdentifier;
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
        }

        var currentStatus = await userTwoFactorService.GetBindingStatusAsync(
            account,
            cancellationToken
        );
        if (currentStatus.HasTwoFactor)
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
                currentStatus,
                "您已绑定双因素认证"
            );
        }

        var key = await userTwoFactorService.GetSetupKeyAsync(
            account,
            request.Value.SetupToken,
            cancellationToken
        );
        if (string.IsNullOrWhiteSpace(key))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail(
                "双因素绑定请求已过期,请重新获取二维码"
            );
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var keyBuffer = Encoding.UTF8.GetBytes(key);
        var keyBase32 = Base32Encoding.ToString(keyBuffer);
        var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(
            keyBase32,
            request.Value.PinCode,
            true
        );
        if (!twoFactorResult)
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("PIN码验证失败");
        }

        await userTwoFactorService.BindAsync(account, request.Value.SetupToken, cancellationToken);
        currentStatus = await userTwoFactorService.GetBindingStatusAsync(
            account,
            cancellationToken
        );
        return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
            currentStatus,
            "双因素认证绑定成功"
        );
    }

    [Authorize]
    [ValidateAntiForgeryToken]
    [HttpPost("two-factor/unbind")]
    public async Task<ResponseResult<UserSecurityBindingStatusResponse>> UnbindTwoFactor(
        [FromBody] BindTwoFactorRequest? request,
        CancellationToken cancellationToken = default
    )
    {
        if (request == null || string.IsNullOrWhiteSpace(request.Value.PinCode))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("请输入双因素验证码");
        }

        var account = User.NameIdentifier;
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
        }

        var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
        if (string.IsNullOrWhiteSpace(key))
        {
            var currentStatus = await userTwoFactorService.GetBindingStatusAsync(
                account,
                cancellationToken
            );
            return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
                currentStatus,
                "您未绑定双因素认证"
            );
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var keyBuffer = Encoding.UTF8.GetBytes(key);
        var keyBase32 = Base32Encoding.ToString(keyBuffer);
        var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(
            keyBase32,
            request.Value.PinCode,
            true
        );
        if (!twoFactorResult)
        {
            return ResponseResult<UserSecurityBindingStatusResponse>.Fail("PIN码验证失败");
        }

        await userTwoFactorService.UnbindAsync(account, cancellationToken);
        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        return ResponseResult<UserSecurityBindingStatusResponse>.Ok(status, "双因素认证已解绑");
    }

    [ValidateAntiForgeryToken]
    [HttpPost("sign-in")]
    public async Task<ResponseResult<WebAuthnAssertionResult>> SignIn(
        [FromBody] AuthSignInRequest? request,
        CancellationToken cancellationToken = default
    )
    {
        if (request == null)
        {
            return ResponseResult<WebAuthnAssertionResult>.Fail("登录请求不能为空");
        }

        var account = request.Account?.Trim();
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<WebAuthnAssertionResult>.Fail("请输入账号");
        }

        if (string.IsNullOrWhiteSpace(request.Password))
        {
            return ResponseResult<WebAuthnAssertionResult>.Fail("请输入密码");
        }

        var returnUrl = NormalizeReturnUrl(request.ReturnUrl);
        HttpContext.SetSessionMetadata(request.SessionInfo);
        if (await accountLoginHistoryService.LockedAsync(account))
        {
            await accountLoginHistoryService.AccountLockedAsync(account);
            return ResponseResult<WebAuthnAssertionResult>.Fail(
                "短时间内多次登录失败,账户已被锁定!"
            );
        }

        var pinValidationResult = await mfaVerificationService.ValidateOptionalTwoFactorAsync(
            account,
            request.PinCode,
            cancellationToken
        );
        if (!pinValidationResult.Success)
        {
            await accountLoginHistoryService.PinCodeErrorAsync(account);
            return ResponseResult<WebAuthnAssertionResult>.Fail(pinValidationResult.Message);
        }

        var result = await signInManager.PasswordSignInAsync(
            account,
            request.Password,
            request.Remember,
            false
        );
        if (!result.Succeeded)
        {
            if (HttpContext.HasAccountDisabledFlag())
            {
                await accountLoginHistoryService.AccountDisabledAsync(account);
                return ResponseResult<WebAuthnAssertionResult>.Fail(
                    "您的账户已被禁用,无法登录!如有疑问,请联系管理员。"
                );
            }

            await accountLoginHistoryService.LoginFailAsync(account);
            return ResponseResult<WebAuthnAssertionResult>.Fail("用户名或密码错误!");
        }

        await accountLoginHistoryService.SuccessAsync(account);
        passkeySessionService.ClearCurrentCredentialId();
        returnUrl = await ResolvePostSignInRedirectUrlAsync(account, returnUrl, cancellationToken);

        return ResponseResult<WebAuthnAssertionResult>.Ok(
            new WebAuthnAssertionResult { RedirectUrl = returnUrl }
        );
    }

    [Authorize]
    [ValidateAntiForgeryToken]
    [HttpPost("change-password")]
    public async Task<ResponseResult<WebAuthnAssertionResult>> ChangePassword(
        ChangePasswordModel model,
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (string.IsNullOrWhiteSpace(account))
        {
            return ResponseResult<WebAuthnAssertionResult>.Fail("用户身份验证失败,请重新登录");
        }

        var pinValidationResult = await mfaVerificationService.ValidateOptionalTwoFactorAsync(
            account,
            model.PinCode,
            cancellationToken
        );
        if (!pinValidationResult.Success)
        {
            await accountLoginHistoryService.PinCodeErrorAsync(account);
            return ResponseResult<WebAuthnAssertionResult>.Fail(pinValidationResult.Message);
        }

        var result = await mediator.Send(
            new ChangeAccountPasswordRequest
            {
                Account = account,
                Password = model.Password!,
                NewPassword = model.NewPassword!,
            },
            HttpContext.RequestAborted
        );

        if (!result.Success)
        {
            return ResponseResult<WebAuthnAssertionResult>.Fail(result.Message ?? "密码修改失败");
        }

        try
        {
            await tokenManager.RevokeAsync(account, null, null, null, cancellationToken);
            await authorizationManager.RevokeAsync(account, null, null, null, cancellationToken);
        }
        catch (Exception e)
        {
            logger.LogError(e, "撤销用户授权和令牌失败");
        }

        await HttpContext.SignOutAsync();
        return ResponseResult<WebAuthnAssertionResult>.Ok(
            new WebAuthnAssertionResult { RedirectUrl = "/sign-in" },
            "密码修改成功!为了您的账户安全,已撤销所有授权,请重新登录"
        );
    }

    private string NormalizeReturnUrl(string? returnUrl)
    {
        if (string.IsNullOrWhiteSpace(returnUrl))
        {
            return Url.Action("Index", "Home") ?? "/";
        }

        return Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
    }

    private async Task<string> ResolvePostSignInRedirectUrlAsync(
        string account,
        string returnUrl,
        CancellationToken cancellationToken
    )
    {
        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        if (status.HasRequiredSecurity)
        {
            return returnUrl;
        }

        return $"/security-onboarding.html?returnUrl={Uri.EscapeDataString(returnUrl)}";
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core 的认证 API 控制器(AuthApiController),主要负责用户认证和安全相关的功能。以下是详细解释:

主要功能模块

1. 双因素认证(2FA)管理

SecurityBindings - 获取安全绑定状态

  • 查询当前登录用户的安全绑定状态(是否绑定了双因素认证)

TwoFactorSetup - 开始设置双因素认证

  • 生成双因素认证的密钥和二维码
  • 返回设置所需的信息:
    • SetupToken:设置令牌
    • 二维码图片URL
    • 手动输入密钥
    • 设置URL

BindTwoFactor - 绑定双因素认证

  • 验证用户输入的 PIN 码
  • 将密钥转换为 Base32 格式并验证
  • 验证成功后绑定双因素认证
  • 需要 CSRF 保护(ValidateAntiForgeryToken

UnbindTwoFactor - 解绑双因素认证

  • 验证 PIN 码后解除双因素认证绑定
  • 同样需要 CSRF 保护

2. 登录功能

SignIn - 用户登录

执行流程:

  1. 验证账号和密码是否为空
  2. 记录会话元数据
  3. 检查账户是否被锁定
  4. 验证可选的双因素认证码
  5. 执行密码登录
  6. 处理登录结果:
    • 账户被禁用
    • 密码错误(记录失败次数)
    • 登录成功
  7. 清除 Passkey 会话
  8. 确定登录后的重定向 URL(如果未完成必要的安全设置,会引导至安全设置页面)

3. 修改密码

ChangePassword - 修改密码

流程:

  1. 验证用户身份
  2. 验证可选的双因素认证
  3. 调用中介者模式执行密码修改
  4. 修改成功后:
    • 撤销所有现有的授权和令牌(提高安全性)
    • 退出当前登录
    • 重定向到登录页面

技术要点

依赖注入

控制器通过主构造函数注入了多个服务:

  • 日志服务
  • 登录历史服务
  • 双因素认证服务
  • MFA 验证服务
  • Passkey 会话服务
  • 登录管理器
  • 中介者(Mediator)
  • OpenIddict 授权和令牌管理器

安全特性

  1. 防 CSRF 攻击:使用 ValidateAntiForgeryToken 保护状态变更操作
  2. 账户锁定:多次登录失败后锁定账户
  3. 会话管理:登录历史记录和会话元数据跟踪
  4. 令牌撤销:密码修改后撤销所有现有令牌

辅助方法

  • NormalizeReturnUrl:标准化返回 URL,防止开放重定向攻击
  • ResolvePostSignInRedirectUrlAsync:根据安全设置状态决定登录后跳转目标

返回值

所有 API 都返回统一的 ResponseResult<T> 格式,包含成功/失败状态和相应的数据或错误消息。

这是一个设计良好的认证控制器,充分考虑了安全性、可扩展性和用户体验。

评论加载中...