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

Concluído

Na unidade anterior, você aprendeu a diferença entre autenticação e autorização. Você também aprendeu como as declarações são usadas pelas políticas de autorização. Nesta unidade, você usa Identity para armazenar declarações e aplicar políticas de acesso condicional.

Proteja a lista de pizzas

Você recebeu um novo requisito de que a página Lista de pizzas deve estar visível apenas para usuários autenticados. Além disso, apenas os administradores têm permissão para criar e excluir pizzas. Vamos bloqueá-lo.

  1. No 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 usuário 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 adicionando Authorize a seguinte linha às using diretivas na parte superior do arquivo:

      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 código obtém informações sobre o usuário autenticado da HttpContext classe pai PageModel . 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 dos OnPost 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");
      }
      

      Você ocultará os elementos da interface do usuário de criação/exclusão para não administradores na próxima etapa. Isso não impede que um adversário com uma ferramenta como HttpRepl ou curl acesse esses endpoints diretamente. Adicionar essa verificação garante que, se isso for tentado, um código de status HTTP 403 será retornado.

  2. Em Pages/Pizza.cshtml, adicione verificações para ocultar elementos da interface do usuário 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>
    }
    

    Ocultar botão Excluir 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>
    

    As alterações anteriores fazem com que os elementos da interface do usuário que devem ser acessíveis apenas aos administradores sejam renderizados somente quando o usuário autenticado for um administrador.

Aplicar uma política de autorização

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

  1. No 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 aplicando a Admin política. Os utilizadores autenticados que não cumprirem os requisitos de política recebem uma mensagem de Acesso negado.

      Gorjeta

      Alternativamente, você poderia ter modificado AdminsOnly.cshtml.cs. Nesse caso, você adicionaria [Authorize(Policy = "Admin")] como um atributo na AdminsOnlyModel classe. Uma vantagem da abordagem mostrada AuthorizePage acima é que a Razor Page que está sendo protegida não requer modificações. Em vez disso, o aspeto de autorização é gerenciado 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 o link Admin no cabeçalho se o usuário não for um administrador. Ele usa a Context propriedade da RazorPage classe para acessar o HttpContext que contém as informações sobre o usuário autenticado.

Adicionar a IsAdmin declaração a um utilizador

Para determinar quais usuários devem receber a IsAdmin=True declaração, seu aplicativo vai contar com 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 reivindicaçã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 contêiner IoC. O IConfiguration contém valores de appsettings.json e é atribuído a uma propriedade somente 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 de caracteres é lida da Configuration propriedade e atribuída a adminEmail.
      • O operador ?? de coalescência nula é usado para garantir adminEmail que seja definido como string.Empty se não houver nenhum valor correspondente em appsettings.json.
      • Se o e-mail do usuário for confirmado com sucesso:
        • O endereço do usuário é comparado ao adminEmail. string.Compare() é usado para comparação que não diferencia maiúsculas de 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. Ele resolve as Claim referências de classe no OnGetAsync método:

      using System.Security.Claims;
      

Testar afirmação de administrador

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

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

  2. Execute o aplicativo com dotnet runo .

  3. Navegue até à 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. Observe que não são apresentados elementos da interface do usuário ao usuário para excluir ou criar pizzas.

  4. Não há nenhum link Administradores no cabeçalho. Na barra de endereço do navegador, 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. Como antes, confirme o endereço de e-mail do novo usuário e faça login.

  8. Depois de entrar com o novo usuário administrativo, selecione o link Lista de pizzas no cabeçalho.

    O usuário administrativo pode criar e excluir pizzas.

  9. Selecione o link Administradores no cabeçalho.

    A página AdminsOnly é exibida.

Examinar a tabela AspNetUserClaims

Usando a extensão do 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 Tipo de reivindicação ClaimValue
admin@contosopizza.com IsAdmin True

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, você modificou o aplicativo para armazenar declarações e aplicar políticas de acesso condicional.