Condividi tramite


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

Contenuto dell'esercitazione;

  • Configurare l'API web in modo che utilizzi i dati di registrazione dell'applicazione
  • Configurare l'API web per utilizzare le autorizzazioni delegate e quelle dell'applicazione registrate nella registrazione dell'app
  • Proteggere gli endpoint dell'API Web

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

Usare un dominio URL personalizzato (facoltativo)

Usare un dominio personalizzato per personalizzare completamente l'URL di autenticazione. Dal punto di vista dell'utente, gli utenti rimangono nel dominio durante il processo di autenticazione, anziché essere reindirizzati a ciamlogin.com nome di dominio.

Per usare un dominio personalizzato, seguire questa procedura:

  1. Usare la procedura descritta in Abilitare domini URL personalizzati per le app nei tenant esterni per abilitare il dominio URL personalizzato per il tenant esterno.

  2. Aprire il file appsettings.json.

    1. Aggiornare il valore della proprietà da Instance a https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here. Sostituire Enter_the_Custom_Domain_Here con il dominio URL personalizzato e Enter_the_Tenant_ID_Here con l'ID tenant. Se non si ha l’ID del tenant, vedere come leggere i dettagli del tenant.
    2. Aggiungere una proprietà knownAuthorities con un valore [Enter_the_Custom_Domain_Here].

Dopo aver apportato le modifiche al file appsettings.json, se il dominio URL personalizzato è login.contoso.com e l'ID tenant è aaaabbbbbb-0000-cccc-1111-dddd2222eee, allora il file sarà simile al frammento di codice seguente:

{
    "AzureAd": {
        "Instance": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
        "TenantId": "Enter_the_Tenant_Id_Here",
        "ClientId": "Enter_the_Application_Id_Here",
        "KnownAuthorities": ["login.contoso.com"]
    },
    "Logging": {...},
  "AllowedHosts": "*"
}

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, detto anche autorizzazione dell'applicazione, affinché le app client ottengano un token di accesso come se stessi, ovvero quando non effettuano il login di 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 Modelli 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 ToDoContext come servizio con 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 Crea, Leggi, Aggiorna e Elimina (CRUD). In questa esercitazione vengono creati solo due elementi di azione. Un elemento di lettura di tutte le azioni e un elemento di azione di creazione per illustrare come proteggere gli endpoint.

  1. Passare alla cartella Controller 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 della 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 delle 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 attestazione idtyp è il modo più semplice per l'API di 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 attestazioni roles 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 External ID 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 il Certificato di sviluppo di NET.

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

Passaggio successivo