演習 - ポリシーベースの認可で要求を使用する

完了

前のユニットでは、認証と認可の違いについて学習しました。 また、認可のためにポリシーによって要求がどのように使用されるかについても学習しました。 このユニットでは、ID を使用して要求を格納し、条件付きアクセスのポリシーを適用します。

ピザ リストをセキュリティで保護する

Pizza List ページが認証済みユーザーにのみ表示される必要があるという新しい要件を受け取りました。 さらに、ピザの作成と削除は管理者のみが許可されます。 ロックダウンしましょう。

  1. Pages/Pizza.cshtml.cs で、次の変更を適用します。

    1. PizzaModel クラスに [Authorize] 属性を追加します。

      [Authorize]
      public class PizzaModel : PageModel
      

      この属性で、ページのユーザー認可要件を記述します。 この場合、ユーザーが認証されること以外に要件はありません。 匿名ユーザーは、ページの表示を許可されず、サインイン ページにリダイレクトされます。

    2. ファイルの先頭にある using ディレクティブに次の行を追加して、Authorize への参照を解決します。

      using Microsoft.AspNetCore.Authorization;
      
    3. PizzaModel クラスに次のプロパティを追加します。

      [Authorize]
      public class PizzaModel : PageModel
      {
          public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString);
      
          public List<Pizza> pizzas = new();
      

      上記のコードでは、認証されたユーザーの IsAdmin 要求の値が True かどうかが判断されます。 このコードは、親 PageModel クラスの HttpContext から認証済みユーザーに関する情報を取得します。 この評価の結果には、IsAdmin という名前の読み取り専用プロパティを使用してアクセスします。

    4. if (!IsAdmin) return Forbid();OnPostOnPostDelete両方のメソッドの先頭に追加します。

      public IActionResult OnPost()
      {
          if (!IsAdmin) return Forbid();
          if (!ModelState.IsValid)
          {
              return Page();
          }
          PizzaService.Add(NewPizza);
          return RedirectToAction("Get");
      }
      
      public IActionResult OnPostDelete(int id)
      {
          if (!IsAdmin) return Forbid();
          PizzaService.Delete(id);
          return RedirectToAction("Get");
      }
      

      次の手順で、管理者以外の UI 要素の作成/削除を非表示にします。 このようにしても、敵対者が HttpRepl や curl などのツールを使ってこれらのエンドポイントに直接アクセスすることの阻止はできません。 このチェックを追加することで、これが試行された場合に、HTTP 403 状態コードが確実に返されるようにします。

  2. Pages/Pizza.cshtml で、管理者以外の UI 要素を非表示にするチェックを追加します。

    新しいピザのフォームを非表示にする

    <h1>Pizza List 🍕</h1>
    @if (Model.IsAdmin)
    {
    <form method="post" class="card p-3">
        <div class="row">
            <div asp-validation-summary="All"></div>
        </div>
        <div class="form-group mb-0 align-middle">
            <label asp-for="NewPizza.Name">Name</label>
            <input type="text" asp-for="NewPizza.Name" class="mr-5">
            <label asp-for="NewPizza.Size">Size</label>
            <select asp-for="NewPizza.Size" asp-items="Html.GetEnumSelectList<PizzaSize>()" class="mr-5"></select>
            <label asp-for="NewPizza.Price"></label>
            <input asp-for="NewPizza.Price" class="mr-5" />
            <label asp-for="NewPizza.IsGlutenFree">Gluten Free</label>
            <input type="checkbox" asp-for="NewPizza.IsGlutenFree" class="mr-5">
            <button class="btn btn-primary">Add</button>
        </div>
    </form>
    }
    

    ピザの削除ボタンを非表示にする

    <table class="table mt-5">
        <thead>
            <tr>
                <th scope="col">Name</th>
                <th scope="col">Price</th>
                <th scope="col">Size</th>
                <th scope="col">Gluten Free</th>
                @if (Model.IsAdmin)
                {
                <th scope="col">Delete</th>
                }
            </tr>
        </thead>
        @foreach (var pizza in Model.pizzas)
        {
            <tr>
                <td>@pizza.Name</td>
                <td>@($"{pizza.Price:C}")</td>
                <td>@pizza.Size</td>
                <td>@Model.GlutenFreeText(pizza)</td>
                @if (Model.IsAdmin)
                {
                <td>
                    <form method="post" asp-page-handler="Delete" asp-route-id="@pizza.Id">
                        <button class="btn btn-danger">Delete</button>
                    </form>
                </td>
                }
            </tr>
        }
    </table>
    

    上記の変更により、認証されたユーザーが管理者である場合にのみ、管理者のみがアクセスできる必要がある UI 要素がレンダリングされます。

認可ポリシーの適用

もう 1 つ、ロックダウンが必要なものがあります。 管理者のみがアクセスできる必要があるページがあり、都合が良いことに Pages/AdminsOnly.cshtml という名前です。 IsAdmin=True 要求をチェックするポリシーを作成しましょう。

  1. Program.cs で、次の変更を行います。

    1. 次の強調表示されているコードを組み込みます。

      // Add services to the container.
      builder.Services.AddRazorPages();
      builder.Services.AddTransient<IEmailSender, EmailSender>();
      builder.Services.AddSingleton(new QRCodeService(new QRCodeGenerator()));
      builder.Services.AddAuthorization(options =>
          options.AddPolicy("Admin", policy =>
              policy.RequireAuthenticatedUser()
                  .RequireClaim("IsAdmin", bool.TrueString)));
      
      var app = builder.Build();
      

      上記のコードでは、Admin という名前の認可ポリシーが定義されています。 そのポリシーでは、ユーザーが認証され、IsAdmin 要求が True に設定されている必要があります。

    2. AddRazorPages の呼び出しを次のように変更します。

      builder.Services.AddRazorPages(options =>
          options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
      

      AuthorizePage メソッド呼び出しでは、Admin ポリシーを適用することで、/AdminsOnly Razor ページのルートをセキュリティで保護します。 ポリシーの要件を満たしていない認証済みユーザーには、アクセス拒否メッセージが表示されます。

      ヒント

      または、代わりに AdminsOnly.cshtml.cs を変更することもできます。 その場合は、AdminsOnlyModel クラスに [Authorize(Policy = "Admin")] を属性として追加します。 上記の AuthorizePage の方法の利点は、セキュリティ保護対象の Razor ページを変更する必要がないことです。 認可については、代わりに Program.cs で管理します。

  2. Pages/Shared/_Layout.cshtml に、次の変更を組み込みます。

    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Pizza">Pizza List</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
        </li>
        @if (Context.User.HasClaim("IsAdmin", bool.TrueString))
        {
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-page="/AdminsOnly">Admins</a>
        </li>
        }
    </ul>
    

    上記の変更によって条件が付き、ユーザーが管理者でない場合は、ヘッダー内の Admin リンクが非表示になります。 これは、RazorPage クラスの Context プロパティを使用して、認証されたユーザーに関する情報を含む HttpContext にアクセスします。

ユーザーに IsAdmin 要求を追加する

IsAdmin=True 要求を受け取るユーザーを決定するために、アプリで、確認済みのメール アドレスに依存して管理者を識別しようとしています。

  1. appsettings.json で、強調表示されているプロパティを追加します。

    {
      "AdminEmail" : "admin@contosopizza.com",
      "Logging": {
    

    これは、要求が割り当てられる確認済みのメール アドレスです。

  2. Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs で、次の変更を行います。

    1. 次の強調表示されているコードを組み込みます。

      public class ConfirmEmailModel : PageModel
      {
          private readonly UserManager<RazorPagesPizzaUser> _userManager;
          private readonly IConfiguration Configuration;
      
          public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager,
                                      IConfiguration configuration)
          {
              _userManager = userManager;
              Configuration = configuration;
          }
      
      

      上記の変更により、IoC コンテナーから IConfiguration を受け取るようにコンストラクターが変更されます。 IConfigurationappsettings.json の値が含まれ、Configuration という名前の読み取り専用プロパティに割り当てられます。

    2. 強調表示された変更を OnGetAsync メソッドに適用します。

      public async Task<IActionResult> OnGetAsync(string userId, string code)
      {
          if (userId == null || code == null)
          {
              return RedirectToPage("/Index");
          }
      
          var user = await _userManager.FindByIdAsync(userId);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{userId}'.");
          }
      
          code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code));
          var result = await _userManager.ConfirmEmailAsync(user, code);
          StatusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email.";
      
          var adminEmail = Configuration["AdminEmail"] ?? string.Empty;
          if(result.Succeeded)
          {
              var isAdmin = string.Compare(user.Email, adminEmail, true) == 0 ? true : false;
              await _userManager.AddClaimAsync(user, 
                  new Claim("IsAdmin", isAdmin.ToString()));
          }
      
          return Page();
      }
      

      上記のコードにより、次のことが行われます。

      • AdminEmail 文字列は、Configuration プロパティから読み取られ、adminEmail に割り当てられます。
      • 対応する値が appsettings.json 内にない場合に、adminEmailstring.Empty に確実に設定されるように、null 合体演算子 ?? が使用されています。
      • ユーザーのメールが正常に確認された場合:
        • ユーザーのアドレスは adminEmail と比較されます。 string.Compare() は、大文字と小文字を区別しない比較に使用されます。
        • UserManager クラスの AddClaimAsync メソッドが呼び出されて、AspNetUserClaims テーブルに IsAdmin 要求が保存されます。
    3. 次のコードをファイルの先頭に追加します。 OnGetAsync メソッド内の Claim クラス参照が解決されます。

      using System.Security.Claims;
      

管理者要求をテストする

最後にテストを 1 つ行って、新しい管理者機能を確認しましょう。

  1. すべての変更を保存したことを確認します。

  2. dotnet run を使用してアプリを実行します。

  3. アプリに移動し、まだサインインしていない場合は、既存のユーザーでサインインします。 ヘッダーから Pizza List を選択します。 ユーザーにピザを削除または作成するための UI 要素が表示されないことに注目します。

  4. ヘッダー内に Admins リンクがありません。 ブラウザーのアドレス バーで、AdminsOnly ページに直接移動します。 URL 内の /Pizza/AdminsOnly に置き換えます。

    そのユーザーは、そのページへの移動を禁止されています。 アクセス拒否メッセージが表示されます。

  5. [Logout](ログアウト) を選択します。

  6. アドレス admin@contosopizza.com を使用して新しいユーザーを登録します。

  7. 前と同様に、新しいユーザーのメール アドレスを確認し、サインインします。

  8. 新しい管理ユーザーでサインインしたら、ヘッダー内の Pizza List リンクを選択します。

    管理ユーザーは、ピザを作成および削除できます。

  9. ヘッダー内の Admins リンクを選択します。

    AdminsOnly ページが表示されます。

AspNetUserClaims テーブルを調べる

VS Code で SQL Server 拡張機能を使用して、次のクエリを実行します。

SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
    INNER JOIN dbo.AspNetUsers AS u
    ON c.UserId = u.Id

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

電子メール ClaimType ClaimValue
admin@contosopizza.com IsAdmin True

IsAdmin 要求は、キーと値のペアとして AspNetUserClaims テーブルに格納されます。 AspNetUserClaims レコードは、AspNetUsers テーブル内のユーザー レコードに関連付けられています。

まとめ

このユニットでは、要求を格納し、条件付きアクセスのポリシーを適用するようにアプリを変更しました。