Implementar autorizações nas APIs web com a Microsoft. Identity.Web

Neste artigo, implementa a autorização nas APIs web ASP.NET Core usando Microsoft.Identity.Web. Irá validar os escopos (permissões delegadas) e as permissões da aplicação (permissões da aplicação) para controlar o acesso a recursos protegidos. Os exemplos utilizam o Microsoft Entra ID como fornecedor de identidade.

Compreender os conceitos de autorização

Esta secção aborda as principais diferenças entre autenticação e autorização e descreve o que o Microsoft.Identity.Web valida nos tokens de acesso.

Autenticação vs autorização

Conceito Purpose Result
Authentication Verificar identidade 401 Não autorizado em caso de falha
Authorization Verificar permissões 403 Proibido se insuficiente

O que é validado

Quando uma API web recebe um token de acesso, Microsoft.Identity.Web valida:

  1. Assinatura de token - É de uma autoridade de confiança?
  2. Audiência de tokens - Destina-se a esta API?
  3. Expiração do token - Ainda é válido?
  4. Scopes/Roles - A aplicação cliente e o sujeito (utilizador) têm as permissões corretas?

Este guia foca-se no #4 - validação de escopos e permissões de aplicações.

Escopos (permissões delegadas)

Os scopes aplicam-se quando um utilizador delega permissão a uma aplicação para agir em seu nome (por exemplo, uma API web chamada em nome de um utilizador iniciado sessão).

Detail Valor
Reivindicação do token scp ou scope (aplicação cliente); roles (utilizador)
Exemplos de valores "access_as_user", "User.Read", "Files.ReadWrite"

Permissões de aplicação (permissões de aplicação)

As permissões da aplicação aplicam-se quando uma aplicação chama a API web como ela própria, sem contexto de utilizador, como um daemon ou serviço em segundo plano usando credenciais do cliente.

Detail Valor
Reivindicação do token roles
Exemplos de valores "Mail.Read.All", "User.Read.All"

Validar escopos com o RequiredScope

O RequiredScope atributo verifica se o token de acesso contém pelo menos um dos escopos especificados. Use este atributo quando a sua API serve apenas pedidos delegados pelo utilizador.

Configurar validação do escopo

Siga estes passos para permitir a validação do âmbito na sua API.

1. Ative a autorização na sua API:

Adicione serviços de autenticação e autorização ao seu pipeline de aplicação:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(); // Required for authorization

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization(); // Must be after UseAuthentication
app.MapControllers();

app.Run();

2. Proteger controladores ou ações:

Aplique os atributos [Authorize] e [RequiredScope] ao seu controlador ou a ações individuais:

using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Only accessible if token has "access_as_user" scope
        return Ok(new[] { "Todo 1", "Todo 2" });
    }
}

Aplicar padrões de âmbito

Escolha o padrão que melhor se adapte à forma como gere os âmbitos na sua aplicação.

Padrão 1: Escopos codificados diretamente

Use este padrão quando os escopos estiverem fixos e conhecidos no momento do desenvolvimento.

[Authorize]
[RequiredScope("access_as_user")]
public class TodoListController : ControllerBase
{
    // All actions require "access_as_user" scope
}

Para aceitar qualquer um de múltiplos âmbitos, liste-os como parâmetros:

[Authorize]
[RequiredScope("read", "write", "admin")]
public class TodoListController : ControllerBase
{
    // Token must have "read" OR "write" OR "admin"
}

Padrão 2: Escopos a partir da configuração

Use este padrão quando os osciloscópios devem ser configuráveis por ambiente. Defina os escopos no seu ficheiro de configuração:

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user read write"
  }
}

Refere a chave de configuração no teu controlador:

[Authorize]
[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]
public class TodoListController : ControllerBase
{
    // Scopes read from configuration
}

Esta abordagem permite-lhe alterar os escopos sem ter de recompilar.

Padrão 3: Escopos de nível de ação

Use este padrão quando diferentes ações exigirem permissões distintas. Aplicar [RequiredScope] a métodos de ação individuais:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [RequiredScope("read")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [RequiredScope("write")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        // Only tokens with "write" scope can create
        return CreatedAtAction(nameof(GetTodos), todo);
    }

    [HttpDelete("{id}")]
    [RequiredScope("admin")]
    public IActionResult DeleteTodo(int id)
    {
        // Only tokens with "admin" scope can delete
        return NoContent();
    }
}

Compreender o fluxo de validação

Quando chega um pedido, o middleware processa-o pela seguinte ordem:

  1. O middleware de autenticação ASP.NET Core valida o token
  2. RequiredScope verificações de atributos para as declarações scp ou scope
  3. Se o token contiver pelo menos um escopo correspondente, o pedido avança.
  4. Se não for encontrado um âmbito correspondente, a API devolve uma resposta 403 Proibida.

O exemplo seguinte mostra uma resposta típica de erro:

{
  "error": "insufficient_scope",
  "error_description": "The token does not have the required scope 'access_as_user'."
}

Validar permissões da aplicação com RequiredScopeOrAppPermission

O RequiredScopeOrAppPermission atributo valida ou os escopos (delegados) ou as permissões da aplicação (aplicação). Use este atributo quando a sua API servir tanto as aplicações delegadas pelo utilizador como as aplicações daemon/serviço a partir do mesmo endpoint.

Se a sua API só servir pedidos delegados pelo utilizador, use RequiredScope em vez disso.

Configurar o âmbito ou validação de permissões da aplicação

Aplique o atributo para aceitar qualquer um dos tipos de token:

using Microsoft.Identity.Web.Resource;

[Authorize]
[RequiredScopeOrAppPermission(
    AcceptedScope = new[] { "access_as_user" },
    AcceptedAppPermission = new[] { "TodoList.ReadWrite.All" }
)]
public class TodoListController : ControllerBase
{
    [HttpGet]
    public IActionResult GetTodos()
    {
        // Accessible with EITHER:
        // - User-delegated token with "access_as_user" scope, OR
        // - App-only token with "TodoList.ReadWrite.All" app permission
        return Ok(todos);
    }
}

Configurar permissões da aplicação a partir das definições

Armazene os escopos e permissões da aplicação na configuração para os alterar sem recompilar.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-api-client-id",
    "Scopes": "access_as_user",
    "AppPermissions": "TodoList.ReadWrite.All TodoList.Admin"
  }
}

Refira-se às chaves de configuração no seu controlador.

[Authorize]
[RequiredScopeOrAppPermission(
    RequiredScopesConfigurationKey = "AzureAd:Scopes",
    RequiredAppPermissionsConfigurationKey = "AzureAd:AppPermissions"
)]
public class TodoListController : ControllerBase
{
    // Scopes and app permissions from configuration
}

Compare as diferenças nas reivindicações de tokens

A tabela seguinte mostra como as reivindicações diferem entre tokens delegados pelo utilizador e tokens apenas de aplicação:

Tipo de token Afirmação Valor de Exemplo
Delegado pelo utilizador scp ou scope "access_as_user User.Read"
Apenas para app roles ["TodoList.ReadWrite.All"]

O exemplo seguinte mostra um token delegado pelo utilizador:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "scp": "access_as_user",
  "sub": "user-object-id",
  ...
}

O exemplo seguinte mostra um token exclusivo da aplicação:

{
  "aud": "api://your-api-client-id",
  "iss": "https://login.microsoftonline.com/.../v2.0",
  "roles": ["TodoList.ReadWrite.All"],
  "sub": "app-object-id",
  ...
}

Criar políticas de autorização

Para cenários de autorização complexos, utilize políticas de autorização ASP.NET Core. As políticas permitem centralizar regras, combinar múltiplos requisitos e escrever lógica de autorização testável.

Benefit Description
Lógica centralizada Define regras de autorização uma vez, reutiliza em todo o lado
Componível Combinar múltiplos requisitos (escopos + declarações + lógica personalizada)
Testável Testar a lógica de autorização com testes unitários tornou-se mais fácil
Flexível Requisitos personalizados para além da validação do âmbito

Padrão 1: Definir uma política com o ExigeScope

Defina políticas nomeadas que exijam escopos específicos e depois referencia-as nos seus controladores:

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("TodoReadPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("read", "access_as_user");
    });

    options.AddPolicy("TodoWritePolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("write", "admin");
    });
});

var app = builder.Build();

Aplicar as políticas às ações do controlador:

[Authorize]
public class TodoListController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "TodoReadPolicy")]
    public IActionResult GetTodos()
    {
        return Ok(todos);
    }

    [HttpPost]
    [Authorize(Policy = "TodoWritePolicy")]
    public IActionResult CreateTodo([FromBody] Todo todo)
    {
        return CreatedAtAction(nameof(GetTodos), todo);
    }
}

Padrão 2: Definir uma política com Requisito de Autorização de Âmbito

Use ScopeAuthorizationRequirement para requisitos de âmbito mais explícitos:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CustomPolicy", policyBuilder =>
    {
        policyBuilder.AddRequirements(
            new ScopeAuthorizationRequirement(new[] { "access_as_user" })
        );
    });
});

Padrão 3: Definir uma política padrão

Defina uma política padrão que se aplique automaticamente a todos os [Authorize] atributos:

builder.Services.AddAuthorization(options =>
{
    var defaultPolicy = new AuthorizationPolicyBuilder()
        .RequireScope("access_as_user")
        .Build();

    options.DefaultPolicy = defaultPolicy;
});

Cada [Authorize] atributo agora requer o access_as_user âmbito:

[Authorize] // Automatically requires "access_as_user" scope
public class TodoListController : ControllerBase
{
    // All actions protected by default policy
}

Padrão 4: Combinar múltiplos requisitos

Combine o âmbito, função e requisitos de autenticação numa única política:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policyBuilder =>
    {
        policyBuilder.RequireScope("admin");
        policyBuilder.RequireRole("Admin"); // Also check role claim
        policyBuilder.RequireAuthenticatedUser();
    });
});

Padrão 5: Construir uma política a partir da configuração

Carregar escopos a partir da configuração para manter as políticas específicas ao ambiente:

var requiredScopes = builder.Configuration["AzureAd:Scopes"]?.Split(' ');

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiAccessPolicy", policyBuilder =>
    {
        if (requiredScopes != null)
        {
            policyBuilder.RequireScope(requiredScopes);
        }
    });
});

Filtrar pedidos por inquilino

Restringa o acesso à API a tokens provenientes de inquilinos específicos do Microsoft Entra. Isto é útil quando a sua API multi-inquilino só deve aceitar pedidos de clientes arrendatários aprovados.

Restringir o acesso aos inquilinos permitidos

Defina uma apólice que verifique a reivindicação do ID do inquilino contra uma lista de permissões:

builder.Services.AddAuthorization(options =>
{
    string[] allowedTenants =
    {
        "14c2f153-90a7-4689-9db7-9543bf084dad", // Contoso tenant
        "af8cc1a0-d2aa-4ca7-b829-00d361edb652", // Fabrikam tenant
        "979f4440-75dc-4664-b2e1-2cafa0ac67d1"  // Northwind tenant
    };

    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });

    // Apply to all endpoints by default
    options.DefaultPolicy = options.GetPolicy("AllowedTenantsOnly");
});

Configurar filtragem de inquilinos a partir das definições

A Store permitiu o armazenamento de IDs dos locatários na configuração para gerenciá-los sem alterações no código.

appsettings.json:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "your-api-client-id",
    "AllowedTenants": [
      "14c2f153-90a7-4689-9db7-9543bf084dad",
      "af8cc1a0-d2aa-4ca7-b829-00d361edb652"
    ]
  }
}

Leia a lista de inquilinos e crie a política no início:

var allowedTenants = builder.Configuration.GetSection("AzureAd:AllowedTenants")
    .Get<string[]>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AllowedTenantsOnly", policyBuilder =>
    {
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants ?? Array.Empty<string>()
        );
    });
});

Combinar escopos com filtragem de locatários

Crie uma apólice que exija tanto um âmbito válido como um inquilino aprovado:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SecureApiAccess", policyBuilder =>
    {
        // Require specific scope
        policyBuilder.RequireScope("access_as_user");

        // AND require specific tenant
        policyBuilder.RequireClaim(
            "http://schemas.microsoft.com/identity/claims/tenantid",
            allowedTenants
        );
    });
});

Siga as melhores práticas

Aplicar estas recomendações para construir uma lógica de autorização segura e sustentável.

Coisas a Fazer

1. Sempre emparelhar [Authorize] com validação de escopo:

[Authorize] // Authentication
[RequiredScope("access_as_user")] // Authorization
public class MyController : ControllerBase { }

2. Utilizar a configuração para escopos específicos do ambiente:

[RequiredScope(RequiredScopesConfigurationKey = "AzureAd:Scopes")]

3. Aplicar o privilégio mínimo:

[HttpGet]
[RequiredScope("read")] // Only read permission needed

[HttpPost]
[RequiredScope("write")] // Write permission for modifications

4. Utilizar políticas para autorizações complexas:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireScope("admin");
        policy.RequireClaim("department", "IT");
    });
});

5. Permitir respostas detalhadas a erros no desenvolvimento:

if (builder.Environment.IsDevelopment())
{
    Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}

Coisas a não fazer

1. Não salte [Authorize] ao usar RequiredScope:

//  Wrong - RequiredScope won't work without [Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

//  Correct
[Authorize]
[RequiredScope("access_as_user")]
public class MyController : ControllerBase { }

2. Não codifique os IDs de cliente em produção:

//  Wrong
policyBuilder.RequireClaim("tid", "14c2f153-90a7-4689-9db7-9543bf084dad");

//  Better - use configuration
var tenants = Configuration.GetSection("AllowedTenants").Get<string[]>();
policyBuilder.RequireClaim("tid", tenants);

3. Não confunda âmbitos com papéis:

//  Wrong - This checks roles claim, not scopes
[RequiredScope("Admin")] // "Admin" is typically a role, not a scope

//  Correct
[RequiredScope("access_as_user")] // Scope
[Authorize(Roles = "Admin")] // Role

4. Não exponha informações sensíveis de escopo em mensagens de erro de produção:

Configure níveis de registo apropriados e o tratamento de erros para ambientes de produção.


Soluções para problemas de autorização

Use as seguintes orientações para diagnosticar problemas comuns de autorização.

403 Forbidden - falta de escopo

Erro: A API devolve 403 mesmo com um token válido.

Diagnóstico:

  1. Decifrar o token em jwt.ms.
  2. Verifica a scp ou a scope reclamação.
  3. Verifica se o valor corresponde ao teu RequiredScope atributo.

Solution:

  • Certifique-se de que a aplicação cliente solicita o âmbito correto ao adquirir o token.
  • Verifique se o âmbito está exposto no registo da aplicação API na Microsoft Entra.
  • Conceda consentimento administrativo se necessário.

RequiredScope não funciona

Sintoma: O atributo parece ser ignorado.

Verifique:

  1. Adicionaste o [Authorize] atributo?
  2. Depois de app.UseAuthorization(), é chamado app.UseAuthentication()?
  3. Está o/a services.AddAuthorization() registado/a?

Chave de configuração não encontrada

Erro: A validação do escopo falha silenciosamente.

Verifique:

{
  "AzureAd": {
    "Scopes": "access_as_user" // Matches RequiredScopesConfigurationKey
  }
}

Garantir que o caminho de configuração corresponda exatamente.