using System.Text;
using Dpz.Core.Authenticator;

namespace Dpz.Core.Auth.Controllers;

public class TwoFactorController(
    ILogger<HomeController> logger,
    IUserTwoFactorService userTwoFactorService
) : Controller
{
    /// <summary>
    /// 绑定双因素验证
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [Authorize]
    [Route("two-factor.html")]
    public async Task<IActionResult> BindTwoFactor(
        string returnUrl = "/",
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
        {
            return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
        }
        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        if (status.HasTwoFactor)
        {
            return Redirect(returnUrl);
        }

        var model = await BuildBindModelAsync(
            account,
            returnUrl,
            cancellationToken: cancellationToken
        );
        return model == null ? Redirect(returnUrl) : this.ShellView(model);
    }

    [HttpGet]
    [Authorize]
    [Route("unbind-two-factor.html")]
    public async Task<IActionResult> UnBindTwoFactor(CancellationToken cancellationToken = default)
    {
        var account = User.NameIdentifier;
        var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
        return this.ShellView(
            BuildUnbindModel(string.IsNullOrWhiteSpace(key) ? "您未绑定双因素验证" : null)
        );
    }

    [ValidateAntiForgeryToken]
    [Authorize]
    [HttpPost]
    [Route("handel-bind-two-factor")]
    public async Task<IActionResult> HandleBindTwoFactor(
        string pinCode,
        string setupToken,
        string returnUrl = "/",
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
        {
            return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
        }

        var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
        if (status.HasTwoFactor)
        {
            return Redirect(returnUrl);
        }
        var key = await userTwoFactorService.GetSetupKeyAsync(
            account,
            setupToken,
            cancellationToken
        );
        if (string.IsNullOrWhiteSpace(key))
        {
            logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
            var model = await BuildBindModelAsync(
                account,
                returnUrl,
                "双因素绑定请求已过期,请重新获取二维码",
                cancellationToken
            );
            return model == null ? Redirect(returnUrl) : this.ShellView(model);
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var keyBuffer = Encoding.UTF8.GetBytes(key);
        var keyBase32 = Base32Encoding.ToString(keyBuffer);
        var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);
        if (!twoFactorResult)
        {
            var model = await BuildBindModelAsync(
                account,
                returnUrl,
                "PIN码验证失败",
                cancellationToken
            );
            return model == null ? Redirect(returnUrl) : this.ShellView(model);
        }

        await userTwoFactorService.BindAsync(account, setupToken, cancellationToken);
        return Redirect(returnUrl);
    }

    [HttpPost]
    [Authorize]
    [Route("unbind-two-factor")]
    public async Task<IActionResult> HandleUnbindTwoFactor(
        [FromQuery] string returnUrl,
        string pinCode,
        CancellationToken cancellationToken = default
    )
    {
        var account = User.NameIdentifier;
        if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
        {
            return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
        }
        var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
        if (string.IsNullOrWhiteSpace(key))
        {
            return this.ShellView(BuildUnbindModel("您未绑定双因素验证"));
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var keyBuffer = Encoding.UTF8.GetBytes(key);
        var keyBase32 = Base32Encoding.ToString(keyBuffer);
        var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);
        if (!twoFactorResult)
        {
            return this.ShellView(BuildUnbindModel("PIN码验证失败"));
        }

        await userTwoFactorService.UnbindAsync(account, cancellationToken);
        return Redirect(returnUrl);
    }

    /// <summary>
    /// 构建 2FA 绑定页面模型,包括生成 setup token、TOTP 密钥和二维码
    /// </summary>
    private async Task<AuthShellViewModel<TwoFactorSetupShellPayload>?> BuildBindModelAsync(
        string account,
        string returnUrl,
        string? message = null,
        CancellationToken cancellationToken = default
    )
    {
        var challenge = await userTwoFactorService.BeginSetupAsync(account, cancellationToken);
        if (challenge == null)
        {
            logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
            return null;
        }

        var twoFactorAuthenticator = new TwoFactorAuthenticator();
        var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
            issuer: "叫我阿胖",
            accountTitleNoSpaces: account,
            accountSecretKey: challenge.Key,
            secretIsBase32: false
        );

        return new AuthShellViewModel<TwoFactorSetupShellPayload>
        {
            EntryName = "identity",
            PageName = "bind-two-factor",
            ReturnUrl = returnUrl,
            Payload = new TwoFactorSetupShellPayload
            {
                SetupToken = challenge.SetupToken,
                Account = setupInfo.Account,
                ManualEntryKey = setupInfo.ManualEntryKey,
                QrCodeSetupImageUrl = setupInfo.QrCodeSetupImageUrl,
                SetupUrl = setupInfo.SetupUrl,
            },
            Message = message,
        };
    }

    /// <summary>
    /// 构建 2FA 解绑页面模型
    /// </summary>
    private static AuthShellViewModel<EmptyAuthShellPayload> BuildUnbindModel(
        string? message = null
    ) =>
        new()
        {
            EntryName = "identity",
            PageName = "unbind-two-factor",
            Message = message,
        };
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core MVC 控制器,用于处理**双因素认证(2FA)**的绑定和解绑功能。以下是详细解释:


整体架构

控制器定义

public class TwoFactorController(
    ILogger<HomeController> logger,
    IUserTwoFactorService userTwoFactorService
) : Controller
  • 使用主构造函数语法(C# 12)注入依赖
  • 注入了日志服务和双因素认证业务服务

核心功能

1. 绑定双因素验证页面 (BindTwoFactor)

[HttpGet]
[Authorize]
[Route("two-factor.html")]

流程:

  1. 验证用户是否已登录
  2. 检查用户是否已绑定 2FA
    • 已绑定 → 重定向到返回 URL
    • 未绑定 → 生成绑定页面模型
  3. 调用 BuildBindModelAsync 生成包含以下信息的页面:
    • Setup Token(临时令牌)
    • TOTP 密钥
    • 二维码 URL

2. 处理绑定请求 (HandleBindTwoFactor)

[HttpPost]
[ValidateAntiForgeryToken]
[Route("handel-bind-two-factor")]

验证流程:

  1. 验证用户身份和绑定状态
  2. 使用 setupToken 获取之前生成的密钥
  3. 验证 PIN 码:
    var twoFactorAuthenticator = new TwoFactorAuthenticator();
    var keyBuffer = Encoding.UTF8.GetBytes(key);
    var keyBase32 = Base32Encoding.ToString(keyBuffer);
    var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);
    
    • 将密钥转为 Base32 编码
    • 验证用户输入的 6 位 PIN 码
  4. 验证成功后调用 BindAsync 完成绑定

3. 解绑双因素验证页面 (UnBindTwoFactor)

[HttpGet]
[Authorize]
[Route("unbind-two-factor.html")]
  • 检查用户是否已绑定 2FA
  • 返回解绑页面视图

4. 处理解绑请求 (HandleUnbindTwoFactor)

[HttpPost]
[Authorize]
[Route("unbind-two-factor")]

流程:

  1. 获取用户已绑定的密钥
  2. 要求用户输入当前有效的 PIN 码进行验证
  3. 验证通过后调用 UnbindAsync 解除绑定

辅助方法

BuildBindModelAsync

生成绑定页面所需的完整数据模型:

var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
    issuer: "叫我阿胖",  // 显示在验证器应用中的名称
    accountTitleNoSpaces: account,
    accountSecretKey: challenge.Key,
    secretIsBase32: false
);

返回包含以下内容的视图模型:

  • SetupToken:临时令牌用于验证绑定请求
  • QrCodeSetupImageUrl:二维码图片 URL
  • ManualEntryKey:手动输入密钥(当无法扫码时)

BuildUnbindModel

生成解绑页面的简单模型,主要用于显示错误信息。


安全特性

  1. 防 CSRF 攻击:绑定操作使用 [ValidateAntiForgeryToken]
  2. 授权验证:所有操作都需要 [Authorize] 认证
  3. 状态检查:每次操作前验证用户当前绑定状态
  4. 时效性控制:Setup Token 有过期机制
  5. PIN 码验证:使用 TOTP 算法验证动态 PIN 码

典型使用场景

  1. 首次绑定:

    • 访问 /two-factor.html → 扫描二维码 → 输入 PIN 码 → 绑定成功
  2. 解绑操作:

    • 访问 /unbind-two-factor.html → 输入当前有效 PIN 码 → 解绑成功
  3. 已绑定用户访问绑定页:

    • 自动重定向到原页面,防止重复绑定
评论加载中...