演習 - Identity をカスタマイズする

完了

前のユニットでは、ASP.NET Core Identity でのカスタマイズのしくみについて学習しました。 このユニットでは、Identity データ モデルを拡張し、対応する UI を変更します。

ユーザー アカウント データをカスタマイズする

このセクションでは、既定の Razor クラス ライブラリの代わりに使用する Identity UI ファイルを作成およびカスタマイズします。

  1. 変更するユーザー登録ファイルをプロジェクトに追加します。

    dotnet aspnet-codegenerator identity --dbContext RazorPagesPizzaAuth --files "Account.Manage.EnableAuthenticator;Account.Manage.Index;Account.Register;Account.ConfirmEmail"
    

    上記のコマンドでは次のことが行われます。

    • --dbContext オプションでは、RazorPagesPizzaAuth という名前の既存の DbContext 派生クラスに関する情報をツールに提供します。
    • --files オプションでは、Identity 領域に追加する一意のファイルのセミコロン区切りのリストを指定します。
      • Account.Manage.Index はプロファイル管理ページです。 このページは、このユニットの後半で変更されます。
      • Account.Register はユーザー登録ページです。 このページもこのユニットで変更されます。
      • Account.Manage.EnableAuthenticatorAccount.ConfirmEmail はスキャフォールディングされますが、このユニットでは変更されません。

    ヒント

    --files オプションの有効な値を表示するには、プロジェクトのルートから次のコマンドを実行します: dotnet aspnet-codegenerator identity --listFiles

    次のファイルが、Areas/Identity ディレクトリに追加されます。

    • Pages/
      • _ViewImports.cshtml
      • Account/
        • _ViewImports.cshtml
        • ConfirmEmail.cshtml
        • ConfirmEmail.cshtml.cs
        • Register.cshtml
        • Register.cshtml.cs
        • Manage/
          • _ManageNav.cshtml
          • _ViewImports.cshtml
          • EnableAuthenticator.cshtml
          • EnableAuthenticator.cshtml.cs
          • Index.cshtml
          • Index.cshtml.cs
          • ManageNavPages.cs

IdentityUser の拡張

ユーザーの名前を保存するという新しい要件が与えられました。 既定の IdentityUser クラスには姓と名のプロパティが含まれていないため、RazorPagesPizzaUser クラスを拡張する必要があります。

`AnalyzeNode` に次の変更を加えます。

  1. FirstName および LastName プロパティを追加します。

    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Identity;
    
    namespace RazorPagesPizza.Areas.Identity.Data;
    
    public class RazorPagesPizzaUser : IdentityUser
    {
        [Required]
        [MaxLength(100)]
        public string FirstName { get; set; } = string.Empty;
    
        [Required]
        [MaxLength(100)]
        public string LastName { get; set; } = string.Empty;
    }
    

    上記のスニペットのプロパティでは、基になる AspNetUsers テーブルに作成する追加の列が表されています。 どちらのプロパティも必須であるため、[Required] 属性で注釈が付けられています。 さらに、[MaxLength] 属性により、許容最大長が 100 文字であることが示されています。 それに応じて、基になるテーブル列のデータ型が定義されます。 このプロジェクトでは null 許容コンテキストが有効になっており、プロパティが null 許容でない文字列であるため、既定値 string.Empty が割り当てられます。

  2. ファイルの先頭に次の using ステートメントを追加します。

    using System.ComponentModel.DataAnnotations;
    

    上記のコードでは、FirstName および LastName プロパティに適用されるデータ注釈属性が解決されます。

データベースを更新する

モデルの変更が行われたので、それに伴う変更をデータベースに対して行う必要があります。

  1. すべての変更が保存されていることを確かめます。

  2. EF Core の移行を作成して適用し、基になるデータ ストアを更新します。

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    UpdateUser EF Core の移行により、DDL 変更スクリプトが AspNetUsers テーブルのスキーマに適用されました。 具体的には、次の移行出力の抜粋に示すように、FirstName および LastName 列が追加されました。

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [FirstName] nvarchar(100) NOT NULL DEFAULT N'';
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (36ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [LastName] nvarchar(100) NOT NULL DEFAULT N'';
    
  3. データベースを調べ、AspNetUsers テーブルのスキーマに対する UpdateUser EF Core の移行の影響を分析します。

    SQL Server ペインで、dbo.AspNetUsers テーブルの Columns ノードを展開します。

    AspNetUsers テーブルのスキーマのスクリーンショット。

    RazorPagesPizzaUser クラスの FirstName および LastName プロパティは、上の画像の FirstName および LastName 列に対応しています。 [MaxLength(100)] 属性のため、nvarchar(100) のデータ型が 2 つの列のそれぞれに割り当てられています。 クラス内の FirstName および LastName が null 非許容文字列であるため、null 以外の制約が追加されました。 既存の行の新しい列には、空の文字列が表示されます。

ユーザー登録フォームをカスタマイズする

FirstNameLastName の新しい列を追加しました。 次は、登録フォームに一致するフィールドを表示するように UI を編集する必要があります。

  1. Areas/Identity/Pages/Account/Register.cshtml に、次の強調表示されているマークアップを追加します。

    <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
        <h2>Create a new account.</h2>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
        <div class="form-floating mb-3">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
            <label asp-for="Input.Email">Email</label>
            <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
    

    上記のマークアップでは、[First name](名)[Last name](姓) のテキスト ボックスがユーザー登録フォームに追加されます。

  2. Areas/Identity/Pages/Account/Register.cshtml.cs では、名前テキスト ボックスのサポートが追加されます。

    1. FirstName および LastName プロパティを入れ子になった InputModel クラスに追加します。

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      

      [Display] 属性では、テキスト ボックスに関連付けられるラベル テキストが定義されています。

    2. OnPostAsync メソッドを変更し、RazorPagesPizza オブジェクトの FirstName および LastName プロパティを設定します。 次の強調表示されている行を追加します。

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
      {
          returnUrl ??= Url.Content("~/");
          ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
          if (ModelState.IsValid)
          {
              var user = CreateUser();
      
              user.FirstName = Input.FirstName;
              user.LastName = Input.LastName;
              
              await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
              await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
              var result = await _userManager.CreateAsync(user, Input.Password);
      
      

      上記の変更により、FirstName および LastName プロパティが登録フォームからのユーザー入力に設定されます。

サイト ヘッダーをカスタマイズする

ユーザー登録の間に収集された姓と名を表示するように、Pages/Shared/_LoginPartial.cshtml を更新します。 次のスニペットで強調表示されている行が必要です。

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    RazorPagesPizzaUser? user = await UserManager.GetUserAsync(User);
    var fullName = $"{user?.FirstName} {user?.LastName}";

    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello, @fullName!</a>
    </li>

UserManager.GetUserAsync(User) は Null 許容の RazorPagesPizzaUser オブジェクトを返します。 null 条件 ?. 演算子は、RazorPagesPizzaUser オブジェクトが null 値でない場合にのみ、FirstName および LastName プロパティにアクセスするために使用されます。

プロファイル管理フォームをカスタマイズする

ユーザー登録フォームに新しいフィールドを追加しましたが、既存のユーザーが編集できるようにプロファイル管理フォームにも追加する必要があります。

  1. Areas/Identity/Pages/Account/Manage/Index.cshtml に、次の強調表示されているマークアップを追加します。 変更内容を保存します。

    <form id="profile-form" method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
        <div class="form-floating mb-3">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Username" class="form-control" disabled />
            <label asp-for="Username" class="form-label"></label>
        </div>
    
  2. Areas/Identity/Pages/Account/Manage/Index.cshtml.cs で、名前テキスト ボックスをサポートするための次の変更を行います。

    1. FirstName および LastName プロパティを入れ子になった InputModel クラスに追加します。

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. 強調表示された変更を LoadAsync メソッドに組み込みます。

      private async Task LoadAsync(RazorPagesPizzaUser user)
      {
          var userName = await _userManager.GetUserNameAsync(user);
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
      
          Username = userName;
      
          Input = new InputModel
          {
              PhoneNumber = phoneNumber,
              FirstName = user.FirstName,
              LastName = user.LastName
          };
      }
      

      上記のコードでは、プロファイル管理フォームの対応するテキスト ボックスに表示される姓と名の取得がサポートされています。

    3. 強調表示された変更を OnPostAsync メソッドに組み込みます。 変更を保存します。

      public async Task<IActionResult> OnPostAsync()
      {
          var user = await _userManager.GetUserAsync(User);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
          }
      
          if (!ModelState.IsValid)
          {
              await LoadAsync(user);
              return Page();
          }
      
          user.FirstName = Input.FirstName;
          user.LastName = Input.LastName;
          await _userManager.UpdateAsync(user);
      
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
          if (Input.PhoneNumber != phoneNumber)
          {
              var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
              if (!setPhoneResult.Succeeded)
              {
                  StatusMessage = "Unexpected error when trying to set phone number.";
                  return RedirectToPage();
              }
          }
      
          await _signInManager.RefreshSignInAsync(user);
          StatusMessage = "Your profile has been updated";
          return RedirectToPage();
      }
      

      上記のコードでは、データベースの AspNetUsers テーブルの姓と名の更新がサポートされています。

確認メールの送信者を構成する

アプリを初めてテストするときに、ユーザーを登録し、リンクをクリックしてユーザーのメール アドレスの確認をシミュレートしました。 "実際の" 確認メールを送信するには、IEmailSender の実装を作成して依存関係挿入システムに登録する必要があります。 わかりやすくするために、このユニットの実装では、実際には簡易メール転送プロトコル (SMTP) サーバーにメールを送信しません。 メールの内容をコンソールに書き込むだけです。

  1. コンソールではプレーン テキストでメールを表示するので、HTML エンコードされたテキストを除外するために生成されたメッセージを変更する必要があります。 Areas/Identity/Pages/Account/Register.cshtml.cs で、次のコードを見つけます。

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
    

    次のように変更します。

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. エクスプローラー ペインで、RazorPagesPizza\Services フォルダーを右クリックし、EmailSender.cs という名前の新しいファイルを作成します。 そのファイルを開き、次のコードを追加します。

    using Microsoft.AspNetCore.Identity.UI.Services;
    namespace RazorPagesPizza.Services;
    
    public class EmailSender : IEmailSender
    {
        public EmailSender() {}
    
        public Task SendEmailAsync(string email, string subject, string htmlMessage)
        {
            Console.WriteLine();
            Console.WriteLine("Email Confirmation Message");
            Console.WriteLine("--------------------------");
            Console.WriteLine($"TO: {email}");
            Console.WriteLine($"SUBJECT: {subject}");
            Console.WriteLine($"CONTENTS: {htmlMessage}");
            Console.WriteLine();
    
            return Task.CompletedTask;
        }
    }
    

    上記のコードでは、メッセージの内容をコンソールに書き込む IEmailSender の実装を作成します。 実世界の実装では、SendEmailAsync で外部メール サービスに接続するか、メールを送信する他のアクションを実行します。

  3. Program.cs で、強調表示されている行を追加します。

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    using Microsoft.AspNetCore.Identity.UI.Services;
    using RazorPagesPizza.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    builder.Services.AddTransient<IEmailSender, EmailSender>();
    
    var app = builder.Build();
    

    上のコードにより、EmailSenderIEmailSender として依存関係挿入システムに登録されます。

登録フォームの変更をテストする

以上です。 登録フォームと確認メールの変更をテストしてみましょう。

  1. すべての変更が保存済みであることを確認します。

  2. ターミナル ペインで、プロジェクトをビルドし、dotnet run を使用してアプリを実行します。

  3. ブラウザーでアプリに移動します。 ログインしている場合は、[ログアウト] を選択します。

  4. [Register](登録) を選択し、更新されたフォームを使用して新しいユーザーを登録します。

    Note

    [First name]\(名) フィールドと [Last name]\(姓) フィールドの検証制約には、InputModelFirstName および LastName プロパティのデータ注釈が反映されます。

  5. 登録後、[登録確認] 画面にリダイレクトされます。 ターミナル ペインで、上にスクロールして、次のようなコンソール出力を見つけます。

    Email Confirmation Message
    --------------------------
    TO: jana.heinrich@contoso.com
    SUBJECT: Confirm your email
    CONTENTS: Please confirm your account by visiting the following URL:
    
    https://localhost:7192/Identity/Account/ConfirmEmail?<query string removed>
    

    Ctrl キーを押しながら ''クリック'' して URL に移動します。 確認画面が表示されます。

    Note

    GitHub Codespaces を使っている場合は、必要に応じて転送される URL の最初の部分に -7192 を追加します。 たとえば、「 scaling-potato-5gr4j4-7192.preview.app.github.dev 」のように入力します。

  6. [ログイン] を選び、新しいユーザーでログインします。 アプリのヘッダーに [Hello, <名> <姓>!] が含まれるようになります。

  7. VS Code の SQL Server ペインで、RazorPagesPizza データベースを右クリックし、[新しいクエリ] を選択します。 表示されるタブで、次のクエリを入力し、Ctrl+Shift+E キーを押して実行します。

    SELECT UserName, Email, FirstName, LastName
    FROM dbo.AspNetUsers
    

    タブに、次のような結果が表示されます。

    UserName Email LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    最初のユーザーは、スキーマに FirstNameLastName を追加する前に登録されていました。 そのため、関連付けられている AspNetUsers テーブルのレコードのこれらの列にはデータがありません。

プロファイル管理フォームに対する変更をテストする

プロファイル管理フォームに加えた変更もテストする必要があります。

  1. Web アプリに、最初に作成したユーザーでサインインします。

  2. [Hello, !] リンクを選んで、プロファイル管理フォームに移動します。

    Note

    このユーザーに対する AspNetUsers テーブルの行には FirstName および LastName の値が含まれていないため、リンクが正しく表示されません。

  3. [First name](名)[Last name](姓) に有効な値を入力します。 [Save](保存) を選択します。

    アプリのヘッダーが [Hello, <名> <姓>!] に更新されます。

  4. アプリを停止するには、VS Code のターミナル ペインで Ctrl+C キーを押します。

まとめ

このユニットでは、カスタム ユーザー情報を格納するために Identity をカスタマイズしました。 確認メールもカスタマイズしました。 次のユニットでは、Identity で多要素認証を実装する方法について学習します。