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
Um registro de API que expõe pelo menos um escopo (permissões delegadas) e uma função de aplicativo (permissão de aplicativo), como ToDoList.Read. Caso ainda não tenha feito isso, registre um API no centro de administração do Microsoft Entra seguindo as etapas de registro. Certifique-se de que tenha o seguinte:
- A ID do aplicativo (cliente) da API Web
- A ID do diretório (locatário) da API Web está registrada
- O subdomínio do diretório (locatário) de onde a API Web está registrada. Por exemplo, se o domínio primário for contoso.onmicrosoft.com, o subdomínio do diretório (locatário) será contoso.
- ToDoList.Read e ToDoList.ReadWrite como as permissões delegadas (escopos) expostas pela API Web.
- ToDoList.Read.All e ToDoList.ReadWrite.All como as permissões de aplicativo (funções de aplicativo) expostas pela API Web.
SDK do .NET 7.0 ou posterior.
Visual Studio Code ou outro editor de código.
Criar uma API Web do ASP.NET Core
Abra o terminal e navegue até a pasta em que você deseja que seu projeto seja armazenado.
Execute os seguintes comandos:
dotnet new webapi -o ToDoListAPI cd ToDoListAPI
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).
Usar um domínio de URL personalizado (opcional)
Use um domínio personalizado para marcar totalmente a URL de autenticação. Do ponto de vista do usuário, os usuários permanecem no seu domínio durante o processo de autenticação, em vez de serem redirecionados para o nome de domínio ciamlogin.com.
Siga essas etapas para usar um domínio personalizado:
Use as etapas em Habilitar domínios de URL personalizados para aplicativos em locatários externos para habilitar uma URL de domínio personalizado para o seu locatário externo.
Abra o arquivo appsettings.json:
- Atualize o valor da propriedade
Instance
para https://Enter_the_Custom_Domain_Here/Enter_the_Tenant_ID_Here. SubstituaEnter_the_Custom_Domain_Here
pelo domínio de URL personalizado eEnter_the_Tenant_ID_Here
pela ID do locatário. Se você não tiver o nome do locatário, saiba como ler os detalhes do locatário. - Adicione a propriedade
knownAuthorities
com um valor [Insira_o_Domínio_Personalizado_Aqui].
- Atualize o valor da propriedade
Após fazer as alterações no seu arquivo appsettings.json, se o seu domínio de URL personalizado for login.contoso.com e sua ID de locatário for aaaabbbb-0000-cccc-1111-dddd2222eeee, seu arquivo deverá ser semelhante ao snippet a seguir:
{
"AzureAd": {
"Instance": "https://login.contoso.com/aaaabbbb-0000-cccc-1111-dddd2222eeee",
"TenantId": "Enter_the_Tenant_Id_Here",
"ClientId": "Enter_the_Application_Id_Here",
"KnownAuthorities": ["login.contoso.com"]
},
"Logging": {...},
"AllowedHosts": "*"
}
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.
Na pasta raiz do projeto, crie uma pasta chamada DbContext.
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; } }
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
chamadaToDoContext
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.
Navegue até a pasta Controladores na pasta raiz do projeto.
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.
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.
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çãoidtyp
é 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 opcionalidtyp
.Se a declaração
idtyp
não estiver habilitada, você poderá usar as declaraçõesroles
escp
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çãoscp
. Os tokens de acesso emitidos para um aplicativo têm a declaraçãoroles
. Os tokens de acesso que contêm ambas as declarações são emitidos apenas para os usuários, em que a declaraçãoscp
designa as permissões delegadas, enquanto a declaraçãoroles
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"); } }
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; }
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.