Wie erstellen Sie ein benutzerdefiniertes AuthorizeAttribute in ASP.NET Core?

428

Ich versuche, ein benutzerdefiniertes Berechtigungsattribut in ASP.NET Core zu erstellen. In früheren Versionen war es möglich, zu überschreibenbool AuthorizeCore(HttpContextBase httpContext) . Dies existiert aber nicht mehr in AuthorizeAttribute.

Wie wird derzeit ein benutzerdefiniertes AuthorizeAttribute erstellt?

Was ich versuche zu erreichen: Ich erhalte eine Sitzungs-ID in der Header-Autorisierung. Anhand dieser ID weiß ich, ob eine bestimmte Aktion gültig ist.

jltrem
quelle
Ich bin nicht sicher, wie es geht, aber MVC ist Open Source. Sie können das Github-Repo ziehen und nach Implementierungen von IAuthorizationFilter suchen. Wenn ich heute Zeit habe, werde ich nach dir suchen und eine tatsächliche Antwort posten, aber keine Versprechen. Github Repo: Github.com/aspnet/Mvc
Bopapa_1979
OK, keine Zeit mehr, aber suchen Sie im MVC-Repo, das AuthorizeAttribute verwendet, im aspnet / Security-Repo nach Verwendungen von AuthorizationPolicy, hier: github.com/aspnet/Security . Alternativ können Sie im MVC-Repo nach dem Namespace suchen, in dem sich das Sicherheitsmaterial zu befinden scheint, nämlich Microsoft.AspNet.Authorization. Entschuldigung, ich kann nicht hilfreicher sein. Viel Glück!
bopapa_1979

Antworten:

445

Der vom ASP.Net Core-Team empfohlene Ansatz besteht darin, das neue Richtliniendesign zu verwenden, das hier vollständig dokumentiert ist . Die Grundidee des neuen Ansatzes besteht darin, das neue Attribut [Authorize] zu verwenden, um eine "Richtlinie" [Authorize( Policy = "YouNeedToBe18ToDoThis")]zu bestimmen (z. B. wenn die Richtlinie in Startup.cs der Anwendung registriert ist, um einen Codeblock auszuführen (dh sicherzustellen, dass der Benutzer einen Altersanspruch hat) wo das Alter 18 oder älter ist).

Das Richtliniendesign ist eine großartige Ergänzung des Frameworks, und das ASP.Net Security Core-Team sollte für seine Einführung gelobt werden. Das heißt, es ist nicht für alle Fälle gut geeignet. Der Nachteil dieses Ansatzes besteht darin, dass er keine bequeme Lösung für die häufigste Notwendigkeit bietet, einfach zu behaupten, dass ein bestimmter Controller oder eine bestimmte Aktion einen bestimmten Anspruchstyp erfordert. In dem Fall, in dem eine Anwendung möglicherweise über Hunderte von diskreten Berechtigungen für CRUD-Operationen für einzelne REST-Ressourcen verfügt ("CanCreateOrder", "CanReadOrder", "CanUpdateOrder", "CanDeleteOrder" usw.), erfordert der neue Ansatz entweder wiederholte Einzelaufgaben eine Zuordnung zwischen einem Richtliniennamen und einem Anspruchsnamen (zoptions.AddPolicy("CanUpdateOrder", policy => policy.RequireClaim(MyClaimTypes.Permission, "CanUpdateOrder));) oder Schreiben von Code, um diese Registrierungen zur Laufzeit durchzuführen (z. B. alle Anspruchstypen aus einer Datenbank lesen und den oben genannten Aufruf in einer Schleife ausführen). Das Problem bei diesem Ansatz besteht in den meisten Fällen darin, dass kein unnötiger Overhead erforderlich ist.

Während das ASP.Net Core Security-Team empfiehlt, niemals eine eigene Lösung zu erstellen, ist dies in einigen Fällen die umsichtigste Option, um damit zu beginnen.

Das Folgende ist eine Implementierung, die den IAuthorizationFilter verwendet, um eine einfache Möglichkeit zum Ausdrücken einer Anspruchsanforderung für einen bestimmten Controller oder eine bestimmte Aktion bereitzustellen:

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
quelle
78
Dies sollte als RICHTIGE ANTWORT markiert werden. Hier sehen Sie, wie die Mitarbeiter von Microsoft das Feedback der Entwickler berücksichtigen. Ich verstehe den Grund nicht, warum sie so "verschlossen" sind, da es eine sehr häufige Situation ist, eine Vielzahl unterschiedlicher Berechtigungen zu haben. Es ist ein völliger Overkill, für jede eine Richtlinie zu codieren. Ich habe so lange danach gesucht ... (Ich habe diese Frage bereits vor fast zwei Jahren gestellt, als vNext hier noch eine Wette war: stackoverflow.com/questions/32181400/… aber wir stecken immer noch dort fest)
Vi100
3
Das ist gutes Zeug. Wir haben Authentifizierungs-Middleware in der Web-API, aber eine verbesserte Sicherheit für die Autorisierungsberechtigungen nach Rollen. Wenn Sie also nur ein Attribut wie das folgende eingeben müssen: [MyAuthorize (MyClaimTypes.Permission, MyClaimValueTypes.Write, MyPermission.Employee)] sieht sehr gut aus.
Mariano Peinador
4
@Derek Greer: Dies ist die beste Antwort. Sie implementieren jedoch einen ActionFilter, der nach Authorize Action Filter ausgeführt wird. Gibt es überhaupt einen Aktionsfilter zu implementieren und zu autorisieren?
Jacob Phan
6
@JacobPhan Sie haben Recht, dies sollte besser über die IAuthorizationFilter-Schnittstelle implementiert werden. Ich habe den Code aktualisiert, um die Änderungen widerzuspiegeln.
Derek Greer
3
so new ForbidResult()funktioniert nicht (verursacht Ausnahme / 500) , weil es keine zugehörige Zulassungssystem hat. Was würde ich für diesen Fall verwenden?
Sinaesthetic
252

Ich bin die asp.net-Sicherheitsperson. Lassen Sie mich zunächst entschuldigen, dass noch nichts davon außerhalb des Beispiels oder der Komponententests des Musikgeschäfts dokumentiert ist und alles noch in Bezug auf exponierte APIs verfeinert wird. Eine ausführliche Dokumentation finden Sie hier .

Wir möchten nicht, dass Sie benutzerdefinierte Berechtigungsattribute schreiben. Wenn Sie das tun müssen, haben wir etwas falsch gemacht. Stattdessen sollten Sie die Berechtigung schreiben Anforderungen .

Die Autorisierung wirkt sich auf Identitäten aus. Identitäten werden durch Authentifizierung erstellt.

Sie sagen in Kommentaren, dass Sie eine Sitzungs-ID in einem Header überprüfen möchten. Ihre Sitzungs-ID wäre die Grundlage für die Identität. Wenn Sie das AuthorizeAttribut verwenden möchten, schreiben Sie eine Authentifizierungs-Middleware, um diesen Header in eine authentifizierte zu verwandeln ClaimsPrincipal. Sie würden dies dann innerhalb einer Autorisierungsanforderung überprüfen. Die Autorisierungsanforderungen können so kompliziert sein, wie Sie möchten. Hier ist beispielsweise eine, bei der ein Geburtsdatum für die aktuelle Identität angegeben wird und die autorisiert wird, wenn der Benutzer über 18 Jahre alt ist.

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

Dann ConfigureServices()würden Sie es in Ihrer Funktion verkabeln

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

Und schließlich wenden Sie es auf einen Controller oder eine Aktionsmethode mit an

[Authorize(Policy = "Over18")]
Blasrohrpfeil
quelle
84
Ich frage mich ... wie würde man damit eine fein abgestimmte Zugangskontrolle implementieren? Angenommen, das Beispiel " ManageStoreAnforderung aus dem Music Store". Da es sich um ein Beispiel handelt, gibt es nur die Möglichkeit, "alles oder nichts zuzulassen". Müssen wir dann für jede mögliche Permutation eine neue Richtlinie erstellen? dh "Benutzer / Lesen", "Benutzer / Erstellen", "Benutzer / Zuweisungsrolle", "Benutzer / Löschen", wenn wir feinkörnige Ansprüche wünschen? Klingt nach ziemlich viel Einrichtungsarbeit, um es zum Laufen zu bringen, und nach einer Fülle von Richtlinien, nur um Ansprüche zu verwalten, anstatt um ein [ClaimsAutzorization("User", "Read", "Create", "Delete", "Assign")]Attribut?
Tseng
83
Ich muss darauf hinweisen, dass dies alles komplexer ist als die Implementierung einer benutzerdefinierten Autorisierungsmethode. Ich weiß, wie die Autorisierung erfolgen soll. Ich könnte sie einfach in MVC 5 schreiben. In MVC 6 fügen sie viel "erledigten" Code hinzu, der tatsächlich komplexer zu verstehen ist als die Implementierung des Kerns "Ding" selbst. Ich sitze vor einer Seite und versuche, etwas herauszufinden, anstatt Code durchzuschreiben. Dies ist auch ein großer Schmerz für Leute, die andere RDBMS als Microsoft (oder No-Sql) verwenden.
Felype
17
Aus meiner Sicht löst dies nicht alle Szenarien. Vor MVC 6 habe ich ein benutzerdefiniertes Berechtigungsattribut verwendet, um mein eigenes "Berechtigungssystem" zu implementieren. Ich könnte allen Aktionen das Attribut "Autorisieren" hinzufügen und eine bestimmte erforderliche Berechtigung (als Enum-Wert) übergeben. Die Berechtigung selbst wurde Gruppen / Benutzern innerhalb der Datenbank zugeordnet. Ich sehe also keine Möglichkeit, mit Richtlinien damit umzugehen!?
Gerwald
43
Ich bin, wie viele andere in diesen Kommentaren, sehr enttäuscht darüber, dass die Verwendung von Attributen für die Autorisierung so stark neutralisiert wurde, was in der Web-API 2 möglich war. Tut mir leid, aber Ihre "Anforderungs" -Abstraktion deckt keinen Fall ab, in dem wir sie zuvor verwenden konnten Attributkonstruktorparameter, um einen zugrunde liegenden Autorisierungsalgorithmus zu informieren. Früher war es hirntot einfach, so etwas zu tun [CustomAuthorize(Operator.And, Permission.GetUser, Permission.ModifyUser)]. Ich könnte ein einzelnes benutzerdefiniertes Attribut auf unendlich viele Arten verwenden, indem ich einfach die Konstruktorparameter ändere.
NathanAldenSr
61
Ich bin auch schockiert, dass der selbsternannte "Lead ASP.NET Security Guy" tatsächlich vorschlägt, magische Zeichenfolgen (die Bedeutung von zu hacken IAuthorizeData.Policy) und benutzerdefinierte Richtlinienanbieter zu verwenden, um dieses offensichtliche Versehen zu überwinden, anstatt es innerhalb des Frameworks anzusprechen. Ich dachte, wir sollten keine eigenen Implementierungen erstellen? Sie haben einigen von uns keine andere Wahl gelassen, als die Autorisierung (erneut) von Grund auf neu zu implementieren, und diesmal ohne den Vorteil des alten AuthorizeAttributs der Web-API . Jetzt müssen wir es auf der Ebene des Aktionsfilters oder der Middleware tun.
NathanAldenSr
104

Es scheint, dass Sie mit ASP.NET Core 2 erneut erben AuthorizeAttributekönnen. Sie müssen lediglich Folgendes implementieren IAuthorizationFilter(oder 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
quelle
4
Sie können dies also nur verwenden, um die Autorisierung zu verweigern , nicht um sie zu erteilen ?
MEMark
1
@MEMark Mit Gewähren meinen Sie, ein anderes Berechtigungsattribut zu überschreiben?
Gius
2
AFAIK, der Zugriff ist standardmäßig zulässig, daher müssen Sie ihn explizit verweigern (z. B. durch Hinzufügen eines AuthorizeAttribute). Überprüfen Sie diese Frage für weitere Details: stackoverflow.com/questions/17272422/…
gius
16
Beachten Sie auch, dass im vorgeschlagenen Beispiel nicht von AuthorizeAttribute geerbt werden muss. Sie können von Attribute und IAuthorizationFilter erben . Auf diese Weise erhalten Sie nicht die folgende Ausnahme, wenn ein nicht standardmäßiger Authentifizierungsmechanismus verwendet wird: InvalidOperationException: Es wurde kein authenticationScheme angegeben und es wurde kein DefaultChallengeScheme gefunden.
Anatolyevich
13
Beachten Sie, dass Sie, wenn Ihre OnAuthorizationImplementierung auf eine asynchrone Methode warten muss, diese implementieren sollten, IAsyncAuthorizationFilteranstatt dass IAuthorizationFilterIhr Filter synchron ausgeführt wird und Ihre Controller-Aktion unabhängig vom Ergebnis des Filters ausgeführt wird.
Codemunkie
34

Basierend auf Derek Greer GREAT Antwort, habe ich es mit Aufzählungen.

Hier ist ein Beispiel für meinen 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
quelle
1
Danke dafür. Ich habe diesen Beitrag mit einer etwas anderen Implementierung und einer Anfrage zur Validierung erstellt. Stackoverflow.com/questions/49551047/…
Anton Swanevelder
2
MumboJumboFunction <3
Marek Urbanowicz
31

Sie können Ihren eigenen AuthorizationHandler erstellen, der benutzerdefinierte Attribute auf Ihren Controllern und Aktionen findet, und diese an die HandleRequirementAsync-Methode übergeben.

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

Anschließend können Sie es für alle benutzerdefinierten Attribute verwenden, die Sie für Ihre Controller oder Aktionen benötigen. Zum Beispiel, um Berechtigungsanforderungen hinzuzufügen. Erstellen Sie einfach Ihr benutzerdefiniertes Attribut.

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

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

Erstellen Sie dann eine Anforderung, die Sie Ihrer Richtlinie hinzufügen möchten

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

Erstellen Sie dann den AuthorizationHandler für Ihr benutzerdefiniertes Attribut und erben Sie den zuvor erstellten AttributeAuthorizationHandler. Es wird eine IEnumerable für alle Ihre benutzerdefinierten Attribute in der HandleRequirementsAsync-Methode übergeben, die von Ihrem Controller und Ihrer Aktion gesammelt wurde.

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

Fügen Sie schließlich in Ihrer Startup.cs ConfigureServices-Methode Ihren benutzerdefinierten AuthorizationHandler zu den Diensten hinzu und fügen Sie Ihre Richtlinie hinzu.

        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

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

Jetzt können Sie Ihre Controller und Aktionen einfach mit Ihrem benutzerdefinierten Attribut dekorieren.

[Permission("AccessCustomers")]
public class CustomersController
{
    [Permission("AddCustomer")]
    IActionResult AddCustomer([FromBody] Customer customer)
    {
        //Add customer
    }
}
Shawn
quelle
1
Ich werde mir das so schnell wie möglich ansehen.
NathanAldenSr
5
Dies ist ziemlich überarbeitet ... Ich habe das gleiche mit einem einfachen AuthorizationFilterAttribute gelöst, das einen Parameter empfängt. Sie brauchen keine Reflexion dafür, es scheint noch künstlicher als die "offizielle" Lösung (die ich ziemlich schlecht finde).
Vi100
2
@ Vi100 Ich konnte nicht viele Informationen zu AuthorizationFilters in ASP.NET Core finden. Auf der offiziellen Dokumentationsseite heißt es, dass sie derzeit an diesem Thema arbeiten. docs.microsoft.com/en-us/aspnet/core/security/authorization/…
Shawn
4
@ Vi100 Kannst du bitte deine Lösung teilen, wenn es einen einfacheren Weg gibt, dies zu erreichen, würde ich gerne wissen.
Shawn
2
Beachten Sie, dass die Verwendung von UnderlyingSystemType oben nicht kompiliert wird, aber das Entfernen scheint zu funktionieren.
Teezeit
25

Was ist der aktuelle Ansatz zum Erstellen eines benutzerdefinierten AuthorizeAttribute?

Einfach: Erstellen Sie keine eigenen AuthorizeAttribute.

Für reine Autorisierungsszenarien (z. B. Einschränkung des Zugriffs nur auf bestimmte Benutzer) wird empfohlen, den neuen Autorisierungsblock zu verwenden: 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() { ... }
}

Für die Authentifizierung wird am besten auf Middleware-Ebene gehandhabt.

Was versuchst du genau zu erreichen?

Kévin Chalet
quelle
1
Ich erhalte eine Sitzungs-ID in der Header-Autorisierung. Anhand dieser ID weiß ich, ob eine bestimmte Aktion gültig ist.
Jltrem
1
Dann ist das kein Autorisierungsproblem. Ich denke, Ihre "Sitzungs-ID" ist tatsächlich ein Token, das die Identität des Anrufers enthält: Dies sollte auf jeden Fall auf Middleware-Ebene erfolgen.
Kévin Chalet
3
Es handelt sich nicht um eine Authentifizierung (Feststellen, wer der Benutzer ist), sondern um eine Autorisierung (Festlegen, ob ein Benutzer Zugriff auf eine Ressource haben soll). Wo schlagen Sie vor, dass ich das lösen möchte?
Jltrem
3
@jltrem, stimmte zu, Sie sprechen von Autorisierung, nicht von Authentifizierung.
bopapa_1979
2
@Pinpoint bin ich nicht. Ich frage ein anderes System nach diesen Informationen. Dieses System authentifiziert (bestimmt den Benutzer) und autorisiert (sagt mir, worauf dieser Benutzer zugreifen kann). Im Moment habe ich es gehackt, um zu arbeiten, indem ich in jeder Controller-Aktion eine Methode aufrufe, damit das andere System die Sitzung überprüft. Ich möchte, dass dies automatisch über ein Attribut geschieht.
Jltrem
4

Wenn jemand nur ein Inhaber-Token in der Autorisierungsphase anhand der aktuellen Sicherheitspraktiken validieren möchte, können Sie

Fügen Sie dies Ihren Startup / ConfigureServices hinzu

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

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

und das in deiner Codebasis,

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

Wenn der Code nicht erreicht context.Succeed(...)wird, schlägt er trotzdem fehl (401).

Und dann können Sie in Ihren Controllern verwenden

 [Authorize(Policy = "Bearer", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Gabriel P.
quelle
Warum sollten Sie Ihre eigene Validierung des Tokens durchführen, wenn die JwtBearer-Middleware dies bereits erledigt? Außerdem wird der richtige Inhalt in den WWW-Authenticate-Antwortheader für einen Authentifizierungs- / Token-Validierungs- / Ablauffehler eingefügt. Wenn Sie auf die Authentifizierungspipeline zugreifen möchten, können Sie auf bestimmte Ereignisse in den AddJwtBearer-Optionen zugreifen (OnAuthenticationFailed, OnChallenge, OnMessageReceived und OnTokenValidated).
Darren Lewis
Dies ist unendlich einfacher als jede andere Lösung, die ich gesehen habe. Speziell für einfache Anwendungsfälle mit API-Schlüsseln. Ein Update: Für 3.1 ist die Umwandlung in AuthorizationFilterContext aufgrund des Endpoint-Routing-Materials nicht mehr gültig. Sie müssen den Kontext über HttpContextAccessor abrufen.
JasonCoder
2

Der moderne Weg ist AuthenticationHandlers

in startup.cs hinzufügen

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 ist ein Dienst, den Sie erstellen, wenn Sie Benutzername und Kennwort haben. Im Grunde gibt es eine Benutzerklasse zurück, auf die Sie Ihre Ansprüche abbilden.

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

Dann können Sie diese Ansprüche abfragen und alle Daten, die Sie zugeordnet haben, es gibt einige, werfen Sie einen Blick auf die ClaimTypes-Klasse

Sie können dies in einer Erweiterungsmethode verwenden, um eine der Zuordnungen abzurufen

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

Dieser neue Weg ist meiner Meinung nach besser als

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
quelle
Diese brillante Antwort funktioniert einfach wie ein Zauber! Vielen Dank dafür und ich wünsche Ihnen, dass es positiv bewertet wird, da dies die beste Antwort ist, die ich nach sechs Stunden Durchsuchen von Blogs, Dokumentationen und Stacks für die Standardauthentifizierung plus Rollenautorisierung gefunden habe.
Piotr Śródka
@ PiotrŚródka, Sie sind herzlich willkommen. Bitte beachten Sie, dass die Antwort ein wenig "vereinfacht" ist. Testen Sie, ob Sie ein ':' im Text haben, da ein böswilliger Benutzer versuchen könnte, Ihren Dienst zum Absturz zu bringen, indem er einfach kein schönes Ende in einem Index ausspielt der Bereichsausnahme. Testen Sie wie immer, was Ihnen von externen Quellen gegeben wird
Walter Vehoeven
2

Zum jetzigen Zeitpunkt glaube ich, dass dies mit der IClaimsTransformation-Schnittstelle in asp.net Core 2 und höher erreicht werden kann. Ich habe gerade einen Proof of Concept implementiert, der gemeinsam genutzt werden kann, um hier zu posten.

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

Um dies in Ihrem Controller zu verwenden, fügen Sie einfach eine entsprechende [Authorize(Roles="whatever")]Methode zu Ihren Methoden hinzu.

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

    result.StatusCode = (int)HttpStatusCode.OK;

    return result;
}

In unserem Fall enthält jede Anforderung einen Autorisierungsheader, der ein JWT ist. Dies ist der Prototyp, und ich glaube, wir werden nächste Woche in unserem Produktionssystem etwas sehr Nahes tun.

Zukünftige Wähler berücksichtigen das Datum des Schreibens, wenn Sie abstimmen. Ab heute works on my machine.möchten Sie wahrscheinlich mehr Fehlerbehandlung und Protokollierung Ihrer Implementierung.

Keine Rückerstattung Keine Rückgabe
quelle
Was ist mit ConfigureServices? Muss etwas hinzugefügt werden?
Daniel
Wie an anderer Stelle besprochen, ja.
Keine Rückerstattung Keine Rückgabe
1

Zur Autorisierung in unserer App. Wir mussten einen Dienst basierend auf den im Berechtigungsattribut übergebenen Parametern aufrufen.

Wenn wir beispielsweise überprüfen möchten, ob der angemeldete Arzt Patiententermine anzeigen kann, übergeben wir "View_Appointment" an das benutzerdefinierte Berechtigungsattribut und überprüfen dieses Recht im DB-Service und basierend auf den Ergebnissen, die wir autorisieren. Hier ist der Code für dieses Szenario:

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

Und bei API-Aktionen verwenden wir es folgendermaßen:

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

    }
Abdullah
quelle
1
Bitte beachten Sie, dass IActionFilter ein Problem darstellt, wenn Sie dasselbe Attribut für Hub-Methoden in SignalR verwenden möchten. SignalR Hubs erwarten IAuthorizationFilter
ilkerkaran
Danke für die Information. Ich verwende SignalR derzeit nicht in meiner Anwendung, daher habe ich es nicht damit getestet.
Abdullah
Dasselbe Prinzip, denke ich, da Sie immer noch den Autorisierungseintrag des Headers verwenden müssen, wird die Implementierung unterschiedlich sein
Walter Vehoeven
0

Die akzeptierte Antwort ( https://stackoverflow.com/a/41348219/4974715 ) ist nicht realistisch wartbar oder geeignet, da "CanReadResource" als Anspruch verwendet wird (sollte aber im Wesentlichen eine Richtlinie in der Realität sein, IMO). Der Ansatz bei der Antwort ist in der Art und Weise, wie er verwendet wurde, nicht in Ordnung. Wenn für eine Aktionsmethode viele verschiedene Anspruchseinstellungen erforderlich sind, müssten Sie mit dieser Antwort wiederholt so etwas wie ...

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

Stellen Sie sich also vor, wie viel Codierung das kosten würde. Im Idealfall sollte "CanReadResource" eine Richtlinie sein, die viele Ansprüche verwendet, um zu bestimmen, ob ein Benutzer eine Ressource lesen kann.

Was ich tue, ist, dass ich meine Richtlinien als Aufzählung erstelle und dann die Anforderungen wie folgt durchlaufe und einrichte ...

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

Die DefaultAuthorizationRequirement-Klasse sieht aus wie ...

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

Beachten Sie, dass der obige Code auch die Vorabzuordnung eines Benutzers zu einer Richtlinie in Ihrem Datenspeicher ermöglichen kann. Wenn Sie also Ansprüche für den Benutzer erstellen, rufen Sie im Grunde genommen die Richtlinien ab, die dem Benutzer direkt oder indirekt vorab zugeordnet wurden (z. B. weil der Benutzer einen bestimmten Anspruchswert hat und dieser Anspruchswert identifiziert und einer Richtlinie zugeordnet wurde, z dass es eine automatische Zuordnung für Benutzer bietet, die auch diesen Anspruchswert haben) und die Richtlinien als Ansprüche eintragen, sodass Sie im Autorisierungshandler einfach überprüfen können, ob die Ansprüche des Benutzers eine Anforderung enthalten. Richtlinie als Wert eines Anspruchselements in ihrem Ansprüche. Dies dient dazu, eine Richtlinienanforderung statisch zu erfüllen, z. B. ist die Anforderung "Vorname" recht statisch. Damit,

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

Bei einer dynamischen Anforderung kann es sich um die Überprüfung des Altersbereichs usw. handeln, und Richtlinien, die solche Anforderungen verwenden, können Benutzern nicht vorab zugeordnet werden.

Ein Beispiel für die dynamische Überprüfung von Richtlinienansprüchen (z. B. um zu überprüfen, ob ein Benutzer älter als 18 Jahre ist) finden Sie bereits in der Antwort von @blowdart ( https://stackoverflow.com/a/31465227/4974715 ).

PS: Ich habe das auf meinem Handy eingegeben. Verzeihen Sie Tippfehler und fehlende Formatierung.

Olumide
quelle