Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Заметка
Это не последняя версия этой статьи. Сведения о текущем выпуске см. в версии .NET 9 этой статьи.
Важный
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Корпорация Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых в отношении информации, предоставленной здесь.
Сведения о текущем выпуске см. в версии .NET 9 этой статьи.
В этой статье объясняется, как настроить приложение ASP.NET Core Blazor WebAssembly с помощью Identity для двухфакторной аутентификации (2FA) с использованием QR-кодов, созданных приложениями проверки подлинности, реализующими алгоритм одноразовых паролей на основе времени (TOTP).
Общие сведения о 2FA с приложениями проверки подлинности TOTP см. в статье Включение создания QR-кода для приложений проверки подлинности TOTP в ASP.NET Core.
Предупреждение
Коды TOTP должны храниться в секрете, так как их можно использовать для проверки подлинности несколько раз до истечения срока их действия.
Примеры пространств имен и примеры кода для статей
Пространства имен, используемые примерами в этой статье, являются:
-
Backend
для проекта веб-API серверного сервера, описанного как "серверный проект" в этой статье. -
BlazorWasmAuth
для автономного приложения фронтенд-клиента Blazor WebAssembly, описанного как "клиентский проект" в этой статье.
Эти пространства имен соответствуют проектам в примере решения BlazorWebAssemblyStandaloneWithIdentity
в dotnet/blazor-samples
репозитории GitHub. Ознакомьтесь с разделом Secure ASP.NET Core Blazor WebAssembly с ASP.NET Core Identityдля получения дополнительной информации.
Если вы не используете пример BlazorWebAssemblyStandaloneWithIdentity
, измените пространства имен в примерах кода, чтобы использовать пространства имен проектов.
Все изменения решения, описанные в этой статье, происходят в проекте BlazorWasmAuth
решения BlazorWebAssemblyStandaloneWithIdentity
.
В примерах статей строки кода разделены для уменьшения горизонтальной прокрутки. Эти разрывы не влияют на выполнение, но их можно удалить при вставке в проект.
Необязательное подтверждение учетной записи и восстановление паролей
Хотя приложения, реализующие 2FA, обычно применяют функции подтверждения учетной записи и восстановления паролей, 2FA не требует его. Рекомендации, приведенные в этой статье, можно использовать для реализации 2FA без следования рекомендациям по подтверждению учетной записи и восстановлению паролей в ASP.NET Core Blazor WebAssembly с ASP.NET Core Identity.
Добавление в приложение библиотеки QR-кода
QR-код, созданный приложением для настройки 2FA с приложением toTP authenticator, должен быть создан библиотекой QR-кода.
В этом руководстве используется manuelbl/QrCodeGenerator
, но вы можете использовать любую библиотеку создания QR-кода.
Добавьте ссылку на пакет в клиентский проект для пакета NuGet Net.Codecrete.QrCodeGenerator
.
Заметка
Рекомендации по добавлению пакетов в приложения .NET см. в статьях, приведенных в разделе Установка пакетов и управление ими в рабочий процесс использования пакетов (документация по NuGet). Проверьте правильные версии пакетов в NuGet.org.
Назначьте имя организации TOTP
Задайте имя сайта в файле параметров приложения клиентского проекта. Используйте понятное имя сайта, которое пользователи могут легко определить в приложении authenticator. Разработчики обычно задают имя сайта, соответствующее имени компании. Мы рекомендуем ограничить длину имени сайта до 30 символов или меньше, чтобы имя сайта отображалось на узких экранах мобильных устройств.
В следующем примере название компании — Weyland-Yutani Corporation
(©1986 20th Century Studios Aliens).
Добавлено в 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
. Этот класс заполняется ответом на запрос 2FA, сделанный в конечную точку /manage/2fa
, принадлежащую MapIdentityApi, в серверном приложении.
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
, который отправляет запрос в конечную точку /login
с кодом 2FA TOTP (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
, который отправляет запрос в конечную точку /login
с кодом восстановления 2FA (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 для пользователя:
- Сбросьте общий ключ 2FA, когда
TwoFactorRequest.ResetSharedKey
будетtrue
. Сброс общего ключа автоматически отключает двухфакторную аутентификацию (2FA). Это заставляет пользователя доказать, что он может предоставить действительный код TOTP из приложения authenticator, чтобы включить 2FA после получения нового общего ключа. - Сброс кодов восстановления пользователя при
TwoFactorRequest.ResetRecoveryCodes
true
. - Забудьте об устройстве при
TwoFactorRequest.ForgetMachine
,true
, что означает, что для следующей попытки входа требуется новый код 2FA TOTP. - Включите 2FA с помощью кода TOTP из приложения проверки подлинности TOTP, если
TwoFactorRequest.Enable
true
иTwoFactorRequest.TwoFactorCode
имеет допустимое значение TOTP. - Получите статус 2FA с пустым запросом, когда все свойства
TwoFactorRequest
являютсяnull
.
Добавьте следующий метод 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 отображается для получения либо кода 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-кодом, чтобы включить 2FA с приложением проверки подлинности TOTP. Пользователь добавляет приложение в приложение authenticator, а затем проверяет приложение authenticator и включает 2FA, предоставляя код TOTP из приложения authenticator.
Если включена 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;
}
}
Ссылка на страницу "Управление 2FA"
Добавьте ссылку в меню навигации, чтобы пользователи достигли страницы компонента 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-сервером.
Дополнительные ресурсы
ASP.NET Core