Tutoriel : Sécuriser une API web ASP.NET Core inscrite dans un locataire externe

Cette série de tutoriels montre comment sécuriser une API web inscrite dans le locataire externe. Dans ce tutoriel, vous allez créer une API web ASP.NET Core qui publie à la fois des autorisations déléguées (étendues) et des autorisations d’application (rôles d’applications).

Dans ce tutoriel,

  • Configurez votre API web pour qu’elle utilise les détails de l’inscription d’applications
  • Configurez votre API web pour qu’elle utilise les autorisations déléguées et les autorisations d’application enregistrées dans l’inscription de l’application
  • Protéger vos points de terminaison d’API web

Prérequis

Créer une API web ASP.NET Core

  1. Ouvrez votre terminal, puis accédez au dossier où vous voulez que votre projet soit actif.

  2. Exécutez les commandes suivantes :

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Quand une boîte de dialogue vous demande si vous souhaitez ajouter les composants nécessaires au projet, sélectionnez Oui.

Installer des packages

Installez les packages suivants :

  • Microsoft.EntityFrameworkCore.InMemory qui permet d’utiliser Entity Framework Core avec une base de données en mémoire. Il n’est pas conçu pour une utilisation en production.
  • Microsoft.Identity.Web simplifie l’ajout de la prise en charge de l’authentification et de l’autorisation dans les applications qui intègrent la plateforme d’identité Microsoft.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Configurer les détails de l’inscription d’application

Ouvrez le fichier appsettings.json dans votre dossier d’application et ajoutez les détails d’inscription d’application que vous avez enregistrés après l’inscription de votre 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": "*"
}

Remplacez les espaces réservés suivants, comme indiqué :

  • Remplacez Enter_the_Application_Id_Here par votre ID d’application (client).
  • Remplacez Enter_the_Tenant_Id_Here par l’ID d’annuaire (locataire).
  • Remplacez Enter_the_Tenant_Subdomain_Here par votre sous-domaine de répertoire (locataire).

Ajouter un rôle et une étendue d’application

Toutes les API doivent publier au moins une étendue, également appelée Permission déléguée, pour que les applications clientes obtiennent un jeton d’accès pour un utilisateur. Les API doivent également publier au moins un rôle d’application pour les applications, également appelé Autorisation d’application, pour que les applications clientes obtiennent un jeton d’accès comme elles-mêmes, c’est-à-dire lorsqu’elles ne connectent pas un utilisateur.

Nous spécifions ces autorisations dans le fichier appsettings.json. Dans ce tutoriel, nous avons inscrit quatre autorisations. ToDoList.ReadWrite et ToDoList.Read comme autorisations déléguées, et ToDoList.ReadWrite.All et ToDoList.Read.All comme autorisations d’application.

{
  "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": "*"
}

Ajouter un schéma d'authentification

Un schéma d’authentification est nommé lorsque le service d’authentification est configuré pendant l’authentification. Dans cet article, nous utilisons le schéma d’authentification du porteur JWT. Ajoutez le code suivant dans le fichier Programs.cs pour ajouter un schéma d’authentification.

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

Créer vos modèles

Créez un dossier nommé Modèles dans le dossier racine du projet. Naviguez jusqu’au dossier et créez un fichier nommé ToDo.cs et ajoutez le code suivant. Ce code crée un modèle appelé 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;
}

Ajouter un contexte de base de données

Le contexte de base de données est la classe principale qui coordonne les fonctionnalités d’Entity Framework pour un modèle de données. Cette classe est créée en dérivant de la classe Microsoft.EntityFrameworkCore.DbContext . Dans ce tutoriel, nous utilisons une base de données en mémoire à des fins de test.

  1. Créez un dossier nommé DbContext dans le dossier racine du projet.

  2. Accédez à ce dossier et créez un fichier appelé ToDoContext.cs, puis ajoutez le contenu suivant à ce fichier :

    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. Ouvrez le fichier Program.cs dans le dossier racine de votre application, puis ajoutez le code suivant dans le fichier. Ce code enregistre une sous-classe DbContext appelée ToDoContext en tant que service délimité dans le fournisseur de services d’application ASP.NET Core (également appelé le conteneur d’injection de dépendances). Le contexte est configuré pour utiliser la base de données en mémoire.

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

Ajouter des contrôleurs

Dans la plupart des cas, un contrôleur dispose de plus d'une action. En règle générale, les actions Créer, Lire, Mettre à jour et Supprimer (CRUD). Dans ce tutoriel, nous ne créons que deux éléments d’action. Un élément d’action lire tout et un élément d’action créer pour montrer comment protéger vos points de terminaison.

  1. Accédez au dossier Controllers dans le dossier racine de votre projet.

  2. Créez un fichier appelé ToDoListController.cs dans ce dossier. Ouvrez le fichier, puis ajoutez le code réutilisable suivant :

    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(){...}
    }
    

Ajouter du code au contrôleur

Dans cette section, nous ajoutons du code aux espaces réservés que nous avons créés. La priorité ici ne concerne pas la création de l'API, mais plutôt sa protection.

  1. Importez les packages nécessaires. Le package Microsoft.Identity.Web est un wrapper MSAL qui nous aide à gérer facilement la logique d’authentification, par exemple en gérant la validation des jetons. Pour nous assurer que nos points de terminaison nécessitent une autorisation, nous utilisons le package intégré Microsoft.AspNetCore.Authorization.

  2. Dans la mesure où nous avons accordé des autorisations pour que cette API soit appelée à l'aide d'autorisations déléguées au nom de l'utilisateur ou d'autorisations d'application permettant au client d'appeler en son propre nom plutôt qu'au nom de l'utilisateur, il est important de déterminer si l'appel est effectué par l'application en son propre nom. La manière la plus simple de le faire est les revendications afin de déterminer si le jeton d'accès contient la revendication facultative idtyp. Cette revendication idtyp constitue la façon la plus simple pour une API de déterminer si un jeton est un jeton d’application ou un jeton d’application + un jeton d’utilisateur. Nous recommandons d’activer la revendication facultative idtyp.

    Si la revendication idtyp n’est pas activée, vous pouvez utiliser les revendications roles et scp pour déterminer si le jeton d’accès est un jeton d’application ou un jeton d’application + un jeton d’utilisateur. Un jeton d’accès émis par l’ID externe Microsoft Entra a au moins l’une des deux revendications. Les jetons d’accès émis pour un utilisateur ont la revendication scp. Les jetons d’accès émis pour une application ont la revendication roles. Les jetons d’accès qui contiennent les deux revendications sont émis uniquement aux utilisateurs, où la revendication scp désigne les permissions déléguées, tandis que la revendication roles désigne le rôle de l’utilisateur. Les jetons d’accès qui n’ont ni l’un ni l’autre ne doivent pas être honorés.

    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. Ajoutez une fonction d’assistance permettant de déterminer si la requête en cours contient suffisamment d’autorisations pour effectuer l’action prévue. Vérifiez la validité de l'identifiant de l'utilisateur pour déterminer si l'application émet la requête en son propre nom ou si elle émet l'appel au nom d'un utilisateur détenteur de la ressource en question.

    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. Insérez vos définitions d’autorisation pour protéger les itinéraires. Protégez votre API en ajoutant l’attribut [Authorize] à la classe de contrôleur. Ceci garantit la possibilité d'appeler les actions du contrôleur uniquement si l'API est invoquée à l'aide d'une identité autorisée. Les définitions d’autorisations définissent les types d’autorisations nécessaires pour effectuer ces actions.

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

    Ajoutez des autorisations au point de terminaison GET all et au point de terminaison POST. Pour cela, utilisez la méthode RequiredScopeOrAppPermission appartenant à l’espace de noms Microsoft.Identity.Web.Resource. Transmettez ensuite les étendues et autorisations à cette méthode via les attributs RequiredScopesConfigurationKey et 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);
    }
    

Exécutez votre API

Exécutez votre API pour garantir une exécution correcte et sans erreur à l'aide de la commande dotnet run. Si vous envisagez d’utiliser le protocole HTTPS même lors des tests, vous devez approuver le certificat de développement .NET.

Pour obtenir un exemple complet de ce code d’API, consultez le fichier d’exemples.

Étape suivante