Tutorial: Schützen einer in einem externen Mandanten registrierten ASP.NET Core-Web-API

In dieser Tutorialreihe wird veranschaulicht, wie Sie eine registrierte Web-API im externen Mandanten schützen. In diesem Tutorial erstellen Sie eine ASP.NET Core-Web-API, die delegierte Berechtigungen für Bereiche und Anwendungsberechtigungen für App-Rollen veröffentlicht.

In diesem Tutorial:

  • Konfigurieren Sie Ihre Web-API, um ihre App-Registrierungsdetails zu verwenden.
  • Konfigurieren Sie Ihre Web-API für die Verwendung delegierter und Anwendungsberechtigungen, die in der App-Registrierung registriert sind
  • Schützen Sie Ihre Web-API-Endpunkte

Voraussetzungen

Erstellen einer ASP.NET Core-Web-API

  1. Öffnen Sie Ihr Terminal, und navigieren Sie zu dem Ordner, in dem sich Ihr Projekt befinden soll.

  2. Führen Sie die folgenden Befehle aus:

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Wenn Sie in einem Dialogfeld angeben müssen, ob Sie dem Projekt die erforderlichen Elemente hinzufügen möchten, wählen Sie Ja aus.

Installieren von Paketen

Installieren Sie die folgenden Pakete:

  • Microsoft.EntityFrameworkCore.InMemory ermöglicht die Verwendung von Entity Framework Core mit einem IMDB-Katalog. Es ist nicht für die Verwendung in der Produktion konzipiert.
  • Microsoft.Identity.Web vereinfacht das Hinzufügen von Authentifizierungs- und Autorisierungsunterstützung für Webanwendungen, die in die Microsoft Identity Platform integriert werden.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Konfigurieren von Anwendungsregistrierungsdetails

Öffnen Sie die Datei appsettings.json in Ihrem App-Ordner, und fügen Sie die App-Registrierungsdetails hinzu, die Sie nach der Registrierung Ihrer Web-API notiert haben.

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

Ersetzen Sie die folgenden Platzhalter wie gezeigt:

  • Ersetzen Sie Enter_the_Application_Id_Here durch Ihre Anwendungs-ID (Client-ID).
  • Ersetzen Sie Enter_the_Tenant_Id_Here durch Ihre Verzeichnis-ID (Mandanten-ID).
  • Ersetzen Sie Enter_the_Tenant_Subdomain_Here durch Ihre Unterdomäne des Verzeichnisses (des Mandanten).

Hinzufügen von Anwendungsrolle und -Bereich

Alle APIs müssen mindestens einen Bereich (auch „delegierte Berechtigung“ genannt) veröffentlichen, damit die Clientanwendungen erfolgreich ein Zugriffstoken für Benutzer*innen abrufen können. APIs sollten ebenfalls mindestens eine App-Rolle für Anwendungen (auch „Anwendungsberechtigung“ genannt) veröffentlichen, damit die Clientanwendungen im eigenen Namen ein Zugriffstoken abrufen können, d. h., wenn kein*e Benutzer*in angemeldet wird.

Wir geben diese Berechtigungen in der Datei appsettings.json an. Für dieses Tutorials haben wir vier Berechtigungen registriert. ToDoList.ReadWrite und ToDoList.Read delegierte Berechtigungen sowie ToDoList.ReadWrite.All und ToDoList.Read.All als Anwendungsberechtigungen.

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

Hinzufügen eines Authentifizierungsschemas

Wenn der Authentifizierungsdienst während der Authentifizierung konfiguriert wird, wird ein Authentifizierungsschema benannt. In diesem Artikel wird das JWT-Bearerauthentifizierungsschema verwendet. Fügen Sie den folgenden Code in der Datei Programs.cs hinzu, um das Authentifizierungsschema hinzuzufügen.

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

Erstellen Ihrer Modelle

Erstellen Sie einen Ordner namens Models im Stammordner Ihres Projekts. Erstellen Sie in diesem Ordner eine Datei namens ToDo.cs, und fügen Sie den folgenden Code hinzu. Dieser Code erstellt ein Modell namens 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;
}

Hinzufügen eines Datenbankkontexts

Der Datenbankkontext ist die Hauptklasse, die die Entity Framework-Funktionen für ein Datenmodell koordiniert. Diese Klasse wird durch Ableitung von der Microsoft.EntityFrameworkCore.DbContext-Klasse-Klasse erstellt. In diesem Tutorial verwenden wir einen IMDB-Katalog zu Testzwecken.

  1. Erstellen Sie im Stammordner des Projekts einen Ordner namens DbContext.

  2. Erstellen Sie in diesem Ordner eine Datei namens ToDoContext.cs, und fügen Sie der Datei dann den folgenden Inhalt hinzu:

    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. Öffnen Sie die Datei Program.cs im Stammordner Ihrer App, und fügen Sie dann den folgenden Code in der Datei hinzu. Mit diesem Code wird eine DbContext-Unterklasse namens ToDoContext als bereichsbezogener Dienst im ASP.NET Core-Anwendungsdienstanbieter registriert (auch als Container für Abhängigkeitsinjektion bezeichnet). Der Kontext ist für die Verwendung des IMDB-Katalogs konfiguriert.

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

Hinzufügen von Controllern

In den meisten Fällen verfügt der Controller über mehrere Aktionen. In der Regel sind dies CRUD-Aktionen (Create, Read, Update, Delete, d. h. Erstellen, Lesen, Aktualisieren, Löschen). In diesem Tutorial erstellen wir nur zwei Aktionselemente. Ein Aktionselement zum Lesen aller Elemente und ein Aktionselement zum Erstellen, um zu veranschaulichen, wie Sie Ihre Endpunkte schützen.

  1. Navigieren Sie zum Ordner Controller im Stammordner Ihres Projekts.

  2. Erstellen Sie in diesem Ordner eine Datei namens ToDoListController.cs. Öffnen Sie die Datei, und fügen Sie dann die folgenden Codebausteine hinzu:

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

Hinzufügen von Code zum Controller

In diesem Abschnitt fügen wir den von uns erstellten Platzhaltern Code hinzu. Hier geht es nicht darum, die API zu erstellen, sondern sie zu schützen.

  1. Importieren Sie die erforderlichen Pakete. Das Paket Microsoft.Identity.Web ist ein MSAL-Wrapper, der uns hilft, die Authentifizierungslogik einfach zu handhaben, z. B. mithilfe der Tokenüberprüfung. Um sicherzustellen, dass unsere Endpunkte eine Autorisierung benötigen, verwenden wir das integrierte Paket Microsoft.AspNetCore.Authorization.

  2. Da wir die Berechtigungen für den Aufruf dieser API entweder mithilfe von delegierten Berechtigungen im Namen des Benutzers oder mithilfe von Anwendungsberechtigungen erteilt haben, bei denen der Aufruf durch den Client selbst und nicht im Namen des Benutzers erfolgt, ist es wichtig zu wissen, ob der Aufruf von der App in ihrem eigenen Namen erfolgt. Die Ansprüche stellen die einfachste Methode dar. So kann ermittelt werden, ob das Zugriffstoken den optionalen Anspruch idtyp enthält. Der Anspruch idtyp ist der einfachste Weg, wie die API feststellen kann, ob ein Token ein App-Token oder ein App- und Benutzertoken ist. Es wird empfohlen, den optionalen Anspruch idtyp zu aktivieren.

    Wenn der Anspruch idtyp nicht aktiviert ist, können Sie die Ansprüche roles und scp verwenden, um zu bestimmen, ob es sich bei dem Zugriffstoken um ein App-Token oder um ein App- und Benutzertoken handelt. Ein von Microsoft Entra External ID ausgestelltes Zugriffstoken enthält mindestens einen der beiden Ansprüche. Für Benutzer*innen ausgestellte Zugriffstoken verfügen über den Anspruch scp. Für eine Anwendung ausgestellte Zugriffstoken verfügen über den Anspruch roles. Zugriffstoken, die beide Ansprüche enthalten, werden nur für Benutzer*innen ausgestellt, wobei der Anspruch scp die delegierten Berechtigungen festlegt, während der Anspruch roles die Rolle des Benutzers oder der Benutzerin festlegt. Zugriffstoken, die keinen der beiden Ansprüche enthalten, sind nicht zu berücksichtigen.

    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. Fügen Sie eine Hilfsfunktion hinzu, die feststellt, ob die gestellte Anforderung genügend Berechtigungen enthält, um die beabsichtigte Aktion auszuführen. Überprüfen Sie, ob die App die Anforderung im eigenen Namen stellt oder ob die App den Aufruf im Namen eines Benutzers oder einer Benutzerin tätigt, der*die Besitzer*in der betreffenden Ressource ist, indem wir die Benutzer-ID überprüfen.

    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. Fügen Sie die Berechtigungsdefinitionen ein, um Routen zu schützen. Schützen Sie Ihre API, indem Sie das Attribut [Authorize] zur Controllerklasse hinzufügen. Dadurch wird sichergestellt, dass die Controlleraktionen nur aufgerufen werden können, wenn die API mit einer autorisierten Identität aufgerufen wird. Die Berechtigungsdefinitionen legen fest, welche Arten von Berechtigungen zur Durchführung dieser Aktionen erforderlich sind.

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

    Fügen Sie Berechtigungen für den GET all-Endpunkt und den POST-Endpunkt hinzu. Dazu verwenden Sie die Methode RequiredScopeOrAppPermission, die Teil des Namespace Microsoft.Identity.Web.Resource ist. Anschließend übergeben Sie Bereiche und Berechtigungen über die Attribute RequiredScopesConfigurationKey und RequiredAppPermissionsConfigurationKey an diese Methode.

    [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);
    }
    

Ausführen Ihrer API

Führen Sie Ihre API aus, um sicherzustellen, dass sie mit dem Befehl dotnet run fehlerfrei ausgeführt wird. Wenn Sie das HTTPS-Protokoll auch während der Testphase verwenden möchten, müssen Sie dem Entwicklungszertifikat von .NET vertrauen.

Ein vollständiges Beispiel für diesen API-Code finden Sie in der Beispieldatei.

Nächster Schritt