Esercitazione: Proteggere un'API Web ASP.NET registrata in un tenant del cliente

Le API Web possono contenere informazioni che richiedono l'autenticazione e l'autorizzazione dell'utente. Le applicazioni possono usare l'accesso delegato, che agisce per conto di un utente connesso o di accesso solo app, fungendo solo da identità dell'applicazione quando si chiamano API Web protette.

In questa esercitazione viene creata un'API Web che pubblica autorizzazioni delegate (ambiti) e autorizzazioni dell'applicazione (ruoli dell'app). Le app client, ad esempio le app Web che acquisiscono token per conto di un utente connesso, usano le autorizzazioni delegate. Le app client, ad esempio le app daemon che acquisiscono i token per se stessi, usano le autorizzazioni dell'applicazione.

In questa esercitazione verranno illustrate le procedure per:

Configurare l'API Web tp use it's app registration details Configure your Web API to use delegated and application permissions registered in the app registration Protect your web API endpoints

Prerequisiti

Creare un'API Web ASP.NET Core

  1. Aprire il terminale, quindi passare alla cartella in cui si vuole che il progetto sia attivo.

  2. Eseguire i comandi seguenti:

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Quando una finestra di dialogo chiede se si vuole aggiungere gli asset necessari al progetto, selezionare .

Installare i pacchetti

Installare i pacchetti seguenti:

  • Microsoft.EntityFrameworkCore.InMemory che consente l'uso di Entity Framework Core con un database in memoria. Non è progettato per l'uso in produzione.
  • Microsoft.Identity.Websemplifica l'aggiunta del supporto per l'autenticazione e l'autorizzazione alle app Web e alle API Web che si integrano con il Microsoft Identity Platform.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Configurare i dettagli di registrazione dell'app

Aprire il file appsettings.json nella cartella dell'app e aggiungere i dettagli di registrazione dell'app registrati dopo la registrazione dell'API Web.

{
    "AzureAd": {
        "Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
        "TenantId": "Enter_the_Tenant_Id_Here",
        "ClientId": "Enter_the_Application_Id_Here",
    },
    "Logging": {...},
  "AllowedHosts": "*"
}

Sostituire i segnaposto seguenti come illustrato:

  • Sostituire Enter_the_Application_Id_Here con l'ID applicazione (client).
  • Sostituire Enter_the_Tenant_Id_Here con l'ID directory (tenant).
  • Sostituire Enter_the_Tenant_Subdomain_Here con il sottodominio Directory (tenant).

Aggiungere il ruolo e l'ambito dell'app

Tutte le API devono pubblicare almeno un ambito, detto anche autorizzazione delegata, affinché le app client ottengano correttamente un token di accesso per un utente. Le API devono anche pubblicare un minimo di un ruolo dell'app per le applicazioni, detto anche autorizzazione dell'applicazione, affinché le app client ottengano un token di accesso come se stessi, ovvero quando non accedono a un utente.

Queste autorizzazioni vengono specificate nel file appsettings.json . In questa esercitazione sono state registrate quattro autorizzazioni. ToDoList.ReadWrite e ToDoList.Read come autorizzazioni delegate e ToDoList.ReadWrite.All e ToDoList.Read.All come autorizzazioni dell'applicazione.

{
  "AzureAd": {
    "Instance": "https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/",
    "TenantId": "Enter_the_Tenant_Id_Here",
    "ClientId": "Enter_the_Application_Id_Here",
    "Scopes": {
      "Read": ["ToDoList.Read", "ToDoList.ReadWrite"],
      "Write": ["ToDoList.ReadWrite"]
    },
    "AppPermissions": {
      "Read": ["ToDoList.Read.All", "ToDoList.ReadWrite.All"],
      "Write": ["ToDoList.ReadWrite.All"]
    }
  },
  "Logging": {...},
  "AllowedHosts": "*"
}

Aggiungere lo schema di autenticazione

Uno schema di autenticazione viene denominato quando il servizio di autenticazione viene configurato durante l'autenticazione. In questo articolo viene usato lo schema di autenticazione del bearer JWT. Aggiungere il codice seguente nel file Programs.cs per aggiungere uno schema di autenticazione.

// Add the following to your imports
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

// Add authentication scheme
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration);

Creare i modelli

Creare una cartella denominata Models nella cartella radice del progetto. Passare alla cartella e creare un file denominato ToDo.cs e quindi aggiungere il codice seguente. Questo codice crea un modello denominato ToDo.

using System;

namespace ToDoListAPI.Models;

public class ToDo
{
    public int Id { get; set; }
    public Guid Owner { get; set; }
    public string Description { get; set; } = string.Empty;
}

Aggiungere un contesto di database

Il contesto di database è la classe principale che coordina le funzionalità di Entity Framework per un modello di dati. Questa classe viene creata derivando dalla classe Microsoft.EntityFrameworkCore.DbContext . In questa esercitazione viene usato un database in memoria a scopo di test.

  1. Creare una cartella denominata DbContext nella cartella radice del progetto.

  2. Passare a tale cartella e creare un file denominato ToDoContext.cs , quindi aggiungere il contenuto seguente al file:

    using Microsoft.EntityFrameworkCore;
    using ToDoListAPI.Models;
    
    namespace ToDoListAPI.Context;
    
    public class ToDoContext : DbContext
    {
        public ToDoContext(DbContextOptions<ToDoContext> options) : base(options)
        {
        }
    
        public DbSet<ToDo> ToDos { get; set; }
    }
    
  3. Aprire il file Program.cs nella cartella radice dell'app, quindi aggiungere il codice seguente nel file. Questo codice registra una DbContext sottoclasse denominata ToDoContext come servizio con ambito nel provider di servizi dell'applicazione ASP.NET Core ( noto anche come contenitore di inserimento delle dipendenze). Il contesto è configurato per l'uso del database in memoria.

    // Add the following to your imports
    using ToDoListAPI.Context;
    using Microsoft.EntityFrameworkCore;
    
    builder.Services.AddDbContext<ToDoContext>(opt =>
        opt.UseInMemoryDatabase("ToDos"));
    

Aggiungere controller

Nella maggior parte dei casi, un controller avrà più di un'azione. In genere, le azioni Create, Read, Update e Delete (CRUD). In questa esercitazione vengono creati solo due elementi azione. Un elemento di azione leggi tutto e un elemento di azione di creazione per illustrare come proteggere gli endpoint.

  1. Passare alla cartella Controllers nella cartella radice del progetto.

  2. Creare un file denominato ToDoListController.cs all'interno di questa cartella. Aprire il file e quindi aggiungere il codice della piastra caldaia seguente:

    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Identity.Web;
    using Microsoft.Identity.Web.Resource;
    using ToDoListAPI.Models;
    using ToDoListAPI.Context;
    
    namespace ToDoListAPI.Controllers;
    
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoListController : ControllerBase
    {
        private readonly ToDoContext _toDoContext;
    
        public ToDoListController(ToDoContext toDoContext)
        {
            _toDoContext = toDoContext;
        }
    
        [HttpGet()]
        [RequiredScopeOrAppPermission()]
        public async Task<IActionResult> GetAsync(){...}
    
        [HttpPost]
        [RequiredScopeOrAppPermission()]
        public async Task<IActionResult> PostAsync([FromBody] ToDo toDo){...}
    
        private bool RequestCanAccessToDo(Guid userId){...}
    
        private Guid GetUserId(){...}
    
        private bool IsAppMakingRequest(){...}
    }
    

Aggiungere codice al controller

In questa sezione viene aggiunto il codice ai segnaposto creati. Il focus qui non riguarda la creazione dell'API, ma la protezione.

  1. Importare i pacchetti necessari. Il pacchetto Microsoft.Identity.Web è un wrapper MSAL che consente di gestire facilmente la logica di autenticazione, ad esempio gestendo la convalida dei token. Per garantire che gli endpoint richiedano l'autorizzazione, viene usato il pacchetto predefinito Microsoft.AspNetCore.Authorization .

  2. Poiché sono state concesse autorizzazioni per questa API da chiamare usando autorizzazioni delegate per conto dell'utente o delle autorizzazioni dell'applicazione in cui il client chiama come se stesso e non per conto dell'utente, è importante sapere se la chiamata viene effettuata dall'app per proprio conto. Il modo più semplice per eseguire questa operazione è costituito dalle attestazioni per determinare se il token di accesso contiene l'attestazione idtyp facoltativa. Questa idtyp attestazione è il modo più semplice per l'API per determinare se un token è un token dell'app o un token app + utente. È consigliabile abilitare l'attestazione idtyp facoltativa.

    Se l'attestazione idtyp non è abilitata, è possibile usare le roles attestazioni e scp per determinare se il token di accesso è un token dell'app o un token app + utente. Un token di accesso rilasciato da Microsoft Entra per ID esterno ha almeno una delle due attestazioni. I token di accesso rilasciati a un utente hanno l'attestazione scp . I token di accesso rilasciati a un'applicazione hanno l'attestazione roles . I token di accesso che contengono entrambe le attestazioni vengono rilasciati solo agli utenti, in cui l'attestazione scp designa le autorizzazioni delegate, mentre l'attestazione roles designa il ruolo dell'utente. I token di accesso che non hanno nessuno dei due non devono essere onorati.

    private bool IsAppMakingRequest()
    {
        if (HttpContext.User.Claims.Any(c => c.Type == "idtyp"))
        {
            return HttpContext.User.Claims.Any(c => c.Type == "idtyp" && c.Value == "app");
        }
        else
        {
            return HttpContext.User.Claims.Any(c => c.Type == "roles") && !HttpContext.User.Claims.Any(c => c.Type == "scp");
        }
    }
    
  3. Aggiungere una funzione helper che determina se la richiesta effettuata contiene autorizzazioni sufficienti per eseguire l'azione desiderata. Controllare se si tratta dell'app che effettua la richiesta per proprio conto o se l'app effettua la chiamata per conto di un utente proprietario della risorsa specificata convalidando l'ID utente.

    private bool RequestCanAccessToDo(Guid userId)
        {
            return IsAppMakingRequest() || (userId == GetUserId());
        }
    
    private Guid GetUserId()
        {
            Guid userId;
            if (!Guid.TryParse(HttpContext.User.GetObjectId(), out userId))
            {
                throw new Exception("User ID is not valid.");
            }
            return userId;
        }
    
  4. Collegare le definizioni delle autorizzazioni per proteggere le route. Proteggere l'API aggiungendo l'attributo [Authorize] alla classe controller. Ciò garantisce che le azioni del controller possano essere chiamate solo se l'API viene chiamata con un'identità autorizzata. Le definizioni di autorizzazione definiscono i tipi di autorizzazioni necessari per eseguire queste azioni.

    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class ToDoListController: ControllerBase{...}
    

    Aggiungere autorizzazioni all'endpoint GET e all'endpoint POST. A tale scopo, utilizzare il metodo RequiredScopeOrAppPermission che fa parte dello spazio dei nomi Microsoft.Identity.Web.Resource . Si passano quindi ambiti e autorizzazioni a questo metodo tramite gli attributi RequiredScopesConfigurationKey e RequiredAppPermissionsConfigurationKey .

    [HttpGet]
    [RequiredScopeOrAppPermission(
        RequiredScopesConfigurationKey = "AzureAD:Scopes:Read",
        RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Read"
    )]
    public async Task<IActionResult> GetAsync()
    {
        var toDos = await _toDoContext.ToDos!
            .Where(td => RequestCanAccessToDo(td.Owner))
            .ToListAsync();
    
        return Ok(toDos);
    }
    
    [HttpPost]
    [RequiredScopeOrAppPermission(
        RequiredScopesConfigurationKey = "AzureAD:Scopes:Write",
        RequiredAppPermissionsConfigurationKey = "AzureAD:AppPermissions:Write"
    )]
    public async Task<IActionResult> PostAsync([FromBody] ToDo toDo)
    {
        // Only let applications with global to-do access set the user ID or to-do's
        var ownerIdOfTodo = IsAppMakingRequest() ? toDo.Owner : GetUserId();
    
        var newToDo = new ToDo()
        {
            Owner = ownerIdOfTodo,
            Description = toDo.Description
        };
    
        await _toDoContext.ToDos!.AddAsync(newToDo);
        await _toDoContext.SaveChangesAsync();
    
        return Created($"/todo/{newToDo!.Id}", newToDo);
    }
    

Eseguire l'API

Eseguire l'API per assicurarsi che funzioni correttamente senza errori usando il comando dotnet run. Se si intende usare il protocollo HTTPS anche durante il test, è necessario considerare attendibile . Certificato di sviluppo di NET.

Per un esempio completo di questo codice API, vedere il file di esempi.

Passaggi successivi