Freigeben über


Konto-Bestätigung und Passwort-Wiederherstellung in ASP.NET Core Blazor WebAssembly mit ASP.NET Core Identity

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 9-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Veröffentlichung erheblich geändert werden kann. Microsoft übernimmt keine Gewährleistungen, ausdrücklich oder konkludent in Bezug auf die hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 9-Version dieses Artikels.

Dieser Artikel erklärt, wie eine ASP.NET Core Blazor WebAssembly-App mit ASP.NET Core Identity mit E-Mail-Bestätigung und Passwortwiederherstellung konfiguriert wird.

Hinweis

Dieser Artikel gilt nur für eigenständige Blazor WebAssembly Apps mit ASP.NET Core Identity. Um die E-Mail-Bestätigung und die Passwortwiederherstellung für Blazor Web Apps zu implementieren, siehe Kontobestätigung und Passwortwiederherstellung in ASP.NET Core Blazor.

Namespaces und Code-Beispiele für Artikel

Die in diesem Artikel verwendeten Namespaces sind:

  • Backend für das Backend-Server-Web-API-Projekt („Serverprojekt“ in diesem Artikel).
  • BlazorWasmAuth für die Front-End-Client-eigenständige Blazor WebAssembly App („Clientprojekt“ in diesem Artikel).

Diese Namespaces entsprechen den Projekten in der BlazorWebAssemblyStandaloneWithIdentityBeispiellösung im dotnet/blazor-samplesGitHub Repository. Weitere Informationen finden Sie unter Sichern Sie ASP.NET-Core Blazor WebAssembly mit ASP.NET-Core Identity.

Wenn Sie die BlazorWebAssemblyStandaloneWithIdentity Beispiel-Lösung nicht verwenden, ändern Sie die Namespaces in den Codebeispielen, um die Namespaces Ihrer Projekte zu verwenden.

Wählen Sie einen E-Mail-Anbieter für das Serverprojekt aus und konfigurieren Sie ihn

In diesem Artikel wird die Transactional API von Mailchimp über Mandrill.net zum Senden von E-Mails verwendet. Es wird empfohlen, einen E-Mail-Dienst statt SMTP zum Senden von E-Mails zu verwenden. SMTP ist schwer zu konfigurieren und zu schützen. Je nachdem, welchen E-Mail-Dienst Sie verwenden, befolgen Sie dessen Anleitungen für .NET-Apps, erstellen ein Konto, konfigurieren einen API-Schlüssel für den Dienst und installieren alle erforderlichen NuGet-Pakete.

Erstellen Sie im Serverprojekt eine Klasse, um den geheimen API-Schlüssel des E-Mail-Anbieters zu speichern. Das Beispiel in diesem Artikel verwendet eine Klasse namens AuthMessageSenderOptions mit einer EmailAuthKey-Eigenschaft, um den Schlüssel zu halten.

AuthMessageSenderOptions.cs:

namespace Backend;

public class AuthMessageSenderOptions
{
    public string? EmailAuthKey { get; set; }
}

Registrieren Sie die AuthMessageSenderOptions-Instanz der Konfiguration in der Program-Datei des Serverprojekts:

builder.Services.Configure<AuthMessageSenderOptions>(builder.Configuration);

Konfigurieren eines geheimen Schlüssels für den Sicherheitsschlüssel des E-Mail-Anbieters

Empfangen Sie den Sicherheitsschlüssel des E-Mail-Anbieters vom Anbieter, und verwenden Sie ihn in den folgenden Anleitungen.

Verwenden Sie entweder oder beide der folgenden Ansätze, um den geheimen Schlüssel für die App zu liefern:

  • Secret Manager-Tool: Das Tool "Geheimer Manager" speichert private Daten auf dem lokalen Computer und wird nur während der lokalen Entwicklung verwendet.
  • Azure Key Vault: Sie können den geheimen Schlüssel in einem Schlüsseltresor speichern, um sie in jeder Umgebung zu verwenden, einschließlich für die Entwicklungsumgebung, wenn Sie lokal arbeiten. Einige Entwickler*innen ziehen es vor, für Staging- und Produktionsbereitstellungen Schlüsseltresore und für die lokale Entwicklung den Geheimnis-Manager zu verwenden.

Wir empfehlen dringend, Geheimnisse nicht im Projektcode oder in Konfigurationsdateien zu speichern. Verwenden Sie sichere Authentifizierungsflows, wie z. B. einen der in diesem Abschnitt beschriebenen Ansätze oder beide davon.

Geheimer Manager-Tool

Wenn das Serverprojekt bereits für das Secret Manager-Tool initialisiert wurde, hat es bereits eine App-Geheimniskennung (<AppSecretsId>) in seiner Projektdatei (.csproj). In Visual Studio können Sie feststellen, ob die App-Geheimnisse-ID vorhanden ist, indem Sie im Eigenschaften-Panel nachsehen, wenn das Projekt im Projektmappen-Explorer ausgewählt ist. Wenn die App nicht initialisiert wurde, führen Sie den folgenden Befehl in einer Kommandozeile aus, die im Verzeichnis des Serverprojekts geöffnet ist. In Visual Studio können Sie die Developer PowerShell-Eingabeaufforderung verwenden (verwenden Sie den cd Befehl, um das Verzeichnis zum Serverprojekt zu ändern, nachdem Sie die Befehlszeile geöffnet haben).

dotnet user-secrets init

Richten Sie den API-Schlüssel mit dem Secret Manager-Tool ein. Im folgenden Beispiel ist der Schlüsselname EmailAuthKey, um AuthMessageSenderOptions.EmailAuthKey zu entsprechen, und der Schlüssel wird durch den {KEY} Platzhalter dargestellt. Führen Sie den folgenden Befehl mit dem API-Schlüssel aus:

dotnet user-secrets set "EmailAuthKey" "{KEY}"

Wenn Sie Visual Studio verwenden, können Sie bestätigen, dass das Geheimnis festgelegt ist, indem Sie mit der rechten Maustaste auf das Serverprojekt im Lösungsexplorer klicken und Benutzergeheimnisse verwalten auswählen.

Weitere Informationen finden Sie unter Sichere Speicherung von App-Geheimnissen bei der Entwicklung in ASP.NET Core.

Warnung

Speichern Sie keine App-Geheimnisse, Verbindungszeichenfolgen, Anmeldeinformationen, Kennwörter, Geheimzahlen (Personal Identication Numbers, PINs), privaten C#-/.NET-Code oder private Schlüssel/Token im clientseitigen Code. Dies ist immer unsicher. In Test- oder Stagingumgebungen und Produktionsumgebungen sollten serverseitiger Blazor-Code und Web-APIs sichere Authentifizierungsflows verwenden, die die Verwaltung von Anmeldeinformationen innerhalb von Projektcode oder Konfigurationsdateien vermeiden. Außerhalb der lokalen Entwicklungstests wird empfohlen, die Verwendung von Umgebungsvariablen zum Speichern vertraulicher Daten zu vermeiden, da Umgebungsvariablen nicht der sicherste Ansatz sind. Für lokale Entwicklungstests wird der Geheimnis-Manager zum Schützen vertraulicher Daten empfohlen. Weitere Informationen finden Sie unter Sichere Verwaltung von vertraulichen Daten und Anmeldeinformationen.

Azure Key Vault (ein Dienst zur sicheren Verwaltung kryptografischer Schlüssel)

Azure Key Vault bietet einen sicheren Ansatz, um das Client-Geheimnis der App für die App bereitzustellen.

Informationen zum Erstellen eines Schlüsseltresors und zum Festlegen eines Geheimnisses finden Sie unter Informationen zu Azure Key Vault-Geheimnissen (Azure-Dokumentation) sowie in den darin aufgeführten Ressourcen für den Einstieg in Azure Key Vault. Zum Implementieren des in diesem Abschnitt aufgeführten Codes notieren Sie sich die URI des Schlüsseltresors und den geheimen Namen, wenn Sie in Azure den Schlüsseltresor und das Geheimnis erstellen. Wenn Sie die Zugriffsrichtlinie für den geheimen Schlüssel im Zugriffsrichtlinienbereich festlegen:

  • Es ist nur die Berechtigung Get "Geheim" erforderlich.
  • Wählen Sie die Anwendung als Prinzipal für den geheimen Schlüssel aus.

Bestätigen Sie im Azure- oder Entra-Portal, dass der App Zugriff auf den geheimen Schlüssel gewährt wurde, den Sie für den E-Mail-Anbieterschlüssel erstellt haben.

Wichtig

Ein Schlüsseltresorgeheimnis wird mit einem Ablaufdatum erstellt. Achten Sie darauf, nachzuverfolgen, wann ein Schlüsseltresorgeheimnis abläuft, und erstellen Sie vor dem Ablauf dieses Datums ein neues Geheimnis für die App.

Wenn Microsoft-Pakete Identity noch nicht Teil der Paketregistrierungen der App sind, fügen Sie dem Serverprojekt für Azure Identity und Azure Key Vault die folgenden Pakete hinzu. Diese Pakete werden transitiv von Microsoft Identity Web-Paketen bereitgestellt, daher müssen Sie sie nur hinzufügen, wenn die App nicht auf Microsoft.Identity.Web verweist:

Fügen Sie dem Serverprojekt die folgende AzureHelper Klasse hinzu. Die GetKeyVaultSecret-Methode ruft einen geheimen Schlüssel aus einem Schlüsseltresor ab. Passen Sie den Namespace (BlazorSample.Helpers) an Ihr Projekt-Namespace-Schema an.

Helpers/AzureHelper.cs:

using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

namespace BlazorSample.Helpers;

public static class AzureHelper
{
    public static string GetKeyVaultSecret(string tenantId, string vaultUri, string secretName)
    {
        DefaultAzureCredentialOptions options = new()
        {
            // Specify the tenant ID to use the dev credentials when running the app locally
            // in Visual Studio.
            VisualStudioTenantId = tenantId,
            SharedTokenCacheTenantId = tenantId
        };

        var client = new SecretClient(new Uri(vaultUri), new DefaultAzureCredential(options));
        var secret = client.GetSecretAsync(secretName).Result;

        return secret.Value.Value;
    }
}

Hinweis

Im vorherigen Beispiel wird DefaultAzureCredential verwendet, um die Authentifizierung zu vereinfachen, indem Anmeldeinformationen in Azure-Hostingumgebungen mit Anmeldeinformationen kombiniert werden, die in der lokalen Entwicklung genutzt werden, während Apps entwickelt werden, die für Azure bereitgestellt werden. Beim Wechsel in die Produktion ist eine Alternative die bessere Wahl, wie zum Beispiel ManagedIdentityCredential. Weitere Informationen finden Sie unter Authentifizieren von von Azure gehosteten .NET-Apps für Azure-Ressourcen mithilfe einer vom System zugewiesenen verwalteten Identität.

Wenn Dienste in der Program-Datei des Serverprojekts registriert sind, abrufen und binden Sie das Geheimnis mit der Optionskonfiguration:

var tenantId = builder.Configuration.GetValue<string>("AzureAd:TenantId")!;
var vaultUri = builder.Configuration.GetValue<string>("AzureAd:VaultUri")!;

var emailAuthKey = AzureHelper.GetKeyVaultSecret(
    tenantId, vaultUri, "EmailAuthKey");

var authMessageSenderOptions = 
    new AuthMessageSenderOptions() { EmailAuthKey = emailAuthKey };
builder.Configuration.GetSection(authMessageSenderOptions.EmailAuthKey)
    .Bind(authMessageSenderOptions);

Wenn Sie die Umgebung steuern möchten, in der der vorangehende Code ausgeführt wird, z. B. um die lokale Ausführung des Codes zu vermeiden, da Sie sich entschieden haben, das tool Geheimen Manager für die lokale Entwicklung zu verwenden, können Sie den vorherigen Code in eine bedingte Anweisung einschließen, die die Umgebung überprüft:

if (!context.HostingEnvironment.IsDevelopment())
{
    ...
}

Fügen Sie im AzureAd Abschnitt, den Sie möglicherweise hinzufügen müssen, wenn er noch nicht vorhanden ist, im appsettings.json-Serverprojekt die folgenden TenantId-Konfigurationsschlüssel und VaultUri-werte hinzu, sofern sie noch nicht vorhanden sind.

"AzureAd": {
  "TenantId": "{TENANT ID}",
  "VaultUri": "{VAULT URI}"
}

Im vorherigen Beispiel:

  • Der Platzhalter {TENANT ID} ist die Mandanten-ID der App in Azure.
  • Der {VAULT URI}-Platzhalter ist der Schlüsseltresor-URI. Fügen Sie den nachgestellten Schrägstrich in die URI ein.

Beispiel:

"TenantId": "00001111-aaaa-2222-bbbb-3333cccc4444",
"VaultUri": "https://contoso.vault.azure.net/"

Die Konfiguration wird verwendet, um die Bereitstellung dedizierter Schlüsseltresore und geheimer Namensdaten basierend auf den Umgebungskonfigurationsdateien der App zu erleichtern. Sie können z. B. unterschiedliche Konfigurationswerte für appsettings.Development.json in der Entwicklung, appsettings.Staging.json beim Staging und appsettings.Production.json für die Produktionsbereitstellung bereitstellen. Weitere Informationen finden Sie unter ASP.NET Core-KonfigurationBlazor.

Implementieren Sie IEmailSender im Serverprojekt

Das folgende Beispiel basiert auf der Transactional API von Mailchimp unter Verwendung von Mandrill.net. Für einen anderen Anbieter lesen Sie in deren Dokumentation nach, wie Sie das Senden einer E-Mail-Nachricht implementieren.

Fügen Sie das Mandrill.net NuGet-Paket zum Serverprojekt hinzu.

Fügen Sie die folgende EmailSender Klasse hinzu, um IEmailSender<TUser> zu implementieren. Im folgenden Beispiel ist AppUser ein IdentityUser. Das HTML-Markup der Nachricht kann weiter angepasst werden. Solange das message an MandrillMessage übergebene Argument mit dem < Zeichen beginnt, geht die Mandrill.net API davon aus, dass der Nachrichtenkörper in HTML verfasst ist.

EmailSender.cs:

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Mandrill;
using Mandrill.Model;

namespace Backend;

public class EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor,
    ILogger<EmailSender> logger) : IEmailSender<AppUser>
{
    private readonly ILogger logger = logger;

    public AuthMessageSenderOptions Options { get; } = optionsAccessor.Value;

    public Task SendConfirmationLinkAsync(AppUser user, string email,
        string confirmationLink) => SendEmailAsync(email, "Confirm your email",
        "<html lang=\"en\"><head></head><body>Please confirm your account by " +
        $"<a href='{confirmationLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetLinkAsync(AppUser user, string email,
        string resetLink) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password by " +
        $"<a href='{resetLink}'>clicking here</a>.</body></html>");

    public Task SendPasswordResetCodeAsync(AppUser user, string email,
        string resetCode) => SendEmailAsync(email, "Reset your password",
        "<html lang=\"en\"><head></head><body>Please reset your password " +
        $"using the following code:<br>{resetCode}</body></html>");

    public async Task SendEmailAsync(string toEmail, string subject, string message)
    {
        if (string.IsNullOrEmpty(Options.EmailAuthKey))
        {
            throw new Exception("Null EmailAuthKey");
        }

        await Execute(Options.EmailAuthKey, subject, message, toEmail);
    }

    public async Task Execute(string apiKey, string subject, string message, 
        string toEmail)
    {
        var api = new MandrillApi(apiKey);
        var mandrillMessage = new MandrillMessage("sarah@contoso.com", toEmail, 
            subject, message);
        await api.Messages.SendAsync(mandrillMessage);

        logger.LogInformation("Email to {EmailAddress} sent!", toEmail);
    }
}

Hinweis

Textkörperinhalte für Nachrichten erfordern möglicherweise eine spezielle Codierung für den E-Mail-Dienstanbieter. Wenn Links im Nachrichtentext in der E-Mail-Nachricht nicht verfolgt werden können, konsultieren Sie die Dokumentation des Dienstanbieters, um das Problem zu beheben.

Fügen Sie die folgende IEmailSender<TUser> Dienstregistrierung zur Program Datei des Serverprojekts hinzu:

builder.Services.AddTransient<IEmailSender<AppUser>, EmailSender>();

Konfigurieren Sie das Serverprojekt so, dass eine E-Mail-Bestätigung erforderlich ist

In der Datei Program des Serverprojekts ist eine bestätigte E-Mail-Adresse erforderlich, um sich bei der App anzumelden.

Suchen Sie die Zeile, die AddIdentityCore aufruft, und setzen Sie die RequireConfirmedEmail-Eigenschaft auf true.

- builder.Services.AddIdentityCore<AppUser>()
+ builder.Services.AddIdentityCore<AppUser>(o => o.SignIn.RequireConfirmedEmail = true)

Aktualisieren Sie die Antwort auf die Kontoanmeldung des Kundenprojekts

Im Register-Komponente (Components/Identity/Register.razor) des Kundenprojekts ändern Sie die Nachricht an die Benutzer bei einer erfolgreichen Kontoanmeldung, um sie anzuweisen, ihr Konto zu bestätigen. Das folgende Beispiel enthält eine Verknüpfung, um den Identity auf dem Server auszulösen, um die Bestätigungs-E-Mail erneut zu senden.

- 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>

Aktualisieren Sie den Seed-Daten-Code, um die Seed-Konten zu bestätigen

In der Seed-Datenklasse des Serverprojekts (SeedData.cs) ändern Sie den Code in der InitializeAsync-Methode, um die vorgegebenen Konten zu bestätigen. Dadurch wird vermieden, dass bei jedem Testlauf der Lösung mit einem der Konten eine E-Mail-Adressbestätigung erforderlich ist:

- if (appUser is not null && user.RoleList is not null)
- {
-     await userManager.AddToRolesAsync(appUser, user.RoleList);
- }
+ if (appUser is not null)
+ {
+     if (user.RoleList is not null)
+     {
+         await userManager.AddToRolesAsync(appUser, user.RoleList);
+     }
+ 
+     var token = await userManager.GenerateEmailConfirmationTokenAsync(appUser);
+     await userManager.ConfirmEmailAsync(appUser, token);
+ }

Aktivieren der Kontobestätigung, nachdem eine Website Benutzer*innen enthält

Durch Aktivieren der Kontobestätigung auf einer Website mit Benutzer*innen werden alle vorhandenen Benutzer*innen gesperrt. Vorhandene Benutzer werden gesperrt, da ihre Konten nicht bestätigt wurden. Verwenden Sie einen der folgenden Ansätze, die über den Rahmen dieses Artikels hinausgehen:

  • Aktualisieren Sie die Datenbank, um alle vorhandenen Benutzer*innen als bestätigt zu kennzeichnen.
  • Batch-E-Mails mit Bestätigungslinks an alle bestehenden Benutzer senden, wobei jeder Benutzer sein eigenes Konto bestätigen muss.

Kennwortwiederherstellung

Die Passwortwiederherstellung erfordert, dass die Server-App einen E-Mail-Anbieter verwendet, um Benutzern Codes zum Zurücksetzen des Passworts zu senden. Daher folgen Sie der Anleitung weiter oben in diesem Artikel, um einen E-Mail-Anbieter auszuwählen:

Kennwortwiederherstellung ist ein zweistufiger Prozess:

  1. Eine POST-Anforderung wird an den /forgotPassword Endpunkt gesendet, der von MapIdentityApi im Serverprojekt bereitgestellt wird. Eine Nachricht in der Benutzeroberfläche weist den Benutzer an, seine E-Mails auf einen Rücksetzungscode zu überprüfen.
  2. Eine POST-Anfrage wird an den /resetPassword Endpunkt des Serverprojekts mit der E-Mail-Adresse des Benutzers, dem Passwort-Zurücksetzungscode und dem neuen Passwort gesendet.

Die vorhergehenden Schritte werden durch den folgenden Implementierungsleitfaden für die Stichprobenlösung veranschaulicht.

Im Client-Projekt fügen Sie die folgenden Methodensignaturen zur IAccountManagement Klasse (Identity/IAccountManagement.cs) hinzu:

public Task<bool> ForgotPasswordAsync(string email);

public Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword);

Im Clientprojekt Implementierungen für die vorhergehenden Methoden in der CookieAuthenticationStateProvider Klasse (Identity/CookieAuthenticationStateProvider.cs) hinzufügen:

public async Task<bool> ForgotPasswordAsync(string email)
{
    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "forgotPassword", new
            {
                email
            });

        if (result.IsSuccessStatusCode)
        {
            return true;
        }
    }
    catch { }

    return false;
}

public async Task<FormResult> ResetPasswordAsync(string email, string resetCode, 
    string newPassword)
{
    string[] defaultDetail = [ "An unknown error prevented password reset." ];

    try
    {
        using var result = await httpClient.PostAsJsonAsync(
            "resetPassword", new
            {
                email,
                resetCode,
                newPassword
            });

        if (result.IsSuccessStatusCode)
        {
            return new FormResult { Succeeded = true };
        }

        var details = await result.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 new FormResult
        {
            Succeeded = false,
            ErrorList = problemDetails == null ? defaultDetail : [.. errors]
        };
    }
    catch { }

    return new FormResult
    {
        Succeeded = false,
        ErrorList = defaultDetail
    };
}

Fügen Sie im Kundenprojekt die folgende ForgotPassword Komponente hinzu.

Components/Identity/ForgotPassword.razor:

@page "/forgot-password"
@using System.ComponentModel.DataAnnotations
@using BlazorWasmAuth.Identity
@inject IAccountManagement Acct

<PageTitle>Forgot your password?</PageTitle>

<h1>Forgot your password?</h1>
<p>Provide your email address and select the <b>Reset password</b> button.</p>
<hr />
<div class="row">
    <div class="col-md-4">
        @if (!passwordResetCodeSent)
        {
            <EditForm Model="Input" FormName="forgot-password" 
                      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="name@example.com" />
                    <label for="Input.Email" class="form-label">
                        Email
                    </label>
                    <ValidationMessage For="() => Input.Email" 
                        class="text-danger" />
                </div>
                <button type="submit" class="w-100 btn btn-lg btn-primary">
                    Request reset code
                </button>
            </EditForm>
        }
        else
        {
            if (passwordResetSuccess)
            {
                if (errors)
                {
                    foreach (var error in errorList)
                    {
                        <div class="alert alert-danger">@error</div>
                    }
                }
                else
                {
                    <div>
                        Your password was reset. You may <a href="login">login</a> 
                        to the app with your new password.
                    </div>
                }
            }
            else
            {
                <div>
                    A password reset code has been sent to your email address. 
                    Obtain the code from the email for this form.
                </div>
                <EditForm Model="Reset" FormName="reset-password" 
                    OnValidSubmit="OnValidSubmitStep2Async" method="post">
                    <DataAnnotationsValidator />
                    <ValidationSummary class="text-danger" role="alert" />

                    <div class="form-floating mb-3">
                        <InputText @bind-Value="Reset.ResetCode" 
                            id="Reset.ResetCode" class="form-control" 
                            autocomplete="username" aria-required="true" />
                        <label for="Reset.ResetCode" class="form-label">
                            Reset code
                        </label>
                        <ValidationMessage For="() => Reset.ResetCode" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" @bind-Value="Reset.NewPassword" 
                            id="Reset.NewPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.NewPassword" class="form-label">
                            New Password
                        </label>
                        <ValidationMessage For="() => Reset.NewPassword" 
                            class="text-danger" />
                    </div>
                    <div class="form-floating mb-3">
                        <InputText type="password" 
                            @bind-Value="Reset.ConfirmPassword" 
                            id="Reset.ConfirmPassword" class="form-control" 
                            autocomplete="new-password" aria-required="true" 
                            placeholder="password" />
                        <label for="Reset.ConfirmPassword" class="form-label">
                            Confirm Password
                        </label>
                        <ValidationMessage For="() => Reset.ConfirmPassword" 
                            class="text-danger" />
                    </div>
                    <button type="submit" class="w-100 btn btn-lg btn-primary">
                        Reset password
                    </button>
                </EditForm>
            }
        }
    </div>
</div>

@code {
    private bool passwordResetCodeSent, passwordResetSuccess, errors;
    private string[] errorList = [];

    [SupplyParameterFromForm(FormName = "forgot-password")]
    private InputModel Input { get; set; } = new();

    [SupplyParameterFromForm(FormName = "reset-password")]
    private ResetModel Reset { get; set; } = new();

    private async Task OnValidSubmitStep1Async()
    {
        passwordResetCodeSent = await Acct.ForgotPasswordAsync(Input.Email);
    }

    private async Task OnValidSubmitStep2Async()
    {
        var result = await Acct.ResetPasswordAsync(Input.Email, Reset.ResetCode, 
            Reset.NewPassword);

        if (result.Succeeded)
        {
            passwordResetSuccess = true;

        }
        else
        {
            errors = true;
            errorList = result.ErrorList;
        }
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; } = string.Empty;
    }

    private sealed class ResetModel
    {
        [Required]
        [Base64String]
        public string ResetCode { get; set; } = string.Empty;

        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at " +
            "max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string NewPassword { get; set; } = string.Empty;

        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation " +
            "password don't match.")]
        public string ConfirmPassword { get; set; } = string.Empty;
    }
}

Im Login-Komponente (Components/Identity/Login.razor) des Kundenprojekts fügen Sie unmittelbar vor dem schließenden </NotAuthorized>-Tag einen Link zum Zurücksetzen des Passworts hinzu, um die ForgotPassword-Komponente zu erreichen:

<div>
    <a href="forgot-password">Forgot password</a>
</div>

Timeout für E-Mails und Aktivitäten

Der Standardzeitraum für die Inaktivität bis zum Timeout beträgt 14 Tage. Im Serverprojekt setzt der folgende Code das Inaktivitäts-Timeout auf fünf Tage mit gleitendem Ablauf:

builder.Services.ConfigureApplicationCookie(options => {
    options.ExpireTimeSpan = TimeSpan.FromDays(5);
    options.SlidingExpiration = true;
});

Ändern Sie alle ASP.NET Core Schutz von Daten Token-Lebensdauern

Im Serverprojekt ändert der folgende Code den Timeout-Zeitraum der Schutz von Daten-Tokens auf drei Stunden:

builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
    options.TokenLifespan = TimeSpan.FromHours(3));

Die integrierten Identity Benutzertoken (AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs) haben ein eintägiges Zeitlimit.

Hinweis

Dokumentationslinks zur .NET-Referenzquelle laden in der Regel den Standardbranch des Repositorys, der die aktuelle Entwicklung für das nächste Release von .NET darstellt. Um ein Tag für ein bestimmtes Release auszuwählen, wählen Sie diesen mit der Dropdownliste Switch branches or tags (Branches oder Tags wechseln) aus. Weitere Informationen finden Sie unter How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) (Auswählen eines Versionstags von ASP.NET Core-Quellcode (dotnet/AspNetCore.Docs #26205)).

Ändern der Lebensdauer von E-Mail-Token

Die Standard-Token-Lebensdauer der Identity Benutzer-Token beträgt einen Tag.

Hinweis

Dokumentationslinks zur .NET-Referenzquelle laden in der Regel den Standardbranch des Repositorys, der die aktuelle Entwicklung für das nächste Release von .NET darstellt. Um ein Tag für ein bestimmtes Release auszuwählen, wählen Sie diesen mit der Dropdownliste Switch branches or tags (Branches oder Tags wechseln) aus. Weitere Informationen finden Sie unter How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) (Auswählen eines Versionstags von ASP.NET Core-Quellcode (dotnet/AspNetCore.Docs #26205)).

Um die Lebensdauer des E-Mail-Tokens zu ändern, fügen Sie ein benutzerdefiniertes DataProtectorTokenProvider<TUser> und DataProtectionTokenProviderOptions zum Serverprojekt hinzu.

CustomTokenProvider.cs:

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;

namespace BlazorSample;

public class CustomEmailConfirmationTokenProvider<TUser>
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomEmailConfirmationTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<EmailConfirmationTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class EmailConfirmationTokenProviderOptions 
    : DataProtectionTokenProviderOptions
{
    public EmailConfirmationTokenProviderOptions()
    {
        Name = "EmailDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(4);
    }
}

public class CustomPasswordResetTokenProvider<TUser> 
    : DataProtectorTokenProvider<TUser> where TUser : class
{
    public CustomPasswordResetTokenProvider(
        IDataProtectionProvider dataProtectionProvider,
        IOptions<PasswordResetTokenProviderOptions> options,
        ILogger<DataProtectorTokenProvider<TUser>> logger)
        : base(dataProtectionProvider, options, logger)
    {
    }
}

public class PasswordResetTokenProviderOptions : 
    DataProtectionTokenProviderOptions
{
    public PasswordResetTokenProviderOptions()
    {
        Name = "PasswordResetDataProtectorTokenProvider";
        TokenLifespan = TimeSpan.FromHours(3);
    }
}

Konfigurieren Sie die Dienste, um den benutzerdefinierten Token-Anbieter in der Serverprojektdatei Program zu nutzen:

builder.Services.AddIdentityCore<AppUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Tokens.ProviderMap.Add("CustomEmailConfirmation",
            new TokenProviderDescriptor(
                typeof(CustomEmailConfirmationTokenProvider<AppUser>)));
        options.Tokens.EmailConfirmationTokenProvider = 
            "CustomEmailConfirmation";
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

builder.Services
    .AddTransient<CustomEmailConfirmationTokenProvider<AppUser>>();

Problembehandlung

Wenn E-Mails nicht funktionieren:

  • Legen Sie einen Breakpoint in EmailSender.Execute fest, um zu überprüfen, ob SendEmailAsync aufgerufen wird.
  • Erstellen Sie eine Konsolen-App, um E-Mails mithilfe von Code zu senden, der EmailSender.Execute zum Debuggen des Problems ähnelt.
  • Überprüfen Sie die E-Mail-Verlaufsseiten des Kontos auf der Website des E-Mail-Anbieters.
  • Überprüfen Sie Ihren Spamordner auf Nachrichten.
  • Versuchen Sie es mit einem anderen E-Mail-Alias bei einem anderen E-Mail-Anbieter, zum Beispiel Microsoft, Yahoo oder Gmail.
  • Versuchen Sie, die E-Mail an andere E-Mail-Konten zu senden.

Zusätzliche Ressourcen