Tutorial: Protección de una API web de ASP.NET Core registrada en un inquilino externo

En esta serie de tutoriales se muestra cómo proteger una API web registrada en el inquilino externo. En este tutorial, construirá una API web ASP.NET Core que publique tanto permisos delegados (ámbitos) como permisos de aplicación (roles de aplicación).

En este tutorial,

  • Configuración de la API web para usar sus detalles de registro de aplicaciones
  • Configuración de la API web para usar permisos delegados y de aplicación registrados en el registro de aplicaciones
  • Proteja los puntos de conexión de su API web

Requisitos previos

Creación de una API web de ASP.NET Core

  1. Abra el terminal y vaya a la carpeta donde desea tener el proyecto.

  2. Ejecute los comandos siguientes:

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Cuando en un cuadro de diálogo se le pregunte si quiere agregar al proyecto los recursos necesarios, seleccione .

Instalar paquetes

Instale los siguientes paquetes:

  • Microsoft.EntityFrameworkCore.InMemory, que permite usar Entity Framework Core con una base de datos en memoria. No está diseñado para usarlo en producción.
  • Microsoft.Identity.Web simplifica la adición de funcionalidad de autenticación y autorización a las aplicaciones y API web que se integran con la Plataforma de identidad de Microsoft.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Configuración de los detalles de registro de una aplicación

Abra el archivo appsettings.json en la carpeta de la aplicación y agregue los detalles de registro de la aplicación que registró después de registrar la 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": "*"
}

Reemplace los siguientes marcadores de posición como se indica:

  • Reemplace Enter_the_Application_Id_Here por id. de la aplicación (cliente).
  • Reemplace Enter_the_Tenant_Id_Here por el id. del directorio (inquilino).
  • Reemplace Enter_the_Tenant_Subdomain_Here por el subdominio del directorio (inquilino).

Adición del rol y el ámbito de la aplicación

Todas las API deben publicar al menos un ámbito, también denominado permiso delegado, para que las aplicaciones cliente obtengan un token de acceso para un usuario. Las API también deben publicar un mínimo de un rol para las aplicaciones, también denominado permiso de aplicación, para que las aplicaciones cliente obtengan un token de acceso como ellas mismas, es decir, sin que inicie sesión un usuario.

Especificamos estos permisos en el archivo appsettings.json. En este tutorial, hemos registrado cuatro permisos. ToDoList.ReadWrite y ToDoList.Read como permisos delegados, y ToDoList.ReadWrite.All y ToDoList.Read.All como permisos de aplicación.

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

Adición de un esquema de autenticación

Un esquema de autenticación se determina cuando el servicio de autenticación se configura durante la autenticación. En este artículo, se usa el esquema de autenticación de portador JWT. Agregue el siguiente código al archivo Programs.cs para agregar un esquema de autenticación.

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

Creación de los modelos

Cree una carpeta denominada Models en la carpeta raíz del proyecto. Vaya a la carpeta, cree un archivo denominado ToDo.cs y agregue el siguiente código. Este código crea un modelo denominado 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;
}

Incorporación de un contexto de base de datos

El contexto de base de datos es la clase principal que coordina la funcionalidad de Entity Framework para un modelo de datos. Esta clase se deriva de la clase Microsoft.EntityFrameworkCore.DbContext. En este tutorial usamos una base de datos en memoria para realizar pruebas.

  1. Cree una nueva carpeta denominada DbContext en la carpeta raíz del proyecto.

  2. Vaya a esa carpeta y cree un archivo denominado ToDoContext.cs y agregue el siguiente contenido a ese archivo:

    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. Abra el archivo Program.cs en la carpeta raíz de la aplicación y agregue el código siguiente en el archivo. En este código se registra una subclase DbContext denominada ToDoContext como un servicio con ámbito en el proveedor de servicios de aplicación de ASP.NET Core (también conocido como el contenedor de inserción de dependencias). El contexto está configurado para usar la base de datos en memoria.

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

Agregar controladores

En la mayoría de los casos, un controlador tendría más de una acción. Normalmente, las acciones Crear, Leer, Actualizar y Eliminar (CRUD). En este tutorial, solo se crean dos elementos de acción. Un elemento de acción para leer todo y un elemento de acción de creación para demostrar cómo proteger los puntos de conexión.

  1. Vaya a la carpeta Controladores en la carpeta raíz del proyecto.

  2. Cree un archivo llamado ToDoListController.cs dentro de esta carpeta. Abra el archivo y agregue el siguiente código boilerplate:

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

Agregar código al controlador

En esta sección, agregamos código a los marcadores de posición que hemos creado. El enfoque aquí no se centra en la compilación de la API, sino en su protección.

  1. Importe los paquetes necesarios. El paquete Microsoft.Identity.Web es un contenedor MSAL que nos ayuda a controlar fácilmente la lógica de autenticación, por ejemplo, mediante el control de la validación de tokens. Para asegurarse de que nuestros puntos de conexión requieren autorización, usamos el paquete Microsoft.AspNetCore.Authorization integrado.

  2. Dado que se han concedido permisos para llamar a esta API mediante permisos delegados en nombre del usuario o permisos de aplicación en los que el cliente llama como sí mismo y no en nombre del usuario, es importante saber si la aplicación realiza la llamada en su propio nombre. La manera más sencilla de hacerlo es que las notificaciones averigüen si el token de acceso contiene la notificación opcional idtyp. Esta notificación idtyp es la forma más sencilla para que una API determine si un token es un token de aplicación o un token de usuario + aplicación. Se recomienda habilitar la notificación opcional idtyp.

    Si la notificación idtyp no está habilitada, puede usar las notificaciones roles y scp para determinar si el token de acceso es un token de aplicación o un token de usuario + aplicación. Un token de acceso emitido por el id. externo de Microsoft Entra tiene al menos una de las dos notificaciones. Los tokens de acceso emitidos para un usuario tienen la notificación scp. Los tokens de acceso emitidos para una aplicación tienen la notificación roles. Los tokens de acceso que contienen ambas notificaciones solo se emiten a los usuarios, donde la notificación scp designa los permisos delegados, mientras que la notificación roles designa el rol del usuario. No se respetan los tokens de acceso que no tienen ninguno.

    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. Agregue una función auxiliar que determine si la solicitud que se realiza contiene suficientes permisos para llevar a cabo la acción prevista. Compruebe si es la aplicación la que realiza la solicitud en su propio nombre o si la aplicación está realizando la llamada en nombre de un usuario propietario del recurso en cuestión mediante la validación del identificador de usuario.

    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. Conecte las definiciones de permisos para proteger las rutas. Proteja la API agregando el atributo [Authorize] a la clase de controlador. Se garantiza así que solo se pueda llamar a las acciones de controlador si se llama a la API con una identidad autorizada. Las definiciones de permisos definen qué tipos de permisos son necesarios para realizar estas acciones.

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

    Agregue permisos al punto de conexión GET y al punto de conexión POST. Para ello, use el método RequiredScopeOrAppPermission que forma parte del espacio de nombres Microsoft.Identity.Web.Resource. A continuación, pase los ámbitos y permisos a este método a través de los atributos RequiredScopesConfigurationKey y 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);
    }
    

Ejecución de la API

Ejecute la API para asegurarse de que se ejecuta correctamente sin errores mediante el comando dotnet run. Si piensa usar el protocolo HTTPS incluso durante las pruebas, debe confiar en el certificado de desarrollo de .NET.

Para obtener un ejemplo completo de este código de API, consulte el archivo de ejemplos.

Paso siguiente