Tutorial: Proteger uma API Web ASP.NET registada num inquilino do cliente

As APIs Web podem conter informações que requerem autenticação e autorização do utilizador. As aplicações podem utilizar o acesso delegado, agindo em nome de um utilizador com sessão iniciada ou acesso apenas à aplicação, agindo apenas como a própria identidade da aplicação ao chamar APIs Web protegidas.

Neste tutorial, criamos uma API Web que publica permissões delegadas (âmbitos) e permissões de aplicação (funções de aplicação). As aplicações cliente, como aplicações Web que adquirem tokens em nome de um utilizador com sessão iniciada, utilizam as permissões delegadas. As aplicações cliente, como aplicações daemon que adquirem tokens para si mesmas, utilizam as permissões da aplicação.

Neste tutorial, ficará a saber como:

Configure a sua API Web tp use it's app registration details Configure your Web API to use delegated and application permissions registered in the app registration Protect your Web API endpoints (Configurar a API Web para utilizar permissões delegadas e de aplicações registadas no registo de aplicações Proteger os pontos finais da API Web)

Pré-requisitos

Criar uma API Web ASP.NET Core

  1. Abra o terminal e, em seguida, navegue para a pasta onde pretende que o seu projeto esteja ativo.

  2. Execute os seguintes comandos:

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Quando uma caixa de diálogo perguntar se pretende adicionar recursos necessários ao projeto, selecione Sim.

Instalar pacotes

Instale os seguintes pacotes:

  • Microsoft.EntityFrameworkCore.InMemory que permite que o Entity Framework Core seja utilizado com uma base de dados dentro da memória. Não foi concebido para utilização de produção.
  • Microsoft.Identity.Websimplifica a adição de suporte de autenticação e autorização a aplicações Web e APIs Web integradas no plataforma de identidades da Microsoft.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Configurar detalhes do registo de aplicações

Abra o ficheiro appsettings.json na pasta da aplicação e adicione os detalhes de registo da aplicação que gravou após registar a 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": "*"
}

Substitua os seguintes marcadores de posição, conforme mostrado:

  • Substitua pelo Enter_the_Application_Id_Here ID da aplicação (cliente).
  • Substitua Enter_the_Tenant_Id_Here pelo ID do Diretório (inquilino).
  • Substitua Enter_the_Tenant_Subdomain_Here pelo subdomínio diretório (inquilino).

Adicionar função e âmbito da aplicação

Todas as APIs têm de publicar um mínimo de um âmbito, também denominado permissão delegada, para que as aplicações cliente obtenham um token de acesso para um utilizador com êxito. As APIs também devem publicar um mínimo de uma função de aplicação para aplicações, também denominada permissão de aplicação, para que as aplicações cliente obtenham um token de acesso como elas próprias, ou seja, quando não estão a iniciar sessão num utilizador.

Especificamos estas permissões no ficheiro appsettings.json . Neste tutorial, registámos quatro permissões. ToDoList.ReadWrite e ToDoList.Read como as permissões delegadas e ToDoList.ReadWrite.All e ToDoList.Read.All como as permissões da aplicação.

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

Adicionar esquema de autenticação

É atribuído um nome a um esquema de autenticação quando o serviço de autenticação é configurado durante a autenticação. Neste artigo, utilizamos o esquema de autenticação do portador JWT. Adicione o seguinte código no ficheiro Programs.cs para adicionar um esquema de autenticação.

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

Criar os seus modelos

Crie uma pasta denominada Modelos na pasta raiz do seu projeto. Navegue para a pasta e crie um ficheiro chamado ToDo.cs e, em seguida, adicione o seguinte código. Este código cria um modelo chamado 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;
}

Adicionar um contexto de base de dados

O contexto da base de dados é a classe principal que coordena a funcionalidade Entity Framework para um modelo de dados. Esta classe é criada ao derivar da classe Microsoft.EntityFrameworkCore.DbContext . Neste tutorial, utilizamos uma base de dados dentro da memória para fins de teste.

  1. Crie uma pasta denominada DbContext na pasta raiz do projeto.

  2. Navegue para essa pasta e crie um ficheiro chamado ToDoContext.cs e, em seguida, adicione os seguintes conteúdos a esse ficheiro:

    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 o ficheiro Program.cs na pasta raiz da sua aplicação e, em seguida, adicione o seguinte código no ficheiro. Este código regista uma DbContext subclasse denominada ToDoContext serviço de âmbito no fornecedor de serviços de aplicações ASP.NET Core (também conhecido como contentor de injeção de dependências). O contexto está configurado para utilizar a base de dados dentro da memória.

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

Adicionar controladores

Na maioria dos casos, um controlador teria mais do que uma ação. Normalmente, as ações Criar, Ler, Atualizar e Eliminar (CRUD). Neste tutorial, criamos apenas dois itens de ação. Leia todos os itens de ação e um item de ação de criação para demonstrar como proteger os seus pontos finais.

  1. Navegue para a pasta Controladores na pasta raiz do projeto.

  2. Crie um ficheiro chamado ToDoListController.cs dentro desta pasta. Abra o ficheiro e, em seguida, adicione o seguinte código de placa de caldeira:

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

Adicionar código ao controlador

Nesta secção, adicionamos código aos marcadores de posição que criámos. O foco aqui não é criar a API, mas sim protegê-la.

  1. Importe os pacotes necessários. O pacote Microsoft.Identity.Web é um wrapper MSAL que nos ajuda a lidar facilmente com a lógica de autenticação, por exemplo, ao processar a validação de tokens. Para garantir que os nossos pontos finais necessitam de autorização, utilizamos o pacote microsoft.AspNetCore.Authorization incorporado.

  2. Uma vez que concedemos permissões para que esta API seja chamada através de permissões delegadas em nome do utilizador ou das permissões de aplicação em que o cliente chama como ela própria e não em nome do utilizador, é importante saber se a chamada está a ser efetuada pela aplicação em seu próprio nome. A forma mais fácil de o fazer são as afirmações para determinar se o token de acesso contém a idtyp afirmação opcional. Esta idtyp afirmação é a forma mais fácil para a API determinar se um token é um token de aplicação ou um token de aplicação + utilizador. Recomendamos que ative a idtyp afirmação opcional.

    Se a idtyp afirmação não estiver ativada, pode utilizar as roles afirmações e scp para determinar se o token de acesso é um token de aplicação ou um token de aplicação + utilizador. Um token de acesso emitido pelo ID externo Microsoft Entra tem, pelo menos, uma das duas afirmações. Os tokens de acesso emitidos a um utilizador têm a scp afirmação. Os tokens de acesso emitidos para uma aplicação têm a roles afirmação. Os tokens de acesso que contêm ambas as afirmações são emitidos apenas para os utilizadores, em que a scp afirmação designa as permissões delegadas, enquanto a roles afirmação designa a função do utilizador. Os tokens de acesso que não têm nenhum deles não devem ser honrados.

    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. Adicione uma função auxiliar que determina se o pedido que está a ser feito contém permissões suficientes para realizar a ação pretendida. Verifique se é a aplicação que está a fazer o pedido em seu próprio nome ou se a aplicação está a efetuar a chamada em nome de um utilizador que possua o recurso especificado ao validar o ID de utilizador.

    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. Ligue as definições de permissão para proteger rotas. Proteja a API ao adicionar o [Authorize] atributo à classe de controlador. Isto garante que as ações do controlador só podem ser chamadas se a API for chamada com uma identidade autorizada. As definições de permissão definem que tipos de permissões são necessárias para efetuar estas ações.

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

    Adicione permissões ao ponto final GET all e ao ponto final POST. Faça-o com o método RequiredScopeOrAppPermission que faz parte do espaço de nomes Microsoft.Identity.Web.Resource . Em seguida, passa âmbitos e permissões para este método através dos atributos 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);
    }
    

Executar a API

Execute a API para garantir que está a funcionar corretamente sem erros ao utilizar o comando dotnet run. Se pretender utilizar o protocolo https mesmo durante os testes, tem de confiar em . Certificado de desenvolvimento do NET.

Para obter um exemplo completo deste código de API, veja o ficheiro de exemplos.

Passos seguintes