Ejercicio: uso de notificaciones con autorización basada en directivas
En la unidad anterior, ha aprendido la diferencia entre autenticación y autorización. También ha aprendido cómo las directivas usan las notificaciones para la autorización. En esta unidad, se usa Identity para almacenar notificaciones y aplicar directivas para el acceso condicional.
Protección de la lista de pizzas
Ha recibido un nuevo requisito de que la página de lista de pizzas solo sea visible para los usuarios autenticados. Además, solo los administradores tendrán permiso para crear y eliminar pizzas. Lo hará a continuación.
En Pages/Pizza.cshtml.cs, aplique los cambios siguientes:
Agregue un atributo
[Authorize]
a la clasePizzaModel
.[Authorize] public class PizzaModel : PageModel
El atributo describe los requisitos de autorización de usuarios para la página. En este caso, no hay ningún requisito aparte de que el usuario se tenga que autenticar. No se permite que los usuarios anónimos vean la página y se les redirige a la página de inicio de sesión.
Resuelva la referencia a
Authorize
mediante la adición de la línea siguiente a las directivasusing
en la parte superior del archivo:using Microsoft.AspNetCore.Authorization;
Agregue la siguiente propiedad a la clase
PizzaModel
:[Authorize] public class PizzaModel : PageModel { public bool IsAdmin => HttpContext.User.HasClaim("IsAdmin", bool.TrueString); public List<Pizza> pizzas = new();
El código anterior determina si el usuario autenticado tiene una notificación
IsAdmin
con un valor deTrue
. Se puede acceder al resultado de esta evaluación a través de una propiedad de solo lectura llamadaIsAdmin
.Agregue
if (!IsAdmin) return Forbid();
al principio de los dos métodosOnPost
yOnPostDelete
: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"); }
En el paso siguiente, ocultará los elementos de creación o eliminación de la interfaz de usuario para los usuarios que no sean administradores. Esto no impide que un adversario con una herramienta como HttpRepl o curl acceda directamente a estos puntos de conexión. La adición de esta comprobación garantiza que, si se intenta, se devuelve un código de estado HTTP 403.
En Pages/Pizza.cshtml, agregue comprobaciones para ocultar los elementos de la interfaz de usuario de administrador a los no administradores:
Ocultar el formulario Nueva 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> }
Ocultar el botón Eliminar 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>
Los cambios anteriores hacen que los elementos de la interfaz de usuario que solo deben ser accesibles para los administradores solo se representen cuando el usuario autenticado es un administrador.
Aplicación de una directiva de autorización
Hay una cosa más que debería hacer. Hay una página que solo debe ser accesible para los administradores, con el nombre Pages/AdminsOnly.cshtml. Ahora se creará una directiva para comprobar la notificación IsAdmin=True
.
En Program.cs, realice los cambios siguientes:
Incorpore el código resaltado siguiente:
// 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();
En el código anterior se define una directiva de autorización llamada
Admin
. La directiva requiere que el usuario se autentique y tenga una notificaciónIsAdmin
establecida enTrue
.Modifique la llamada a
AddRazorPages
como se indica a continuación:builder.Services.AddRazorPages(options => options.Conventions.AuthorizePage("/AdminsOnly", "Admin"));
La llamada de método
AuthorizePage
protege la ruta de la página de Razor /AdminsOnly mediante la aplicación de la directivaAdmin
. A los usuarios autenticados que no cumplan los requisitos de la directiva se les aparece un mensaje de Acceso denegado.Sugerencia
Como alternativa, podría haber modificado AdminsOnly.cshtml.cs. En ese caso, tendría que agregar
[Authorize(Policy = "Admin")]
como atributo en la claseAdminsOnlyModel
. Una ventaja del enfoque deAuthorizePage
es que la página de Razor que se protege no necesita ninguna modificación. En su lugar, el aspecto de la autorización se administra en Program.cs.
En Pages/Shared/_Layout.cshtml, incorpore los cambios siguientes:
<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>
El cambio anterior oculta condicionalmente el vínculo Admin en el encabezado si el usuario no es un administrador.
Adición de la notificación IsAdmin
a un usuario
Para determinar qué usuarios deben obtener la notificación IsAdmin=True
, la aplicación se basará en una dirección de correo electrónico confirmada para identificar al administrador.
En appsettings.json, agregue la propiedad resaltada:
{ "AdminEmail" : "admin@contosopizza.com", "Logging": {
Esta es la dirección de correo electrónico confirmada que obtiene la notificación asignada.
En Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs, realice los cambios siguientes:
Incorpore el código resaltado siguiente:
public class ConfirmEmailModel : PageModel { private readonly UserManager<RazorPagesPizzaUser> _userManager; private readonly IConfiguration Configuration; public ConfirmEmailModel(UserManager<RazorPagesPizzaUser> userManager, IConfiguration configuration) { _userManager = userManager; Configuration = configuration; }
El cambio anterior modifica el constructor para recibir un elemento
IConfiguration
del contenedor de IoC.IConfiguration
contiene valores de appsettings.json y se asigna a una propiedad de solo lectura denominadaConfiguration
.Aplique los cambios resaltados en el método
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(); }
En el código anterior:
- La cadena
AdminEmail
se lee de la propiedadConfiguration
y se asigna aadminEmail
. - El operador de fusión de NULL
??
se usa para asegurarse de queadminEmail
se establezca enstring.Empty
si no hay ningún valor correspondiente en appsettings.json. - Si el correo electrónico del usuario se confirma correctamente:
- La dirección del usuario se compara con
adminEmail
.string.Compare()
se usa para la comparación sin distinción de mayúsculas y minúsculas. - Se invoca el método
AddClaimAsync
de la claseUserManager
para guardar una notificaciónIsAdmin
en la tablaAspNetUserClaims
.
- La dirección del usuario se compara con
- La cadena
Agregue el código siguiente en la parte superior del archivo. Resuelve las referencias de clase
Claim
en el métodoOnGetAsync
:using System.Security.Claims;
Prueba de notificación de administrador
Ahora se realizará una última prueba para comprobar la nueva funcionalidad de administrador.
Asegúrese de que ha guardado todos los cambios.
Ejecute la aplicación con
dotnet run
.Vaya a la aplicación y, si aún no ha iniciado sesión, hágalo con un usuario existente. Seleccione Lista de pizzas en el encabezado. Observe que al usuario no se le muestran elementos de la interfaz de usuario para eliminar o crear pizzas.
No hay ningún vínculo Admins en el encabezado. En la barra de direcciones del explorador, vaya directamente a la página AdminsOnly. Reemplace
/Pizza
en la dirección URL por/AdminsOnly
.Se prohíbe al usuario navegar a la página. Se muestra un mensaje de Acceso denegado.
Seleccione Cerrar sesión.
Registre un nuevo usuario con la dirección
admin@contosopizza.com
.Como antes, confirme la dirección de correo electrónico del nuevo usuario e inicie sesión.
Una vez que haya iniciado sesión con el nuevo usuario administrativo, seleccione el vínculo Lista de pizzas en el encabezado.
El usuario administrativo puede crear y eliminar pizzas.
Seleccione el vínculo Administradores en el encabezado.
Aparecerá la página AdminsOnly.
Revisión de la tabla AspNetUserClaims
Con la extensión SQL Server en VS Code, ejecute la siguiente consulta:
SELECT u.Email, c.ClaimType, c.ClaimValue
FROM dbo.AspNetUserClaims AS c
INNER JOIN dbo.AspNetUsers AS u
ON c.UserId = u.Id
Aparece una pestaña con resultados similares a los siguientes:
Correo electrónico | ClaimType | ClaimValue |
---|---|---|
admin@contosopizza.com | IsAdmin | True |
La notificación IsAdmin
se almacena como un par clave-valor en la tabla AspNetUserClaims
. El registro AspNetUserClaims
está asociado con el registro de usuario en la tabla AspNetUsers
.
Resumen
En esta unidad, ha modificado la aplicación para almacenar notificaciones y aplicar directivas para el acceso condicional.