Autorisation basée sur des stratégies dans ASP.NET Core

Sous les couvertures, l’autorisation basée sur les rôles et l’autorisation basée sur les revendications utilisent une exigence, un gestionnaire de conditions requises et une stratégie préconfigurée. Ces blocs de construction prennent en charge l’expression des évaluations d’autorisation dans le code. Le résultat est une structure d’autorisation plus riche, réutilisable et testable.

Une stratégie d’autorisation se compose d’une ou plusieurs exigences. Inscrivez-le dans le cadre de la configuration du service d’autorisation, dans le fichier de l’application Program.cs :

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

Dans l’exemple précédent, une stratégie « AtLeast21 » est créée. Il a une exigence unique, celle d’un âge minimum, qui est fourni en tant que paramètre de la condition requise.

IAuthorizationService

Le service principal qui détermine si l’autorisation a réussi est 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);
}

Le code précédent met en évidence les deux méthodes de IAuthorizationService.

IAuthorizationRequirement est un service de marqueur sans méthode et le mécanisme permettant de déterminer si l’autorisation a réussi.

Chacun IAuthorizationHandler est chargé de vérifier si les exigences sont remplies :

/// <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 classe AuthorizationHandlerContext est ce que le gestionnaire utilise pour indiquer si les exigences ont été remplies :

 context.Succeed(requirement)

Le code suivant montre l’implémentation par défaut simplifiée (et annotée avec des commentaires) du service d’autorisation :

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

Le code suivant montre une configuration de service d’autorisation classique :

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

Utilisez IAuthorizationService, [Authorize(Policy = "Something")]ou RequireAuthorization("Something") pour l’autorisation.

Appliquer des stratégies aux contrôleurs MVC

Pour les applications qui utilisent Razor Pages, consultez la section Appliquer des stratégies aux Razor pages.

Les stratégies sont appliquées aux contrôleurs à l’aide de l'attribut [Authorize] avec le nom de la stratégie :

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

Si plusieurs stratégies sont appliquées au niveau du contrôleur et de l’action, toutes les stratégies doivent passer avant que l’accès soit accordé :

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

Appliquer des stratégies aux Razor pages

Les stratégies sont appliquées aux Pages Razor à l’aide de l'attribut [Authorize] avec le nom de la stratégie. Par exemple :

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

namespace AuthorizationPoliciesSample.Pages;

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

Les stratégies ne peuvent pas être appliqués au niveau du Razor gestionnaire de pages, ils doivent être appliqués à la page.

Les stratégies peuvent également être appliquées aux pages Razor à l’aide d’une convention d’autorisation.

Appliquer des stratégies aux points de terminaison

Appliquer des politiques aux terminaux en utilisant RequireAuthorization avec le nom de la stratégie. Par exemple :

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

Configuration requise

Une exigence d’autorisation est une collection de paramètres de données qu’une stratégie peut utiliser pour évaluer le principal d’utilisateur actuel. Dans notre stratégie « AtLeast21 », l’exigence est un paramètre unique : l’âge minimum. Une exigence implémente IAuthorizationRequirement, qui est une interface de marqueur vide. Une exigence d’âge minimal paramétrable peut être implémentée comme suit :

using Microsoft.AspNetCore.Authorization;

namespace AuthorizationPoliciesSample.Policies.Requirements;

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

    public int MinimumAge { get; }
}

Si une stratégie d’autorisation contient plusieurs exigences d’autorisation, toutes les exigences doivent être remplies pour que l’évaluation de la stratégie réussisse. En d’autres termes, plusieurs exigences d’autorisation ajoutées à une stratégie d’autorisation unique sont traitées sur une base AND.

Notes

Une exigence n’a pas besoin d’avoir des données ou des propriétés.

Gestionnaires d’autorisation

Un gestionnaire d’autorisation est responsable de l’évaluation des propriétés d’une exigence. Le gestionnaire d’autorisation évalue les exigences par rapport à un AuthorizationHandlerContext fourni pour déterminer si l’accès est autorisé.

Une exigence peut avoir plusieurs gestionnaires. Un gestionnaire peut hériter de AuthorizationHandler<TRequirement>, où TRequirement est l’exigence à gérer. Un gestionnaire peut également implémenter IAuthorizationHandler directement pour gérer plusieurs types d’exigences.

Utiliser un gestionnaire pour une exigence

L’exemple suivant montre une relation un-à-un dans laquelle un gestionnaire d’âge minimum gère une seule exigence :

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

Le code précédent détermine si le principal de l’utilisateur actuel a une revendication de date de naissance qui a été émise par un émetteur connu et approuvé. L’autorisation ne peut pas se produire lorsque la revendication est manquante, auquel cas une tâche terminée est retournée. Lorsqu’une revendication est présente, l’âge de l’utilisateur est calculé. Si l’utilisateur répond à l’âge minimal défini par l’exigence, l’autorisation est considérée comme réussie. Lorsque l’autorisation réussit, context.Succeed est appelé avec l’exigence satisfaite comme seul paramètre.

Utiliser un gestionnaire pour plusieurs exigences

L’exemple suivant montre une relation un-à-plusieurs dans laquelle un gestionnaire d’autorisations peut gérer trois types d’exigences différents :

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

Le code précédent traverse PendingRequirements, une propriété contenant des exigences non marquées comme réussies. Pour une exigence ReadPermission, l’utilisateur doit être propriétaire ou sponsor pour accéder à la ressource demandée. Pour une exigence EditPermission ou DeletePermission, il doit être propriétaire pour accéder à la ressource demandée.

Inscription du gestionnaire

Inscrivez des gestionnaires dans la collection de services pendant la configuration. Par exemple :

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

Le code précédent s’inscrit MinimumAgeHandler en tant que singleton. Les gestionnaires peuvent être inscrits à l’aide de n’importe quelle durée de vie de service intégrée.

Il est possible de regrouper à la fois une exigence et un gestionnaire dans une classe unique implémentant à la fois IAuthorizationRequirement et IAuthorizationHandler. Ce regroupement crée un couplage étroit entre le gestionnaire et la spécification et est recommandé uniquement pour les exigences et les gestionnaires simples. La création d’une classe qui implémente les deux interfaces supprime la nécessité d’inscrire le gestionnaire dans DI en raison du passThroughAuthorizationHandler intégré qui permet aux exigences de se gérer elles-mêmes.

Consultez la classe AssertionRequirement pour obtenir un bon exemple où AssertionRequirement est à la fois une exigence et le gestionnaire dans une classe entièrement autonome.

Que doit retourner un gestionnaire ?

Notez que la méthode Handledans l’exemple de gestionnaire ne retourne aucune valeur. Comment un état de réussite ou d’échec est-il indiqué ?

  • Un gestionnaire indique la réussite en appelant context.Succeed(IAuthorizationRequirement requirement), en passant l’exigence qui a été validée avec succès.

  • Un gestionnaire n’a pas besoin de gérer les défaillances en général, car d’autres gestionnaires pour la même exigence peuvent réussir.

  • Pour garantir l’échec, même si d’autres gestionnaires de conditions requises réussissent, appelez context.Fail.

Si un gestionnaire appelle context.Succeed ou context.Fail, tous les autres gestionnaires sont toujours appelés. Cela permet aux exigences de produire des effets secondaires, tels que la journalisation, qui se produisent même si un autre gestionnaire a correctement validé ou échoué à une exigence. Lorsqu’elle est définie sur false, la InvokeHandlersAfterFailure propriété court-circuite l’exécution des gestionnaires quand context.Fail est appelé. InvokeHandlersAfterFailure la valeur par défaut est true, auquel cas tous les gestionnaires sont appelés.

Notes

Les gestionnaires d’autorisation sont appelés même si l’authentification échoue. De même, les gestionnaires peuvent s’exécuter dans n’importe quel ordre. Ne dépendez donc pas de leur appel dans un ordre particulier.

Pourquoi voudrais-je plusieurs gestionnaires pour une exigence ?

Dans les cas où vous souhaitez que l’évaluation soit sur une base OR, implémentez plusieurs gestionnaires pour une seule exigence. Par exemple, Microsoft a des portes qui s’ouvrent uniquement avec des cartes de clés. Si vous laissez votre clé carte à la maison, la réceptionniste imprime un autocollant temporaire et ouvre la porte pour vous. Dans ce scénario, vous auriez une seule exigence, BuildingEntry, mais plusieurs gestionnaires, chacun examinant une seule exigence.

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

Vérifiez que les deux gestionnaires sont inscrits. Si l’un des gestionnaires réussit lorsqu’une stratégie évalue le BuildingEntryRequirement, l’évaluation de la stratégie réussit.

Utiliser un func pour remplir une stratégie

Il peut y avoir des situations dans lesquelles l’exécution d’une stratégie est simple à exprimer dans le code. Il est possible de fournir un Func<AuthorizationHandlerContext, bool> lors de la configuration d’une stratégie avec le générateur de stratégies RequireAssertion .

Par exemple, le BadgeEntryHandler précédent pourrait être réécrit comme ceci :

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

Accéder au contexte de requête MVC dans les gestionnaires

La méthode HandleRequirementAsync a deux paramètres : un AuthorizationHandlerContext et le TRequirement qui est géré. Les frameworks tels que MVC ou SignalR sont libres d’ajouter n’importe quel objet à la propriété Resource sur le AuthorizationHandlerContext pour transmettre des informations supplémentaires.

Lors de l’utilisation du routage de point de terminaison, l’autorisation est généralement gérée par le middleware d’autorisation. Dans ce cas, la propriété Resource est une instance de HttpContext. Le contexte peut être utilisé pour accéder au point de terminaison actuel, qui peut être utilisé pour sonder la ressource sous-jacente vers laquelle vous effectuez le routage. Par exemple :

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

Avec le routage traditionnel, ou lorsque l’autorisation se produit dans le cadre du filtre d’autorisation de MVC, la valeur de Resource est une instance AuthorizationFilterContext. Cette propriété fournit l’accès à HttpContext, RouteData, et à tous les autres éléments fournis par MVC et Razor Pages.

L’utilisation de la propriété Resource est spécifique à l’infrastructure. L’utilisation des informations contenues dans la propriété Resource limite vos stratégies d’autorisation à des infrastructures particulières. Castez la propriété Resource à l’aide du mot clé is, puis vérifiez que la conversion a réussi pour vous assurer que votre code ne se bloque pas avec un InvalidCastException lors de l’exécution sur d’autres frameworks :

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

Exiger globalement que tous les utilisateurs soient authentifiés

Pour plus d’informations sur la façon d’exiger l’authentification globale de tous les utilisateurs, consultez Exiger des utilisateurs authentifiés.

Autorisation avec un exemple de service externe

L’exemple de code sur AspNetCore.Docs.Samples montre comment implémenter des exigences d’autorisation supplémentaires avec un service d’autorisation externe. L’exemple de Contoso.API projet est sécurisé avec Azure AD. Une autorisation supplémentaire vérifie que le Contoso.Security.API projet retourne une charge utile indiquant si l’application Contoso.API cliente peut appeler l’APIGetWeather.

Configurer l'exemple

  1. Créez une inscription d’application dans votre locataire Azure Active Directory (Azure AD) :
  • Attribuez-lui un AppRole.
  • Sous Autorisations d’API, ajoutez AppRole en tant qu’autorisation et accordez Administration consentement. Notez que dans cette configuration, cette inscription d’application représente à la fois l’API et le client appelant l’API. Si vous le souhaitez, vous pouvez créer deux inscriptions d’application. Si vous utilisez cette configuration, veillez à effectuer uniquement les autorisations d’API, ajoutez AppRole comme étape d’autorisation uniquement pour le client. Seule l’inscription de l’application cliente nécessite la génération d’une clé secrète client.
  1. Configurez le Contoso.API projet avec les paramètres suivants :
{
  "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. Configurez Contoso.Security.API en utilisant les paramètres suivants :
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "AllowedClients": [
    "<Use the appropriate Client Id representing the Client calling the API>"
  ]
}
  1. Importez le fichier ContosoAPI.postman_collection.json dans Postman et configurez un environnement avec les éléments suivants :

    • ClientId : ID client de l’inscription de l’application représentant le client appelant l’API.
    • clientSecret : Secret client de l’inscription de l’application représentant le client appelant l’API.
    • TenantId : ID de locataire des propriétés AAD
  2. Exécutez la solution et utilisez Postman pour appeler l’API. Vous pouvez ajouter des points d’arrêt dans le Contoso.Security.API.SecurityPolicyController et observer que l’ID client est transmis qui est utilisé pour affirmer s’il est autorisé à Obtenir la météo.

Ressources supplémentaires

Sous les couvertures, l’autorisation basée sur les rôles et l’autorisation basée sur les revendications utilisent une exigence, un gestionnaire de conditions requises et une stratégie préconfigurée. Ces blocs de construction prennent en charge l’expression des évaluations d’autorisation dans le code. Le résultat est une structure d’autorisation plus riche, réutilisable et testable.

Une stratégie d’autorisation se compose d’une ou plusieurs exigences. Il est enregistré dans le cadre de la configuration du service d'autorisation, dans la méthode Startup.ConfigureServices :

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

Dans l’exemple précédent, une stratégie « AtLeast21 » est créée. Il a une exigence unique, celle d’un âge minimum, qui est fourni en tant que paramètre de la condition requise.

IAuthorizationService

Le service principal qui détermine si l’autorisation a réussi est 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);
}

Le code précédent met en évidence les deux méthodes de IAuthorizationService.

IAuthorizationRequirement est un service de marqueur sans méthode et le mécanisme permettant de déterminer si l’autorisation a réussi.

Chacun IAuthorizationHandler est chargé de vérifier si les exigences sont remplies :

/// <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 classe AuthorizationHandlerContext est ce que le gestionnaire utilise pour indiquer si les exigences ont été remplies :

 context.Succeed(requirement)

Le code suivant montre l’implémentation par défaut simplifiée (et annotée avec des commentaires) du service d’autorisation :

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

Le code suivant montre un ConfigureServices typique :

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

Utilisez IAuthorizationService ou [Authorize(Policy = "Something")] pour l’autorisation.

Appliquer des stratégies au contrôleur MVC

Si vous utilisez Razor Pages, consultez Appliquer des stratégies aux Razor pages dans ce document.

Les stratégies sont appliquées aux contrôleurs à l’aide de l'attribut [Authorize] avec le nom de la stratégie. Par exemple :

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

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

Appliquer des stratégies aux Razor pages

Les stratégies sont appliquées aux PagesRazor à l’aide de l'attribut [Authorize] avec le nom de la stratégie. Par exemple :

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

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

Les stratégies ne peuvent pas être appliqués au niveau du Razor gestionnaire de pages, ils doivent être appliqués à la page.

Les stratégies peuvent être appliquées aux pages Razor à l’aide d’une convention d’autorisation.

Spécifications

Une exigence d’autorisation est une collection de paramètres de données qu’une stratégie peut utiliser pour évaluer le principal d’utilisateur actuel. Dans notre stratégie « AtLeast21 », l’exigence est un paramètre unique : l’âge minimum. Une exigence implémente IAuthorizationRequirement, qui est une interface de marqueur vide. Une exigence d’âge minimal paramétrable peut être implémentée comme suit :

using Microsoft.AspNetCore.Authorization;

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

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

Si une stratégie d’autorisation contient plusieurs exigences d’autorisation, toutes les exigences doivent être remplies pour que l’évaluation de la stratégie réussisse. En d’autres termes, plusieurs exigences d’autorisation ajoutées à une stratégie d’autorisation unique sont traitées sur une base AND.

Notes

Une exigence n’a pas besoin d’avoir des données ou des propriétés.

Gestionnaires d’autorisation

Un gestionnaire d’autorisation est responsable de l’évaluation des propriétés d’une exigence. Le gestionnaire d’autorisation évalue les exigences par rapport à un AuthorizationHandlerContext fourni pour déterminer si l’accès est autorisé.

Une exigence peut avoir plusieurs gestionnaires. Un gestionnaire peut hériter de AuthorizationHandler<TRequirement>, où TRequirement est l’exigence à gérer. Un gestionnaire peut également implémenter IAuthorizationHandler pour gérer plusieurs types d’exigences.

Utiliser un gestionnaire pour une exigence

L’exemple suivant montre une relation un-à-un dans laquelle un gestionnaire d’âge minimum utilise une seule exigence :

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

Le code précédent détermine si le principal de l’utilisateur actuel a une revendication de date de naissance qui a été émise par un émetteur connu et approuvé. L’autorisation ne peut pas se produire lorsque la revendication est manquante, auquel cas une tâche terminée est retournée. Lorsqu’une revendication est présente, l’âge de l’utilisateur est calculé. Si l’utilisateur répond à l’âge minimal défini par l’exigence, l’autorisation est considérée comme réussie. Lorsque l’autorisation réussit, context.Succeed est appelé avec l’exigence satisfaite comme seul paramètre.

Utiliser un gestionnaire pour plusieurs exigences

L’exemple suivant montre une relation un-à-plusieurs dans laquelle un gestionnaire d’autorisations peut gérer trois types d’exigences différents :

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

Le code précédent traverse PendingRequirements, une propriété contenant des exigences non marquées comme réussies. Pour une exigence ReadPermission, l’utilisateur doit être propriétaire ou sponsor pour accéder à la ressource demandée. Pour une exigence EditPermission ou DeletePermission, l’utilisateur doit être propriétaire pour accéder à la ressource demandée.

Inscription du gestionnaire

Les gestionnaires sont inscrits dans la collection de services pendant la configuration. Par exemple :

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

Le code précédent s’inscrit MinimumAgeHandler en tant que singleton en invoquant services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. Les gestionnaires peuvent être inscrits à l’aide de n’importe quelle durée de vie de service intégrée.

Il est possible de regrouper à la fois une exigence et un gestionnaire dans une classe unique implémentant à la fois IAuthorizationRequirement et IAuthorizationHandler. Ce regroupement crée un couplage étroit entre le gestionnaire et la spécification et est recommandé uniquement pour les exigences et les gestionnaires simples. La création d’une classe qui implémente les deux interfaces supprime la nécessité d’inscrire le gestionnaire dans DI en raison du passThroughAuthorizationHandler intégré qui permet aux exigences de se gérer elles-mêmes.

Consultez la classe AssertionRequirement pour obtenir un bon exemple où AssertionRequirement est à la fois une exigence et le gestionnaire dans une classe entièrement autonome.

Que doit retourner un gestionnaire ?

Notez que la méthode Handledans l’exemple de gestionnaire ne retourne aucune valeur. Comment un état de réussite ou d’échec est-il indiqué ?

  • Un gestionnaire indique la réussite en appelant context.Succeed(IAuthorizationRequirement requirement), en passant l’exigence qui a été validée avec succès.

  • Un gestionnaire n’a pas besoin de gérer les défaillances en général, car d’autres gestionnaires pour la même exigence peuvent réussir.

  • Pour garantir l’échec, même si d’autres gestionnaires de conditions requises réussissent, appelez context.Fail.

Si un gestionnaire appelle context.Succeed ou context.Fail, tous les autres gestionnaires sont toujours appelés. Cela permet aux exigences de produire des effets secondaires, tels que la journalisation, qui se produisent même si un autre gestionnaire a correctement validé ou échoué à une exigence. Lorsqu’elle est définie sur false, la InvokeHandlersAfterFailure propriété court-circuite l’exécution des gestionnaires quand context.Fail est appelé. InvokeHandlersAfterFailure la valeur par défaut est true, auquel cas tous les gestionnaires sont appelés.

Notes

Les gestionnaires d’autorisation sont appelés même si l’authentification échoue.

Pourquoi voudrais-je plusieurs gestionnaires pour une exigence ?

Dans les cas où vous souhaitez que l’évaluation soit sur une base OR, implémentez plusieurs gestionnaires pour une seule exigence. Par exemple, Microsoft a des portes qui s’ouvrent uniquement avec des cartes de clés. Si vous laissez votre clé carte à la maison, la réceptionniste imprime un autocollant temporaire et ouvre la porte pour vous. Dans ce scénario, vous auriez une seule exigence, BuildingEntry, mais plusieurs gestionnaires, chacun examinant une seule exigence.

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

Vérifiez que les deux gestionnaires sont inscrits. Si l’un des gestionnaires réussit lorsqu’une stratégie évalue le BuildingEntryRequirement, l’évaluation de la stratégie réussit.

Utiliser un func pour remplir une stratégie

Il peut y avoir des situations dans lesquelles l’exécution d’une stratégie est simple à exprimer dans le code. Il est possible de fournir un Func<AuthorizationHandlerContext, bool> lors de la configuration de votre stratégie avec le générateur de stratégies RequireAssertion.

Par exemple, le BadgeEntryHandler précédent pourrait être réécrit comme ceci :

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

Accéder au contexte de requête MVC dans les gestionnaires

La méthode HandleRequirementAsync que vous implémentez dans un gestionnaire d’autorisation a deux paramètres : un AuthorizationHandlerContext et le TRequirement que vous gérez. Les frameworks tels que MVC ou SignalR sont libres d’ajouter n’importe quel objet à la propriété Resource sur le AuthorizationHandlerContext pour transmettre des informations supplémentaires.

Lors de l’utilisation du routage de point de terminaison, l’autorisation est généralement gérée par le middleware d’autorisation. Dans ce cas, la propriété Resource est une instance de HttpContext. Le contexte peut être utilisé pour accéder au point de terminaison actuel, qui peut être utilisé pour sonder la ressource sous-jacente vers laquelle vous effectuez le routage. Par exemple :

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

Avec le routage traditionnel, ou lorsque l’autorisation se produit dans le cadre du filtre d’autorisation de MVC, la valeur de Resource est une instance AuthorizationFilterContext. Cette propriété fournit l’accès à HttpContext, RouteData, et à tous les autres éléments fournis par MVC et Razor Pages.

L’utilisation de la propriété Resource est spécifique à l’infrastructure. L’utilisation des informations contenues dans la propriété Resource limite vos stratégies d’autorisation à des infrastructures particulières. Castez la propriété Resource à l’aide du mot clé is, puis vérifiez que la conversion a réussi pour vous assurer que votre code ne se bloque pas avec un InvalidCastException lors de l’exécution sur d’autres frameworks :

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

Exiger globalement que tous les utilisateurs soient authentifiés

Pour plus d’informations sur la façon d’exiger l’authentification globale de tous les utilisateurs, consultez la section Exiger des utilisateurs authentifiés.