注意
這不是本文的最新版本。 關於目前版本,請參閱 本文的 .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
如果您未使用 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 資料夾。 此類別用於伺服器應用程式中 /login 的 MapIdentityApi 端點的請求。
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/2fa 的 MapIdentityApi 端點的請求。
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/2fa 的 MapIdentityApi 端點對 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);
更新 cookie 驗證狀態提供者
將 CookieAuthenticationStateProvider 更新以添加以下功能:
- 使用 TOTP 驗證器應用程式程式代碼或復原程式代碼來驗證使用者。
- 在應用程式中管理 2FA。
在 CookieAuthenticationStateProvider.cs 檔案的頂端為 using新增一個 System.Text.Json.Serialization 語句:
using System.Text.Json.Serialization;
在 JsonSerializerOptions中,添加已設定為 DefaultIgnoreCondition的 JsonIgnoreCondition.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.ResetSharedKey是true時,重設共用的 2FA 金鑰。 重設共用金鑰會隱含停用 2FA。 這會強制使用者證明他們可以從驗證器應用程式提供有效的 TOTP 程式代碼,以便在收到新的共用金鑰之後啟用 2FA。 - 當
TwoFactorRequest.ResetRecoveryCodes是true時,重設使用者的恢復碼。 - 當
TwoFactorRequest.ForgetMachinetrue時忘記計算機,這表示下次登入嘗試時需要新的 2FA TOTP 程式代碼。 - 當
TwoFactorRequest.Enabletrue且TwoFactorRequest.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 服務同步。