Exercice - Utiliser des revendications avec une autorisation basée sur des stratégies

Effectué

Dans l’unité précédente, vous avez appris la différence entre authentification et autorisation. Vous avez également appris comment les revendications sont utilisées par les stratégies pour l’autorisation. Dans cette unité, vous utilisez Identity pour stocker des revendications et appliquer des stratégies pour l’accès conditionnel.

Sécuriser la liste des pizzas

Vous avez reçu une nouvelle demande selon laquelle la page Liste des pizzas doit être visible uniquement pour les utilisateurs authentifiés. De plus, seuls les administrateurs sont autorisés à créer et à supprimer des pizzas. C’est parti pour le verrouillage.

  1. Dans Pages/Pizza.cshtml.cs, appliquez les modifications suivantes :

    1. Ajoutez un attribut [Authorize] à la classe PizzaModel.

      [Authorize]
      public class PizzaModel : PageModel
      

      L’attribut décrit les conditions d’autorisation utilisateur pour la page. Dans le cas présent, il n’y a aucune autre condition que l’authentification de l’utilisateur. Les utilisateurs anonymes ne sont pas autorisés à voir la page et sont redirigés vers la page de connexion.

    2. Résolvez la référence à Authorize en ajoutant la ligne suivante aux directives using en haut du fichier :

      using Microsoft.AspNetCore.Authorization;
      
    3. Ajoutez la propriété suivante à la classe PizzaModel :

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

      Le code précédent détermine si l’utilisateur authentifié a une revendication IsAdmin avec une valeur True. Le résultat de cette évaluation est accessible via une propriété en lecture seule appelée IsAdmin.

    4. Ajoutez if (!IsAdmin) return Forbid(); au début des deux méthodes OnPost et OnPostDelete :

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

      Vous allez masquer les éléments de l’interface utilisateur de création/suppression pour les non-administrateurs à l’étape suivante. Cela n’empêche pas un adversaire avec un outil tel que HttpRepl ou Postman d’accéder directement à ces points de terminaison. L’ajout de cette vérification garantit que s’il y a tentative, un code d’état HTTP 403 est retourné.

  2. Dans Pages/Pizza.cshtml, ajoutez des vérifications pour masquer les éléments de l’interface utilisateur administrateur aux non-administrateurs :

    Masquer le formulaire Nouvelle pizza

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

    Masquer le bouton Supprimer la pizza

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

    Les modifications précédentes entraînent l’affichage des éléments d’interface utilisateur qui ne doivent être accessibles qu’aux administrateurs uniquement lorsque l’utilisateur authentifié est administrateur.

Appliquer une stratégie d’autorisation

Il y a autre chose que vous devez verrouiller. Il existe une page qui doit être accessible uniquement aux administrateurs, la bien nommée Pages/AdminsOnly.cshtml. Créons une stratégie pour vérifier la revendication IsAdmin=True.

  1. Dans Program.cs, apportez les modifications suivantes :

    1. Incorporez le code en surbrillance suivant :

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

      Le code précédent définit une stratégie d’autorisation appelée Admin. La stratégie demande que l’utilisateur soit authentifié et qu’une revendication IsAdmin soit définie sur True.

    2. Modifiez l’appel à AddRazorPages comme suit :

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

      L’appel de la méthode AuthorizePage sécurise la route de la page Razor /AdminsOnly en appliquant la stratégie Admin. Les utilisateurs authentifiés qui ne répondent pas aux conditions de la stratégie voient le message Accès refusé.

      Conseil

      Sinon, vous auriez également pu modifier AdminsOnly.cshtml.cshtml.cs. Dans ce cas, vous auriez dû ajouter [Authorize(Policy = "Admin")] comme attribut dans la classe AdminsOnlyModel. L’avantage de l’approche AuthorizePage illustrée ci-dessus est que la page Razor en cours de sécurisation ne demande aucune modification. En effet, l’aspect autorisation est en fait géré dans Program.cs.

  2. Dans Pages/Shared/_Layout.cshtml, incorporez les modifications suivantes :

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

    La modification précédente masque de manière conditionnelle le lien Admin dans l’en-tête si l’utilisateur n’est pas administrateur.

Ajouter la revendication IsAdmin à un utilisateur

Pour déterminer quels utilisateurs doivent obtenir la revendication IsAdmin=True, votre application va s’appuyer sur une adresse e-mail confirmée pour identifier l’administrateur.

  1. Dans appsettings.json, ajoutez la propriété mise en surbrillance :

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

    Il s’agit de l’adresse e-mail confirmée qui obtient la revendication affectée.

  2. Dans Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs, apportez les modifications suivantes :

    1. Incorporez le code en surbrillance suivant :

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

      La modification précédente modifie le constructeur pour recevoir un IConfiguration du conteneur IoC. IConfiguration contient les valeurs d’appsettings.json et est affecté à une propriété en lecture seule nommée Configuration.

    2. Appliquez les changements en surbrillance à la méthode 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();
      }
      

      Dans le code précédent :

      • La chaîne AdminEmail est lue à partir de la propriété Configuration et affectée à adminEmail.
      • L’opérateur de coalescence nulle ?? est utilisé pour s’assurer que adminEmail est défini sur string.Empty s’il n’existe aucune valeur correspondante dans appsettings.json.
      • Si l’e-mail de l’utilisateur est confirmé avec succès :
        • L’adresse de l’utilisateur est comparée à adminEmail. string.Compare() est utilisé pour la comparaison insensible à la casse.
        • La méthode AddClaimAsync de la classe UserManager est appelée pour enregistrer une revendication IsAdmin dans la table AspNetUserClaims.
    3. Ajoutez le code suivant au début du fichier. Il résout les références de classe Claim dans la méthode OnGetAsync :

      using System.Security.Claims;
      

Tester la revendication d’administration

Nous allons effectuer un dernier test pour vérifier la nouvelle fonctionnalité d’administrateur.

  1. Vérifiez que vous avez enregistré tous vos changements.

  2. Exécutez l’application avec dotnet run.

  3. Accédez à votre application et connectez-vous avec un utilisateur existant, si vous n’êtes pas déjà connecté. Sélectionnez Liste de pizzas dans l’en-tête. Notez que l’utilisateur ne voit pas les éléments d’interface utilisateur pour supprimer ou créer des pizzas.

  4. Il n’existe aucun lien Admins dans l’en-tête. Dans la barre d’adresse du navigateur, accédez directement à la page AdminsOnly. Remplacez /Pizza dans l’URL par /AdminsOnly.

    L’utilisateur se voit refuser l’accès à la page. Un message Accès refusé s’affiche.

  5. Sélectionnez Se déconnecter.

  6. Inscrivez un nouvel utilisateur avec l’adresse admin@contosopizza.com.

  7. Comme précédemment, confirmez l’adresse e-mail du nouvel utilisateur et connectez-vous.

  8. Une fois connecté avec le nouvel utilisateur administrateur, sélectionnez le lien Liste de pizzas dans l’en-tête.

    L’utilisateur administrateur peut créer et supprimer des pizzas.

  9. Sélectionnez le lien Admins dans l’en-tête.

    La page AdminsOnly s’affiche.

Examiner la table AspNetUserClaims

À l’aide de l’extension SQL Server dans VS Code, exécutez la requête suivante :

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

Un onglet avec des résultats similaires à ce qui suit s’affiche :

Courrier ClaimType ClaimValue
admin@contosopizza.com IsAdmin True

La revendication IsAdmin est stockée sous la forme d’une paire clé-valeur dans la table AspNetUserClaims. L’enregistrement AspNetUserClaims est associé à l’enregistrement utilisateur dans la table AspNetUsers.

Résumé

Dans cette unité, vous avez modifié l’application pour stocker des revendications et appliquer des stratégies pour l’accès conditionnel.