Tutorial: Proteger uma API Web do ASP.NET Core registrada em um locatário externo

Esta série de tutoriais demonstra como proteger uma API Web registrada no locatário externo. Nesse tutorial, você vai criar uma API web do ASP.NET Core que publica tanto permissões delegadas (escopos) quanto permissões de aplicativo (funções de aplicativo).

Neste tutorial;

  • Configurar a API Web para usar seus detalhes de registro de aplicativo
  • Configurar a API Web para usar permissões delegadas e de aplicativo registradas no registro do aplicativo
  • Proteger os pontos de extremidade da API Web

Pré-requisitos

Criar uma API Web do ASP.NET Core

  1. Abra o terminal e navegue até a pasta em que você deseja que seu projeto seja armazenado.

  2. Execute os seguintes comandos:

    dotnet new webapi -o ToDoListAPI
    cd ToDoListAPI
    
  3. Quando uma caixa de diálogo perguntar se você deseja adicionar os ativos necessários ao projeto, selecione Sim.

Instalar Pacotes

Instale os seguintes pacotes:

  • Microsoft.EntityFrameworkCore.InMemory que permite que o Entity Framework Core seja usado com um banco de dados em memória. Ele não foi projetado para uso em produção.
  • Microsoft.Identity.Web simplifica a adição de suporte de autenticação e autorização a aplicativos Web e APIs Web que se integram à plataforma de identidade da Microsoft.
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.Identity.Web

Configurar detalhes de registro do aplicativo

Abra o arquivo appsettings.json na pasta do aplicativo e adicione os detalhes de registro do aplicativo que você registrou depois de registrar sua 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 espaços reservados, conforme mostrado a seguir:

  • Substitua Enter_the_Application_Id_Here pela ID do aplicativo (cliente).
  • Substitua Enter_the_Tenant_Id_Here pela ID do diretório (locatário).
  • Substitua Enter_the_Tenant_Subdomain_Here pelo subdomínio do diretório (locatário).

Adicionar a função e o escopo do aplicativo

Todas as APIs precisam publicar ao menos um escopo, também chamado de permissão delegada, para que os aplicativos clientes obtenham com êxito com token de acesso para um usuário. As APIs também devem disponibilizar pelo menos uma função de aplicativo, também chamada de permissão de aplicativo, para que os aplicativos cliente possam obter um token de acesso em seu próprio nome, ou seja, quando não estão autenticando um usuário.

Especificaremos essas permissões no arquivo appsettings.json. Neste tutorial, registraremos 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 do aplicativo.

{
  "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

Um esquema de autenticação será nomeado quando o serviço de autenticação for configurado durante a autenticação. Neste artigo, usaremos o esquema de autenticação de portador JWT. Adicione o código a seguir no arquivo 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 seus modelos

Na pasta raiz do projeto, crie uma pasta chamada Models. Navegue até a pasta, crie um arquivo chamado ToDo.cs e adicione o código a seguir. Esse código criará 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 banco de dados

O contexto de banco de dados é a classe principal que coordena a funcionalidade do Entity Framework para um modelo de dados. Essa classe é criada derivando da classe Microsoft.EntityFrameworkCore.DbContext. Neste tutorial, usaremos um banco de dados em memória para fins de teste.

  1. Na pasta raiz do projeto, crie uma pasta chamada DbContext.

  2. Navegue até essa pasta, crie um arquivo chamado ToDoContext.cs e adicione o seguinte conteúdo a esse arquivo:

    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 arquivo Program.cs na pasta raiz do aplicativo e adicione o código a seguir ao arquivo. Esse código registra uma subclasse DbContext chamada ToDoContext como um serviço com escopo no provedor de serviços de aplicativo ASP.NET Core (também conhecido como contêiner de injeção de dependência). O contexto é configurado para usar o banco de dados em 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 de uma ação. Normalmente, as ações Criar, Ler, Atualizar e Excluir (CRUD). Neste tutorial, criaremos apenas dois itens de ação. Um item de ação de leitura completa e um item de ação de criação para demonstrar como proteger seus pontos de extremidade.

  1. Navegue até a pasta Controladores na pasta raiz do projeto.

  2. Crie um arquivo chamado ToDoListController.cs dentro dessa pasta. Abra o arquivo e adicione o seguinte código clichê:

    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 seção, adicionaremos código aos espaços reservados que criamos. O foco aqui não é criar a API, mas 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, manipulando a validação de token. Para garantir que nossos pontos de extremidade exijam autorização, usaremos o pacote embutido Microsoft.AspNetCore.Authorization.

  2. Como concedemos permissões para que esta API possa ser chamada tanto usando permissões delegadas em nome do usuário quanto permissões de aplicativo, onde o cliente faz a chamada em seu próprio nome e não em nome do usuário, é importante saber se a chamada está sendo feita pelo aplicativo em seu próprio nome. A maneira mais fácil de fazer isso é verificando as declarações para descobrir se o token de acesso contém a declaração opcional idtyp. Essa declaração idtyp é a maneira mais fácil para uma API determinar se um token é um token de aplicativo ou um token de aplicativo + usuário. Recomendamos habilitar a declaração opcional idtyp.

    Se a declaração idtyp não estiver habilitada, você poderá usar as declarações roles e scp para determinar se o token de acesso é um token de aplicativo ou um token de aplicativo + usuário. Um token de acesso emitido pelo Microsoft Entra tem pelo menos um das duas declarações. Os tokens de acesso emitidos para um usuário têm a declaração scp. Os tokens de acesso emitidos para um aplicativo têm a declaração roles. Os tokens de acesso que contêm ambas as declarações são emitidos apenas para os usuários, em que a declaração scp designa as permissões delegadas, enquanto a declaração roles designa a função do usuário. Os tokens de acesso que não têm nenhuma delas não devem ser respeitados.

    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 determinará se a solicitação que está sendo feita contém permissões suficientes para executar a ação pretendida. Verificar se é o próprio aplicativo que está fazendo a solicitação em seu próprio nome ou se o aplicativo está fazendo a chamada em nome de um usuário que é proprietário do recurso fornecido, validando a ID do usuário.

    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 suas definições de permissão para proteger rotas. Proteja sua API adicionando o atributo [Authorize] à classe de controlador. Isso garante que as ações do controlador possam ser chamadas somente se a API for chamada com uma identidade autorizada. As definições de permissão definem quais tipos de permissões são necessárias para executar essas ações.

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

    Adicione permissões ao ponto de extremidade GET de todos os recursos e ao ponto de extremidade POST. Faça isso usando o método RequiredScopeOrAppPermission que faz parte do namespace Microsoft.Identity.Web.Resource. Em seguida, passe escopos e permissões para esse método por meio 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 sua API

Execute sua API para garantir que ela esteja funcionando bem e sem erros usando o comando dotnet run. Se você pretende usar o protocolo HTTPS mesmo durante os testes, é necessário confiar no certificado de desenvolvimento do .NET.

Para obter um exemplo completo desse código de API, consulte o arquivo de exemplos.

Próxima etapa