-
Notifications
You must be signed in to change notification settings - Fork 25.1k
2FA/TOTP coverage for WASM+Identity #34189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
df05640
1a76a33
f11cb7e
732f927
8f12c38
7310d53
22cbd85
ef67733
19f22b9
e67b8a8
8e328eb
8dfe0bb
5a80e7e
e3d87d3
22f2568
c1e2bd8
d716300
6060ce8
c128164
5025c32
e57702c
17be3fb
0be9c72
9c5525e
7d71141
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,12 +11,12 @@ uid: blazor/security/qrcodes-for-authenticator-apps | |
|
|
||
| [!INCLUDE[](~/includes/not-latest-version-without-not-supported-content.md)] | ||
|
|
||
| This article explains how to configure an ASP.NET Core Blazor Web App with QR code generation for TOTP authenticator apps. | ||
| This article explains how to configure an ASP.NET Core Blazor Web App for two-factor authentication (2FA) with QR codes generated by Time-based One-time Password Algorithm (TOTP) authenticator apps. | ||
|
|
||
| For an introduction to two-factor authentication (2FA) with authenticator apps using a Time-based One-time Password Algorithm (TOTP), see <xref:security/authentication/identity-enable-qrcodes>. | ||
| For an introduction to 2FA with TOTP authenticator apps, see <xref:security/authentication/identity-enable-qrcodes>. | ||
|
|
||
| > [!WARNING] | ||
| > An ASP.NET Core TOTP code should be kept secret because it can be used to authenticate successfully multiple times before it expires. | ||
| > TOTP codes should be kept secret because they can be used to authenticate multiple times before they expire. | ||
|
|
||
| ## Scaffold the Enable Authenticator component into the app | ||
guardrex marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
@@ -33,14 +33,16 @@ For more information, see <xref:security/authentication/scaffold-identity>. For | |
|
|
||
| ## Adding QR codes to the 2FA configuration page | ||
|
|
||
| These instructions use [Shim Sangmin](https://hogangnono.com)'s [qrcode.js: Cross-browser QRCode generator for JavaScript](https://davidshimjs.github.io/qrcodejs/) ([`davidshimjs/qrcodejs` GitHub repository](https://github.com/davidshimjs/qrcodejs)). | ||
| A QR code generated by the app to set up 2FA with an TOTP authenticator app must be generated by a QR code library. | ||
|
|
||
| The guidance in this article uses [`nimiq/qr-creator`](https://github.com/nimiq/qr-creator), but you can use any QR code generation library. | ||
|
||
|
|
||
| Download the [`qrcode.min.js`](https://davidshimjs.github.io/qrcodejs/) library to the `wwwroot` folder of the solution's server project. The library has no dependencies. | ||
| Download the [`qr-creator.min.js`](https://cdn.jsdelivr.net/npm/qr-creator/dist/qr-creator.min.js) library to the solution's server project. The library has no dependencies. If you intend to use the library to generate other QR codes elsewhere in the app for your own purposes, there's also a module version of the library available from the maintainer (see the [`nimiq/qr-creator`](https://github.com/nimiq/qr-creator) GitHub repository for details). | ||
|
|
||
| In the `App` component (`Components/App.razor`), place a library script reference after [Blazor's `<script>` tag](xref:blazor/project-structure#location-of-the-blazor-script): | ||
|
|
||
| ```razor | ||
| <script src="qrcode.min.js"></script> | ||
| ```html | ||
| <script src="qr-creator.min.js"></script> | ||
| ``` | ||
|
|
||
| The `EnableAuthenticator` component, which is part of the QR code system in the app and displays the QR code to users, adopts static server-side rendering (static SSR) with enhanced navigation. Therefore, normal scripts can't execute when the component loads or updates under enhanced navigation. Extra steps are required to trigger the QR code to load in the UI when the page is loaded. To accomplish loading the QR code, the approach explained in <xref:blazor/js-interop/ssr> is adopted. | ||
|
|
@@ -72,7 +74,14 @@ Add the following [collocated JS file](xref:blazor/js-interop/javascript-locatio | |
| ```javascript | ||
| export function onLoad() { | ||
| const uri = document.getElementById('qrCodeData').getAttribute('data-url'); | ||
| new QRCode(document.getElementById('qrCode'), uri); | ||
| QrCreator.render({ | ||
| text: uri, | ||
| radius: 0, | ||
| ecLevel: 'H', | ||
| fill: '#000000', | ||
| background: null, | ||
| size: 190 | ||
| }, document.querySelector('#qrCode')); | ||
| } | ||
| ``` | ||
|
|
||
|
|
@@ -111,7 +120,7 @@ Make the following changes: | |
| + <div id="qrCodeData" data-url="@authenticatorUri"></div> | ||
| ``` | ||
|
|
||
| Change the site name in the `GenerateQrCodeUri` method of the `EnableAuthenticator` component. The default value is `Microsoft.AspNetCore.Identity.UI`. Change the value to a meaningful site name that users can identify easily in their authenticator app. Developers usually set a site name that matches the company's name. Examples: Yahoo, Amazon, Etsy, Microsoft, Zoho. We recommend limiting the site name length to 30 characters or less to allow the site name to display on narrow mobile device screens. | ||
| Change the site name in the `GenerateQrCodeUri` method of the `EnableAuthenticator` component. The default value is `Microsoft.AspNetCore.Identity.UI`. Change the value to a meaningful site name that users can identify easily in their authenticator app. Developers usually set a site name that matches the company's name. We recommend limiting the site name length to 30 characters or less to allow the site name to display on narrow mobile device screens. | ||
|
|
||
| In the following example, the default value `Microsoft.AspNetCore.Identity.UI` is changed to the company name `Weyland-Yutani Corporation` (©1986 20th Century Studios [*Aliens*](https://www.20thcenturystudios.com/movies/aliens)). | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,19 @@ author: guardrex | |
| description: Learn how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery. | ||
| ms.author: riande | ||
| monikerRange: '>= aspnetcore-8.0' | ||
| ms.date: 10/31/2024 | ||
| ms.date: 11/21/2024 | ||
| uid: blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery | ||
| --- | ||
| # Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity | ||
|
|
||
| [!INCLUDE[](~/includes/not-latest-version-without-not-supported-content.md)] | ||
|
|
||
| This article explains how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery. | ||
|
|
||
| > [!NOTE] | ||
| > This article only applies standalone Blazor WebAssembly apps with ASP.NET Core Identity. To implement email confirmation and password recovery for Blazor Web Apps, see <xref:blazor/security/account-confirmation-and-password-recovery>. | ||
|
|
||
| ## Namespace | ||
| ## Namespaces and article code examples | ||
|
|
||
| The namespaces used by the examples in this article are: | ||
|
|
||
|
|
@@ -156,15 +158,11 @@ Locate the line that calls <xref:Microsoft.Extensions.DependencyInjection.Identi | |
| In the client project's `Register` component (`Components/Identity/Register.razor`), change the message to users on a successful account registration to instruct them to confirm their account. The following example includes a link to trigger Identity on the server to resend the confirmation email. | ||
|
|
||
| ```diff | ||
| - <div class="alert alert-success"> | ||
| - You successfully registered. Now you can <a href="login">login</a>. | ||
| - </div> | ||
| + <div class="alert alert-success"> | ||
| + You successfully registered. You must now confirm your account by clicking | ||
| + the link in the email that was sent to you. After confirming your account, | ||
| + you can <a href="login">login</a> to the app. | ||
| + <a href="resendConfirmationEmail">Resend confirmation email</a> | ||
| + </div> | ||
| - You successfully registered and can <a href="login">login</a> to the app. | ||
| + You successfully registered. You must now confirm your account by clicking | ||
| + the link in the email that was sent to you. After confirming your account, | ||
| + you can <a href="login">login</a> to the app. | ||
| + <a href="resendConfirmationEmail">Resend confirmation email</a> | ||
| ``` | ||
|
|
||
| ## Update seed data code to confirm seeded accounts | ||
|
|
@@ -222,52 +220,33 @@ public Task<FormResult> ResetPasswordAsync(string email, string resetCode, | |
| In the client project, add implementations for the preceding methods in the `CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`): | ||
|
|
||
| ```csharp | ||
| /// <summary> | ||
| /// Begin the password recovery process by issuing a POST request to the | ||
| /// '/forgotPassword' endpoint. | ||
| /// </summary> | ||
| /// <param name="email">The user's email address.</param> | ||
| /// <returns>A <see cref="bool"/> indicating success or failure.</returns> | ||
| public async Task<bool> ForgotPasswordAsync(string email) | ||
| { | ||
| try | ||
| { | ||
| // make the request | ||
| var result = await httpClient.PostAsJsonAsync( | ||
| "forgotPassword", new | ||
| { | ||
| }); | ||
|
|
||
| // successful? | ||
| if (result.IsSuccessStatusCode) | ||
| { | ||
| return true; | ||
| } | ||
| } | ||
| catch { } | ||
|
|
||
| // unknown error | ||
| return false; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reset the user's password by issuing a POST request to the | ||
| /// '/resetPassword' endpoint. | ||
| /// </summary> | ||
| /// <param name="email">The user's email address.</param> | ||
| /// <param name="resetCode">The user's reset code.</param> | ||
| /// <param name="newPassword">The user's new password.</param> | ||
| /// <returns>The result serialized to a <see cref="FormResult"/>. | ||
| /// </returns> | ||
| public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, | ||
| string newPassword) | ||
| { | ||
| string[] defaultDetail = ["An unknown error prevented resetting the password."]; | ||
|
|
||
| try | ||
| { | ||
| // make the request | ||
| var result = await httpClient.PostAsJsonAsync( | ||
| "resetPassword", new | ||
| { | ||
|
|
@@ -276,13 +255,11 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, | |
| newPassword | ||
| }); | ||
|
|
||
| // successful? | ||
| if (result.IsSuccessStatusCode) | ||
| { | ||
| return new FormResult { Succeeded = true }; | ||
| } | ||
|
|
||
| // body should contain details about why it failed | ||
| var details = await result.Content.ReadAsStringAsync(); | ||
| var problemDetails = JsonDocument.Parse(details); | ||
| var errors = new List<string>(); | ||
|
|
@@ -303,7 +280,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, | |
| } | ||
| } | ||
|
|
||
| // return the error list | ||
| return new FormResult | ||
| { | ||
| Succeeded = false, | ||
|
|
@@ -312,7 +288,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, | |
| } | ||
| catch { } | ||
|
|
||
| // unknown error | ||
| return new FormResult | ||
| { | ||
| Succeeded = false, | ||
|
|
@@ -323,9 +298,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, | |
|
|
||
| In the client project, add the following `ForgotPassword` component. | ||
|
|
||
| > [!NOTE] | ||
| > Code lines in the following example are broken across two or more lines to eliminate or reduce horizontal scrolling in this article, but you can place the following code as shown into a test app. The code executes regardless of the artificial line breaks. | ||
|
|
||
| `Components/Identity/ForgotPassword.razor`: | ||
|
|
||
| ```razor | ||
|
|
@@ -344,14 +316,15 @@ In the client project, add the following `ForgotPassword` component. | |
| @if (!passwordResetCodeSent) | ||
| { | ||
| <EditForm Model="Input" FormName="forgot-password" | ||
| OnValidSubmit="OnValidSubmitStep1Async" method="post"> | ||
| OnValidSubmit="OnValidSubmitStep1Async" method="post"> | ||
| <DataAnnotationsValidator /> | ||
| <ValidationSummary class="text-danger" role="alert" /> | ||
|
|
||
| <div class="form-floating mb-3"> | ||
| <InputText @bind-Value="Input.Email" id="Input.Email" | ||
| class="form-control" autocomplete="username" | ||
| aria-required="true" placeholder="[email protected]" /> | ||
| <InputText @bind-Value="Input.Email" | ||
| id="Input.Email" class="form-control" | ||
| autocomplete="username" aria-required="true" | ||
| placeholder="[email protected]" /> | ||
| <label for="Input.Email" class="form-label"> | ||
| </label> | ||
|
|
@@ -389,7 +362,7 @@ In the client project, add the following `ForgotPassword` component. | |
| Obtain the code from the email for this form. | ||
| </div> | ||
| <EditForm Model="Reset" FormName="reset-password" | ||
| OnValidSubmit="OnValidSubmitStep2Async" method="post"> | ||
| OnValidSubmit="OnValidSubmitStep2Async" method="post"> | ||
guardrex marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <DataAnnotationsValidator /> | ||
| <ValidationSummary class="text-danger" role="alert" /> | ||
|
|
||
|
|
@@ -415,7 +388,8 @@ In the client project, add the following `ForgotPassword` component. | |
| class="text-danger" /> | ||
| </div> | ||
| <div class="form-floating mb-3"> | ||
| <InputText type="password" @bind-Value="Reset.ConfirmPassword" | ||
| <InputText type="password" | ||
| @bind-Value="Reset.ConfirmPassword" | ||
| id="Reset.ConfirmPassword" class="form-control" | ||
| autocomplete="new-password" aria-required="true" | ||
| placeholder="password" /> | ||
|
|
@@ -435,8 +409,7 @@ In the client project, add the following `ForgotPassword` component. | |
| </div> | ||
|
|
||
| @code { | ||
| private bool passwordResetCodeSent; | ||
| private bool passwordResetSuccess, errors; | ||
| private bool passwordResetCodeSent, passwordResetSuccess, errors; | ||
| private string[] errorList = []; | ||
|
|
||
| [SupplyParameterFromForm(FormName = "forgot-password")] | ||
|
|
@@ -490,7 +463,7 @@ In the client project, add the following `ForgotPassword` component. | |
| [DataType(DataType.Password)] | ||
| [Display(Name = "Confirm password")] | ||
| [Compare("NewPassword", ErrorMessage = "The new password and confirmation " + | ||
| "password do not match.")] | ||
| "password don't match.")] | ||
| public string ConfirmPassword { get; set; } = string.Empty; | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.