2024-05-02 07:43:47 +01:00
|
|
|
@page "/Account/Manage/EnableAuthenticator"
|
|
|
|
|
@using System.ComponentModel.DataAnnotations
|
|
|
|
|
@using System.Globalization
|
|
|
|
|
@using System.Text
|
|
|
|
|
@using System.Text.Encodings.Web
|
2024-05-03 03:24:40 +01:00
|
|
|
@using Microsoft.AspNetCore.Identity
|
2024-05-02 07:43:47 +01:00
|
|
|
|
2024-05-03 22:54:50 +01:00
|
|
|
@inject UserManager<IdentityUser> UserManager
|
2024-05-02 07:43:47 +01:00
|
|
|
@inject IdentityUserAccessor UserAccessor
|
|
|
|
|
@inject UrlEncoder UrlEncoder
|
|
|
|
|
@inject IdentityRedirectManager RedirectManager
|
|
|
|
|
@inject ILogger<EnableAuthenticator> Logger
|
|
|
|
|
|
|
|
|
|
<PageTitle>Configure authenticator app</PageTitle>
|
|
|
|
|
|
|
|
|
|
@if(recoveryCodes is not null)
|
|
|
|
|
{
|
|
|
|
|
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()" StatusMessage="@message"/>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
<StatusMessage Message="@message"/>
|
2024-05-03 03:24:40 +01:00
|
|
|
|
2024-05-02 07:43:47 +01:00
|
|
|
<h3>Configure authenticator app</h3>
|
|
|
|
|
<div>
|
|
|
|
|
<p>To use an authenticator app go through the following steps:</p>
|
|
|
|
|
<ol class="list">
|
|
|
|
|
<li>
|
|
|
|
|
<p>
|
|
|
|
|
Download a two-factor authenticator app like 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> or
|
|
|
|
|
Google Authenticator for
|
|
|
|
|
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en">Android</a> and
|
|
|
|
|
<a href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">iOS</a>.
|
|
|
|
|
</p>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
<p>Scan the QR Code or enter this key <kbd>@sharedKey</kbd> into your two factor authenticator app. Spaces and casing do not matter.</p>
|
|
|
|
|
<div class="alert alert-info">Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable QR code generation</a>.</div>
|
|
|
|
|
<div></div>
|
|
|
|
|
<div data-url="@authenticatorUri"></div>
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
<p>
|
|
|
|
|
Once you have scanned the QR code or input the key above, your two factor authentication app will provide you
|
|
|
|
|
with a unique code. Enter the code in the confirmation box below.
|
|
|
|
|
</p>
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<EditForm FormName="send-code" method="post" Model="Input" OnValidSubmit="OnValidSubmitAsync">
|
|
|
|
|
<DataAnnotationsValidator/>
|
|
|
|
|
<div class="form-floating mb-3">
|
|
|
|
|
<InputText autocomplete="off" @bind-Value="Input.Code" class="form-control" placeholder="Please enter the code."/>
|
|
|
|
|
<label class="control-label form-label" for="code">Verification Code</label>
|
|
|
|
|
<ValidationMessage class="text-danger" For="() => Input.Code"/>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn btn-lg btn-primary w-100" type="submit">Verify</button>
|
|
|
|
|
<ValidationSummary class="text-danger" role="alert"/>
|
|
|
|
|
</EditForm>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
</ol>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
|
|
|
|
|
|
|
|
|
|
private string? message;
|
2024-05-03 22:54:50 +01:00
|
|
|
private IdentityUser user = default!;
|
2024-05-02 07:43:47 +01:00
|
|
|
private string? sharedKey;
|
|
|
|
|
private string? authenticatorUri;
|
|
|
|
|
private IEnumerable<string>? recoveryCodes;
|
|
|
|
|
|
|
|
|
|
[CascadingParameter]
|
|
|
|
|
private HttpContext HttpContext { get; set; } = default!;
|
|
|
|
|
|
|
|
|
|
[SupplyParameterFromForm]
|
|
|
|
|
private InputModel Input { get; set; } = new();
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
user = await UserAccessor.GetRequiredUserAsync(HttpContext);
|
|
|
|
|
|
|
|
|
|
await LoadSharedKeyAndQrCodeUriAsync(user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task OnValidSubmitAsync()
|
|
|
|
|
{
|
|
|
|
|
// Strip spaces and hyphens
|
2024-05-03 03:24:40 +01:00
|
|
|
string verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
2024-05-02 07:43:47 +01:00
|
|
|
|
2024-05-03 03:24:40 +01:00
|
|
|
bool is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync(user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
2024-05-02 07:43:47 +01:00
|
|
|
|
|
|
|
|
if(!is2faTokenValid)
|
|
|
|
|
{
|
|
|
|
|
message = "Error: Verification code is invalid.";
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await UserManager.SetTwoFactorEnabledAsync(user, true);
|
2024-05-03 03:24:40 +01:00
|
|
|
string userId = await UserManager.GetUserIdAsync(user);
|
2024-05-02 07:43:47 +01:00
|
|
|
Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId);
|
|
|
|
|
|
|
|
|
|
message = "Your authenticator app has been verified.";
|
|
|
|
|
|
|
|
|
|
if(await UserManager.CountRecoveryCodesAsync(user) == 0)
|
|
|
|
|
{
|
|
|
|
|
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-03 22:54:50 +01:00
|
|
|
private async ValueTask LoadSharedKeyAndQrCodeUriAsync(IdentityUser user)
|
2024-05-02 07:43:47 +01:00
|
|
|
{
|
|
|
|
|
// Load the authenticator key & QR code URI to display on the form
|
2024-05-03 03:24:40 +01:00
|
|
|
string? unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
|
2024-05-02 07:43:47 +01:00
|
|
|
|
|
|
|
|
if(string.IsNullOrEmpty(unformattedKey))
|
|
|
|
|
{
|
|
|
|
|
await UserManager.ResetAuthenticatorKeyAsync(user);
|
|
|
|
|
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sharedKey = FormatKey(unformattedKey!);
|
|
|
|
|
|
2024-05-03 03:24:40 +01:00
|
|
|
string? email = await UserManager.GetEmailAsync(user);
|
2024-05-02 07:43:47 +01:00
|
|
|
authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string FormatKey(string unformattedKey)
|
|
|
|
|
{
|
|
|
|
|
var result = new StringBuilder();
|
2024-05-03 03:24:40 +01:00
|
|
|
var currentPosition = 0;
|
2024-05-02 07:43:47 +01:00
|
|
|
|
|
|
|
|
while(currentPosition + 4 < unformattedKey.Length)
|
|
|
|
|
{
|
|
|
|
|
result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' ');
|
|
|
|
|
currentPosition += 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(currentPosition < unformattedKey.Length)
|
|
|
|
|
{
|
|
|
|
|
result.Append(unformattedKey.AsSpan(currentPosition));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.ToString().ToLowerInvariant();
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-03 03:24:40 +01:00
|
|
|
private string GenerateQrCodeUri(string email, string unformattedKey) => string.Format(CultureInfo.InvariantCulture, AuthenticatorUriFormat, UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), UrlEncoder.Encode(email), unformattedKey);
|
2024-05-02 07:43:47 +01:00
|
|
|
|
|
|
|
|
private sealed class InputModel
|
|
|
|
|
{
|
|
|
|
|
[Required]
|
|
|
|
|
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
|
|
|
|
|
[DataType(DataType.Text)]
|
|
|
|
|
[Display(Name = "Verification Code")]
|
|
|
|
|
public string Code { get; set; } = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|