Comment créez-vous un AuthorizeAttribute personnalisé dans ASP.NET Core?

429

J'essaie de créer un attribut d'autorisation personnalisé dans ASP.NET Core. Dans les versions précédentes, il était possible de passer outre bool AuthorizeCore(HttpContextBase httpContext). Mais cela n'existe plus en AuthorizeAttribute.

Quelle est l'approche actuelle pour créer un AuthorizeAttribute personnalisé?

Ce que j'essaie d'accomplir: je reçois un ID de session dans l'autorisation d'en-tête. À partir de cet ID, je saurai si une action particulière est valide.

jltrem
la source
Je ne sais pas comment le faire, mais MVC est open source. Vous pouvez extraire le dépôt github et rechercher les implémentations de IAuthorizationFilter. Si j'ai le temps aujourd'hui, je vais vous chercher et poster une réponse réelle, mais pas de promesses. github repo: github.com/aspnet/Mvc
bopapa_1979
OK, hors du temps, mais recherchez les utilisations de AuthorizationPolicy dans le MVC Repo, qui utilise AuthorizeAttribute, dans le répo aspnet / Security, ici: github.com/aspnet/Security . Alternativement, recherchez dans le référentiel MVC l'espace de noms où les éléments de sécurité dont vous vous souciez semblent résider, qui est Microsoft.AspNet.Authorization. Désolé, je ne peux pas être plus utile. Bonne chance!
bopapa_1979

Réponses:

446

L'approche recommandée par l'équipe ASP.Net Core consiste à utiliser la nouvelle conception de stratégie qui est entièrement documentée ici . L'idée de base derrière la nouvelle approche est d'utiliser le nouvel attribut [Autoriser] pour désigner une "politique" (par exemple, [Authorize( Policy = "YouNeedToBe18ToDoThis")]où la politique est enregistrée dans le Startup.cs de l'application pour exécuter un bloc de code (c'est-à-dire que l'utilisateur a une revendication d'âge). lorsque l'âge est de 18 ans ou plus).

La conception de la politique est un excellent ajout au cadre et l'équipe ASP.Net Security Core doit être félicitée pour son introduction. Cela dit, il n'est pas adapté à tous les cas. L'inconvénient de cette approche est qu'elle ne fournit pas de solution pratique pour le besoin le plus courant de simplement affirmer qu'un contrôleur ou une action donné nécessite un type de revendication donné. Dans le cas où une application peut avoir des centaines d'autorisations discrètes régissant les opérations CRUD sur des ressources REST individuelles ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder", etc.), la nouvelle approche nécessite soit une répétition de une à une. un mappage entre un nom de politique et un nom de revendication (par exempleoptions.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));), ou en écrivant du code pour effectuer ces enregistrements au moment de l'exécution (par exemple, lire tous les types de revendications dans une base de données et effectuer l'appel susmentionné en boucle). Le problème avec cette approche dans la majorité des cas, c'est qu'il s'agit de frais généraux inutiles.

Bien que l'équipe ASP.Net Core Security recommande de ne jamais créer votre propre solution, dans certains cas, cela peut être l'option la plus prudente pour commencer.

Voici une implémentation qui utilise IAuthorizationFilter pour fournir un moyen simple d'exprimer une exigence de revendication pour un contrôleur ou une action donnée:

public class ClaimRequirementAttribute : TypeFilterAttribute
{
    public ClaimRequirementAttribute(string claimType, string claimValue) : base(typeof(ClaimRequirementFilter))
    {
        Arguments = new object[] {new Claim(claimType, claimValue) };
    }
}

public class ClaimRequirementFilter : IAuthorizationFilter
{
    readonly Claim _claim;

    public ClaimRequirementFilter(Claim claim)
    {
        _claim = claim;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == _claim.Type && c.Value == _claim.Value);
        if (!hasClaim)
        {
            context.Result = new ForbidResult();
        }
    }
}


[Route("api/resource")]
public class MyController : Controller
{
    [ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")]
    [HttpGet]
    public IActionResult GetResource()
    {
        return Ok();
    }
}
Derek Greer
la source
80
Cela devrait être marqué comme la RÉPONSE CORRECTE. Ici, vous voyez comment les gens de Microsoft considèrent les commentaires des développeurs. Je ne comprends pas la raison pour laquelle ils sont si "fermés" autour de cela, car c'est une situation très courante d'avoir une multitude d'autorisations différentes, devoir coder une politique pour chacune est une exagération complète. Je cherchais cela depuis si longtemps ... (J'ai déjà posé cette question il y a presque deux ans, quand vNext était encore un pari ici: stackoverflow.com/questions/32181400/… mais nous sommes toujours coincés là-bas)
Vi100
3
Ce sont de bonnes choses. Nous avons un middleware d'authentification sur l'API Web mais une sécurité renforcée sur les autorisations d'autorisation par rôle; donc avoir juste à jeter un attribut comme: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] semble très bien.
Mariano Peinador
4
@Derek Greer: C'est la meilleure réponse. Cependant, vous implémentez un ActionFilter qui s'exécute après Authorize Action Filter. Y a-t-il une façon d'implémenter et d'autoriser le filtre d'action?
Jacob Phan
6
@JacobPhan Vous avez raison, cela serait mieux implémenté en utilisant l'interface IAuthorizationFilter. J'ai mis à jour le code pour refléter les changements.
Derek Greer, le
3
new ForbidResult()ne fonctionne donc pas (provoque l'exception / 500) car il n'a pas de schéma d'autorisation associé. Que devrais-je utiliser pour ce cas?
Sinaesthetic
253

Je suis le responsable de la sécurité d'asp.net. Tout d'abord, permettez-moi de m'excuser qu'aucun de ces éléments n'est encore documenté en dehors de l'échantillon ou des tests unitaires du magasin de musique, et tout cela est encore en cours de perfectionnement en termes d'API exposées. La documentation détaillée est ici .

Nous ne voulons pas que vous écriviez des attributs d'autorisation personnalisés. Si vous devez le faire, nous avons fait quelque chose de mal. Au lieu de cela, vous devez rédiger des exigences d' autorisation .

L'autorisation agit sur les identités. Les identités sont créées par authentification.

Vous dites dans les commentaires que vous souhaitez vérifier un ID de session dans un en-tête. Votre identifiant de session serait la base de l'identité. Si vous vouliez utiliser l' Authorizeattribut, vous écririez un middleware d'authentification pour prendre cet en-tête et le transformer en authentifié ClaimsPrincipal. Vous vérifieriez ensuite cela à l'intérieur d'une exigence d'autorisation. Les exigences d'autorisation peuvent être aussi compliquées que vous le souhaitez, par exemple en voici une qui prend une date de naissance sur l'identité actuelle et autorisera si l'utilisateur a plus de 18 ans;

public class Over18Requirement : AuthorizationHandler<Over18Requirement>, IAuthorizationRequirement
{
        public override void Handle(AuthorizationHandlerContext context, Over18Requirement requirement)
        {
            if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
            {
                context.Fail();
                return;
            }

            var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value);
            int age = DateTime.Today.Year - dateOfBirth.Year;
            if (dateOfBirth > DateTime.Today.AddYears(-age))
            {
                age--;
            }

            if (age >= 18)
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
        }
    }
}

Ensuite, dans votre ConfigureServices()fonction, vous le câbleriez

services.AddAuthorization(options =>
{
    options.AddPolicy("Over18", 
        policy => policy.Requirements.Add(new Authorization.Over18Requirement()));
});

Et enfin, appliquez-le à un contrôleur ou à une méthode d'action avec

[Authorize(Policy = "Over18")]
Blowdart
la source
84
Je me demande ... comment pourrait-on implémenter un contrôle d'accès à grain fin avec ça? Disons l'exemple de l' ManageStoreexigence de Music Store. Comme c'est dans l'exemple, il n'y a qu'un moyen de «permettre tout ou rien» de le faire. Faut-il alors créer une nouvelle politique pour chaque permutation possible? c'est-à-dire "Utilisateurs / Lire", "Utilisateurs / Créer", "Utilisateurs / Attribuer le rôle", "Utilisateurs / Supprimer" si nous voulons des revendications précises? Sonne comme un travail de configuration pour le faire fonctionner et une abondance de politiques juste pour gérer les revendications plutôt qu'un [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]attribut?
Tseng
84
Je dois dire que tout cela est plus complexe que la mise en œuvre d'une méthode d'autorisation personnalisée. Je sais comment je veux que l'autorisation soit faite.Je pourrais simplement l'écrire dans MVC 5, dans MVC 6, ils ajoutent beaucoup de code "fait" qui est en fait plus complexe à comprendre que l'implémentation de la "chose" principale elle-même. Me fait asseoir devant une page essayant de comprendre quelque chose au lieu d'écrire du code tout au long, aussi une grande douleur pour les personnes qui utilisent RDBMS autre que Microsoft (ou No-Sql).
Felype
17
De mon point de vue, cela ne résout pas tous les scénarios. Avant MVC 6, j'utilisais un attribut d'autorisation personnalisé pour implémenter mon propre «système d'autorisation». Je pourrais ajouter l'attribut Authorize à toutes les actions et passer une autorisation spécifique requise (comme Enum-Value). L'autorisation elle-même a été mappée à des groupes / utilisateurs dans la base de données. Donc, je ne vois pas de moyen de gérer cela avec des politiques!?
Gerwald
43
Comme beaucoup d'autres dans ces commentaires, je suis très déçu que l'utilisation d'attributs pour l'autorisation ait été si fortement neutralisée par rapport à ce qui était possible dans l'API Web 2. Désolé les gars, mais votre abstraction "d'exigence" ne couvre aucun cas où nous pourrions auparavant utiliser attribuer des paramètres de constructeur pour informer un algorithme d'autorisation sous-jacent. Auparavant, il était extrêmement simple de faire quelque chose comme ça [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Je pourrais utiliser un seul attribut personnalisé dans un nombre infini de façons simplement en modifiant les paramètres du constructeur.
NathanAldenSr
61
Je suis également choqué que le "responsable de la sécurité ASP.NET" autoproclamé suggère en fait d'utiliser des chaînes magiques (piratant le sens de IAuthorizeData.Policy) et des fournisseurs de politiques personnalisés pour surmonter cette omission flagrante, plutôt que de la traiter dans le cadre. Je pensais que nous n'étions pas censés créer nos propres implémentations? Vous n'avez laissé à plusieurs d'entre nous le choix que de réimplémenter (à nouveau) l'autorisation à partir de zéro, et cette fois sans même bénéficier de l'ancien Authorizeattribut de l' API Web . Maintenant, nous devons le faire au niveau du filtre d'action ou du middleware.
NathanAldenSr
104

Il semble qu'avec ASP.NET Core 2, vous pouvez à nouveau hériter AuthorizeAttribute, il vous suffit juste d'implémenter IAuthorizationFilter(ou IAsyncAuthorizationFilter):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    private readonly string _someFilterParameter;

    public CustomAuthorizeAttribute(string someFilterParameter)
    {
        _someFilterParameter = someFilterParameter;
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;

        if (!user.Identity.IsAuthenticated)
        {
            // it isn't needed to set unauthorized result 
            // as the base class already requires the user to be authenticated
            // this also makes redirect to a login page work properly
            // context.Result = new UnauthorizedResult();
            return;
        }

        // you can also use registered services
        var someService = context.HttpContext.RequestServices.GetService<ISomeService>();

        var isAuthorized = someService.IsUserAuthorized(user.Identity.Name, _someFilterParameter);
        if (!isAuthorized)
        {
            context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
            return;
        }
    }
}
gius
la source
4
Vous ne pouvez donc utiliser cela que pour refuser une autorisation, pas pour l' octroyer ?
MEMark
1
@MEMark En accordant , vous voulez dire remplacer un autre attribut d'autorisation?
gius
2
AFAIK, l'accès est autorisé par défaut, vous devez donc le refuser explicitement (par exemple, en ajoutant un AuthorizeAttribute). Consultez cette question pour plus de détails: stackoverflow.com/questions/17272422/…
gius
16
Notez également que dans l'exemple suggéré, il n'est pas nécessaire d'hériter de AuthorizeAttribute. Vous pouvez hériter de Attribute et IAuthorizationFilter . De cette façon, vous n'obtiendrez pas l'exception suivante si un mécanisme d'authentification non standard est utilisé: InvalidOperationException: aucun authenticationScheme n'a été spécifié et aucun DefaultChallengeScheme n'a été trouvé.
Anatolyevich
13
Notez que si votre OnAuthorizationimplémentation doit attendre une méthode asynchrone, vous devez l'implémenter IAsyncAuthorizationFilterau lieu de IAuthorizationFiltersinon votre filtre s'exécutera de manière synchrone et votre action de contrôleur s'exécutera quel que soit le résultat du filtre.
Codemunkie
34

Sur la base de la bonne réponse de Derek Greer , je l'ai fait avec des énumérations.

Voici un exemple de mon code:

public enum PermissionItem
{
    User,
    Product,
    Contact,
    Review,
    Client
}

public enum PermissionAction
{
    Read,
    Create,
}


public class AuthorizeAttribute : TypeFilterAttribute
{
    public AuthorizeAttribute(PermissionItem item, PermissionAction action)
    : base(typeof(AuthorizeActionFilter))
    {
        Arguments = new object[] { item, action };
    }
}

public class AuthorizeActionFilter : IAuthorizationFilter
{
    private readonly PermissionItem _item;
    private readonly PermissionAction _action;
    public AuthorizeActionFilter(PermissionItem item, PermissionAction action)
    {
        _item = item;
        _action = action;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        bool isAuthorized = MumboJumboFunction(context.HttpContext.User, _item, _action); // :)

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

public class UserController : BaseController
{
    private readonly DbContext _context;

    public UserController( DbContext context) :
        base()
    {
        _logger = logger;
    }

    [Authorize(PermissionItem.User, PermissionAction.Read)]
    public async Task<IActionResult> Index()
    {
        return View(await _context.User.ToListAsync());
    }
}
bruno.almeida
la source
1
Merci pour cela. J'ai créé ce post avec une implémentation légèrement différente et une demande de validation stackoverflow.com/questions/49551047/…
Anton Swanevelder
2
MumboJumboFunction <3
Marek Urbanowicz
31

Vous pouvez créer votre propre AuthorizationHandler qui trouvera des attributs personnalisés sur vos contrôleurs et actions et les transmettre à la méthode HandleRequirementAsync.

public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
    {
        var attributes = new List<TAttribute>();

        var action = (context.Resource as AuthorizationFilterContext)?.ActionDescriptor as ControllerActionDescriptor;
        if (action != null)
        {
            attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
            attributes.AddRange(GetAttributes(action.MethodInfo));
        }

        return HandleRequirementAsync(context, requirement, attributes);
    }

    protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

    private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
    }
}

Ensuite, vous pouvez l'utiliser pour tous les attributs personnalisés dont vous avez besoin sur vos contrôleurs ou actions. Par exemple, pour ajouter des exigences d'autorisation. Créez simplement votre attribut personnalisé.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class PermissionAttribute : AuthorizeAttribute
{
    public string Name { get; }

    public PermissionAttribute(string name) : base("Permission")
    {
        Name = name;
    }
}

Créez ensuite une exigence à ajouter à votre politique

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    //Add any custom requirement properties if you have them
}

Créez ensuite le AuthorizationHandler pour votre attribut personnalisé, en héritant du AttributeAuthorizationHandler que nous avons créé précédemment. Il sera transmis un IEnumerable pour tous vos attributs personnalisés dans la méthode HandleRequirementsAsync, accumulés à partir de votre contrôleur et de votre action.

public class PermissionAuthorizationHandler : AttributeAuthorizationHandler<PermissionAuthorizationRequirement, PermissionAttribute>
{
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement, IEnumerable<PermissionAttribute> attributes)
    {
        foreach (var permissionAttribute in attributes)
        {
            if (!await AuthorizeAsync(context.User, permissionAttribute.Name))
            {
                return;
            }
        }

        context.Succeed(requirement);
    }

    private Task<bool> AuthorizeAsync(ClaimsPrincipal user, string permission)
    {
        //Implement your custom user permission logic here
    }
}

Enfin, dans votre méthode ConfigureServices Startup.cs, ajoutez votre AuthorizationHandler personnalisé aux services et ajoutez votre stratégie.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Permission", policyBuilder =>
            {
                policyBuilder.Requirements.Add(new PermissionAuthorizationRequirement());
            });
        });

Maintenant, vous pouvez simplement décorer vos contrôleurs et actions avec votre attribut personnalisé.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}
Shawn
la source
1
Je vais jeter un œil à cet ASAP.
NathanAldenSr
5
Ceci est assez ingénieux ... J'ai résolu le même problème en utilisant un simple AuthorizationFilterAttribute qui reçoit un paramètre. Vous n'avez pas besoin de réflexion pour cela, cela semble encore plus artificiel que la solution "officielle" (que je trouve assez pauvre).
Vi100
2
@ Vi100 Je n'ai pas trouvé beaucoup d'informations sur AuthorizationFilters dans ASP.NET Core. La page de documentation officielle indique qu'ils travaillent actuellement sur ce sujet. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Shawn
4
@ Vi100 Pouvez-vous s'il vous plaît partager votre solution, s'il existe un moyen plus simple d'y parvenir, j'aimerais savoir.
Shawn
2
Une chose à noter l'utilisation de UnderlyingSystemType ci-dessus ne se compile pas, mais sa suppression semble fonctionner.
heure du thé
25

Quelle est l'approche actuelle pour créer un AuthorizeAttribute personnalisé

Facile: ne créez pas le vôtre AuthorizeAttribute.

Pour les scénarios d'autorisation purs (comme restreindre l'accès à des utilisateurs spécifiques uniquement), l'approche recommandée consiste à utiliser le nouveau bloc d'autorisation: https://github.com/aspnet/MusicStore/blob/1c0aeb08bb1ebd846726232226279bbe001782e1/samples/MusicStore/Startup.cs#L84 -L92

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<AuthorizationOptions>(options =>
        {
            options.AddPolicy("ManageStore", policy => policy.RequireClaim("Action", "ManageStore"));
        });
    }
}

public class StoreController : Controller
{
    [Authorize(Policy = "ManageStore"), HttpGet]
    public async Task<IActionResult> Manage() { ... }
}

Pour l'authentification, il est préférable de le gérer au niveau du middleware.

Qu'essayez-vous de réaliser exactement?

Chalet Kévin
la source
1
Je reçois un ID de session dans l'autorisation d'en-tête. À partir de cet ID, je saurai si une action particulière est valide.
jltrem
1
Ce n'est donc pas un problème d'autorisation. Je suppose que votre "ID de session" est en fait un jeton contenant l'identité de l'appelant: cela devrait certainement être fait au niveau du middleware.
Kévin Chalet
3
Ce n'est pas une authentification (établir qui est l'utilisateur) mais une autorisation (déterminer si un utilisateur doit avoir accès à une ressource). Alors, où proposez-vous que je cherche à résoudre ce problème?
jltrem
3
@jltrem, d'accord, vous parlez d'autorisation, pas d'authentification.
bopapa_1979
2
@Pinpoint, je ne le suis pas. Je demande un autre système pour cette information. Ce système authentifie (détermine l'utilisateur) et autorise (me dit à quoi cet utilisateur peut accéder). En ce moment, je l'ai piraté pour fonctionner en appelant une méthode dans chaque action de contrôleur pour que l'autre système vérifie la session. J'aimerais que cela se produise automatiquement via un attribut.
jltrem
4

Si quelqu'un veut simplement valider un jeton au porteur dans la phase d'autorisation en utilisant les pratiques de sécurité actuelles, vous pouvez,

ajoutez ceci à votre Startup / ConfigureServices

    services.AddSingleton<IAuthorizationHandler, BearerAuthorizationHandler>();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();

    services.AddAuthorization(options => options.AddPolicy("Bearer",
        policy => policy.AddRequirements(new BearerRequirement())
        )
    );

et cela dans votre base de code,

public class BearerRequirement : IAuthorizationRequirement
{
    public async Task<bool> IsTokenValid(SomeValidationContext context, string token)
    {
        // here you can check if the token received is valid 
        return true;
    }
}

public class BearerAuthorizationHandler : AuthorizationHandler<BearerRequirement> 
{

    public BearerAuthorizationHandler(SomeValidationContext thatYouCanInject)
    {
       ...
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BearerRequirement requirement)
    {
        var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
        string authHeader = authFilterCtx.HttpContext.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.Contains("Bearer"))
        {
            var token = authHeader.Replace("Bearer ", string.Empty);
            if (await requirement.IsTokenValid(thatYouCanInject, token))
            {
                context.Succeed(requirement);
            }
        }
    }
}

Si le code n'atteint pas, context.Succeed(...)il échouera quand même (401).

Et puis dans vos contrôleurs, vous pouvez utiliser

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Gabriel P.
la source
Pourquoi choisiriez-vous d'effectuer votre propre validation du jeton alors que le middleware JwtBearer s'en occupe déjà? Il place également le contenu correct dans l'en-tête de réponse WWW-Authenticate pour un échec de validation / expiration d'authentification / jeton. Si vous souhaitez accéder au pipeline d'authentification, vous pouvez utiliser des événements spécifiques dans les options AddJwtBearer (OnAuthenticationFailed, OnChallenge, OnMessageReceived et OnTokenValidated).
Darren Lewis
C'est infiniment plus simple que toute autre solution que j'ai vue. Surtout pour les cas d'utilisation de clés API simples. Une mise à jour: pour 3.1, le transtypage en AuthorizationFilterContext n'est plus valide en raison du routage du point de terminaison. Vous devez saisir le contexte via HttpContextAccessor.
JasonCoder
2

La méthode moderne est AuthenticationHandlers

dans startup.cs add

services.AddAuthentication("BasicAuthentication").AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        private readonly IUserService _userService;

        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IUserService userService)
            : base(options, logger, encoder, clock)
        {
            _userService = userService;
        }

        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
                return AuthenticateResult.Fail("Missing Authorization Header");

            User user = null;
            try
            {
                var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
                var username = credentials[0];
                var password = credentials[1];
                user = await _userService.Authenticate(username, password);
            }
            catch
            {
                return AuthenticateResult.Fail("Invalid Authorization Header");
            }

            if (user == null)
                return AuthenticateResult.Fail("Invalid User-name or Password");

            var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
    }

IUserService est un service que vous créez lorsque vous avez un nom d'utilisateur et un mot de passe. Fondamentalement, il renvoie une classe d'utilisateurs que vous utilisez pour mapper vos revendications.

var claims = new[] {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                new Claim(ClaimTypes.Name, user.Username),
            }; 

Ensuite, vous pouvez interroger ces revendications et toutes les données que vous avez mappées, il y en a plusieurs, jetez un œil à la classe ClaimTypes

vous pouvez l'utiliser dans une méthode d'extension et obtenir l'un des mappages

public int? GetUserId()
{
   if (context.User.Identity.IsAuthenticated)
    {
       var id=context.User.FindFirst(ClaimTypes.NameIdentifier);
       if (!(id is null) && int.TryParse(id.Value, out var userId))
            return userId;
     }
      return new Nullable<int>();
 }

Cette nouvelle façon, je pense que c'est mieux que

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.Headers.Authorization != null)
        {
            var authToken = actionContext.Request.Headers.Authorization.Parameter;
            // decoding authToken we get decode value in 'Username:Password' format
            var decodeauthToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));
            // spliting decodeauthToken using ':'
            var arrUserNameandPassword = decodeauthToken.Split(':');
            // at 0th postion of array we get username and at 1st we get password
            if (IsAuthorizedUser(arrUserNameandPassword[0], arrUserNameandPassword[1]))
            {
                // setting current principle
                Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(arrUserNameandPassword[0]), null);
            }
            else
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            }
        }
        else
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }

    public static bool IsAuthorizedUser(string Username, string Password)
    {
        // In this method we can handle our database logic here...
        return Username.Equals("test") && Password == "test";
    }
}
Walter Vehoeven
la source
Cette réponse brillante fonctionne comme un charme! Merci pour cela et je vous souhaite que cela soit voté, car c'est la meilleure réponse que j'ai trouvée après environ six heures de recherche dans les blogs, la documentation et la pile pour l'authentification de base plus l'autorisation de rôle.
Piotr Śródka
@ PiotrŚródka, vous êtes les bienvenus, veuillez noter que la réponse est un peu "simplifiée", testez si vous avez un ':' dans le texte car un utilisateur malveillant pourrait essayer de planter votre service en ne jouant simplement pas une belle fin dans un index out d'exception de plage. comme toujours tester ce qui vous est donné par des sources externes
Walter Vehoeven
2

Au moment d'écrire ces lignes, je pense que cela peut être accompli avec l'interface IClaimsTransformation dans asp.net core 2 et au-dessus. Je viens de mettre en œuvre une preuve de concept qui est suffisamment partageable pour être publiée ici.

public class PrivilegesToClaimsTransformer : IClaimsTransformation
{
    private readonly IPrivilegeProvider privilegeProvider;
    public const string DidItClaim = "http://foo.bar/privileges/resolved";

    public PrivilegesToClaimsTransformer(IPrivilegeProvider privilegeProvider)
    {
        this.privilegeProvider = privilegeProvider;
    }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        if (principal.Identity is ClaimsIdentity claimer)
        {
            if (claimer.HasClaim(DidItClaim, bool.TrueString))
            {
                return principal;
            }

            var privileges = await this.privilegeProvider.GetPrivileges( ... );
            claimer.AddClaim(new Claim(DidItClaim, bool.TrueString));

            foreach (var privilegeAsRole in privileges)
            {
                claimer.AddClaim(new Claim(ClaimTypes.Role /*"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" */, privilegeAsRole));
            }
        }

        return principal;
    }
}

Pour l'utiliser dans votre contrôleur, ajoutez simplement une [Authorize(Roles="whatever")]méthode appropriée à vos méthodes.

[HttpGet]
[Route("poc")]
[Authorize(Roles = "plugh,blast")]
public JsonResult PocAuthorization()
{
    var result = Json(new
    {
        when = DateTime.UtcNow,
    });

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

Dans notre cas, chaque demande comprend un en-tête d'autorisation qui est un JWT. Ceci est le prototype et je crois que nous ferons quelque chose de très proche de cela dans notre système de production la semaine prochaine.

Futurs électeurs, pensez à la date de rédaction lorsque vous votez. À ce jour, this works on my machine.™ Vous voudrez probablement plus de gestion des erreurs et de journalisation de votre implémentation.

Aucun remboursement, aucun retour
la source
Qu'en est-il de ConfigureServices? Faut-il ajouter quelque chose?
Daniel
Comme discuté ailleurs, oui.
Aucun remboursement Aucun retour
1

Pour autorisation dans notre application. Nous avons dû appeler un service basé sur les paramètres passés dans l'attribut d'autorisation.

Par exemple, si nous voulons vérifier si un médecin connecté peut voir les rendez-vous des patients, nous passerons "View_Appointment" à l'attribut d'autorisation personnalisé et vérifierons ce droit dans le service DB et en fonction des résultats, nous autoriserons. Voici le code de ce scénario:

    public class PatientAuthorizeAttribute : TypeFilterAttribute
    {
    public PatientAuthorizeAttribute(params PatientAccessRights[] right) : base(typeof(AuthFilter)) //PatientAccessRights is an enum
    {
        Arguments = new object[] { right };
    }

    private class AuthFilter : IActionFilter
    {
        PatientAccessRights[] right;

        IAuthService authService;

        public AuthFilter(IAuthService authService, PatientAccessRights[] right)
        {
            this.right = right;
            this.authService = authService;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            var allparameters = context.ActionArguments.Values;
            if (allparameters.Count() == 1)
            {
                var param = allparameters.First();
                if (typeof(IPatientRequest).IsAssignableFrom(param.GetType()))
                {
                    IPatientRequest patientRequestInfo = (IPatientRequest)param;
                    PatientAccessRequest userAccessRequest = new PatientAccessRequest();
                    userAccessRequest.Rights = right;
                    userAccessRequest.MemberID = patientRequestInfo.PatientID;
                    var result = authService.CheckUserPatientAccess(userAccessRequest).Result; //this calls DB service to check from DB
                    if (result.Status == ReturnType.Failure)
                    {
                        //TODO: return apirepsonse
                        context.Result = new StatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
                    }
                }
                else
                {
                    throw new AppSystemException("PatientAuthorizeAttribute not supported");
                }
            }
            else
            {
                throw new AppSystemException("PatientAuthorizeAttribute not supported");
            }
        }
    }
}

Et sur l'action de l'API, nous l'utilisons comme ceci:

    [PatientAuthorize(PatientAccessRights.PATIENT_VIEW_APPOINTMENTS)] //this is enum, we can pass multiple
    [HttpPost]
    public SomeReturnType ViewAppointments()
    {

    }
Abdullah
la source
1
Veuillez noter que IActionFilter sera un problème lorsque vous souhaitez utiliser le même attribut pour les méthodes Hub dans SignalR.SignalR Hubs attend IAuthorizationFilter
ilkerkaran
Merci pour l'info. Je n'utilise pas SignalR dans mon application en ce moment, je ne l'ai donc pas testé avec.
Abdullah
Même principe, je suppose que vous devrez toujours utiliser l'entrée d'autorisation de l'en-tête, la mise en œuvre sera différente
Walter Vehoeven
0

La réponse acceptée ( https://stackoverflow.com/a/41348219/4974715 ) n'est pas réaliste ni appropriée car "CanReadResource" est utilisé comme une réclamation (mais devrait essentiellement être une politique en réalité, OMI). L'approche de la réponse n'est pas correcte dans la façon dont elle a été utilisée, car si une méthode d'action nécessite de nombreuses configurations de revendications différentes, alors avec cette réponse, vous devrez écrire à plusieurs reprises quelque chose comme ...

[ClaimRequirement(MyClaimTypes.Permission, "CanReadResource")] 
[ClaimRequirement(MyClaimTypes.AnotherPermision, "AnotherClaimVaue")]
//and etc. on a single action.

Alors, imaginez combien de codage cela prendrait. Idéalement, «CanReadResource» est censé être une stratégie qui utilise de nombreuses revendications pour déterminer si un utilisateur peut lire une ressource.

Ce que je fais, c'est que je crée mes politiques en tant qu'énumération, puis que je boucle et configure les exigences comme ceci ...

services.AddAuthorization(authorizationOptions =>
        {
            foreach (var policyString in Enum.GetNames(typeof(Enumerations.Security.Policy)))
            {
                authorizationOptions.AddPolicy(
                    policyString,
                    authorizationPolicyBuilder => authorizationPolicyBuilder.Requirements.Add(new DefaultAuthorizationRequirement((Enumerations.Security.Policy)Enum.Parse(typeof(Enumerations.Security.Policy), policyWrtString), DateTime.UtcNow)));

      /* Note that thisn does not stop you from 
          configuring policies directly against a username, claims, roles, etc. You can do the usual.
     */
            }
        }); 

La classe DefaultAuthorizationRequirement ressemble à ...

public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
    public Enumerations.Security.Policy Policy {get; set;} //This is a mere enumeration whose code is not shown.
    public DateTime DateTimeOfSetup {get; set;} //Just in case you have to know when the app started up. And you may want to log out a user if their profile was modified after this date-time, etc.
}

public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
{
    private IAServiceToUse _aServiceToUse;

    public DefaultAuthorizationHandler(
        IAServiceToUse aServiceToUse
        )
    {
        _aServiceToUse = aServiceToUse;
    }

    protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
    {
        /*Here, you can quickly check a data source or Web API or etc. 
           to know the latest date-time of the user's profile modification...
        */
        if (_aServiceToUse.GetDateTimeOfLatestUserProfileModication > requirement.DateTimeOfSetup)
        {
            context.Fail(); /*Because any modifications to user information, 
            e.g. if the user used another browser or if by Admin modification, 
            the claims of the user in this session cannot be guaranteed to be reliable.
            */
            return;
        }

        bool shouldSucceed = false; //This should first be false, because context.Succeed(...) has to only be called if the requirement specifically succeeds.

        bool shouldFail = false; /*This should first be false, because context.Fail() 
        doesn't have to be called if there's no security breach.
        */

        // You can do anything.
        await doAnythingAsync();

       /*You can get the user's claims... 
          ALSO, note that if you have a way to priorly map users or users with certain claims 
          to particular policies, add those policies as claims of the user for the sake of ease. 
          BUT policies that require dynamic code (e.g. checking for age range) would have to be 
          coded in the switch-case below to determine stuff.
       */

        var claims = context.User.Claims;

        // You can, of course, get the policy that was hit...
        var policy = requirement.Policy

        //You can use a switch case to determine what policy to deal with here...
        switch (policy)
        {
            case Enumerations.Security.Policy.CanReadResource:
                 /*Do stuff with the claims and change the 
                     value of shouldSucceed and/or shouldFail.
                */
                 break;
            case Enumerations.Security.Policy.AnotherPolicy:
                 /*Do stuff with the claims and change the 
                    value of shouldSucceed and/or shouldFail.
                 */
                 break;
                // Other policies too.

            default:
                 throw new NotImplementedException();
        }

        /* Note that the following conditions are 
            so because failure and success in a requirement handler 
            are not mutually exclusive. They demand certainty.
        */

        if (shouldFail)
        {
            context.Fail(); /*Check the docs on this method to 
            see its implications.
            */
        }                

        if (shouldSucceed)
        {
            context.Succeed(requirement); 
        } 
     }
}

Notez que le code ci-dessus peut également activer le pré-mappage d'un utilisateur à une stratégie dans votre magasin de données. Ainsi, lorsque vous composez des revendications pour l'utilisateur, vous récupérez essentiellement les politiques qui ont été pré-mappées à l'utilisateur directement ou indirectement (par exemple, parce que l'utilisateur a une certaine valeur de revendication et que cette valeur de revendication a été identifiée et mappée à une politique, telle que qu'il fournit un mappage automatique pour les utilisateurs qui ont également cette valeur de revendication), et inscrivez les stratégies en tant que revendications, de telle sorte que dans le gestionnaire d'autorisations, vous pouvez simplement vérifier si les revendications de l'utilisateur contiennent des exigences.Politique comme valeur d'un élément de revendication dans leur réclamations. C'est pour une manière statique de satisfaire une exigence de politique, par exemple l'exigence de "prénom" est de nature assez statique. Donc,

[Authorize(Policy = nameof(Enumerations.Security.Policy.ViewRecord))] 

Une exigence dynamique peut consister à vérifier la tranche d'âge, etc. et les politiques qui utilisent de telles exigences ne peuvent pas être pré-mappées aux utilisateurs.

Un exemple de vérification dynamique des revendications de politique (par exemple pour vérifier si un utilisateur a plus de 18 ans) se trouve déjà dans la réponse donnée par @blowdart ( https://stackoverflow.com/a/31465227/4974715 ).

PS: j'ai tapé ceci sur mon téléphone. Pardonnez les fautes de frappe et le manque de formatage.

Olumide
la source