Skip to content

Commit 016a23b

Browse files
authored
Merge pull request #34339 from dotnet/main
2 parents 7ad5efc + 9e42cc7 commit 016a23b

File tree

7 files changed

+964
-122
lines changed

7 files changed

+964
-122
lines changed

aspnetcore/blazor/fundamentals/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Blazor documentation adopts several conventions for showing and discussing compo
6969
* [Component parameter](xref:blazor/components/index#component-parameters) values lead with a [Razor reserved `@` symbol](xref:mvc/views/razor#razor-syntax), but it isn't required. Literals (for example, boolean values), keywords (for example, `this`), and `null` as component parameter values aren't prefixed with `@`, but this is also merely a documentation convention. Your own code can prefix literals with `@` if you wish.
7070
* C# classes use the [`this` keyword](/dotnet/csharp/language-reference/keywords/this) and avoid prefixing fields with an underscore (`_`) that are assigned to in constructors, which differs from the [ASP.NET Core framework engineering guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines).
7171
* In examples that use [primary constructors (C# 12 or later)](/dotnet/csharp/whats-new/tutorials/primary-constructors), primary constructor parameters are typically used directly by class members.
72+
In article examples, code lines are split to reduce horizontal scrolling. These breaks don't affect execution but can be removed when pasting into your project.
7273

7374
Additional information on Razor component syntax is provided in the *Razor syntax* section of <xref:blazor/components/index#razor-syntax>.
7475

aspnetcore/blazor/security/qrcodes-for-authenticator-apps.md

Lines changed: 42 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -11,107 +11,62 @@ uid: blazor/security/qrcodes-for-authenticator-apps
1111

1212
[!INCLUDE[](~/includes/not-latest-version-without-not-supported-content.md)]
1313

14-
This article explains how to configure an ASP.NET Core Blazor Web App with QR code generation for TOTP authenticator apps.
14+
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.
1515

16-
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>.
16+
For an introduction to 2FA with TOTP authenticator apps, see <xref:security/authentication/identity-enable-qrcodes>.
1717

18-
> [!WARNING]
19-
> An ASP.NET Core TOTP code should be kept secret because it can be used to authenticate successfully multiple times before it expires.
20-
21-
## Scaffold the Enable Authenticator component into the app
22-
23-
Follow the guidance in <xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project> to scaffold `Pages\Manage\EnableAuthenticator` into the app.
24-
25-
<!-- UPDATE 9.0 Update NOTE per followup on the issue -->
18+
The guidance in this article relies upon either creating the app with **Individual Accounts** for the new app's **Authentication type** or [scaffolding Identity into an existing app](xref:security/authentication/scaffold-identity#scaffold-identity-into-a-blazor-project). For guidance on using the .NET CLI instead of Visual Studio for scaffolding Identity into an existing app, see <xref:fundamentals/tools/dotnet-aspnet-codegenerator>.
2619

27-
> [!NOTE]
28-
> Although only the `EnableAuthenticator` component is selected for scaffolding in this example, scaffolding currently adds all of the Identity components to the app. Additionally, exceptions may be thrown during the process of scaffolding into the app. If exceptions occur when database migrations occur, stop the app and restart the app on each exception. For more information, see [Scaffolding exceptions for Blazor Web App (`dotnet/Scaffolding` #2694)](https://github.com/dotnet/Scaffolding/issues/2694).
29-
30-
Be patient while migrations are executed. Depending on the speed of the system, it can take up to a minute or two for database migrations to finish.
31-
32-
For more information, see <xref:security/authentication/scaffold-identity>. For guidance on using the .NET CLI instead of Visual Studio, see <xref:fundamentals/tools/dotnet-aspnet-codegenerator>.
20+
> [!WARNING]
21+
> TOTP codes should be kept secret because they can be used to authenticate multiple times before they expire.
3322
3423
## Adding QR codes to the 2FA configuration page
3524

36-
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)).
37-
38-
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.
39-
40-
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):
25+
A QR code generated by the app to set up 2FA with an TOTP authenticator app must be generated by a QR code library.
4126

42-
```razor
43-
<script src="qrcode.min.js"></script>
44-
```
45-
46-
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.
47-
48-
Add the following [JavaScript initializer](xref:blazor/fundamentals/startup#javascript-initializers) to the server project's `wwwroot` folder. The `{NAME}` placeholder must be the name of the app's assembly in order for Blazor to locate and load the file automatically. If the server app's assembly name is `BlazorSample`, the file is named `BlazorSample.lib.module.js`.
49-
50-
`wwwroot/{NAME}.lib.module.js`:
27+
The guidance in this article uses [`manuelbl/QrCodeGenerator`](https://github.com/manuelbl/QrCodeGenerator), but you can use any QR code generation library.
5128

52-
[!INCLUDE[](~/blazor/includes/js-interop/blazor-page-script.md)]
29+
Add a package reference for the [`Net.Codecrete.QrCodeGenerator`](https://www.nuget.org/packages/Net.Codecrete.QrCodeGenerator) NuGet package.
5330

54-
Add the following shared `PageScript` component to the server app.
31+
[!INCLUDE[](~/includes/package-reference.md)]
5532

56-
`Components/PageScript.razor`:
33+
Open the `EnableAuthenticator` component in the `Components/Account/Pages/Manage` folder. At the top of the file under the `@page` directive, add an `@using` directive for the QrCodeGenerator namespace:
5734

5835
```razor
59-
<page-script src="@Src"></page-script>
60-
61-
@code {
62-
[Parameter]
63-
[EditorRequired]
64-
public string Src { get; set; } = default!;
65-
}
66-
```
67-
68-
Add the following [collocated JS file](xref:blazor/js-interop/javascript-location#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component) for the `EnableAuthenticator` component, which is located at `Components/Account/Pages/Manage/EnableAuthenticator.razor`. The `onLoad` function creates the QR code with Sangmin's `qrcode.js` library using the QR code URI produced by the `GenerateQrCodeUri` method in the component's `@code` block.
69-
70-
`Components/Account/Pages/Manage/EnableAuthenticator.razor.js`:
71-
72-
```javascript
73-
export function onLoad() {
74-
const uri = document.getElementById('qrCodeData').getAttribute('data-url');
75-
new QRCode(document.getElementById('qrCode'), uri);
76-
}
36+
@using Net.Codecrete.QrCodeGenerator
7737
```
7838

79-
Under the `<PageTitle>` component in the `EnableAuthenticator` component, add the `PageScript` component with the path to the collocated JS file:
80-
81-
```razor
82-
<PageScript Src="./Components/Account/Pages/Manage/EnableAuthenticator.razor.js" />
83-
```
84-
85-
> [!NOTE]
86-
> An alternative to using the approach with the `PageScript` component is to use an event listener (`blazor.addEventListener("enhancedload", {CALLBACK})`) registered in an [`afterWebStarted` JS initializer](xref:blazor/fundamentals/startup#javascript-initializers) to listen for page updates caused by enhanced navigation. The callback (`{CALLBACK}` placeholder) performs the QR code initialization logic.
87-
>
88-
> Using the callback approach with `enhancedload`, the code executes for every enhanced navigation, even when the QR code `<div>` isn't rendered. Therefore, additional code must be added to check for the presence of the `<div>` before executing the code that adds a QR code.
89-
90-
91-
Delete the `<div>` element that contains the QR code instructions:
39+
Delete the `<div>` element that contains the QR code instructions and the two `<div>` elements where the QR code should appear and where the QR code data is stored in the page:
9240

9341
```diff
9442
- <div class="alert alert-info">
9543
- Learn how to <a href="https://go.microsoft.com/fwlink/?Linkid=852423">enable
9644
- QR code generation</a>.
9745
- </div>
46+
- <div></div>
47+
- <div data-url="@authenticatorUri"></div>
9848
```
9949

100-
Locate the two `<div>` elements where the QR code should appear and where the QR code data is stored in the page.
50+
Replace the deleted elements with the following markup:
10151

102-
Make the following changes:
52+
```razor
53+
<div>
54+
<svg xmlns="http://www.w3.org/2000/svg" height="300" width="300" stroke="none"
55+
version="1.1" viewBox="0 0 50 50">
56+
<rect width="300" height="300" fill="#ffffff" />
57+
<path d="@svgGraphicsPath" fill="#000000" />
58+
</svg>
59+
</div>
60+
```
10361

104-
* For the empty `<div>`, give the element an `id` of `qrCode`.
105-
* For the `<div>` with the `data-url` attribute, give the element an `id` of `qrCodeData`.
62+
Just inside the opening `@code` block, change the variable declaration for `authenticatorUri` to `svgGraphicsPath`:
10663

10764
```diff
108-
- <div></div>
109-
- <div data-url="@authenticatorUri"></div>
110-
+ <div id="qrCode"></div>
111-
+ <div id="qrCodeData" data-url="@authenticatorUri"></div>
65+
- private string? authenticatorUri;
66+
+ private string? svgGraphicsPath;
11267
```
11368

114-
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.
69+
Change the site name in the `GenerateQrCodeUri` method. 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.
11570

11671
In the following example, the default value `Microsoft.AspNetCore.Identity.UI` is changed to the company name `Weyland-Yutani Corporation` (&copy;1986 20th Century Studios [*Aliens*](https://www.20thcenturystudios.com/movies/aliens)).
11772

@@ -122,6 +77,20 @@ In the `GenerateQrCodeUri` method:
12277
+ UrlEncoder.Encode("Weyland-Yutani Corporation"),
12378
```
12479

80+
At the bottom of the `LoadSharedKeyAndQrCodeUriAsync` method, add the [`var` keyword](/dotnet/csharp/programming-guide/classes-and-structs/implicitly-typed-local-variables) to the line that sets `authenticatorUri`, making it an implicitly-typed local variable:
81+
82+
```diff
83+
- authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
84+
+ var authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!);
85+
```
86+
87+
Add the following lines of code at the bottom of the `LoadSharedKeyAndQrCodeUriAsync` method:
88+
89+
```csharp
90+
var qr = QrCode.EncodeText(authenticatorUri, QrCode.Ecc.Medium);
91+
svgGraphicsPath = qr.ToGraphicsPath();
92+
```
93+
12594
Run the app and ensure that the QR code is scannable and that the code validates.
12695

12796
> [!WARNING]

aspnetcore/blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery.md

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ author: guardrex
44
description: Learn how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery.
55
ms.author: riande
66
monikerRange: '>= aspnetcore-8.0'
7-
ms.date: 10/31/2024
7+
ms.date: 11/21/2024
88
uid: blazor/security/webassembly/standalone-with-identity/account-confirmation-and-password-recovery
99
---
1010
# Account confirmation and password recovery in ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity
1111

12+
[!INCLUDE[](~/includes/not-latest-version-without-not-supported-content.md)]
13+
1214
This article explains how to configure an ASP.NET Core Blazor WebAssembly app with ASP.NET Core Identity with email confirmation and password recovery.
1315

1416
> [!NOTE]
1517
> 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>.
1618
17-
## Namespace
19+
## Namespaces and article code examples
1820

1921
The namespaces used by the examples in this article are:
2022

@@ -156,15 +158,11 @@ Locate the line that calls <xref:Microsoft.Extensions.DependencyInjection.Identi
156158
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.
157159

158160
```diff
159-
- <div class="alert alert-success">
160-
- You successfully registered. Now you can <a href="login">login</a>.
161-
- </div>
162-
+ <div class="alert alert-success">
163-
+ You successfully registered. You must now confirm your account by clicking
164-
+ the link in the email that was sent to you. After confirming your account,
165-
+ you can <a href="login">login</a> to the app.
166-
+ <a href="resendConfirmationEmail">Resend confirmation email</a>
167-
+ </div>
161+
- You successfully registered and can <a href="login">login</a> to the app.
162+
+ You successfully registered. You must now confirm your account by clicking
163+
+ the link in the email that was sent to you. After confirming your account,
164+
+ you can <a href="login">login</a> to the app.
165+
+ <a href="resendConfirmationEmail">Resend confirmation email</a>
168166
```
169167

170168
## Update seed data code to confirm seeded accounts
@@ -222,52 +220,33 @@ public Task<FormResult> ResetPasswordAsync(string email, string resetCode,
222220
In the client project, add implementations for the preceding methods in the `CookieAuthenticationStateProvider` class (`Identity/CookieAuthenticationStateProvider.cs`):
223221

224222
```csharp
225-
/// <summary>
226-
/// Begin the password recovery process by issuing a POST request to the
227-
/// '/forgotPassword' endpoint.
228-
/// </summary>
229-
/// <param name="email">The user's email address.</param>
230-
/// <returns>A <see cref="bool"/> indicating success or failure.</returns>
231223
public async Task<bool> ForgotPasswordAsync(string email)
232224
{
233225
try
234226
{
235-
// make the request
236227
var result = await httpClient.PostAsJsonAsync(
237228
"forgotPassword", new
238229
{
239230
email
240231
});
241232

242-
// successful?
243233
if (result.IsSuccessStatusCode)
244234
{
245235
return true;
246236
}
247237
}
248238
catch { }
249239

250-
// unknown error
251240
return false;
252241
}
253242

254-
/// <summary>
255-
/// Reset the user's password by issuing a POST request to the
256-
/// '/resetPassword' endpoint.
257-
/// </summary>
258-
/// <param name="email">The user's email address.</param>
259-
/// <param name="resetCode">The user's reset code.</param>
260-
/// <param name="newPassword">The user's new password.</param>
261-
/// <returns>The result serialized to a <see cref="FormResult"/>.
262-
/// </returns>
263243
public async Task<FormResult> ResetPasswordAsync(string email, string resetCode,
264244
string newPassword)
265245
{
266246
string[] defaultDetail = ["An unknown error prevented resetting the password."];
267247

268248
try
269249
{
270-
// make the request
271250
var result = await httpClient.PostAsJsonAsync(
272251
"resetPassword", new
273252
{
@@ -276,13 +255,11 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode,
276255
newPassword
277256
});
278257

279-
// successful?
280258
if (result.IsSuccessStatusCode)
281259
{
282260
return new FormResult { Succeeded = true };
283261
}
284262

285-
// body should contain details about why it failed
286263
var details = await result.Content.ReadAsStringAsync();
287264
var problemDetails = JsonDocument.Parse(details);
288265
var errors = new List<string>();
@@ -303,7 +280,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode,
303280
}
304281
}
305282

306-
// return the error list
307283
return new FormResult
308284
{
309285
Succeeded = false,
@@ -312,7 +288,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode,
312288
}
313289
catch { }
314290

315-
// unknown error
316291
return new FormResult
317292
{
318293
Succeeded = false,
@@ -323,9 +298,6 @@ public async Task<FormResult> ResetPasswordAsync(string email, string resetCode,
323298

324299
In the client project, add the following `ForgotPassword` component.
325300

326-
> [!NOTE]
327-
> 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.
328-
329301
`Components/Identity/ForgotPassword.razor`:
330302

331303
```razor
@@ -344,14 +316,15 @@ In the client project, add the following `ForgotPassword` component.
344316
@if (!passwordResetCodeSent)
345317
{
346318
<EditForm Model="Input" FormName="forgot-password"
347-
OnValidSubmit="OnValidSubmitStep1Async" method="post">
319+
OnValidSubmit="OnValidSubmitStep1Async" method="post">
348320
<DataAnnotationsValidator />
349321
<ValidationSummary class="text-danger" role="alert" />
350322
351323
<div class="form-floating mb-3">
352-
<InputText @bind-Value="Input.Email" id="Input.Email"
353-
class="form-control" autocomplete="username"
354-
aria-required="true" placeholder="[email protected]" />
324+
<InputText @bind-Value="Input.Email"
325+
id="Input.Email" class="form-control"
326+
autocomplete="username" aria-required="true"
327+
placeholder="[email protected]" />
355328
<label for="Input.Email" class="form-label">
356329
Email
357330
</label>
@@ -415,7 +388,8 @@ In the client project, add the following `ForgotPassword` component.
415388
class="text-danger" />
416389
</div>
417390
<div class="form-floating mb-3">
418-
<InputText type="password" @bind-Value="Reset.ConfirmPassword"
391+
<InputText type="password"
392+
@bind-Value="Reset.ConfirmPassword"
419393
id="Reset.ConfirmPassword" class="form-control"
420394
autocomplete="new-password" aria-required="true"
421395
placeholder="password" />
@@ -435,8 +409,7 @@ In the client project, add the following `ForgotPassword` component.
435409
</div>
436410
437411
@code {
438-
private bool passwordResetCodeSent;
439-
private bool passwordResetSuccess, errors;
412+
private bool passwordResetCodeSent, passwordResetSuccess, errors;
440413
private string[] errorList = [];
441414
442415
[SupplyParameterFromForm(FormName = "forgot-password")]
@@ -490,7 +463,7 @@ In the client project, add the following `ForgotPassword` component.
490463
[DataType(DataType.Password)]
491464
[Display(Name = "Confirm password")]
492465
[Compare("NewPassword", ErrorMessage = "The new password and confirmation " +
493-
"password do not match.")]
466+
"password don't match.")]
494467
public string ConfirmPassword { get; set; } = string.Empty;
495468
}
496469
}

0 commit comments

Comments
 (0)