Exercice - Utiliser des revendications avec une autorisation basée sur des stratégies
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.
Dans Pages/Pizza.cshtml.cs, appliquez les modifications suivantes :
Ajoutez un attribut
[Authorize]
à la classePizzaModel
.[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.
Résolvez la référence à
Authorize
en ajoutant la ligne suivante aux directivesusing
en haut du fichier :using Microsoft.AspNetCore.Authorization;
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 valeurTrue
. Le code obtient des informations sur l’utilisateur authentifié à partir duHttpContext
dans la classePageModel
parente. Le résultat de cette évaluation est accessible via une propriété en lecture seule appeléeIsAdmin
.Ajoutez
if (!IsAdmin) return Forbid();
au début des deux méthodesOnPost
etOnPostDelete
: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 une personne mal intentionnée disposant d’un outil tel que HttpRepl ou curl 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é.
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
.
Dans Program.cs, apportez les modifications suivantes :
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 revendicationIsAdmin
soit définie surTrue
.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égieAdmin
. 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 classeAdminsOnlyModel
. L’avantage de l’approcheAuthorizePage
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.
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. Il utilise la propriété
Context
de la classeRazorPage
pour accéder auHttpContext
qui contient les informations sur l’utilisateur authentifié.
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.
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.
Dans Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs, apportez les modifications suivantes :
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éeConfiguration
.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 queadminEmail
est défini surstring.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 classeUserManager
est appelée pour enregistrer une revendicationIsAdmin
dans la tableAspNetUserClaims
.
- L’adresse de l’utilisateur est comparée à
- La chaîne
Ajoutez le code suivant au début du fichier. Il résout les références de classe
Claim
dans la méthodeOnGetAsync
:using System.Security.Claims;
Tester la revendication d’administration
Nous allons effectuer un dernier test pour vérifier la nouvelle fonctionnalité d’administrateur.
Vérifiez que vous avez enregistré tous vos changements.
Exécutez l’application avec
dotnet run
.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.
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.
Sélectionnez Se déconnecter.
Inscrivez un nouvel utilisateur avec l’adresse
admin@contosopizza.com
.Comme précédemment, confirmez l’adresse e-mail du nouvel utilisateur et connectez-vous.
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.
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.