Ejercicio: uso de notificaciones con autorización basada en directivas

Completado

En esta unidad, se creará un nuevo usuario con privilegios administrativos. Se proporciona una demostración de la creación y el almacenamiento de notificaciones de usuario. También se define una directiva de autorización para determinar si un usuario autenticado tiene privilegios elevados en la interfaz de usuario.

Protección del catálogo de productos

La página catálogo de productos solo debe estar visible para los usuarios autenticados. Sin embargo, solo los administradores pueden editar, crear y eliminar productos.

  1. En Pages/Products/Index.cshtml.cs, aplique los cambios siguientes:

    1. Reemplace el comentario // Add [Authorize] attribute por el atributo siguiente:

      [Authorize]
      

      El atributo anterior describe los requisitos de autenticación del usuario necesarios en la página. En este caso, no hay ningún requisito aparte de que el usuario se tenga que autenticar. Los usuarios anónimos no pueden ver la página y se redirigen a la página de inicio de sesión.

    2. Quite la marca de comentario de la línea //using Microsoft.AspNetCore.Authorization; en la parte superior del archivo.

      El cambio anterior resuelve el atributo [Authorize] del paso anterior.

    3. Reemplace el comentario // Add IsAdmin property por la propiedad siguiente:

      public bool IsAdmin =>
          HttpContext.User.HasClaim("IsAdmin", bool.TrueString);
      

      El código anterior determina si el usuario autenticado tiene una notificación IsAdmin con un valor de True. Se puede acceder al resultado de esta evaluación a través de una propiedad de solo lectura llamada IsAdmin.

    4. En el método OnDelete, reemplace el comentario // Add IsAdmin check por el código siguiente:

      if (!IsAdmin)
      {
          return Forbid();
      }
      

      Cuando un empleado autenticado intenta eliminar un producto a través de la interfaz de usuario o mediante el envío manual de una solicitud HTTP DELETE a esta página, se devuelve un código de estado HTTP 403.

  2. En Pages/Products/Index.cshtml, actualice los vínculos Editar, Eliminar y Agregar producto con el código resaltado:

    Editar y eliminar vínculos:

    <td>
        @if (Model.IsAdmin)
        {
        <a asp-page="Edit" asp-route-id="@product.Id">Edit</a> <span>|</span>
        <a href="#" onclick="deleteProduct('@product.Id', antiForgeryToken())">Delete</a>
        }
    </td>
    

    Vínculo Agregar producto:

    @if (Model.IsAdmin)
    {
    <a asp-page="./Create">Add Product</a>
    }
    

    Los cambios anteriores hacen que los vínculos se representen únicamente cuando el usuario autenticado es un administrador.

Registro y aplicación de la directiva de autorización

Las páginas Crear producto y Editar producto deben ser accesibles solo para los administradores. Con el fin de encapsular los criterios de autorización para dichas páginas, se creará una directiva Admin.

  1. En el método ConfigureServices de Startup.cs, realice los cambios siguientes:

    1. Reemplace el comentario // Add call to AddAuthorization por el código siguiente:

      services.AddAuthorization(options =>
          options.AddPolicy("Admin", policy =>
              policy.RequireAuthenticatedUser()
                  .RequireClaim("IsAdmin", bool.TrueString)));
      

      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ón IsAdmin establecida en True.

    2. Incorpore el código resaltado siguiente:

      services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");
      services.AddRazorPages(options =>
          options.Conventions.AuthorizePage("/Products/Edit", "Admin"));
      services.AddControllers();
          
      

      La llamada de método AuthorizePage protege la ruta de la página de Razor /Products/Edit aplicando la directiva Admin. Una ventaja de este enfoque es que la página de Razor protegida no requiere ninguna modificación. En su lugar, el aspecto de la autorización se administra en Startup.cs. A los usuarios anónimos se les redirigirá a la página de inicio de sesión. A los usuarios autenticados que no cumplan los requisitos de la directiva se les aparece un mensaje de Acceso denegado.

  2. En Pages/Products/Create.cshtml.cs, aplique los cambios siguientes:

    1. Reemplace el comentario // Add [Authorize(Policy = "Admin")] attribute por el atributo siguiente:

      [Authorize(Policy = "Admin")]
      

      El código anterior representa una alternativa a la llamada de método AuthorizePage en Startup.cs. El atributo [Authorize] exige que se cumplan los requisitos de la directiva Admin. A los usuarios anónimos se les redirigirá a la página de inicio de sesión. A los usuarios autenticados que no cumplan los requisitos de la directiva se les aparece un mensaje de Acceso denegado.

    2. Quite la marca de comentario de la línea //using Microsoft.AspNetCore.Authorization; en la parte superior del archivo.

      El cambio anterior resuelve el atributo [Authorize(Policy = "Admin")] del paso anterior.

Modificación de la página de registro

Modifique la página de registro para permitir que los administradores se registren mediante los pasos siguientes.

  1. En Areas/Identity/Pages/Account/Register.cshtml.cs, realice los cambios siguientes:

    1. Agregue la propiedad siguiente a la clase anidada InputModel:

      public class InputModel
      {
          [DataType(DataType.Password)]
          [Display(Name = "Admin enrollment key")]
          public ulong? AdminEnrollmentKey { get; set; }
      
          [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; }
      
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
          [DataType(DataType.Password)]
          [Display(Name = "Password")]
          public string Password { get; set; }
      
          [DataType(DataType.Password)]
          [Display(Name = "Confirm password")]
          [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
          public string ConfirmPassword { get; set; }
      }
      
    2. Aplique los cambios resaltados en el método OnPostAsync:

      public async Task<IActionResult> OnPostAsync(
          [FromServices] AdminRegistrationTokenService tokenService,
          string returnUrl = null)
      {
          returnUrl = returnUrl ?? Url.Content("~/");
          ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
          if (ModelState.IsValid)
          {
              var user = new ContosoPetsUser
              {
                  FirstName = Input.FirstName,
                  LastName = Input.LastName,
                  UserName = Input.Email,
                  Email = Input.Email,
              };
              var result = await _userManager.CreateAsync(user, Input.Password);
              if (result.Succeeded)
              {
                  _logger.LogInformation("User created a new account with password.");
      
                  await _userManager.AddClaimAsync(user, 
                      new Claim("IsAdmin", 
                          (Input.AdminEnrollmentKey == tokenService.CreationKey).ToString()));
      
                  var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                  code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                  var callbackUrl = Url.Page(
                      "/Account/ConfirmEmail",
                      pageHandler: null,
                      values: new { area = "Identity", userId = user.Id, code = code },
                      protocol: Request.Scheme);
      
                  await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                      $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
      
                  if (_userManager.Options.SignIn.RequireConfirmedAccount)
                  {
                      return RedirectToPage("RegisterConfirmation", new { email = Input.Email });
                  }
                  else
                  {
                      await _signInManager.SignInAsync(user, isPersistent: false);
                      return LocalRedirect(returnUrl);
                  }
              }
              foreach (var error in result.Errors)
              {
                  ModelState.AddModelError(string.Empty, error.Description);
              }
          }
      
          // If we got this far, something failed, redisplay form
          return Page();
      }
      

      En el código anterior:

      • El atributo [FromServices] proporciona una instancia de AdminRegistrationTokenService a partir del contenedor de IoC.
      • Se invoca el método AddClaimAsync de la clase UserManager para guardar una notificación IsAdmin en la tabla AspNetUserClaims.
    3. Agregue el código siguiente en la parte superior del archivo. Resuelve las referencias de clase AdminRegistrationTokenService y Claim en el método OnPostAsync:

      using ContosoPets.Ui.Services;
      using System.Security.Claims;
      
  2. En Areas/Identity/Pages/Account/Register.cshtml, agregue el marcado siguiente:

    <div class="form-group">
        <label asp-for="Input.ConfirmPassword"></label>
        <input asp-for="Input.ConfirmPassword" class="form-control" />
        <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Input.AdminEnrollmentKey"></label>
        <input asp-for="Input.AdminEnrollmentKey" class="form-control" />
        <span asp-validation-for="Input.AdminEnrollmentKey" class="text-danger"></span>
    </div>
    <button type="submit" class="btn btn-primary">Register</button>
    

Prueba de notificación de administrador

  1. Ejecute el siguiente comando para compilar la aplicación:

    dotnet build --no-restore
    

    La opción --no-restore se incluye porque no se han agregado paquetes NuGet desde la última compilación. El proceso de compilación omite la restauración de paquetes NuGet y se realiza correctamente sin ninguna advertencia. Si se produce un error en la compilación, compruebe la salida con el fin de obtener información para solucionar problemas.

  2. Implemente la aplicación en Azure App Service ejecutando el comando siguiente:

    az webapp up
    
  3. Vaya a la aplicación e inicie sesión con un usuario existente, si aún no ha iniciado sesión. En el encabezado, seleccione Productos. Observe que al usuario no le aparecen vínculos para editar, eliminar o crear productos.

  4. En la barra de direcciones del explorador, vaya directamente a la página Crear producto. Para obtener la dirección URL de esa página, ejecute el comando siguiente:

    echo "$webAppUrl/Products/Create"
    

    Se prohíbe al usuario navegar a la página. Se muestra un mensaje de Acceso denegado. Del mismo modo, se prohibirá que el usuario navegue a una ruta como /Products/Edit/1.

  5. Seleccione Cerrar sesión.

  6. Obtenga un token de inscripción automática de administrador con el comando siguiente:

    echo $(wget -q -O - $webAppUrl/admintoken)
    

    Advertencia

    El mecanismo de inscripción automática de administrador se hace solo con fines ilustrativos. El punto de conexión /api/Admin que sirve para obtener un token debe protegerse antes de usarse en un entorno de producción.

  7. En la aplicación web, registre un nuevo usuario. El token del paso anterior debe proporcionarse en el cuadro de texto Clave de inscripción de administrador.

  8. Una vez que haya iniciado sesión con el nuevo usuario administrativo, haga clic en el vínculo Productos del encabezado.

    El usuario administrativo puede ver, editar y crear productos.

Revisión de la tabla AspNetUserClaims

Ejecute el comando siguiente:

db -c 'SELECT u."Email", c."ClaimType", c."ClaimValue" FROM "AspNetUserClaims" AS c INNER JOIN "AspNetUsers" AS u ON c."UserId" = u."Id"'

Aparece una variación del resultado siguiente:

        Email         | ClaimType | ClaimValue
----------------------+-----------+------------
 scott@contoso.com    | IsAdmin   | True
(1 row)
db -Q "SELECT u.Email, c.ClaimType, c.ClaimValue FROM dbo.AspNetUserClaims AS c INNER JOIN dbo.AspNetUsers AS u ON c.UserId = u.Id" -Y25 -y10

Aparece una variación del resultado siguiente:

Email                     ClaimType  ClaimValue
------------------------- ---------- ----------
scott@contoso.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.