Autorización basada en directivas en ASP.NET Core

De manera oculta, la autorización basada en roles y la autorización basada en notificaciones usan un requisito, un controlador de requisitos y una directiva preconfigurada. Estos bloques de creación admiten la expresión de evaluaciones de autorización en el código. El resultado es una estructura de autorización más completa, reutilizable y probable.

Una directiva de autorización consta de uno o varios requisitos. Regístrelo como parte de la configuración del servicio de autorización, en el archivo Program.cs de la aplicación:

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

En el ejemplo anterior, se crea una directiva "AtLeast21". Tiene un único requisito: el de una edad mínima, que se proporciona como parámetro para el requisito.

IAuthorizationService

El servicio principal que determina si la autorización se realiza correctamente es 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);
}

El código anterior resalta los dos métodos de IAuthorizationService.

IAuthorizationRequirement es un servicio de marcador sin métodos y el mecanismo para realizar el seguimiento de si la autorización es correcta.

Cada IAuthorizationHandler es responsable de comprobar si se cumplen los requisitos:

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

La clase AuthorizationHandlerContext es la que usa el controlador para marcar si se han cumplido los requisitos:

 context.Succeed(requirement)

En el código siguiente se muestra la implementación predeterminada simplificada (y anotada con comentarios) del servicio de autorización:

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

En el código siguiente se muestra una configuración típica del servicio de autorización:

// 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")] o RequireAuthorization("Something") para la autorización.

Aplicación de directivas a controladores MVC

Para las aplicaciones que usan Razor Pages, consulte la sección Aplicar directivas a Razor Pages.

Aplique directivas a los controladores usando el atributo [Authorize] con el nombre de la directiva:

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

Si se aplican varias directivas en los niveles de controlador y acción, todas las directivas deben pasar antes de que se conceda acceso:

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

Aplicar directivas a Razor Pages

Aplique directivas a Razor Pages usando el atributo [Authorize] con el nombre de la directiva. Por ejemplo:

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

namespace AuthorizationPoliciesSample.Pages;

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

Las directivas no pueden aplicarse a nivel de controlador de Razor Page, deben aplicarse a la página.

Las directivas también se pueden aplicar a Razor Pages mediante una convención de autorización.

Aplicación de directivas a puntos de conexión

Aplique las directivas a los puntos de conexión usando RequireAuthorization con el nombre de la directiva. Por ejemplo:

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

Requisitos

Un requisito de autorización es una colección de parámetros de datos que una directiva puede usar para evaluar la entidad de seguridad de usuario actual. En nuestra directiva "AtLeast21", el requisito es un único parámetro: la edad mínima. Un requisito implementa IAuthorizationRequirement, que es una interfaz de marcador vacía. Un requisito de edad mínima parametrizada podría implementarse de la siguiente manera:

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

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

    public int MinimumAge { get; }
}

Si una directiva de autorización contiene varios requisitos de autorización, todos los requisitos deben pasar para que la evaluación de la directiva se realice correctamente. En otras palabras, se tratan varios requisitos de autorización agregados a una sola directiva de autorización por AND .

Nota:

Un requisito no necesita tener datos ni propiedades.

Controladores de autorización

Un controlador de autorización es responsable de la evaluación de las propiedades de un requisito. El controlador de autorización evalúa los requisitos de un AuthorizationHandlerContext proporcionado para determinar si se permite el acceso.

Un requisito puede tener varios controladores. Un controlador puede heredar AuthorizationHandler<TRequirement>, donde TRequirement es el requisito que se va a controlar. Como alternativa, un controlador puede implementar IAuthorizationHandler directamente para controlar más de un tipo de requisito.

Uso de un controlador para un requisito

En el ejemplo siguiente se muestra una relación uno a uno en la que un controlador de edad mínima controla un ú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;
    }
}

El código anterior determina si la entidad de seguridad de usuario actual tiene una fecha de nacimiento emitida por un emisor conocido y de confianza. La autorización no se puede producir cuando falta la notificación, en cuyo caso se devuelve una tarea completada. Cuando hay una notificación, se calcula la edad del usuario. Si el usuario cumple la edad mínima definida por el requisito, la autorización se considera correcta. Cuando la autorización se realiza correctamente, context.Succeed se invoca con el requisito satisfecho como único parámetro.

Uso de un controlador para varios requisitos

En el ejemplo siguiente se muestra una relación uno a varios en la que un controlador de permisos puede controlar tres 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;
    }
}

El código anterior atraviesa PendingRequirements: una propiedad que contiene requisitos no marcados como correctos. Para un requisito ReadPermission, el usuario debe ser propietario o patrocinador para acceder al recurso solicitado. Para un requisito EditPermission o DeletePermission, debe ser propietario para acceder al recurso solicitado.

Registro del controlador

Registre controladores en la colección de servicios durante la configuración. Por ejemplo:

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

El código anterior registra MinimumAgeHandler como un singleton. Los controladores se pueden registrar mediante cualquiera de las duraciones de servicio integradas.

Es posible agrupar un requisito y un controlador en una sola clase que implemente IAuthorizationRequirement y IAuthorizationHandler. Esta agrupación crea un acoplamiento estricto entre el controlador y el requisito y solo se recomienda para los requisitos y controladores simples. La creación de una clase que implemente ambas interfaces elimina la necesidad de registrar el controlador en DI debido al PassThroughAuthorizationHandler incorporado que permite que los requisitos se controlen a sí mismos.

Consulte la clase AssertionRequirement para ver un buen ejemplo en el que AssertionRequirement es a la vez un requisito y el controlador en una clase totalmente autónoma.

¿Qué debe devolver un controlador?

Observe que el método Handle del ejemplo del controlador no devuelve ningún valor. ¿Cómo se indica un estado de éxito o error?

  • Un controlador indica el éxito llamando a context.Succeed(IAuthorizationRequirement requirement), pasándose el requisito que ha sido validado con éxito.

  • Por lo general, un controlador no necesita controlar los errores, ya que otros controladores para el mismo requisito pueden tener éxito.

  • Para garantizar el error, incluso si otros controladores de requisitos tienen éxito, llame a context.Fail.

Si un controlador llama a context.Succeed o context.Fail, se sigue llamando a todos los demás controladores. Esto permite que los requisitos produzcan efectos secundarios, como el registro, que tiene lugar incluso si otro controlador ha validado correctamente o ha producido un error en un requisito. Cuando se establece en false, la propiedad InvokeHandlersAfterFailure cortocircuita la ejecución de controladores cuando se llama a context.Fail. El valor predeterminado de InvokeHandlersAfterFailure es true, en cuyo caso se llama a todos los controladores.

Nota:

Se llama a los controladores de autorización incluso si se produce un error en la autenticación. Además, los controladores pueden ejecutarse en cualquier orden, por lo que no dependen de que se les llame en un orden determinado.

¿Por qué quiero varios controladores para un requisito?

En los casos en los que quiera que la evaluación sea en función de OR, implemente varios controladores para un único requisito. Por ejemplo, Microsoft tiene puertas que solo se abren con tarjetas llave. Si deja su tarjeta llave en casa, el recepcionista imprime una pegatina temporal y abre la puerta por usted. En este escenario, tendría un único requisito, BuildingEntry, pero varios controladores, cada uno examinando un ú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;
    }
}

Asegúrese de que ambos controladores están registrados. Si cualquiera de los controladores tiene éxito cuando una directiva evalúa BuildingEntryRequirement, la evaluación de la directiva tiene éxito.

Usar una función para cumplir una directiva

Puede haber situaciones en las que el cumplimiento de una directiva es fácil de expresar en el código. Es posible proporcionar un Func<AuthorizationHandlerContext, bool> al configurar una directiva con el generador de directivas RequireAssertion.

Por ejemplo, el BadgeEntryHandler anterior podría reescribirse de la siguiente manera:

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")));
});

Acceso al contexto de solicitud de MVC en controladores

El método HandleRequirementAsync tiene dos parámetros: un AuthorizationHandlerContext y el TRequirement que se está controlando. Los marcos de trabajo como MVC o SignalR son libres de agregar cualquier objeto a la propiedad Resource del AuthorizationHandlerContext para pasarse información adicional.

Al usar el enrutamiento de puntos de conexión, el middleware de autorización suele controlar la autorización. En este caso, la propiedad Resource es una instancia de HttpContext. El contexto se puede usar para acceder al punto de conexión actual, que se puede usar para sondear el recurso subyacente al que se va a enrutar. Por ejemplo:

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

Con el enrutamiento tradicional, o cuando la autorización se produce como parte del filtro de autorización de MVC, el valor de Resource es una instancia de AuthorizationFilterContext. Esta propiedad proporciona acceso a HttpContext, RouteData y todo lo demás proporcionado por MVC y Razor Pages.

El uso de la propiedad Resource es específico del marco. El uso de información en la propiedad Resource limita las directivas de autorización a marcos concretos. Convierta la propiedad Resource usando la palabra clave is, y después confirme que la conversión se ha realizado con éxito para asegurarse de que su código no se bloquea con un InvalidCastException cuando se ejecute en otros marcos :

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

Requerir globalmente que todos los usuarios se autentiquen

Para obtener información sobre cómo requerir globalmente la autenticación de todos los usuarios, vea Requerir usuarios autenticados.

Ejemplo de autorización con servicio externo

El código de ejemplo de AspNetCore.Docs.Samples muestra cómo implementar requisitos de autorización adicionales con un servicio de autorización externo. El proyecto de ejemplo Contoso.API está protegido con Azure AD. Una comprobación de autorización adicional del proyecto Contoso.Security.API devuelve una carga útil que describe si la aplicación cliente Contoso.API puede invocar la API de GetWeather.

Configuración del ejemplo

  1. Cree un registro de aplicación en el inquilino de Azure Active Directory (Azure AD):
  • Asígnele un AppRole.
  • En Permisos de API, agregue AppRole como permiso y conceda el consentimiento de Administrador. Tenga en cuenta que, en esta configuración, este registro de aplicación representa tanto la API como el cliente que invoca la API. Si lo desea, puede crear dos registros de aplicaciones. Si usa esta configuración, asegúrese de realizar solo los permisos de API, agregue AppRole como paso de permiso solo para el cliente. Solo el registro de la aplicación cliente requiere que se genere un secreto de cliente.
  1. Configure el proyecto de Contoso.API con las siguientes opciones:
{
  "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. Configure Contoso.Security.API con la siguiente configuración:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. Importe el archivo ContosoAPI.postman_collection.json en Postman y configure un entorno con lo siguiente:

    • ClientId: id. de cliente del registro de la aplicación que representa al cliente que llama a la API.
    • clientSecret: secreto de cliente del registro de la aplicación que representa al cliente que llama a la API.
    • TenantId: identificador de inquilino de las propiedades de AAD
  2. Ejecute la solución y use Postman para invocar la API. Puede agregar puntos de interrupción en Contoso.Security.API.SecurityPolicyController y observar que se pasa el identificador de cliente que se usa para afirmar si se permite obtener tiempo.

Recursos adicionales

De manera oculta, la autorización basada en roles y la autorización basada en notificaciones usan un requisito, un controlador de requisitos y una directiva preconfigurada. Estos bloques de creación admiten la expresión de evaluaciones de autorización en el código. El resultado es una estructura de autorización más completa, reutilizable y probable.

Una directiva de autorización consta de uno o varios requisitos. Se registra como parte de la configuración del servicio de autorización en el 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)));
    });
}

En el ejemplo anterior, se crea una directiva "AtLeast21". Tiene un único requisito: el de una edad mínima, que se proporciona como parámetro para el requisito.

IAuthorizationService

El servicio principal que determina si la autorización se realiza correctamente es 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);
}

El código anterior resalta los dos métodos de IAuthorizationService.

IAuthorizationRequirement es un servicio de marcador sin métodos y el mecanismo para realizar el seguimiento de si la autorización es correcta.

Cada IAuthorizationHandler es responsable de comprobar si se cumplen los requisitos:

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

La clase AuthorizationHandlerContext es la que usa el controlador para marcar si se han cumplido los requisitos:

 context.Succeed(requirement)

En el código siguiente se muestra la implementación predeterminada simplificada (y anotada con comentarios) del servicio de autorización:

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

El código siguiente muestra un 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 o [Authorize(Policy = "Something")] para la autorización.

Aplicar directivas al controlador MVC

Si usa Razor Pages, consulte Aplicar directivas a Razor Pages en este documento.

Las directivas se aplican a los controladores mediante el atributo [Authorize] con el nombre de directiva. Por ejemplo:

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

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

Aplicar directivas a Razor Pages

Las directivas se aplican a Razor Pages usando el atributo [Authorize] con el nombre de la directiva. Por ejemplo:

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

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

Las directivas no pueden aplicarse a nivel de controlador de Razor Page, deben aplicarse a la página.

Las directivas se pueden aplicar a Razor Pages mediante una convención de autorización.

Requisitos

Un requisito de autorización es una colección de parámetros de datos que una directiva puede usar para evaluar la entidad de seguridad de usuario actual. En nuestra directiva "AtLeast21", el requisito es un único parámetro: la edad mínima. Un requisito implementa IAuthorizationRequirement, que es una interfaz de marcador vacía. Un requisito de edad mínima parametrizada podría implementarse de la siguiente manera:

using Microsoft.AspNetCore.Authorization;

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

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

Si una directiva de autorización contiene varios requisitos de autorización, todos los requisitos deben pasar para que la evaluación de la directiva se realice correctamente. En otras palabras, se tratan varios requisitos de autorización agregados a una sola directiva de autorización por AND .

Nota:

Un requisito no necesita tener datos ni propiedades.

Controladores de autorización

Un controlador de autorización es responsable de la evaluación de las propiedades de un requisito. El controlador de autorización evalúa los requisitos de un AuthorizationHandlerContext proporcionado para determinar si se permite el acceso.

Un requisito puede tener varios controladores. Un controlador puede heredar AuthorizationHandler<TRequirement>, donde TRequirement es el requisito que se va a controlar. Como alternativa, un controlador puede implementar IAuthorizationHandler para controlar más de un tipo de requisito.

Uso de un controlador para un requisito

En el ejemplo siguiente se muestra una relación uno a uno en la que un controlador de edad mínima usa un ú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;
    }
}

El código anterior determina si la entidad de seguridad de usuario actual tiene una fecha de nacimiento emitida por un emisor conocido y de confianza. La autorización no se puede producir cuando falta la notificación, en cuyo caso se devuelve una tarea completada. Cuando hay una notificación, se calcula la edad del usuario. Si el usuario cumple la edad mínima definida por el requisito, la autorización se considera correcta. Cuando la autorización se realiza correctamente, context.Succeed se invoca con el requisito satisfecho como único parámetro.

Uso de un controlador para varios requisitos

En el ejemplo siguiente se muestra una relación uno a varios en la que un controlador de permisos puede controlar tres 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;
    }
}

El código anterior atraviesa PendingRequirements: una propiedad que contiene requisitos no marcados como correctos. Para un requisito ReadPermission, el usuario debe ser propietario o patrocinador para acceder al recurso solicitado. Para un requisito EditPermission o DeletePermission, el usuario debe ser propietario para acceder al recurso solicitado.

Registro del controlador

Los controladores se registran en la colección de servicios durante la configuración. Por ejemplo:

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>();
}

El código anterior registra MinimumAgeHandler como singleton invocando services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. Los controladores se pueden registrar mediante cualquiera de las duraciones de servicio integradas.

Es posible agrupar un requisito y un controlador en una sola clase que implemente IAuthorizationRequirement y IAuthorizationHandler. Esta agrupación crea un acoplamiento estricto entre el controlador y el requisito y solo se recomienda para los requisitos y controladores simples. La creación de una clase que implemente ambas interfaces elimina la necesidad de registrar el controlador en DI debido al PassThroughAuthorizationHandler incorporado que permite que los requisitos se controlen a sí mismos.

Consulte la clase AssertionRequirement para ver un buen ejemplo en el que AssertionRequirement es a la vez un requisito y el controlador en una clase totalmente autónoma.

¿Qué debe devolver un controlador?

Observe que el método Handle del ejemplo del controlador no devuelve ningún valor. ¿Cómo se indica un estado de éxito o error?

  • Un controlador indica el éxito llamando a context.Succeed(IAuthorizationRequirement requirement), pasándose el requisito que ha sido validado con éxito.

  • Por lo general, un controlador no necesita controlar los errores, ya que otros controladores para el mismo requisito pueden tener éxito.

  • Para garantizar el error, incluso si otros controladores de requisitos tienen éxito, llame a context.Fail.

Si un controlador llama a context.Succeed o context.Fail, se sigue llamando a todos los demás controladores. Esto permite que los requisitos produzcan efectos secundarios, como el registro, que tiene lugar incluso si otro controlador ha validado correctamente o ha producido un error en un requisito. Cuando se establece en false, la propiedad InvokeHandlersAfterFailure cortocircuita la ejecución de controladores cuando se llama a context.Fail. El valor predeterminado de InvokeHandlersAfterFailure es true, en cuyo caso se llama a todos los controladores.

Nota:

Se llama a los controladores de autorización incluso si se produce un error en la autenticación.

¿Por qué quiero varios controladores para un requisito?

En los casos en los que quiera que la evaluación sea en función de OR, implemente varios controladores para un único requisito. Por ejemplo, Microsoft tiene puertas que solo se abren con tarjetas llave. Si deja su tarjeta llave en casa, el recepcionista imprime una pegatina temporal y abre la puerta por usted. En este escenario, tendría un único requisito, BuildingEntry, pero varios controladores, cada uno examinando un ú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;
    }
}

Asegúrese de que ambos controladores están registrados. Si cualquiera de los controladores tiene éxito cuando una directiva evalúa BuildingEntryRequirement, la evaluación de la directiva tiene éxito.

Usar una función para cumplir una directiva

Puede haber situaciones en las que el cumplimiento de una directiva es fácil de expresar en el código. Es posible proporcionar un Func<AuthorizationHandlerContext, bool> al configurar la directiva con el generador de directivas RequireAssertion.

Por ejemplo, el BadgeEntryHandler anterior podría reescribirse de la siguiente manera:

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

Acceso al contexto de solicitud de MVC en controladores

El método HandleRequirementAsync que implementa en un controlador de autorización tiene dos parámetros: un AuthorizationHandlerContext y el TRequirement que está controlando. Los marcos de trabajo como MVC o SignalR son libres de agregar cualquier objeto a la propiedad Resource del AuthorizationHandlerContext para pasarse información adicional.

Al usar el enrutamiento de puntos de conexión, el middleware de autorización suele controlar la autorización. En este caso, la propiedad Resource es una instancia de HttpContext. El contexto se puede usar para acceder al punto de conexión actual, que se puede usar para sondear el recurso subyacente al que se va a enrutar. Por ejemplo:

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

Con el enrutamiento tradicional, o cuando la autorización se produce como parte del filtro de autorización de MVC, el valor de Resource es una instancia de AuthorizationFilterContext. Esta propiedad proporciona acceso a HttpContext, RouteData y todo lo demás proporcionado por MVC y Razor Pages.

El uso de la propiedad Resource es específico del marco. El uso de información en la propiedad Resource limita las directivas de autorización a marcos concretos. Convierta la propiedad Resource usando la palabra clave is, y después confirme que la conversión se ha realizado con éxito para asegurarse de que su código no se bloquea con un InvalidCastException cuando se ejecute en otros marcos :

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

Requerir globalmente que todos los usuarios se autentiquen

Para obtener información sobre cómo requerir globalmente la autenticación de todos los usuarios, vea Requerir usuarios autenticados.