Exercício – utilizar afirmações com autorização baseada em políticas

Concluído

Na unidade anterior, aprendeu a diferença entre autenticação e autorização. Também aprendeu como as afirmações são utilizadas pelas políticas de autorização. Nesta unidade, vai utilizar a Identidade para armazenar afirmações e aplicar políticas de acesso condicional.

Proteger a lista de pizzas

Recebeu um novo requisito de que a página Lista de Pizzas só deve estar visível para utilizadores autenticados. Além disso, apenas os administradores podem criar e eliminar pizzas. Vamos bloqueá-lo.

  1. Em Pages/Pizza.cshtml.cs, aplique as seguintes alterações:

    1. Adicione um [Authorize] atributo à PizzaModel classe.

      [Authorize]
      public class PizzaModel : PageModel
      

      O atributo descreve os requisitos de autorização do utilizador para a página. Nesse caso, não há requisitos além de o utilizador ser autenticado. Os utilizadores anónimos não têm permissão para ver a página e são redirecionados para a página de início de sessão.

    2. Resolva a referência ao Authorize adicionar a seguinte linha às using diretivas na parte superior do ficheiro:

      using Microsoft.AspNetCore.Authorization;
      
    3. Adicione a seguinte propriedade à classe PizzaModel:

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

      O código anterior determina se o utilizador autenticado tem uma afirmação IsAdmin com um valor de True. O resultado desta avaliação é acedido através de uma propriedade só de leitura com o nome IsAdmin.

    4. Adicione if (!IsAdmin) return Forbid(); ao início dosOnPost métodos e 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");
      }
      

      Vai ocultar os elementos de IU de criação/eliminação para não administradores no próximo passo. Isto não impede que um adversário com uma ferramenta como HttpRepl ou Postman aceda diretamente a estes pontos finais. Adicionar esta verificação garante que, se for tentado, é devolvido um código de estado HTTP 403.

  2. Em Pages/Pizza.cshtml, adicione verificações para ocultar elementos da IU do administrador de não administradores:

    Ocultar novo formulário de 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>
    }
    

    Botão Ocultar piza Eliminar

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

    As alterações anteriores fazem com que os elementos da IU que só devem ser acessíveis aos administradores sejam compostos quando o utilizador autenticado é um administrador.

Aplicar uma política de autorização

Há mais uma coisa que devias bloquear. Existe uma página que deve ser acessível apenas aos administradores, convenientemente denominada Pages/AdminsOnly.cshtml. Vamos criar uma política para verificar a IsAdmin=True afirmação.

  1. Em Program.cs, faça as seguintes alterações:

    1. Incorpore o seguinte código realçado:

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

      O código anterior define uma política de autorização com o nome Admin. A política requer que o utilizador seja autenticado e tenha uma afirmação IsAdmin definida como True.

    2. Modifique a chamada da AddRazorPages seguinte forma:

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

      A AuthorizePage chamada de método protege a rota /AdminsOnly Razor Page ao aplicar a Admin política. Os utilizadores autenticados que não cumprirem os requisitos de política recebem uma mensagem de Acesso negado.

      Dica

      Em alternativa, poderia ter modificado AdminsOnly.cshtml.cs. Nesse caso, adicionaria [Authorize(Policy = "Admin")] como um atributo na AdminsOnlyModel classe. Uma vantagem para a AuthorizePage abordagem mostrada acima é que a Página do Razor que está a ser protegida não requer modificações. Em vez disso, o aspeto da autorização é gerido em Program.cs.

  2. Em Pages/Shared/_Layout.cshtml, incorpore as seguintes alterações:

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

    A alteração anterior oculta condicionalmente a ligação Administração no cabeçalho se o utilizador não for um administrador.

Adicionar a IsAdmin afirmação a um utilizador

Para determinar que utilizadores devem obter a IsAdmin=True afirmação, a sua aplicação irá depender de um endereço de e-mail confirmado para identificar o administrador.

  1. Em appsettings.json, adicione a propriedade realçada:

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

    Este é o endereço de e-mail confirmado que recebe a afirmação atribuída.

  2. Em Áreas/Identidade/Páginas/Conta/ConfirmEmail.cshtml.cs, faça as seguintes alterações:

    1. Incorpore o seguinte código realçado:

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

      A alteração anterior modifica o construtor para receber um IConfiguration do contentor IoC. Contém IConfiguration valores de appsettings.json e é atribuído a uma propriedade só de leitura chamada Configuration.

    2. Incorpore as alterações realçadas no 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();
      }
      

      No código anterior:

      • A AdminEmail cadeia é lida a Configuration partir da propriedade e atribuída a adminEmail.
      • O operador ?? de agrupamento nulo é utilizado para garantir que adminEmail está definido como string.Empty se não houver um valor correspondente em appsettings.json.
      • Se o e-mail do utilizador for confirmado com êxito:
        • O endereço do utilizador é comparado a adminEmail. string.Compare() é utilizado para comparação não sensível a maiúsculas e minúsculas.
        • Na classe UserManager, o método AddClaimAsync é invocado para guardar uma afirmação IsAdmin na tabela AspNetUserClaims.
    3. Adicione o seguinte código na parte superior do ficheiro. Resolve as referências de Claim classe no OnGetAsync método:

      using System.Security.Claims;
      

Testar afirmação de administrador

Vamos fazer um último teste para verificar a nova funcionalidade de administrador.

  1. Certifique-se de que guardou todas as suas alterações.

  2. Execute a aplicação com dotnet run.

  3. Navegue para a sua aplicação e inicie sessão com um utilizador existente, se ainda não tiver sessão iniciada. Selecione Lista de Pizzas no cabeçalho. Repare que o utilizador não é apresentado elementos de IU para eliminar ou criar pizzas.

  4. Não existe nenhuma ligação administradores no cabeçalho. Na barra de endereço do browser, navegue diretamente para a página AdminsOnly . Substitua /Pizza no URL por /AdminsOnly.

    O utilizador é proibido de navegar para a página. É apresentada uma mensagem de Acesso negado.

  5. Selecione Logout (Terminar sessão).

  6. Registe um novo utilizador com o endereço admin@contosopizza.com.

  7. Tal como anteriormente, confirme o endereço de e-mail do novo utilizador e inicie sessão.

  8. Depois de iniciar sessão com o novo utilizador administrativo, selecione a ligação Lista de Pizzas no cabeçalho.

    O utilizador administrativo pode criar e eliminar pizzas.

  9. Selecione a ligação Administradores no cabeçalho.

    É apresentada a página AdminsOnly .

Examinar a tabela AspNetUserClaims

Com a extensão SQL Server no VS Code, execute a seguinte consulta:

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

É apresentado um separador com resultados semelhantes aos seguintes:

E-mail ClaimType ClaimValue
admin@contosopizza.com IsAdmin Verdadeiro

A afirmação IsAdmin é armazenada como par chave-valor na tabela AspNetUserClaims. O registo AspNetUserClaims é associado ao registo de utilizador na tabela AspNetUsers.

Resumo

Nesta unidade, modificou a aplicação para armazenar afirmações e aplicar políticas de acesso condicional.