Esercitazione: Proteggere un'API Web ASP.NET Core registrata in un tenant esterno

Questa serie di esercitazioni illustra come proteggere un'API Web registrata nel tenant esterno. In questa esercitazione si creerà un'API Web ASP.NET Core che pubblica autorizzazioni delegate (ambiti) e autorizzazioni dell'applicazione (ruoli dell'app).

In questa esercitazione;

  • Configurare l'API Web per l'uso dei dettagli di registrazione dell'app
  • Configurare l'API Web per l'uso delle autorizzazioni delegate e dell'applicazione registrate nella registrazione dell'app
  • Proteggere gli endpoint dell'API Web

Prerequisiti

  • Registrazione API che espone almeno un ambito (autorizzazioni delegate) e un ruolo dell'app (autorizzazione dell'applicazione), ad esempio ToDoList.Read. Se non è già stato fatto, registrare un'API nell'interfaccia di amministrazione di Microsoft Entra seguendo la procedura di registrazione. Assicurarsi di disporre degli elementi seguenti:

    • ID applicazione (client) dell'API Web
    • ID directory (tenant) dell'API Web registrato
    • Sottodominio della directory (tenant) in cui è registrata l'API Web. Ad esempio, se il dominio primario è contoso.onmicrosoft.com, il sottodominio Directory (tenant) è contoso.
    • ToDoList.Read e ToDoList.ReadWrite come autorizzazioni delegate (ambiti) esposte dall'API Web.
    • ToDoList.Read.All e ToDoList.ReadWrite.All come autorizzazioni dell'applicazione (ruoli dell'app) esposte dall'API Web.
  • .NET 7.0 SDK o versione successiva.

  • Visual Studio Code o un altro editor di codice.

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.Web semplifica l'aggiunta del supporto di autenticazione e autorizzazione alle app Web e alle API Web che si integrano con 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 un ruolo e un 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 almeno un ruolo dell'app per le applicazioni, detta 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 uno 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 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 come servizio con ToDoContext ambito nel provider di servizi dell'applicazione core di ASP.NET (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, azioni Create, Read, Update e Delete (CRUD). In questa esercitazione vengono creati solo due elementi di azione. Elemento di lettura di tutte le azioni 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 lastra 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. L'attenzione qui non riguarda la compilazione dell'API, ma piuttosto 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 assicurarsi 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 utente e un'app. Un token di accesso rilasciato da Microsoft Entra per ID esterno ha almeno una delle due attestazioni. I token di accesso emessi 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 emessi 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 rispettati.

    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 è l'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 di autorizzazione per proteggere le route. Proteggere l'API aggiungendo l'attributo [Authorize] alla classe controller. In questo modo, le azioni del controller possono 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. Eseguire questa operazione usando 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);
    }
    

Esecuzione dell'API

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

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

Passaggio successivo