Partager via


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 marqueur sans méthodes, et le mécanisme permettant de vérifier si l'autorisation a été accordée.

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<IAuthorizationHandler> 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 AtLeast21Controller2 : Controller
{
    [Authorize(Policy = "IdentificationValidated")]
    public IActionResult Index() => View();
}

Appliquer des politiques 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

Appliquez des stratégies aux points de terminaison à l'aide de RequireAuthorization avec le nom de la stratégie. Par exemple :

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

Exigences

Une exigence d'autorisation est un ensemble de paramètres de données qu'une stratégie peut utiliser pour évaluer l'entité utilisateur actuelle. 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.

Note

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 l'entité principale utilisateur actuelle dispose d'une revendication de date de naissance émise par un émetteur connu et approuvé. L'autorisation ne peut pas être accordée lorsque la réclamation est manquante, auquel cas une tâche achevé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 parcourt 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.

Enregistrement des gestionnaires

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

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

Le code précédent enregistre MinimumAgeHandler en tant que singleton. Les gestionnaires peuvent être enregistrés à 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 de l’intégration PassThroughAuthorizationHandler qui permet aux exigences de se gérer elles-mêmes.

Consultez l’implémentation de la AssertionRequirement classe pour obtenir un bon exemple où l’élément 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 d'exigences 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.

Note

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 carte de clé à la maison, le 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 une fonction pour remplir une politique

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 cadres particuliers. 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 vérification supplémentaire d'autorisation du projet Contoso.Security.API retourne une charge utile décrivant si l'application cliente Contoso.API peut appeler l'API GetWeather.

Configurer l'exemple

  • Créez une inscription d’application dans votre client Microsoft Entra ID :

  • Veuillez lui attribuer un rôle d'application.

  • Sous les autorisations d'API, ajoutez AppRole en tant qu’autorisation et fournissez le consentement de l’administrateur. 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.

  • 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": "*"
}
  • 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>"
  ]
}
  • Ouvrez le fichier ContosoAPI.collection.json 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
  • Extrayez les commandes du fichier ContosoAPI.collection.json et utilisez-les pour construire des commandes cURL afin de tester l’application.

  • Exécutez la solution et utilisez cURL 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 vérifier s'il est autorisé à obtenir les informations météorologiques.

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 marqueur sans méthodes, et le mécanisme permettant de vérifier si l'autorisation a été accordée.

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 Pages Razor, veuillez consulter la section Appliquer des stratégies aux pages Razor de 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 politiques 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.

Exigences

Une exigence d'autorisation est un ensemble de paramètres de données qu'une stratégie peut utiliser pour évaluer l'entité utilisateur actuelle. 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.

Note

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 l'entité principale utilisateur actuelle dispose d'une revendication de date de naissance émise par un émetteur connu et approuvé. L'autorisation ne peut pas être accordée si la revendication est manquante. Dans ce cas, une tâche terminée est renvoyé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 parcourt 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.

Enregistrement des gestionnaires

Les gestionnaires sont enregistrés dans la collection de services lors de 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 enregistre MinimumAgeHandler en tant que singleton en invoquant services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();. Les gestionnaires peuvent être enregistrés à 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 d'exigences 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.

Note

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 carte de clé à la maison, le 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 une fonction pour appliquer une politique

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 cadres particuliers. 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.