Autorização baseada em políticas no ASP.NET Core

Por baixo dos panos, a autorização baseada em funções e a autorização baseada em declarações usam um requisito, um manipulador de requisitos e uma política pré-configurada. Esses blocos de construção dão suporte à expressão de avaliações de autorização no código. O resultado é uma estrutura de autorização mais rica, reutilizável e testável.

Uma política de autorização consiste em um ou mais requisitos. Registre-o como parte da configuração do serviço de autorização, no arquivo Program.cs do aplicativo:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

No exemplo anterior, é criada uma política "AtLeast21". Ele tem um único requisito: o de idade mínima, que é fornecido como um parâmetro para o requisito.

IAuthorizationService

O serviço primário que determina se a autorização foi bem-sucedida é IAuthorizationService:

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

O código anterior realça os dois métodos do IAuthorizationService.

IAuthorizationRequirement é um serviço de marcador sem métodos e o mecanismo para acompanhar se a autorização foi bem-sucedida.

Cada IAuthorizationHandler é responsável por verificar se os requisitos são atendidos:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

A classe AuthorizationHandlerContext é o que o manipulador usa para marcar se os requisitos foram atendidos:

 context.Succeed(requirement)

O código a seguir mostra a implementação padrão simplificada (e anotada com comentários) do serviço de autorização:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

O código a seguir mostra uma configuração típica de serviço de autorização:

// Add all of your handlers to DI.
builder.Services.AddSingleton<IAuthorizationHandler, MyHandler1>();
// MyHandler2, ...

builder.Services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

// Configure your policies
builder.Services.AddAuthorization(options =>
      options.AddPolicy("Something",
      policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));

Use IAuthorizationService, [Authorize(Policy = "Something")]ou RequireAuthorization("Something") para autorização.

Aplicar políticas a controladores MVC

Para aplicativos que usam o Razor Pages, confira a seção Aplicar políticas ao Razor Pages.

As políticas são aplicadas aos controladores usando o atributo [Authorize] com o nome da política:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

Se várias políticas forem aplicadas nos níveis de controlador e ação, todas as políticas deverão ser aprovadas antes que o acesso seja concedido:

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Controller : Controller
{
    public IActionResult Index() => View();
}

Aplicar políticas ao Razor Pages

Aplique políticas ao Razor Pages usando o atributo [Authorize] com o nome da política. Por exemplo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace AuthorizationPoliciesSample.Pages;

[Authorize(Policy = "AtLeast21")]
public class AtLeast21Model : PageModel { }

As políticas não podem ser aplicadas no nível do manipulador do Razor Page, elas devem ser aplicadas ao Page.

As políticas também podem ser aplicadas ao Razor Pages usando uma convenção de autorização.

Aplicar políticas a pontos de extremidade

Aplique políticas aos pontos de extremidade usando RequireAuthorization com o nome da política. Por exemplo:

app.MapGet("/helloworld", () => "Hello World!")
    .RequireAuthorization("AtLeast21");

Requisitos

Um requisito de autorização é uma coleção de parâmetros de dados que uma política pode usar para avaliar a entidade de segurança do usuário atual. Em nossa política "AtLeast21", o requisito é um único parâmetro: a idade mínima. Um requisito implementa IAuthorizationRequirement, que é uma interface de marcador vazia. Um requisito de idade mínima parametrizado pode ser implementado da seguinte maneira:

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge) =>
        MinimumAge = minimumAge;

    public int MinimumAge { get; }
}

Se uma política de autorização contiver vários requisitos de autorização, todos os requisitos deverão ser aprovados para que a avaliação da política tenha êxito. Em outras palavras, vários requisitos de autorização adicionados a uma única política de autorização são tratados com base em AND.

Observação

Um requisito não precisa ter dados ou propriedades.

Manipuladores de autorização

Um manipulador de autorização é responsável pela avaliação das propriedades de um requisito. O manipulador de autorização avalia os requisitos em relação a um AuthorizationHandlerContext fornecido para determinar se o acesso é permitido.

Um requisito pode ter vários manipuladores. Um manipulador pode herdar AuthorizationHandler<TRequirement>, em que TRequirement é o requisito a ser tratado. Como alternativa, um manipulador pode implementar IAuthorizationHandler diretamente para lidar com mais de um tipo de requisito.

Usar um manipulador para um requisito

O exemplo a seguir mostra uma relação um-para-um na qual um manipulador de idade mínima lida com um único requisito:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, MinimumAgeRequirement requirement)
    {
        var dateOfBirthClaim = context.User.FindFirst(
            c => c.Type == ClaimTypes.DateOfBirth && c.Issuer == "http://contoso.com");

        if (dateOfBirthClaim is null)
        {
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

O código anterior determina se a entidade de segurança de usuário atual tem uma declaração de data de nascimento que foi emitida por um emissor conhecido e confiável. A autorização não pode ocorrer quando a declaração está ausente, caso em que uma tarefa concluída é retornada. Quando uma declaração está presente, a idade do usuário é calculada. Se o usuário atender à idade mínima definida pelo requisito, a autorização será considerada bem-sucedida. Quando a autorização é bem-sucedida, context.Succeed é invocado com o requisito atendido como seu único parâmetro.

Usar um manipulador para vários requisitos

O exemplo a seguir mostra uma relação um-para-muitos na qual um manipulador de permissões pode lidar com três tipos diferentes de requisitos:

using System.Security.Claims;
using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource)
                    || IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission || requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        return Task.CompletedTask;
    }

    private static bool IsOwner(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }

    private static bool IsSponsor(ClaimsPrincipal user, object? resource)
    {
        // Code omitted for brevity
        return true;
    }
}

O código anterior atravessa PendingRequirements: uma propriedade que contém requisitos não marcados como bem-sucedidos. Para um requisito ReadPermission, o usuário deve ser um proprietário ou um responsável para acessar o recurso solicitado. Para um requisito EditPermission ou DeletePermission, ele deve ser um proprietário para acessar o recurso solicitado.

Registro do manipulador

Registre manipuladores na coleção de serviços durante a configuração. Por exemplo:

builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

O código anterior registra MinimumAgeHandler como um singleton. Os manipuladores podem ser registrados usando qualquer um dos tempos de vida de serviço internos.

É possível agrupar um requisito e um manipulador em uma única classe implementando IAuthorizationRequirement e IAuthorizationHandler. Esse agrupamento cria um acoplamento apertado entre o manipulador e o requisito e é recomendado apenas para requisitos e manipuladores simples. A criação de uma classe que implementa ambas as interfaces remove a necessidade de registrar o manipulador no DI devido ao PassThroughAuthorizationHandler interno que permite que os requisitos se manipulem.

Confira a classe AssertionRequirement para obter um bom exemplo em que o AssertionRequirement é um requisito e o manipulador em uma classe totalmente independente.

O que um manipulador deve retornar?

Observe que o método Handle no exemplo de manipulador não retorna nenhum valor. Como um status de êxito ou falha é indicado?

  • Um manipulador indica êxito chamando context.Succeed(IAuthorizationRequirement requirement), passando o requisito que foi validado com êxito.

  • Um manipulador não precisa lidar com falhas em geral, pois outros manipuladores para o mesmo requisito podem ter êxito.

  • Para garantir a falha, mesmo que outros manipuladores de requisitos tenham êxito, chame context.Fail.

Se um manipulador chamar context.Succeed ou context.Fail, todos os outros manipuladores ainda serão chamados. Isso permite que os requisitos produzam efeitos colaterais, como o registro em log, que ocorre mesmo que outro manipulador tenha validado ou falhado com êxito em um requisito. Quando definida como false, a propriedade InvokeHandlersAfterFailure causa um curto-circuito na execução dos manipuladores quando context.Fail é chamada. InvokeHandlersAfterFailure tem como padrão true; nesse caso todos os manipuladores são chamados.

Observação

Manipuladores de autorização são chamados mesmo se a autenticação falhar. Além disso, os manipuladores podem ser executados em qualquer ordem, portanto, não dependem deles serem chamados em qualquer ordem específica.

Por que eu iria querer vários manipuladores para um requisito?

Nos casos em que você deseja que a avaliação seja baseada em OR, implemente vários manipuladores para um único requisito. Por exemplo, a Microsoft tem portas que só abrem com cartões-chave. Se você deixar seu cartão-chave em casa, a recepcionista imprimirá um adesivo temporário e abrirá a porta para você. Nesse cenário, você teria um único requisito, BuildingEntry, mas vários manipuladores, cada um examinando um único requisito.

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

public class BuildingEntryRequirement : IAuthorizationRequirement { }

BadgeEntryHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "BadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using AuthorizationPoliciesSample.Policies.Requirements;
using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Handlers;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context, BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(
            c => c.Type == "TemporaryBadgeId" && c.Issuer == "https://microsoftsecurity"))
        {
            // Code to check expiration date omitted for brevity.
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Verifique se ambos os manipuladores estão registrados. Se um dos manipuladores tiver êxito quando uma política avaliar o BuildingEntryRequirement, a avaliação da política terá êxito.

Use uma função para cumprir uma política

Pode haver situações em que o cumprimento de uma política é simples de expressar no código. É possível fornecer um Func<AuthorizationHandlerContext, bool> ao configurar uma política com o construtor de políticas RequireAssertion.

Por exemplo, o BadgeEntryHandler anterior pode ser reescrito da seguinte maneira:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context => context.User.HasClaim(c =>
            (c.Type == "BadgeId" || c.Type == "TemporaryBadgeId")
            && c.Issuer == "https://microsoftsecurity")));
});

Acessar o contexto de solicitação MVC em manipuladores

O método HandleRequirementAsync tem dois parâmetros: um AuthorizationHandlerContext e o TRequirement que está sendo tratado. Estruturas como MVC ou SignalR são livres para adicionar qualquer objeto à propriedade Resource no AuthorizationHandlerContext para passar informações extras.

Ao usar o roteamento de ponto de extremidade, a autorização normalmente é tratada pelo Middleware de Autorização. Nesse caso, a propriedade Resource é uma instância de HttpContext. O contexto pode ser usado para acessar o ponto de extremidade atual, que pode ser usado para investigar o recurso subjacente ao qual você está roteando. Por exemplo:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

Com o roteamento tradicional ou quando a autorização ocorre como parte do filtro de autorização do MVC, o valor de Resource é uma instância AuthorizationFilterContext. Essa propriedade fornece acesso a HttpContext, RouteData e todo o resto fornecido pelo MVC e Razor Pages.

O uso da propriedade Resource é específico da estrutura. O uso de informações na propriedade Resource limita suas políticas de autorização a estruturas específicas. Converta a propriedade Resource usando a palavra-chave is e confirme se a conversão foi bem-sucedida para garantir que o código não falhe com um InvalidCastException quando executado em outras estruturas:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

Exigir globalmente que todos os usuários sejam autenticados

Para obter informações sobre como exigir que todos os usuários sejam autenticados globalmente, confira Exigir usuários autenticados.

Exemplo de autorização com serviço externo

O código de exemplo em AspNetCore.Docs.Samples mostra como implementar requisitos de autorização adicionais com um serviço de autorização externo. O projeto de exemplo Contoso.API é protegido com o Azure AD. Uma verificação de autorização adicional do projeto Contoso.Security.API retorna um conteúdo que descreve se o aplicativo cliente Contoso.API pode invocar a API GetWeather.

Configurar o exemplo

  1. Crie um registro de aplicativo no locatário do Azure Active Directory (Azure AD):
  • Atribua a ele um AppRole.
  • Em Permissões de API, adicione o AppRole como uma permissão e dê consentimento de administrador. Observe que, nessa configuração, esse registro de aplicativo representa a API e o cliente invocando a API. Se desejar, você pode criar dois registros de aplicativo. Se você estiver usando essa configuração, execute apenas as permissões de API e adicione o AppRole como uma etapa de permissão somente para o cliente. Somente o registro do aplicativo cliente requer que um segredo do cliente seja gerado.
  1. Defina o projeto Contoso.API com as seguintes configurações:
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<Tenant name from AAD properties>.onmicrosoft.com">,
    "TenantId": "<Tenant Id from AAD properties>",
    "ClientId": "<Client Id from App Registration representing the API>"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
  1. Defina o Contoso.Security.API com as seguintes configurações:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. Importe o arquivo ContosoAPI.postman_collection.json para o Postman e configure um ambiente com o seguinte:

    • ClientId: ID do cliente do registro do aplicativo que representa o cliente que chama a API.
    • clientSecret: segredo do cliente do registro do aplicativo que representa o cliente que chama a API.
    • TenantId: ID do locatário das propriedades do AAD
  2. Execute a solução e use o Postman para invocar a API. Você pode adicionar pontos de interrupção no Contoso.Security.API.SecurityPolicyController e observar a ID do cliente sendo passada para verificar se ela tem permissão para Obter Clima.

Recursos adicionais

Por baixo dos panos, a autorização baseada em função e a autorização baseada em declarações usam um requisito, um manipulador de requisitos e uma política pré-configurada. Esses blocos de construção dão suporte à expressão de avaliações de autorização no código. O resultado é uma estrutura de autorização mais rica, reutilizável e testável.

Uma política de autorização consiste em um ou mais requisitos. Ele é registrado como parte da configuração do serviço de autorização no método Startup.ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });
}

No exemplo anterior, é criada uma política "AtLeast21". Ele tem um único requisito: o de idade mínima, que é fornecido como um parâmetro para o requisito.

IAuthorizationService

O serviço primário que determina se a autorização foi bem-sucedida é IAuthorizationService:

/// <summary>
/// Checks policy based permissions for a user
/// </summary>
public interface IAuthorizationService
{
    /// <summary>
    /// Checks if a user meets a specific set of requirements for the specified resource
    /// </summary>
    /// <param name="user">The user to evaluate the requirements against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="requirements">The requirements to evaluate.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// This value is <value>true</value> when the user fulfills the policy; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check 
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, 
                                     IEnumerable<IAuthorizationRequirement> requirements);

    /// <summary>
    /// Checks if a user meets a specific authorization policy
    /// </summary>
    /// <param name="user">The user to check the policy against.</param>
    /// <param name="resource">
    /// An optional resource the policy should be checked with.
    /// If a resource is not required for policy evaluation you may pass null as the value
    /// </param>
    /// <param name="policyName">The name of the policy to check against a specific 
    /// context.</param>
    /// <returns>
    /// A flag indicating whether authorization has succeeded.
    /// Returns a flag indicating whether the user, and optional resource has fulfilled 
    /// the policy.    
    /// <value>true</value> when the policy has been fulfilled; 
    /// otherwise <value>false</value>.
    /// </returns>
    /// <remarks>
    /// Resource is an optional parameter and may be null. Please ensure that you check
    /// it is not null before acting upon it.
    /// </remarks>
    Task<AuthorizationResult> AuthorizeAsync(
                                ClaimsPrincipal user, object resource, string policyName);
}

O código anterior realça os dois métodos do IAuthorizationService.

IAuthorizationRequirement é um serviço de marcador sem métodos e o mecanismo para acompanhar se a autorização foi bem-sucedida.

Cada IAuthorizationHandler é responsável por verificar se os requisitos são atendidos:

/// <summary>
/// Classes implementing this interface are able to make a decision if authorization
/// is allowed.
/// </summary>
public interface IAuthorizationHandler
{
    /// <summary>
    /// Makes a decision if authorization is allowed.
    /// </summary>
    /// <param name="context">The authorization information.</param>
    Task HandleAsync(AuthorizationHandlerContext context);
}

A classe AuthorizationHandlerContext é o que o manipulador usa para marcar se os requisitos foram atendidos:

 context.Succeed(requirement)

O código a seguir mostra a implementação padrão simplificada (e anotada com comentários) do serviço de autorização:

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

O código a seguir mostra um ConfigureServices típico:

public void ConfigureServices(IServiceCollection services)
{
    // Add all of your handlers to DI.
    services.AddSingleton<IAuthorizationHandler, MyHandler1>();
    // MyHandler2, ...

    services.AddSingleton<IAuthorizationHandler, MyHandlerN>();

    // Configure your policies
    services.AddAuthorization(options =>
          options.AddPolicy("Something",
          policy => policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything")));


    services.AddControllersWithViews();
    services.AddRazorPages();
}

Use IAuthorizationService ou [Authorize(Policy = "Something")] para autorização.

Aplicar políticas ao controlador MVC

Se você estiver usando o Razor Pages, confira Aplicar políticas ao Razor Pages nesse documento.

As políticas são aplicadas aos controladores usando o atributo [Authorize] com o nome da política. Por exemplo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseController : Controller
{
    public IActionResult Index() => View();
}

Aplicar políticas ao Razor Pages

As políticas são aplicadas ao Razor Pages usando o atributo [Authorize] com o nome da política. Por exemplo:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

[Authorize(Policy = "AtLeast21")]
public class AlcoholPurchaseModel : PageModel
{
}

As políticas não podem ser aplicadas no nível do manipulador do Razor Page, elas devem ser aplicadas ao Page.

As políticas podem ser aplicadas ao Razor Pages por meio de uma convenção de autorização.

Requisitos

Um requisito de autorização é uma coleção de parâmetros de dados que uma política pode usar para avaliar a entidade de segurança do usuário atual. Em nossa política "AtLeast21", o requisito é um único parâmetro: a idade mínima. Um requisito implementa IAuthorizationRequirement, que é uma interface de marcador vazia. Um requisito de idade mínima parametrizado pode ser implementado da seguinte maneira:

using Microsoft.AspNetCore.Authorization;

public class MinimumAgeRequirement : IAuthorizationRequirement
{
    public int MinimumAge { get; }

    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }
}

Se uma política de autorização contiver vários requisitos de autorização, todos os requisitos deverão ser aprovados para que a avaliação da política tenha êxito. Em outras palavras, vários requisitos de autorização adicionados a uma única política de autorização são tratados com base em AND.

Observação

Um requisito não precisa ter dados ou propriedades.

Manipuladores de autorização

Um manipulador de autorização é responsável pela avaliação das propriedades de um requisito. O manipulador de autorização avalia os requisitos em relação a um AuthorizationHandlerContext fornecido para determinar se o acesso é permitido.

Um requisito pode ter vários manipuladores. Um manipulador pode herdar AuthorizationHandler<TRequirement>, em que TRequirement é o requisito a ser tratado. Como alternativa, um manipulador pode implementar IAuthorizationHandler para lidar com mais de um tipo de requisito.

Usar um manipulador para um requisito

O exemplo a seguir mostra uma relação um-para-um na qual um manipulador de idade mínima utiliza um único requisito:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   MinimumAgeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
                                        c.Issuer == "http://contoso.com"))
        {
            //TODO: Use the following if targeting a version of
            //.NET Framework older than 4.6:
            //      return Task.FromResult(0);
            return Task.CompletedTask;
        }

        var dateOfBirth = Convert.ToDateTime(
            context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth && 
                                        c.Issuer == "http://contoso.com").Value);

        int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
        if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
        {
            calculatedAge--;
        }

        if (calculatedAge >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

O código anterior determina se a entidade de segurança de usuário atual tem uma declaração de data de nascimento que foi emitida por um emissor conhecido e confiável. A autorização não pode ocorrer quando a declaração está ausente, caso em que uma tarefa concluída é retornada. Quando uma declaração está presente, a idade do usuário é calculada. Se o usuário atender à idade mínima definida pelo requisito, a autorização será considerada bem-sucedida. Quando a autorização é bem-sucedida, context.Succeed é invocado com o requisito atendido como seu único parâmetro.

Usar um manipulador para vários requisitos

O exemplo a seguir mostra uma relação um-para-muitos na qual um manipulador de permissões pode lidar com três tipos diferentes de requisitos:

using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class PermissionHandler : IAuthorizationHandler
{
    public Task HandleAsync(AuthorizationHandlerContext context)
    {
        var pendingRequirements = context.PendingRequirements.ToList();

        foreach (var requirement in pendingRequirements)
        {
            if (requirement is ReadPermission)
            {
                if (IsOwner(context.User, context.Resource) ||
                    IsSponsor(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
            else if (requirement is EditPermission ||
                     requirement is DeletePermission)
            {
                if (IsOwner(context.User, context.Resource))
                {
                    context.Succeed(requirement);
                }
            }
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }

    private bool IsOwner(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }

    private bool IsSponsor(ClaimsPrincipal user, object resource)
    {
        // Code omitted for brevity

        return true;
    }
}

O código anterior atravessa PendingRequirements: uma propriedade que contém requisitos não marcados como bem-sucedidos. Para um requisito ReadPermission, o usuário deve ser um proprietário ou um responsável para acessar o recurso solicitado. Para um requisito EditPermission ou DeletePermission, o usuário deve ser um proprietário para acessar o recurso solicitado.

Registro do manipulador

Os manipuladores são registrados na coleção de serviços durante a configuração. Por exemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

    services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}

O código anterior registra MinimumAgeHandler como um singleton ao invocar services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. Os manipuladores podem ser registrados usando qualquer um dos tempos de vida de serviço internos.

É possível agrupar um requisito e um manipulador em uma única classe implementando IAuthorizationRequirement e IAuthorizationHandler. Esse agrupamento cria um acoplamento apertado entre o manipulador e o requisito e é recomendado apenas para requisitos e manipuladores simples. A criação de uma classe que implementa ambas as interfaces remove a necessidade de registrar o manipulador no DI devido ao PassThroughAuthorizationHandler interno que permite que os requisitos se manipulem.

Confira a classe AssertionRequirement para obter um bom exemplo em que o AssertionRequirement é um requisito e o manipulador em uma classe totalmente independente.

O que um manipulador deve retornar?

Observe que o método Handle no exemplo de manipulador não retorna nenhum valor. Como um status de êxito ou falha é indicado?

  • Um manipulador indica êxito chamando context.Succeed(IAuthorizationRequirement requirement), passando o requisito que foi validado com êxito.

  • Um manipulador não precisa lidar com falhas em geral, pois outros manipuladores para o mesmo requisito podem ter êxito.

  • Para garantir a falha, mesmo que outros manipuladores de requisitos tenham êxito, chame context.Fail.

Se um manipulador chamar context.Succeed ou context.Fail, todos os outros manipuladores ainda serão chamados. Isso permite que os requisitos produzam efeitos colaterais, como o registro em log, que ocorre mesmo que outro manipulador tenha validado ou falhado com êxito em um requisito. Quando definida como false, a propriedade InvokeHandlersAfterFailure causa um curto-circuito na execução dos manipuladores quando context.Fail é chamada. InvokeHandlersAfterFailure tem como padrão true; nesse caso todos os manipuladores são chamados.

Observação

Manipuladores de autorização são chamados mesmo se a autenticação falhar.

Por que eu iria querer vários manipuladores para um requisito?

Nos casos em que você deseja que a avaliação seja baseada em OR, implemente vários manipuladores para um único requisito. Por exemplo, a Microsoft tem portas que só abrem com cartões-chave. Se você deixar seu cartão-chave em casa, a recepcionista imprimirá um adesivo temporário e abrirá a porta para você. Nesse cenário, você teria um único requisito, BuildingEntry, mas vários manipuladores, cada um examinando um único requisito.

BuildingEntryRequirement.cs

using Microsoft.AspNetCore.Authorization;

public class BuildingEntryRequirement : IAuthorizationRequirement
{
}

BadgeEntryHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class BadgeEntryHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "BadgeId" &&
                                       c.Issuer == "http://microsoftsecurity"))
        {
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

TemporaryStickerHandler.cs

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using PoliciesAuthApp1.Services.Requirements;

public class TemporaryStickerHandler : AuthorizationHandler<BuildingEntryRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
                                                   BuildingEntryRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "TemporaryBadgeId" &&
                                       c.Issuer == "https://microsoftsecurity"))
        {
            // We'd also check the expiration date on the sticker.
            context.Succeed(requirement);
        }

        //TODO: Use the following if targeting a version of
        //.NET Framework older than 4.6:
        //      return Task.FromResult(0);
        return Task.CompletedTask;
    }
}

Verifique se ambos os manipuladores estão registrados. Se um dos manipuladores tiver êxito quando uma política avaliar o BuildingEntryRequirement, a avaliação da política terá êxito.

Use uma função para cumprir uma política

Pode haver situações em que o cumprimento de uma política é simples de expressar no código. É possível fornecer um Func<AuthorizationHandlerContext, bool> ao configurar sua política com o construtor de políticas RequireAssertion.

Por exemplo, o BadgeEntryHandler anterior pode ser reescrito da seguinte maneira:

services.AddAuthorization(options =>
{
     options.AddPolicy("BadgeEntry", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c =>
                (c.Type == "BadgeId" ||
                 c.Type == "TemporaryBadgeId") &&
                 c.Issuer == "https://microsoftsecurity")));
});

Acessar o contexto de solicitação MVC em manipuladores

O método HandleRequirementAsync que você implementa em um manipulador de autorização tem dois parâmetros: um AuthorizationHandlerContext e o TRequirement que você está tratando. Estruturas como MVC ou SignalR são livres para adicionar qualquer objeto à propriedade Resource no AuthorizationHandlerContext para passar informações extras.

Ao usar o roteamento de ponto de extremidade, a autorização normalmente é tratada pelo Middleware de Autorização. Nesse caso, a propriedade Resource é uma instância de HttpContext. O contexto pode ser usado para acessar o ponto de extremidade atual, que pode ser usado para investigar o recurso subjacente ao qual você está roteando. Por exemplo:

if (context.Resource is HttpContext httpContext)
{
    var endpoint = httpContext.GetEndpoint();
    var actionDescriptor = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>();
    ...
}

Com o roteamento tradicional ou quando a autorização ocorre como parte do filtro de autorização do MVC, o valor de Resource é uma instância AuthorizationFilterContext. Essa propriedade fornece acesso a HttpContext, RouteData e todo o resto fornecido pelo MVC e Razor Pages.

O uso da propriedade Resource é específico da estrutura. O uso de informações na propriedade Resource limita suas políticas de autorização a estruturas específicas. Converta a propriedade Resource usando a palavra-chave is e confirme se a conversão foi bem-sucedida para garantir que o código não falhe com um InvalidCastException quando executado em outras estruturas:

// Requires the following import:
//     using Microsoft.AspNetCore.Mvc.Filters;
if (context.Resource is AuthorizationFilterContext mvcContext)
{
    // Examine MVC-specific things like routing data.
}

Exigir globalmente que todos os usuários sejam autenticados

Para obter informações sobre como exigir que todos os usuários sejam autenticados globalmente, confira Exigir usuários autenticados.