Esercizio - Usare le attestazioni con l'autorizzazione basata su criteri

Completato

Nell'unità precedente si è appresa la differenza tra autenticazione e autorizzazione. Si è anche osservato in che modo le attestazioni vengono usate dai criteri per l'autorizzazione. In questa unità si userà l'identità per archiviare le attestazioni e applicare i criteri per l'accesso condizionale.

Proteggere l'elenco delle pizze

È stato ricevuto un nuovo requisito in base al quale la pagina Elenco delle pizze deve essere visibile solo agli utenti autenticati. Inoltre, solo gli amministratori sono autorizzati a modificare, creare ed eliminare le pizze. Si vedrà ora come bloccarlo.

  1. In Pages/Pizza.cshtml.cs applicare le modifiche seguenti:

    1. Aggiungere un attributo [Authorize] alla classe PizzaModel.

      [Authorize]
      public class PizzaModel : PageModel
      

      L'attributo descrive i requisiti di autorizzazione utente per la pagina. In questo caso, non sono previsti requisiti oltre all'autenticazione dell'utente. Gli utenti anonimi non sono autorizzati a visualizzare la pagina e vengono reindirizzati alla pagina di accesso.

    2. Risolvere il riferimento a Authorize aggiungendo la riga seguente alle direttive using all'inizio del file:

      using Microsoft.AspNetCore.Authorization;
      
    3. Aggiungere la proprietà seguente alla classe PizzaModel:

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

      Il codice precedente determina se l'utente autenticato ha un'attestazione IsAdmin con un valore True. Il codice ottiene informazioni sull'utente autenticato dal HttpContext nella classe padre PageModel. Il risultato di questa valutazione è accessibile tramite una proprietà di sola lettura denominata IsAdmin.

    4. Aggiungere if (!IsAdmin) return Forbid(); all'inizio di entrambi i metodi OnPost 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");
      }
      

      Nel passaggio successivo verranno nascosti gli elementi dell'interfaccia utente di creazione/eliminazione per gli utenti non amministratori. Ciò non impedisce a un antagonista con uno strumento come HttpRepl o curl di accedere direttamente a questi endpoint. L'aggiunta di questo controllo garantisce che, in caso di tentativo, venga restituito un codice di stato HTTP 403.

  2. In Pages/Pizza.cshtml aggiungere i controlli per nascondere gli elementi dell'interfaccia utente amministratore agli utenti non amministratori:

    Nascondi modulo Nuova 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>
    }
    

    Nascondi pulsante Elimina 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>
    

    In seguito alle modifiche precedenti, il rendering degli elementi dell'interfaccia utente che devono essere accessibili solo agli amministratori verrà eseguito solo quando l'utente autenticato è un amministratore.

Applicare un criterio di autorizzazione

È opportuno bloccare un altro elemento. Esiste una pagina che deve essere accessibile solo agli amministratori, denominata Pages/AdminsOnly.cshtml. Si procederà quindi alla creazione di un criterio per controllare l'attestazione IsAdmin=True.

  1. In Program.cs apportare le modifiche seguenti:

    1. Incorporare il codice evidenziato seguente:

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

      Il codice precedente definisce un criterio di autorizzazione denominato Admin. Il criterio richiede che l'utente sia autenticato e abbia un'attestazione IsAdmin impostata su True.

    2. Modificare la chiamata a AddRazorPages come indicato di seguito:

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

      La chiamata al metodo AuthorizePage protegge la route della pagina Razor /AdminsOnly applicando il criterio Admin. Agli utenti autenticati che non soddisfano i requisiti del criterio viene mostrato un messaggio di accesso negato.

      Suggerimento

      In alternativa, è possibile modificare AdminsOnly.cshtml.cs. In tal caso, si aggiungerà [Authorize(Policy = "Admin")] come attributo nella classe AdminsOnlyModel. Un vantaggio dell'approccio di AuthorizePage illustrato sopra sta nel fatto che la pagina Razor protetta non richiede alcuna modifica. L'aspetto relativo all'autorizzazione è invece gestito in Program.cs.

  2. In Pages/Shared/_Layout.cshtml incorporare le modifiche seguenti:

    <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 modifica precedente nasconde in modo condizionale il collegamento Admin nell'intestazione se l'utente non è un amministratore. Usa la proprietà Context della classe RazorPage per accedere al HttpContext contenente le informazioni sull'utente autenticato.

Aggiungere l'attestazione IsAdmin a un utente

Per determinare quali utenti devono ottenere l'attestazione IsAdmin=True, l'app si baserà su un indirizzo di posta elettronica confermato per identificare l'amministratore.

  1. In appsettings.json aggiungere la proprietà evidenziata:

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

    Sarà l'indirizzo di posta elettronica confermato a cui viene assegnata l'attestazione.

  2. In Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs apportare le modifiche seguenti:

    1. Incorporare il codice evidenziato seguente:

      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 modifica precedente modifica il costruttore per ricevere un oggetto IConfiguration dal contenitore IoC. L'oggetto IConfiguration contiene i valori di appsettings.json e viene assegnato a una proprietà di sola lettura denominata Configuration.

    2. Applicare le modifiche evidenziate al metodo 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();
      }
      

      Nel codice precedente:

      • La stringa AdminEmail viene letta dalla proprietà Configuration e assegnata a adminEmail.
      • L'operatore di coalescenza di valori Null ?? viene usato per assicurarsi adminEmail che sia impostato su string.Empty se non è presente alcun valore corrispondente in appsettings.json.
      • Se l'indirizzo di posta elettronica dell'utente viene confermato:
        • L'indirizzo dell'utente viene confrontato con adminEmail. Si usa string.Compare() per il confronto senza distinzione tra maiuscole e minuscole.
        • Viene richiamato il metodo AddClaimAsync della classe UserManager per salvare un'attestazione IsAdmin nella tabella AspNetUserClaims.
    3. Aggiungere il codice seguente all'inizio del file. Risolve i riferimenti alla classe Claim nel metodo OnGetAsync:

      using System.Security.Claims;
      

Testare le attestazioni amministratore

Eseguire un ultimo test per verificare la nuova funzionalità di amministratore.

  1. Assicurarsi di avere salvato tutte le modifiche.

  2. Eseguire l'app con dotnet run.

  3. Passare all'app e accedere con un utente esistente, se non è già stato eseguito l'accesso. Selezionare Pizza List (Elenco delle pizze) nell'intestazione. Si noti che all'utente non vengono mostrati elementi dell'interfaccia utente per eliminare o creare pizze.

  4. Nell'intestazione non è presente alcun collegamento Admins. Nella barra degli indirizzi del browser passare direttamente alla pagina AdminsOnly. Sostituire /Pizza nell'URL con /AdminsOnly.

    L'utente non è autorizzato a passare alla pagina. Viene visualizzato un messaggio di accesso negato.

  5. Selezionare Logout (Disconnetti).

  6. Registrare un nuovo utente con l'indirizzo admin@contosopizza.com.

  7. Come in precedenza, confermare l'indirizzo di posta elettronica del nuovo utente ed eseguire l'accesso.

  8. Una volta eseguito l'accesso con il nuovo utente amministratore, fare clic sul collegamento Pizza List (Elenco delle pizze) nell'intestazione.

    L'utente amministratore può creare ed eliminare pizze.

  9. Fare clic sul collegamento Admins nell'intestazione.

    Verrà visualizzata la pagina AdminsOnly.

Esaminare la tabella AspNetUserClaims

Usando l'estensione SQL Server in VS Code, eseguire la query seguente:

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

Verrà visualizzata una scheda con risultati simili ai seguenti:

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

L'attestazione IsAdmin viene archiviata come coppia chiave-valore nella tabella AspNetUserClaims. Il record AspNetUserClaims è associato al record utente nella tabella AspNetUsers.

Riepilogo

In questa unità è stata modificata l'app per archiviare le attestazioni e applicare i criteri per l'accesso condizionale.