ASP.NET Core Blazor WebAssembly を使用した ASP.NET Core Identity でのアカウント確認とパスワード回復

これは、この記事の最新バージョンではありません。 現在のリリースについては、 この記事の .NET 10 バージョンを参照してください。

この記事では、メール確認およびパスワード回復と、ASP.NET Core Blazor WebAssembly を使用して ASP.NET Core Identity アプリを構成する方法について説明します。

この記事では、ASP.NET Core Blazor WebAssembly を使用したスタンドアロン Identity アプリのみを適用します。 Blazor Web App のメール確認とパスワード回復を実装するには、「ASP.NET Core Blazor でのアカウント確認とパスワード回復」を参照してください。

名前空間と記事コードの例

この記事の例で使用される名前空間は次のとおりです。

  • バックエンド サーバー Web API プロジェクト用の Backend (この記事の「サーバー プロジェクト」)。
  • フロントエンド クライアント スタンドアロン BlazorWasmAuth アプリ用の Blazor WebAssembly (この記事の「クライアント プロジェクト」)。

これらの名前空間は、BlazorWebAssemblyStandaloneWithIdentitydotnet/blazor-samples サンプル ソリューション内のプロジェクトに対応します。 詳細については、「ASP.NET Core Blazor WebAssembly を使用して ASP.NET Core Identity をセキュリティで保護する」を参照してください。

BlazorWebAssemblyStandaloneWithIdentity サンプル ソリューションを使用していない場合は、プロジェクトの名前空間を使用するようにコード サンプルの名前空間を変更します。

サーバー プロジェクト用のメール プロバイダーを選択して構成する

この記事では、Mailchimp のトランザクション APIMandrill.net 経由で使用してメールを送信します。 メール送信には、SMTP ではなく、メール サービスを使うことをお勧めします。 SMTP を適切に構成してセキュリティで保護することは困難です。 どのメール サービスを使用する場合でも、.NET アプリのガイダンスにアクセスしてアカウントを作成し、サービスの API キーを構成して、必要な NuGet パッケージをインストールします。

サーバー プロジェクトで、シークレット メール プロバイダー API キーを保持するクラスを作成します。 この記事の例では、AuthMessageSenderOptions プロパティを持つ EmailAuthKey という名前のクラスを使用してキーを保持します。

AuthMessageSenderOptions.cs:

namespace Backend;

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

AuthMessageSenderOptions 構成インスタンスをサーバー プロジェクトの Program ファイルに登録します。

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

電子メール プロバイダーのセキュリティ キーのシークレットを構成する

プロバイダーから電子メール プロバイダーのセキュリティ キーを受け取り、次のガイダンスで使用します。

次のいずれかの方法または両方の方法を使用して、シークレットをアプリに提供します。

  • Secret Manager ツール: シークレット マネージャー ツールは、ローカル コンピューターにプライベート データを格納し、ローカル開発時にのみ使用します。
  • Azure Key Vault: ローカルで作業する際の Development 環境を含め、任意の環境で使用するために、シークレットをキー コンテナーに格納できます。 一部の開発者は、ステージングおよび運用環境のデプロイにキー コンテナーを使用し、ローカル開発に Secret Manager ツールを使用することを好みます。

プロジェクト コードまたは構成ファイルにシークレットを格納しないことを強くお勧めします。 このセクションの方法のいずれかまたは両方など、セキュリティで保護された認証フローを使用します。

シークレット マネージャー ツール

サーバー プロジェクトが既に Secret Manager ツール用に初期化されている場合は、プロジェクト ファイル (<UserSecretsId>) にユーザー シークレット識別子 (.csproj) が既に存在します。 Visual Studio では、ソリューション エクスプローラーでプロジェクトが選択されているときに [プロパティ] パネルを見て、ユーザー シークレット ID が存在するかどうかを確認できます。 アプリが初期化されていない場合は、サーバー プロジェクトのディレクトリに対して開かれたコマンド シェルで次のコマンドを実行します。 Visual Studio では、開発者用 PowerShell コマンド プロンプトを使用できます (コマンド シェルを開いた後、cd コマンドを使用してディレクトリをサーバー プロジェクトに変更します)。

dotnet user-secrets init

シークレット マネージャー ツールを使用して API キーを設定します。 次の例では、EmailAuthKey に対応する AuthMessageSenderOptions.EmailAuthKey というキー名が使用されており、キーは {KEY} プレースホルダーで表されています。 API キーを指定して次のコマンドを実行します。

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

Visual Studio を使用している場合は、ソリューション エクスプローラーでサーバー プロジェクトを右クリックし、[ユーザー シークレットの管理] を選択することで、シークレットが設定されていることを確認できます。

詳細については、「ASP.NET Core での開発におけるアプリ シークレットの安全な保存」を参照してください。

警告

アプリ シークレット、接続文字列、資格情報、パスワード、個人識別番号 (PIN)、プライベート C#/.NET コード、秘密キー/トークンを、クライアント側コードに保存しないでください。このコードは常に安全ではありません。 テスト/ステージング環境と運用環境では、サーバー側の Blazor コードと Web API で安全な認証フローを使用して、プロジェクト コードまたは構成ファイル内に資格情報を保持しないようにする必要があります。 ローカルの開発テスト以外では、環境変数は最も安全なアプローチとは言えないため、機密データを格納するのに環境変数を使用しないことをお勧めします。 ローカルの開発テストでは、機密データの保護には Secret Manager ツールをお勧めします。 詳細については、「機密データと資格情報を安全に維持する」を参照してください。

Azure Key Vault

azure Key Vault は、アプリのクライアント シークレットをアプリに提供するための安全なアプローチを提供します。

キー コンテナーを作成してシークレットを設定するには、「Azure Key Vault シークレットについて (Azure ドキュメント)」を参照してください。Azure Key Vault の使用を開始するためにリソースをクロスリンクします。 このセクションのコードを実装するには、キー コンテナーとシークレットを作成するときに、Azure のキー コンテナー URI とシークレット名を記録します。 [アクセス ポリシー] パネルでシークレットのアクセス ポリシーを設定する場合:

  • Get シークレット アクセス許可の取得のみが必要です。
  • シークレットのプリンシパルとしてアプリケーションを選択します。

Azure または Entra ポータルで、電子メール プロバイダー キー用に作成したシークレットへのアクセス権がアプリに付与されていることを確認します。

重要

キー ボールト シークレットは、有効期限が設定された状態で作成されます。 キー ボルト シークレットの有効期限が切れるタイミングを追跡し、その日付が経過する前に、アプリ用の新しいシークレットを作成してください。

Microsoft Identity パッケージがまだアプリのパッケージ登録に含まれていない場合は、Azure Identity と Azure Key Vault のサーバー プロジェクトに次のパッケージを追加します。 これらのパッケージは Microsoft Identity Web パッケージによって推移的に提供されるため、アプリが Microsoft.Identity.Webを参照していない場合にのみ追加する必要があります。

次の AzureHelper クラスをサーバー プロジェクトに追加します。 GetKeyVaultSecret メソッドは、キー ボールトからシークレットを取得します。 プロジェクトの名前空間スキームに合わせて名前空間 (BlazorSample.Helpers) を調整します。

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

前の例では、 DefaultAzureCredential を使用して認証を簡略化し、Azure ホスティング環境で使用される資格情報とローカル開発で使用される資格情報を組み合わせて Azure にデプロイするアプリを開発しています。 運用環境に移行する場合は、 ManagedIdentityCredentialなど、別の選択肢を選択することをお勧めします。 詳細については、「 システム割り当てマネージド ID を使用して Azure リソースに対して Azure でホストされる .NET アプリを認証する」を参照してください。

サービスがサーバー プロジェクトの Program ファイルに登録されている場合は、Options 構成でシークレットを取得してバインドします。

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

上記のコードが動作する環境を制御する場合 (たとえば、ローカル開発に Secret Manager ツール を使用することを選択したためにコードをローカルで実行しないようにする場合は、環境をチェックする条件付きステートメントで上記のコードをラップできます。

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

サーバー プロジェクトの AzureAdappsettings.json セクション (まだ存在しない場合は追加する必要がある場合があります) で、次の TenantId および VaultUri の構成キーと値を追加します (まだ存在しない場合)。

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

前の例では、次のようになります。

  • {TENANT ID} プレースホルダーは、Azure のアプリのテナント ID です。
  • {VAULT URI} プレースホルダーは、キー ボールトの URI です。 URI に末尾のスラッシュを含めます。

例:

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

構成は、アプリの環境構成ファイルに基づいて専用のキー コンテナーとシークレット名を簡単に指定するために使用されます。 たとえば、開発中の appsettings.Development.json、ステージング時の appsettings.Staging.json、運用デプロイの appsettings.Production.json に対して、さまざまな構成値を指定できます。 詳細については、ASP.NET Core Blazor 構成を参照してください。

サーバー プロジェクトに IEmailSender を実装する

次の例は、Mandrill.net を使用する Mailchimp のトランザクション API に基づいています。 別のプロバイダーについては、メール メッセージの送信を実装する方法に関するドキュメントを参照してください。

サーバー プロジェクトに、Mandrill.net NuGet パッケージを追加します。

EmailSender を実装するには、次の IEmailSender<TUser> クラスを追加します。 次の例の AppUserIdentityUser です。 メッセージの HTML マークアップは、さらにカスタマイズできます。 message に渡される MandrillMessage< 文字で始まっている限り、Mandrill.net API ではメッセージ本文が HTML で構成されていると想定されます。

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);
    }
}

メッセージの本文コンテンツでは、メール サービス プロバイダーの特別なエンコードが必要になる場合があります。 メール メッセージで、メッセージ本文に含まれるリンクのリンク先に移動できない場合は、サービス プロバイダーのドキュメントを参照して問題のトラブルシューティングを行ってください。

次の IEmailSender<TUser> サービス登録をサーバー プロジェクトの Program ファイルに追加します。

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

メールの確認を要求するようにサーバー プロジェクトを構成する

サーバー プロジェクトの Program ファイルで、アプリにサインインするには確認済みのメール アドレスが必要です。

AddIdentityCore を呼び出す行を見つけて、RequireConfirmedEmail プロパティを true に設定します。

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

クライアント プロジェクトのアカウント登録応答を更新する

クライアント プロジェクトの Register コンポーネント (Components/Identity/Register.razor) で、アカウント登録が成功したユーザーに表示されるメッセージを変更し、アカウントを確認するように指示します。 次の例には、確認メールを再送信するためにサーバー上の Identity をトリガーするリンクが含まれています。

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

シードされたアカウントを確認するためにシード データ コードを更新する

サーバー プロジェクトのシード データ クラス (SeedData.cs) で、シードされたアカウントを確認するように InitializeAsync メソッドのコードを変更します。これにより、アカウントの 1 つを使用してソリューションのテストを実行するたびにメール アドレスを確認する必要がなくなります。

- 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);
+ }

サイトにユーザーがいる用になった後でアカウントの確認を有効にする

ユーザーがいるサイトでアカウントの確認を有効にすると、すべての既存のユーザーがロックアウトされます。 既存のユーザーは、アカウントが確認されていないためにロックアウトされます。 次のいずれかのアプローチを使用しますが、これはこの記事の範囲外です。

  • データベースを更新して、既存のすべてのユーザーを確認済みとしてマークします。
  • 確認用リンクを含むメールを既存のすべてのユーザーに一括送信します。そのためには、各ユーザーが自分のアカウントを確認する必要があります。

パスワードの回復

パスワード回復では、ユーザーにパスワード リセット コードを送信するために、サーバー アプリでメール プロバイダーを採用する必要があります。 そのため、この記事の前半のガイダンスに従って、メール プロバイダーを採用してください。

パスワード回復は 2 段階のプロセスです。

  1. サーバー プロジェクトで /forgotPassword によって提供される MapIdentityApi エンドポイントに対して POST 要求が行われます。 UI のメッセージは、メールに記載されているリセット コードを確認するようユーザーに指示するものです。
  2. ユーザーのメール アドレス、パスワード リセット コード、および新しいパスワードを使用して、サーバー プロジェクトの /resetPassword エンドポイントに対して POST 要求が行われます。

上記の手順は、サンプル ソリューションの次の実装ガイダンスで示されています。

クライアント プロジェクトで、次のメソッド シグネチャを IAccountManagement クラス (Identity/IAccountManagement.cs) に追加します。

public Task<bool> ForgotPasswordAsync(string email);

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

クライアント プロジェクトで、上記のメソッドの実装を CookieAuthenticationStateProvider クラス (Identity/CookieAuthenticationStateProvider.cs) に追加します。

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

クライアント プロジェクトで、次の ForgotPassword コンポーネントを追加します。

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

クライアント プロジェクトの Login コンポーネント (Components/Identity/Login.razor) の終了 </NotAuthorized> タグの直前に、ForgotPassword コンポーネントにアクセスするためのパスワードを忘れた場合のリンクを追加します。

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

メールとアクティビティのタイムアウト

非アクティブ状態の既定のタイムアウトは 14 日です。 サーバー プロジェクトでは、次のコードは、非アクティブ タイムアウトを 5 日間に設定し、スライド式有効期限を設定します。

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

すべての ASP.NET Core データ保護トークンの有効期限を変更する

サーバー プロジェクトでは、次のコードによってデータ保護トークンのタイムアウト期間が 3 時間に変更されます。

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

組み込み Identity ユーザー トークン (AspNetCore/src/Identity/Extensions.Core/src/TokenOptions.cs) は、1 日でタイムアウトとなります。

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

メール トークンの有効期間を変更する

Identityユーザー トークンの既定のトークン有効期間は 1 日です。

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

メール トークンの有効期間を変更するには、カスタム DataProtectorTokenProvider<TUser>DataProtectionTokenProviderOptions をサーバー プロジェクトに追加します。

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);
    }
}

サーバー プロジェクトの Program ファイルでカスタム トークン プロバイダーを使用するようにサービスを構成します。

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>>();

トラブルシューティング

メールが機能しない場合:

  • EmailSender.Execute にブレークポイントを設定して、SendEmailAsync が呼び出されることを確認します。
  • EmailSender.Execute のようなコードを使用してメールを送信するコンソール アプリを作成し、問題をデバッグします。
  • メール プロバイダーの Web サイトでアカウントのメール履歴ページを確認します。
  • 迷惑メールフォルダにメッセージが届いていないか確認します。
  • 別のメール プロバイダー (Microsoft、Yahoo、Gmail など) で別のメール エイリアスを試します。
  • 別のメール アカウントに送信してみます。

その他のリソース