Come si crea un attributo Authorize personalizzato in ASP.NET Core?


428

Sto cercando di creare un attributo di autorizzazione personalizzato in ASP.NET Core. Nelle versioni precedenti era possibile eseguire l'override bool AuthorizeCore(HttpContextBase httpContext). Ma questo non esiste più in AuthorizeAttribute.

Qual è l'approccio attuale per realizzare un AuthorizeAttribute personalizzato?

Cosa sto cercando di realizzare: sto ricevendo un ID sessione nell'autorizzazione dell'intestazione. Da quell'ID saprò se una determinata azione è valida.


Non sono sicuro di come farlo, ma MVC è open source. È possibile estrarre il repository github e cercare implementazioni di IAuthorizationFilter. Se oggi ho tempo, ti cercherò e pubblicherò una risposta reale, ma nessuna promessa. repository github: github.com/aspnet/Mvc
bopapa_1979

OK, fuori tempo, ma cerca gli usi di AuthorizationPolicy nel Repo MVC, che utilizza AuthorizeAttribute, nel repository aspnet / Security, qui: github.com/aspnet/Security . In alternativa, cerca nel repository MVC lo spazio dei nomi in cui risiedono gli elementi di sicurezza che ti interessano, ovvero Microsoft.AspNet.Authorization. Mi dispiace non posso essere più utile. In bocca al lupo!
bopapa_1979,

Risposte:


446

L'approccio raccomandato dal team ASP.Net Core consiste nell'utilizzare il nuovo modello di politica che è completamente documentato qui . L'idea di base alla base del nuovo approccio è quella di utilizzare il nuovo attributo [Autorizza] per designare una "politica" (ad es. [Authorize( Policy = "YouNeedToBe18ToDoThis")]Laddove la politica sia registrata in Startup.cs dell'applicazione per eseguire un blocco di codice (ovvero assicurarsi che l'utente abbia una richiesta di età dove l'età ha 18 anni o più).

Il design della politica è una grande aggiunta al framework e il team ASP.Net Security Core dovrebbe essere lodato per la sua introduzione. Detto questo, non è adatto a tutti i casi. Il difetto di questo approccio è che non riesce a fornire una soluzione conveniente per la necessità più comune di affermare semplicemente che un determinato controller o azione richiede un determinato tipo di reclamo. Nel caso in cui un'applicazione possa disporre di centinaia di autorizzazioni discrete che regolano le operazioni CRUD su singole risorse REST ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder", ecc.), Il nuovo approccio richiede uno o l'altro ripetitivi una mappatura tra un nome di politica e un nome di rivendicazione (ad esoptions.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));) o scrivendo un codice per eseguire queste registrazioni in fase di esecuzione (ad esempio, leggere tutti i tipi di reclami da un database ed eseguire la suddetta chiamata in un ciclo). Il problema con questo approccio nella maggior parte dei casi è che non è necessario un sovraccarico.

Mentre il team ASP.Net Core Security consiglia di non creare mai la propria soluzione, in alcuni casi questa potrebbe essere l'opzione più prudente con cui iniziare.

Di seguito è un'implementazione che utilizza IAuthorizationFilter per fornire un modo semplice per esprimere un requisito di reclamo per un determinato controller o azione:

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

80
Questo dovrebbe essere contrassegnato come RISPOSTA CORRETTA. Qui puoi vedere come le persone di Microsoft considerano il feedback degli sviluppatori. Non capisco il motivo per cui sono così "chiusi" intorno a questo, dal momento che è una situazione molto comune avere una miriade di autorizzazioni diverse, dover codificare una politica per ognuna è un overkill completo. Lo stavo cercando da tanto tempo ... (ho già fatto questa domanda quasi due anni fa, quando vNext era ancora una scommessa qui: stackoverflow.com/questions/32181400/… ma siamo ancora bloccati lì)
Vi100

3
Questa è roba buona. Abbiamo un middleware di autenticazione sull'API Web ma abbiamo una gran sicurezza delle autorizzazioni di autorizzazione per ruolo; quindi dover semplicemente inserire un attributo come: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] sembra molto bello.
Mariano Peinador,

4
@Derek Greer: questa è la risposta migliore. Tuttavia, stai implementando un ActionFilter che viene eseguito dopo Autorizza filtro azioni. Esiste un modo per implementare e autorizzare il filtro azioni?
Jacob Phan,

6
@JacobPhan Hai ragione, questo sarebbe meglio implementato usando l'interfaccia IAuthorizationFilter. Ho aggiornato il codice per riflettere le modifiche.
Derek Greer,

3
quindi new ForbidResult()non funziona (causa l'eccezione / 500) perché non ha uno schema di autorizzazione associato. Cosa dovrei usare per questo caso?
Sinaesthetic

252

Sono la persona di sicurezza asp.net. Innanzi tutto, mi scuso che nulla di tutto ciò sia documentato al di fuori del campione del negozio di musica o dei test unitari, e tutto è ancora in fase di perfezionamento in termini di API esposte. La documentazione dettagliata è qui .

Non vogliamo che tu scriva attributi di autorizzazione personalizzati. Se devi farlo, abbiamo fatto qualcosa di sbagliato. Invece, dovresti scrivere i requisiti di autorizzazione .

L'autorizzazione agisce sulle identità. Le identità vengono create mediante autenticazione.

Dici nei commenti che vuoi controllare un ID di sessione in un'intestazione. L'ID della sessione sarebbe la base per l'identità. Se si desidera utilizzare l' Authorizeattributo, si scriverebbe un middleware di autenticazione per prendere quell'intestazione e trasformarlo in un autenticato ClaimsPrincipal. Verificherebbe quindi che all'interno di un requisito di autorizzazione. I requisiti di autorizzazione possono essere complicati a piacimento, ad esempio eccone uno che richiede una data di richiesta di nascita sull'identità attuale e autorizzerà se l'utente ha più di 18 anni;

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

Quindi nella tua ConfigureServices()funzione lo collegheresti

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

E infine, applicalo a un controller o a un metodo di azione con

[Authorize(Policy = "Over18")]

84
Mi chiedo ... come si potrebbe implementare un controllo di accesso a grana fine con quello? Diciamo l'esempio ManageStoreRequisito dal Music Store. Come nell'esempio, esiste solo un modo "consenti tutto o niente" per farlo. Dobbiamo quindi creare una nuova politica per ogni possibile permutazione? vale a dire "Utenti / Leggi", "Utenti / Crea", "Utenti / Assegna ruolo", "Utenti / Elimina" se vogliamo rivendicazioni approfondite? Sembra praticamente che il lavoro di installazione funzioni e l'abbondanza di politiche solo per gestire le richieste anziché un [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]attributo?
Tseng,

84
Devo commentare che tutto ciò è più complesso dell'implementazione di un metodo di autorizzazione personalizzato. So come voglio ottenere l'autorizzazione. Potrei semplicemente andare a scriverlo in MVC 5, in MVC 6 aggiungono molto codice "fatto" che è in realtà più complesso da capire rispetto all'implementazione della "cosa" stessa. Mi fa sedere di fronte a una pagina cercando di capire qualcosa invece di scrivere codice, anche un grande dolore per le persone che usano RDBMS diverso da Microsoft (o No-Sql).
Felype il

17
Dal mio punto di vista, questo non risolve tutti gli scenari. Prima di MVC 6, ho usato un attributo di autorizzazione personalizzato per implementare il mio "Sistema di autorizzazione". Potrei aggiungere l'attributo Autorizza a tutte le azioni e passare una specifica autorizzazione necessaria (come Enum-Value). L'autorizzazione stessa è stata mappata a gruppi / utenti all'interno del DB. Quindi, non vedo un modo per gestirlo con le politiche !?
Gerwald,

43
Come molti altri in questi commenti, sono molto deluso dal fatto che l'utilizzo degli attributi per l'autorizzazione sia stato così fortemente castrato su ciò che era possibile nell'API Web 2. Mi dispiace ragazzi, ma l'astrazione del "requisito" non riesce a coprire qualsiasi caso in cui potessimo precedentemente utilizzare attribuire parametri del costruttore per informare un algoritmo di autorizzazione sottostante. Una volta era un cervello morto semplice fare qualcosa del genere [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Potrei usare un singolo attributo personalizzato in un numero infinito di modi semplicemente modificando i parametri del costruttore.
NathanAldenSr

61
Sono anche scioccato dal fatto che l'autoproclamato "Lead ASP.NET security guy" stia effettivamente suggerendo di usare stringhe magiche (hacking il significato di IAuthorizeData.Policy) e provider di politiche personalizzate per superare questa palese svista, piuttosto che affrontarla nel quadro. Pensavo che non avremmo dovuto creare le nostre implementazioni? Molti di noi non hanno lasciato altra scelta se non quella di ri-implementare l'autorizzazione da zero (di nuovo), e questa volta senza nemmeno il vantaggio del vecchio Authorizeattributo dell'API Web . Ora dobbiamo farlo a livello di filtro azione o middleware.
NathanAldenSr

104

Sembra che con ASP.NET Core 2, puoi ereditare di nuovo AuthorizeAttribute, devi solo implementare IAuthorizationFilter(o 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;
        }
    }
}

4
Quindi puoi usarlo solo per negare l' autorizzazione, non concederlo ?
Iscrizione

1
@MEMark Con la concessione , vuoi dire ignorando un altro attributo autorizzazione?
gius,

2
AFAIK, l'accesso è consentito per impostazione predefinita, quindi è necessario negarlo esplicitamente (ad esempio, aggiungendo un AuthorizeAttribute). Controlla questa domanda per maggiori dettagli: stackoverflow.com/questions/17272422/…
gius

16
Inoltre, nell'esempio suggerito non è necessario ereditare da AuthorizeAttribute. È possibile ereditare da Attribute e IAuthorizationFilter . In questo modo non si otterrebbe la seguente eccezione se si utilizzasse un meccanismo di autenticazione non standard: InvalidOperationException: non è stato specificato alcun metodo di autenticazione e non è stato trovato DefaultChallengeScheme.
Anatolyevich,

13
Se l' OnAuthorizationimplementazione deve attendere un metodo asincrono, è necessario implementare IAsyncAuthorizationFilterinvece che IAuthorizationFilteraltrimenti il ​​filtro verrà eseguito in modo sincrono e l'azione del controller verrà eseguita indipendentemente dal risultato del filtro.
Codemunkie,

34

Basato sulla risposta GRANDE di Derek Greer , l'ho fatto con enumerazioni.

Ecco un esempio del mio codice:

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

1
Grazie per questo. Ho creato questo post con un'implementazione leggermente diversa e una richiesta di convalida stackoverflow.com/questions/49551047/…
Anton Swanevelder

2
MumboJumboFunction <3
Marek Urbanowicz

31

È possibile creare il proprio AuthorizationHandler che troverà attributi personalizzati sui controller e sulle azioni e li passerà al metodo 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>();
    }
}

Quindi puoi usarlo per qualsiasi attributo personalizzato di cui hai bisogno sui tuoi controller o azioni. Ad esempio per aggiungere requisiti di autorizzazione. Basta creare il tuo attributo personalizzato.

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

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

Quindi crea un Requisito da aggiungere alla tua Politica

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

Quindi creare AuthorizationHandler per l'attributo personalizzato, ereditando AttributeAuthorizationHandler che abbiamo creato in precedenza. Verrà passato un IEnumerable per tutti gli attributi personalizzati nel metodo HandleRequirementsAsync, accumulati dal controller e dall'azione.

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

Infine, nel metodo ConfigureServices Startup.cs, aggiungi il tuo AuthorizationHandler personalizzato ai servizi e aggiungi la tua Politica.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

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

Ora puoi semplicemente decorare i tuoi controller e le tue azioni con il tuo attributo personalizzato.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}

1
Daremo un'occhiata al più presto possibile.
NathanAldenSr

5
Questo è abbastanza ingegnoso ... Ho risolto lo stesso usando un semplice AuthorizationFilterAttribute che riceve un parametro. Non hai bisogno di riflessioni per questo, sembra ancora più artificiale della soluzione "ufficiale" (che trovo piuttosto scadente).
Vi100,

2
@ Vi100 Non sono riuscito a trovare molte informazioni su AuthorizationFilters in ASP.NET Core. La pagina della documentazione ufficiale dice che stanno attualmente lavorando su questo argomento. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Shawn,

4
@ Vi100 Puoi per favore condividere la tua soluzione, se c'è un modo più semplice per raggiungere questo obiettivo, mi piacerebbe saperlo.
Shawn,

2
Una cosa da notare l'uso di UnderlyingSystemType sopra non si compila, ma rimuoverlo sembra funzionare.
ora del

25

Qual è l'approccio attuale per creare un AuthorizeAttribute personalizzato

Facile: non crearne uno tuo AuthorizeAttribute.

Per scenari di autorizzazione puri (come limitare l'accesso solo a utenti specifici), l'approccio consigliato è utilizzare il nuovo blocco di autorizzazione: 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() { ... }
}

Per l'autenticazione, è gestita al meglio a livello di middleware.

Cosa stai cercando di ottenere esattamente?


1
Ricevo un ID sessione nell'autorizzazione dell'intestazione. Da quell'ID saprò se una determinata azione è valida.
jltrem,

1
Quindi non è un problema di autorizzazione. Immagino che il tuo "ID sessione" sia in realtà un token contenente l'identità del chiamante: questo dovrebbe sicuramente essere fatto a livello di middleware.
Kévin Chalet

3
Non è un'autenticazione (che stabilisce chi è l'utente) ma è un'autorizzazione (che determina se un utente deve avere accesso a una risorsa). Allora, dove mi stai suggerendo di cercare di risolvere questo?
jltrem,

3
@jltrem, d'accordo, quello di cui stai parlando è l'autorizzazione, non l'autenticazione.
bopapa_1979,

2
@Pinpoint non lo sono. Richiedo un altro sistema per tali informazioni. Tale sistema autentica (determina l'utente) e autorizza (mi dice a cosa l'utente può accedere). In questo momento ho hackerato per funzionare chiamando un metodo in ciascuna azione del controller per fare in modo che l'altro sistema verifichi la sessione. Vorrei che ciò avvenisse automaticamente tramite un attributo.
jltrem,

4

Se qualcuno desidera semplicemente convalidare un token di connessione nella fase di autorizzazione utilizzando le pratiche di sicurezza correnti che puoi,

aggiungilo ai tuoi Startup / ConfigureServices

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

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

e questo nella tua base di codice,

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

Se il codice non viene raggiunto context.Succeed(...), fallirà comunque (401).

E poi nei controller puoi usare

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Perché dovresti scegliere di eseguire la tua convalida del token quando il middleware JwtBearer si occupa già di questo? Inserisce inoltre il contenuto corretto nell'intestazione della risposta WWW-Authenticate per un errore di convalida / scadenza di autenticazione / token. Se si desidera accedere alla pipeline di autenticazione, ci sono eventi specifici in cui è possibile accedere alle opzioni AddJwtBearer (OnAuthenticationFailed, OnChallenge, OnMessageReceived e OnTokenValidated).
Darren Lewis,

Questo è infinitamente più semplice di qualsiasi altra soluzione che abbia mai visto. Soprattutto per semplici casi d'uso di chiavi API. Un aggiornamento: per 3.1 il cast di AuthorizationFilterContext non è più valido a causa del routing dell'endpoint. Devi prendere il contesto tramite HttpContextAccessor.
JasonCoder

2

Il modo moderno è AuthenticationHandlers

in startup.cs aggiungere

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 è un servizio che fai dove hai nome utente e password. sostanzialmente restituisce una classe utente che usi per mappare i tuoi reclami.

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

Quindi puoi interrogare queste affermazioni e lei tutti i dati che hai mappato, ce ne sono parecchi, dai un'occhiata alla classe ClaimTypes

puoi usarlo in un metodo di estensione e ottenere una qualsiasi delle mappature

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

In questo nuovo modo, penso sia meglio di

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

Questa brillante risposta funziona proprio come un fascino! Grazie per questo e ti auguro che verrà votato, in quanto è la migliore risposta che ho trovato dopo circa sei ore di ricerche in blog, documentazione e stack per l'autenticazione di base più l'autorizzazione del ruolo.
Piotr Śródka,

@ PiotrŚródka, sei il benvenuto, tieni presente che la risposta è un po '"semplificata", prova se hai un': 'nel testo in quanto un utente malintenzionato potrebbe tentare di interrompere il servizio semplicemente non riproducendo il bel finale in un indice di eccezione gamma. come sempre prova ciò che ti è stato dato da fonti esterne
Walter Vehoeven il

2

Al momento della stesura di questo documento, credo che ciò possa essere realizzato con l'interfaccia IClaimsTransformation in asp.net core 2 e versioni successive. Ho appena implementato una prova di concetto che è abbastanza condivisibile per pubblicare qui.

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

Per usarlo nel tuo Controller basta aggiungere un appropriato [Authorize(Roles="whatever")]ai tuoi metodi.

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

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

Nel nostro caso ogni richiesta include un'intestazione di autorizzazione che è un JWT. Questo è il prototipo e credo che faremo qualcosa di molto vicino a questo nel nostro sistema di produzione la prossima settimana.

Elettori futuri, considera la data di scrittura quando voti. Ad oggi, this works on my machine.™ Probabilmente avrai bisogno di più gestione degli errori e accesso alla tua implementazione.


Che dire di ConfigureServices? È necessario aggiungere qualcosa?
Daniel

Come discusso altrove, sì.
Nessun rimborso Nessun reso

1

Per l'autorizzazione nella nostra app. Abbiamo dovuto chiamare un servizio in base ai parametri passati nell'attributo di autorizzazione.

Ad esempio, se vogliamo verificare se il medico registrato può visualizzare gli appuntamenti dei pazienti, passeremo "View_Appointment" per autorizzare l'attributo personalizzato e verificarlo nel servizio DB e sulla base dei risultati che autorizzeremo. Ecco il codice per questo scenario:

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

E su azione API lo usiamo in questo modo:

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

    }

1
Si noti che IActionFilter sarà un problema quando si desidera utilizzare lo stesso attributo per i metodi Hub in SignalR.SignalR Gli hub si aspettano IAuthorizationFilter
ilkerkaran l'

Grazie per le informazioni. Non sto usando SignalR nella mia applicazione in questo momento, quindi non ho testato con esso.
Abdullah,

Stesso principio, suppongo che dovrai ancora utilizzare la voce di autorizzazione dell'intestazione, l'implementazione sarà diversa
Walter Vehoeven,

0

La risposta accettata ( https://stackoverflow.com/a/41348219/4974715 ) non è realisticamente mantenibile o adatta perché "CanReadResource" viene utilizzato come reclamo (ma dovrebbe essere essenzialmente una politica nella realtà, IMO). L'approccio alla risposta non è OK nel modo in cui è stato utilizzato, perché se un metodo di azione richiede molte configurazioni di attestazioni diverse, quindi con quella risposta dovresti scrivere più volte qualcosa come ...

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

Quindi, immagina quanta codifica ci vorrebbe. Idealmente, "CanReadResource" dovrebbe essere una politica che utilizza molte affermazioni per determinare se un utente può leggere una risorsa.

Quello che faccio è creare i miei criteri come enumerazione e quindi scorrere e impostare i requisiti in questo modo ...

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 sembra ...

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

Si noti che il codice sopra può anche abilitare la pre-mappatura di un utente su una politica nell'archivio dati. Pertanto, quando si compongono attestazioni per l'utente, sostanzialmente si recuperano le politiche che sono state pre-mappate all'utente direttamente o indirettamente (ad es. Perché l'utente ha un determinato valore di rivendicazione e tale valore di rivendicazione è stato identificato e mappato su una politica, ad esempio che fornisce la mappatura automatica per gli utenti che hanno anche quel valore di rivendicazione) ed elenca le politiche come attestazioni, in modo tale che nel gestore dell'autorizzazione, è possibile semplicemente verificare se le rivendicazioni dell'utente contengono requisiti. Polizza come valore di un elemento di rivendicazione nel loro reclami. Questo è per un modo statico di soddisfare un requisito politico, ad esempio il requisito "Nome" è di natura abbastanza statica. Così,

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

Un requisito dinamico può riguardare il controllo della fascia di età, ecc. E le politiche che utilizzano tali requisiti non possono essere pre-mappate agli utenti.

Un esempio di controllo dinamico delle dichiarazioni di polizza (ad es. Per verificare se un utente ha più di 18 anni) è già alla risposta fornita da @blowdart ( https://stackoverflow.com/a/31465227/4974715 ).

PS: ho digitato questo sul mio telefono. Perdonate qualsiasi errore di battitura e mancanza di formattazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.