在具有 ASP.NET Core Blazor WebAssembly 的 ASP.NET Core Identity 中啟用 TOTP 驗證器應用程式的 QR 代碼產生

注意

這不是本文的最新版本。 關於目前版本,請參閱 本文的 .NET 10 版本

本文說明如何使用時間為基礎的一次性密碼算法(TOTP)驗證器應用程式所生成的 QR 碼,配置帶有雙因素驗證(2FA)的 ASP.NET Core Blazor WebAssembly 應用程式與 Identity。

關於使用 TOTP 驗證器應用程式進行 2FA 的介紹,請參見 啟用 QR 碼產生以促進 TOTP 認證

警告

TOTP 程式代碼應該保持秘密,因為它們可用來在到期前多次進行驗證。

命名空間和文章程式碼範例

本文範例所使用的命名空間如下:

  • Backend 後端伺服器 Web API 專案,本文描述為「伺服器專案」。
  • BlazorWasmAuth 用於前端的獨立 Blazor WebAssembly 應用程式,如本文中所述的「用戶端專案」。

這些命名空間會對應至 BlazorWebAssemblyStandaloneWithIdentity GitHub 存放庫中 dotnet/blazor-samples 範例解決方案中的專案,。 如需詳細資訊,請參閱使用 ASP.NET Core Secure ASP.NET Core

如果您未使用 BlazorWebAssemblyStandaloneWithIdentity 範例,請將程式代碼範例中的命名空間變更為使用專案的命名空間。

本文涵蓋的所有解決方案變更都在 BlazorWasmAuth 解決方案的 BlazorWebAssemblyStandaloneWithIdentity 專案中進行。

在文章範例中,程式代碼行會分割以減少水平捲動。 這些分隔不會影響執行,但在貼入專案時可以移除。

選擇性帳戶確認和密碼復原

雖然實作 2FA 的應用程式通常會採用帳戶確認和密碼復原功能,但 2FA 不需要它。 您可以遵循本文中的指引來實作 2FA,而不必遵循 ASP.NET Core 中的 帳戶確認和 密碼復原中的指引。

將 QR 代碼連結庫新增至應用程式

應用程式產生用於與 TOTP 驗證器應用程式設定 2FA 的 QR 代碼,必須由 QR 代碼庫產生。

本文中的指引使用 manuelbl/QrCodeGenerator,但您可以使用任何 QR 碼生成庫。

Net.Codecrete.QrCodeGenerator NuGet 套件的套件參考新增至客戶端專案。

注意

如需將套件新增至 .NET 應用程式的指引,請參閱 套件取用工作流程(NuGet 檔)安裝及管理套件 下的文章。 在 NuGet.org確認正確的套件版本。

設定 TOTP 組織名稱

在客戶端專案的應用程式設定檔中設定網站名稱。 使用有意義的網站名稱,用戶可以輕鬆地在驗證器應用程式中識別。 開發人員通常會設定符合公司名稱的網站名稱。 建議將網站名稱長度限制為 30 個字元或更少,以允許網站名稱顯示在窄的行動裝置畫面上。

在下列範例中,公司名稱是 Weyland-Yutani Corporation(©1986 20世紀工作室 外星人)。

已新增至 wwwroot/appsettings.json

"TotpOrganizationName": "Weyland-Yutani Corporation"

完成 TOTP 組織名稱設定後的應用程式設定檔:

{
  "BackendUrl": "https://localhost:7211",
  "FrontendUrl": "https://localhost:7171",
  "TotpOrganizationName": "Weyland-Yutani Corporation"
}

新增模型類別

將下列 LoginResponse 類別新增至 Models 資料夾。 此類別用於伺服器應用程式中 /loginMapIdentityApi 端點的請求。

Identity/Models/LoginResponse.cs

namespace BlazorWasmAuth.Identity.Models;

public class LoginResponse
{
    public string? Type { get; set; }
    public string? Title { get; set; }
    public int Status { get; set; }
    public string? Detail { get; set; }
}

將下列 TwoFactorRequest 類別新增至 Models 資料夾。 此類別用於伺服器應用程式中 /manage/2faMapIdentityApi 端點的請求。

Identity/Models/TwoFactorRequest.cs

namespace BlazorWasmAuth.Identity.Models;

public class TwoFactorRequest
{
    public bool? Enable { get; set; }
    public string? TwoFactorCode { get; set; }
    public bool? ResetSharedKey { get; set; }
    public bool? ResetRecoveryCodes { get; set; }
    public bool? ForgetMachine {  get; set; }
}

將下列 TwoFactorResponse 類別新增至 Models 資料夾。 這個類別是由伺服器應用程式中 /manage/2faMapIdentityApi 端點對 2FA 要求的回應所填入的。

Identity/Models/TwoFactorResponse.cs

namespace BlazorWasmAuth.Identity.Models;

public class TwoFactorResponse
{
    public string SharedKey { get; set; } = string.Empty;
    public int RecoveryCodesLeft { get; set; } = 0;
    public string[] RecoveryCodes { get; set; } = [];
    public bool IsTwoFactorEnabled { get; set; }
    public bool IsMachineRemembered { get; set; }
    public string[] ErrorList { get; set; } = [];
}

IAccountManagement 介面

將下列類別簽章新增至 IAccountManagement 介面。 類別簽名表示將方法新增到 cookie 驗證狀態提供者,以回應下列用戶端要求:

  • 使用 2FA TOTP 程式代碼登入(/login 端點):LoginTwoFactorCodeAsync
  • 使用 2FA 復原代碼登入(/login 端點):LoginTwoFactorRecoveryCodeAsync
  • 提出 2FA 管理要求(/manage/2fa 端點):TwoFactorRequestAsync

Identity/IAccountManagement.cs (將下列程式代碼貼到檔案底部):

public Task<FormResult> LoginTwoFactorCodeAsync(
    string email, 
    string password, 
    string twoFactorCode);

public Task<FormResult> LoginTwoFactorRecoveryCodeAsync(
    string email, 
    string password, 
    string twoFactorRecoveryCode);

public Task<TwoFactorResponse> TwoFactorRequestAsync(
    TwoFactorRequest twoFactorRequest);

CookieAuthenticationStateProvider 更新以添加以下功能:

  • 使用 TOTP 驗證器應用程式程式代碼或復原程式代碼來驗證使用者。
  • 在應用程式中管理 2FA。

CookieAuthenticationStateProvider.cs 檔案的頂端為 using新增一個 System.Text.Json.Serialization 語句:

using System.Text.Json.Serialization;

JsonSerializerOptions中,添加已設定為 DefaultIgnoreConditionJsonIgnoreCondition.WhenWritingNull 選項,以避免序列化 null 屬性:

private readonly JsonSerializerOptions jsonSerializerOptions =
    new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+       DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

LoginAsync 方法會以下列邏輯更新:

  • 嘗試使用電子郵件地址和密碼在 /login 端點進行一般登入。
  • 如果伺服器回應成功狀態代碼,此方法會傳回 FormResult,並將 Succeeded 屬性設定為 true
  • 如果伺服器回應 401 - 未經授權 狀態代碼與 “RequiresTwoFactor” 的詳細代碼,則會返回 FormResult,並將 Succeeded 設定為 false 並在錯誤清單中包含 RequiresTwoFactor 的詳細資訊。

Identity/CookieAuthenticationStateProvider.cs中,以下列程式代碼取代 LoginAsync 方法:

public async Task<FormResult> LoginAsync(string email, string password)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
        else if (result.StatusCode == HttpStatusCode.Unauthorized)
        {
            using var responseJson = await result.Content.ReadAsStringAsync();
            var response = JsonSerializer.Deserialize<LoginResponse>(
                responseJson, jsonSerializerOptions);

            if (response?.Detail == "RequiresTwoFactor")
            {
                return new FormResult
                {
                    Succeeded = false,
                    ErrorList = [ "RequiresTwoFactor" ]
                };
            }
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid email and/or password." ]
    };
}

已新增 LoginTwoFactorCodeAsync 方法,它會使用 2FA TOTP 程式代碼(/login)將要求傳送至 twoFactorCode 端點。 方法會以與一般、非 2FA 登入要求類似的方式處理回應。

將下列方法和類別新增至 Identity/CookieAuthenticationStateProvider.cs (將下列程式代碼貼到類別檔案的底部):

public async Task<FormResult> LoginTwoFactorCodeAsync(
    string email, string password, string twoFactorCode)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password,
                twoFactorCode
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid two-factor code." ]
    };
}

已新增 LoginTwoFactorRecoveryCodeAsync 方法,它會使用 2FA 修復碼(/login)將要求傳送至 twoFactorRecoveryCode 端點。 方法會以與一般、非 2FA 登入要求類似的方式處理回應。

將下列方法和類別新增至 Identity/CookieAuthenticationStateProvider.cs (將下列程式代碼貼到類別檔案的底部):

public async Task<FormResult> LoginTwoFactorRecoveryCodeAsync(string email, 
    string password, string twoFactorRecoveryCode)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "login?useCookies=true", new
            {
                email,
                password,
                twoFactorRecoveryCode
            });

        if (result.IsSuccessStatusCode)
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());

            return new FormResult { Succeeded = true };
        }
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "App error");
    }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = [ "Invalid recovery code." ]
    };
}

已新增 TwoFactorRequestAsync 方法,用來管理使用者的 2FA:

  • TwoFactorRequest.ResetSharedKeytrue時,重設共用的 2FA 金鑰。 重設共用金鑰會隱含停用 2FA。 這會強制使用者證明他們可以從驗證器應用程式提供有效的 TOTP 程式代碼,以便在收到新的共用金鑰之後啟用 2FA。
  • TwoFactorRequest.ResetRecoveryCodestrue時,重設使用者的恢復碼。
  • TwoFactorRequest.ForgetMachinetrue時忘記計算機,這表示下次登入嘗試時需要新的 2FA TOTP 程式代碼。
  • TwoFactorRequest.EnabletrueTwoFactorRequest.TwoFactorCode 具有有效的 TOTP 值時,使用 TOTP 驗證器應用程式的 TOTP 代碼來啟用 2FA。
  • 當所有 TwoFactorRequest的屬性均為 null時,透過空白請求取得 2FA 狀態。

將下列 TwoFactorRequestAsync 方法新增至 Identity/CookieAuthenticationStateProvider.cs (貼上類別檔案底部的下列程式代碼):

public async Task<TwoFactorResponse> TwoFactorRequestAsync(TwoFactorRequest twoFactorRequest)
{
    string[] defaultDetail = 
        [ "An unknown error prevented two-factor authentication." ];

    using var response = await httpClient.PostAsJsonAsync("manage/2fa", twoFactorRequest, 
        jsonSerializerOptions);

    // successful?
    if (response.IsSuccessStatusCode)
    {
        return await response.Content
            .ReadFromJsonAsync<TwoFactorResponse>() ??
            new()
            { 
                ErrorList = [ "There was an error processing the request." ]
            };
    }

    // body should contain details about why it failed
    var details = await response.Content.ReadAsStringAsync();
    var problemDetails = JsonDocument.Parse(details);
    var errors = new List<string>();
    var errorList = problemDetails.RootElement.GetProperty("errors");

    foreach (var errorEntry in errorList.EnumerateObject())
    {
        if (errorEntry.Value.ValueKind == JsonValueKind.String)
        {
            errors.Add(errorEntry.Value.GetString()!);
        }
        else if (errorEntry.Value.ValueKind == JsonValueKind.Array)
        {
            errors.AddRange(
                errorEntry.Value.EnumerateArray().Select(
                    e => e.GetString() ?? string.Empty)
                .Where(e => !string.IsNullOrEmpty(e)));
        }
    }

    // return the error list
    return new TwoFactorResponse
    {
        ErrorList = problemDetails == null ? defaultDetail : [.. errors]
    };
}

取代 Login 元件

取代 Login 元件。 下列 Login 元件版本:

  • 接受使用者的電子郵件地址和密碼,以進行初始登入嘗試。
  • 如果登入成功(2FA 已停用),元件會通知用戶他們已驗證。
  • 如果登入嘗試導致回應指出需要 2FA,則會顯示一個 2FA 輸入欄位,以接收驗證器應用程式提供的 2FA TOTP 驗證碼或是復原碼。 根據使用者輸入的代碼,系統會呼叫 LoginTwoFactorCodeAsync 以獲取 TOTP 驗證碼,或呼叫 LoginTwoFactorRecoveryCodeAsync 以獲取復原碼,然後再次嘗試登入。

Components/Identity/Login.razor

@page "/login"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@using BlazorWasmAuth.Identity.Models
@inject IAccountManagement Acct
@inject ILogger<Login> Logger
@inject NavigationManager Navigation

<PageTitle>Login</PageTitle>

<h1>Login</h1>

<AuthorizeView>
    <Authorized>
        <div class="alert alert-success">
            You're logged in as @context.User.Identity?.Name.
        </div>
    </Authorized>
    <NotAuthorized>
        @foreach (var error in formResult.ErrorList)
        {
            <div class="alert alert-danger">@error</div>
        }
        <div class="row">
            <div class="col">
                <section>
                    <EditForm Model="Input" method="post" OnValidSubmit="LoginUser" 
                            FormName="login" Context="editform_context">
                        <DataAnnotationsValidator />
                        <h2>Use a local account to log in.</h2>
                        <hr />
                        <div style="display:@(requiresTwoFactor ? "none" : "block")">
                            <div class="form-floating mb-3">
                                <InputText @bind-Value="Input.Email" 
                                    id="Input.Email" 
                                    class="form-control" 
                                    autocomplete="username" 
                                    aria-required="true" 
                                    placeholder="name@example.com" />
                                <label for="Input.Email" class="form-label">
                                    Email
                                </label>
                                <ValidationMessage For="() => Input.Email" 
                                    class="text-danger" />
                            </div>
                            <div class="form-floating mb-3">
                                <InputText type="password" 
                                    @bind-Value="Input.Password" 
                                    id="Input.Password" 
                                    class="form-control" 
                                    autocomplete="current-password" 
                                    aria-required="true" 
                                    placeholder="password" />
                                <label for="Input.Password" class="form-label">
                                    Password
                                </label>
                                <ValidationMessage For="() => Input.Password" 
                                    class="text-danger" />
                            </div>
                        </div>
                        <div style="display:@(requiresTwoFactor ? "block" : "none")">
                            <div class="form-floating mb-3">
                                <InputText @bind-Value="Input.TwoFactorCodeOrRecoveryCode" 
                                    id="Input.TwoFactorCodeOrRecoveryCode" 
                                    class="form-control" 
                                    autocomplete="off" 
                                    placeholder="###### or #####-#####" />
                                <label for="Input.TwoFactorCodeOrRecoveryCode" class="form-label">
                                    Two-factor Code or Recovery Code
                                </label>
                                <ValidationMessage For="() => Input.TwoFactorCodeOrRecoveryCode" 
                                    class="text-danger" />
                            </div>
                        </div>
                        <div>
                            <button type="submit" class="w-100 btn btn-lg btn-primary">
                                Log in
                            </button>
                        </div>
                        <div class="mt-3">
                            <p>
                                <a href="forgot-password">Forgot password</a>
                            </p>
                            <p>
                                <a href="register">Register as a new user</a>
                            </p>
                        </div>
                    </EditForm>
                </section>
            </div>
        </div>
    </NotAuthorized>
</AuthorizeView>

@code {
    private FormResult formResult = new();
    private bool requiresTwoFactor;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromQuery]
    private string? ReturnUrl { get; set; }

    public async Task LoginUser()
    {
        if (requiresTwoFactor)
        {
            if (!string.IsNullOrEmpty(Input.TwoFactorCodeOrRecoveryCode))
            {
                // The [RegularExpression] data annotation ensures that the input 
                // is either a six-digit authenticator code (######) or an 
                // eleven-character alphanumeric recovery code (#####-#####)
                if (Input.TwoFactorCodeOrRecoveryCode.Length == 6)
                {
                    formResult = await Acct.LoginTwoFactorCodeAsync(
                        Input.Email, Input.Password, 
                        Input.TwoFactorCodeOrRecoveryCode);
                }
                else
                {
                    formResult = await Acct.LoginTwoFactorRecoveryCodeAsync(
                        Input.Email, Input.Password, 
                        Input.TwoFactorCodeOrRecoveryCode);

                    if (formResult.Succeeded)
                    {
                        var twoFactorResponse = await Acct.TwoFactorRequestAsync(new());
                    }
                }
            }
            else
            {
                formResult = 
                    new FormResult
                    {
                        Succeeded = false,
                        ErrorList = [ "Invalid two-factor code." ]
                    };
            }
        }
        else
        {
            formResult = await Acct.LoginAsync(Input.Email, Input.Password);
            requiresTwoFactor = formResult.ErrorList.Contains("RequiresTwoFactor");
            Input.TwoFactorCodeOrRecoveryCode = string.Empty;

            if (requiresTwoFactor)
            {
                formResult.ErrorList = [];
            }
        }

        if (formResult.Succeeded && !string.IsNullOrEmpty(ReturnUrl))
        {
            Navigation.NavigateTo(ReturnUrl);
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; } = string.Empty;

        [RegularExpression(@"^([0-9]{6})|([A-Z0-9]{5}[-]{1}[A-Z0-9]{5})$", 
            ErrorMessage = "Must be a six-digit authenticator code (######) or " +
            "eleven-character alphanumeric recovery code (#####-#####, dash " +
            "required)")]
        [Display(Name = "Two-factor Code or Recovery Code")]
        public string TwoFactorCodeOrRecoveryCode { get; set; } = string.Empty;
    }
}

使用上述元件時,當用戶在驗證器應用程式中使用有效的 TOTP 程式代碼成功登入後,用戶的資訊會被記住。 在成功進行雙因素登入之後,如果您想要一律要求 TOTP 驗證碼進行登入,且不記住設備,請立即呼叫 TwoFactorRequestAsync 方法,並將 TwoFactorRequest.ForgetMachine 設定為 true

if (Input.TwoFactorCodeOrRecoveryCode.Length == 6)
{
    formResult = await Acct.LoginTwoFactorCodeAsync(Input.Email, Input.Password, 
        Input.TwoFactorCodeOrRecoveryCode);

+    if (formResult.Succeeded)
+    {
+        var forgetMachine = 
+            await Acct.TwoFactorRequestAsync(new() { ForgetMachine = true });
+    }
}

新增元件以顯示復原碼

將下列 ShowRecoveryCodes 元件新增至應用程式,以向使用者顯示修復碼。

Components/Identity/ShowRecoveryCodes.razor

<h3>Recovery codes</h3>

<div class="alert alert-warning" role="alert">
    <p>
        <strong>Put these codes in a safe place.</strong>
    </p>
    <p>
        If you lose your device and don't have an unused 
        recovery code, you can't access your account.
    </p>
</div>
<div class="row">
    <div class="col-md-12">
        @foreach (var recoveryCode in RecoveryCodes)
        {
            <div>
                <code class="recovery-code">@recoveryCode</code>
            </div>
        }
    </div>
</div>

@code {
    [Parameter]
    public string[] RecoveryCodes { get; set; } = [];
}

管理 2FA 頁面

將下列 Manage2fa 元件新增至應用程式,以管理使用者的 2FA。

如果未啟用 2FA,元件會載入具有 QR 代碼的表單,以使用 TOTP 驗證器應用程式啟用 2FA。 使用者將應用程式新增至驗證器應用程式,然後驗證驗證器應用程式,並從驗證器應用程式提供TOTP程式代碼來啟用2FA。

如果已啟用 2FA,按鈕會顯示為停用 2FA 並重新產生修復碼。

Components/Identity/Manage2fa.razor

@page "/manage-2fa"
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@using System.Text
@using System.Text.Encodings.Web
@using Net.Codecrete.QrCodeGenerator
@using BlazorWasmAuth.Identity
@using BlazorWasmAuth.Identity.Models
@attribute [Authorize]
@inject IAccountManagement Acct
@inject IAuthorizationService AuthorizationService
@inject IConfiguration Config
@inject ILogger<Manage2fa> Logger

<PageTitle>Manage 2FA</PageTitle>

<h1>Manage Two-factor Authentication</h1>
<hr />
<div class="row">
    <div class="col">
        @if (loading)
        {
            <p>Loading ...</p>
        }
        else
        {
            @if (twoFactorResponse is not null)
            {
                @foreach (var error in twoFactorResponse.ErrorList)
                {
                    <div class="alert alert-danger">@error</div>
                }
                @if (twoFactorResponse.IsTwoFactorEnabled)
                {
                    <div class="alert alert-success" role="alert">
                        Two-factor authentication is enabled for your account.
                    </div>

                    <div class="m-1">
                        <button @onclick="Disable2FA" class="btn btn-lg btn-primary">
                            Disable 2FA
                        </button>
                    </div>

                    @if (twoFactorResponse.RecoveryCodes is null)
                    {
                        <div class="m-1">
                            Recovery Codes Remaining: 
                            @twoFactorResponse.RecoveryCodesLeft
                        </div>
                        <div class="m-1">
                            <button @onclick="GenerateNewCodes" 
                                    class="btn btn-lg btn-primary">
                                Generate New Recovery Codes
                            </button>
                        </div>
                    }
                    else
                    {
                        <ShowRecoveryCodes 
                            RecoveryCodes="twoFactorResponse.RecoveryCodes" />
                    }
                }
                else
                {
                    <h3>Configure authenticator app</h3>
                    <div>
                        <p>To use an authenticator app:</p>
                        <ol class="list">
                            <li>
                                <p>
                                    Download a two-factor authenticator app, such 
                                    as either of the following:
                                    <ul>
                                        <li>
                                            Microsoft Authenticator for
                                            <a href="https://go.microsoft.com/fwlink/?Linkid=825072">
                                                Android
                                            </a> and
                                            <a href="https://go.microsoft.com/fwlink/?Linkid=825073">
                                                iOS
                                            </a>
                                        </li>
                                        <li>
                                            Google Authenticator for
                                            <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">
                                                Android
                                            </a> and
                                            <a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">
                                                iOS
                                            </a>
                                        </li>
                                    </ul>
                                </p>
                            </li>
                            <li>
                                <p>
                                    Scan the QR Code or enter this key 
                                    <kbd>@twoFactorResponse.SharedKey</kbd> into your 
                                    two-factor authenticator app. Spaces and casing 
                                    don't matter.
                                </p>
                                <div>
                                    <svg xmlns="http://www.w3.org/2000/svg" height="300" 
                                            width="300" stroke="none" version="1.1" 
                                            viewBox="0 0 50 50">
                                        <rect width="300" height="300" fill="#ffffff" />
                                        <path d="@svgGraphicsPath" fill="#000000" />
                                    </svg>
                                </div>
                            </li>
                            <li>
                                <p>
                                    After you have scanned the QR code or input the 
                                    key above, your two-factor authenticator app 
                                    will provide you with a unique two-factor code. 
                                    Enter the code in the confirmation box below.
                                </p>
                                <div class="row">
                                    <div class="col-xl-6">
                                        <EditForm Model="Input" 
                                                FormName="send-code" 
                                                OnValidSubmit="OnValidSubmitAsync" 
                                                method="post">
                                            <DataAnnotationsValidator />
                                            <div class="form-floating mb-3">
                                                <InputText 
                                                    @bind-Value="Input.Code" 
                                                    id="Input.Code" 
                                                    class="form-control" 
                                                    autocomplete="off" 
                                                    placeholder="Enter the code" />
                                                <label for="Input.Code" 
                                                        class="control-label form-label">
                                                    Verification Code
                                                </label>
                                                <ValidationMessage 
                                                    For="() => Input.Code" 
                                                    class="text-danger" />
                                            </div>
                                            <button type="submit" 
                                                    class="w-100 btn btn-lg btn-primary">
                                                Verify
                                            </button>
                                        </EditForm>
                                    </div>
                                </div>
                            </li>
                        </ol>
                    </div>
                }
            }
        }
    </div>
</div>

@code {
    private TwoFactorResponse twoFactorResponse = new();
    private bool loading = true;
    private string? svgGraphicsPath;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = new();

    [CascadingParameter]
    private Task<AuthenticationState>? authenticationState { get; set; }

    protected override async Task OnInitializedAsync()
    {
        twoFactorResponse = await Acct.TwoFactorRequestAsync(new());
        svgGraphicsPath = await GetQrCode(twoFactorResponse.SharedKey);
        loading = false;
    }

    private async Task<string> GetQrCode(string sharedKey)
    {
        if (authenticationState is not null && !string.IsNullOrEmpty(sharedKey))
        {
            var authState = await authenticationState;
            var email = authState?.User?.Identity?.Name!;
            var uri = string.Format(
                CultureInfo.InvariantCulture,
                "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6",
                UrlEncoder.Default.Encode(Config["TotpOrganizationName"]!),
                email,
                twoFactorResponse.SharedKey);
            var qr = QrCode.EncodeText(uri, QrCode.Ecc.Medium);

            return qr.ToGraphicsPath();
        }

        return string.Empty;
    }

    private async Task Disable2FA()
    {
        await Acct.TwoFactorRequestAsync(new() { ForgetMachine = true });
        twoFactorResponse = 
            await Acct.TwoFactorRequestAsync(new() { ResetSharedKey = true });
        svgGraphicsPath = await GetQrCode(twoFactorResponse.SharedKey);
    }

    private async Task GenerateNewCodes()
    {
        twoFactorResponse = 
            await Acct.TwoFactorRequestAsync(new() { ResetRecoveryCodes = true });
    }

    private async Task OnValidSubmitAsync()
    {
        twoFactorResponse = await Acct.TwoFactorRequestAsync(
            new() 
            { 
                Enable = true, 
                TwoFactorCode = Input.Code 
            });
        Input.Code = string.Empty;

        // When 2FA is first enabled, recovery codes are returned.
        // However, subsequently disabling and re-enabling 2FA
        // leaves the existing codes in place and doesn't generate
        // a new set of recovery codes. The following code ensures
        // that a new set of recovery codes is generated each
        // time 2FA is enabled.
        if (twoFactorResponse.RecoveryCodes is null || 
            twoFactorResponse.RecoveryCodes.Length == 0)
        {
            await GenerateNewCodes();
        }
    }

    private sealed class InputModel
    {
        [Required]
        [RegularExpression(@"^([0-9]{6})$", 
            ErrorMessage = "Must be a six-digit authenticator code (######)")]
        [DataType(DataType.Text)]
        [Display(Name = "Verification Code")]
        public string Code { get; set; } = string.Empty;
    }
}

為使用者在導覽選單中新增一個連結,以連至 Manage2fa 元件頁面。

<Authorized><AuthorizeView>Components/Layout/NavMenu.razor 內容中,新增下列標記:

<AuthorizeView>
    <Authorized>

        ...

+       <div class="nav-item px-3">
+           <NavLink class="nav-link" href="manage-2fa">
+               <span class="bi bi-key" aria-hidden="true"></span> Manage 2FA
+           </NavLink>
+       </div>

        ...

    </Authorized>
</AuthorizeView>

TOTP 時間偏差造成的失敗

TOTP 驗證依賴於 TOTP 驗證器應用程式裝置和應用程式主機的時間準確同步。 TOTP 令牌僅有效 30 秒。 如果因為 TOTP 代碼被拒絕而登入失敗,請確認時間維持正確,最好與精確的 NTP 服務同步。

其他資源