Laden...

GraphQL Zugriffskontrollen richtig handhaben?

Erstellt von Olii vor 4 Jahren Letzter Beitrag vor 4 Jahren 1.653 Views
O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 4 Jahren
GraphQL Zugriffskontrollen richtig handhaben?

Hallo liebe Forumuser,

ich beschäftige mich immer noch viel mit GraphQL und bin auf einen Punkt gestoßen, über den ich heute schon den ganzen Tag nachdenke.

Es um die Zugriffskontrollen mit einer GraphQL Schnittstelle. Ich erkläre das ganze mal anhand eines Beispiels:

Eine Client - Server Anwendung, nehmen wir als Beispiel mal eine Socialmedia Plattform oder dergleichen:

In einer relationalen Datenbank werden alle User gespeichert. In der Anwendung können User z.B. mit anderen Usern befreundet sein und auch mit diesen privat schreiben.
Somit hätte man sag ich mal einfach zwei Tabellen:

  • User
  • User_Relation
  • User_Relation_Chat_Messages

Jeder User darf natürlich seine eigenen Kontakte und Unterhaltungen mit diesen einsehen. Ein User darf aber nicht die Unterhaltungen von anderen bzw. fremden Usern einsehen, sonder wirklich nur seine eigenen.

Mit GraphQL ist das ja nun so, das man einen Endpoint hat, an den alle Anfragen geschickt werden.

Endpoints sind immer mit z.B.


[Authorize(Roles = "User")]

gekennzeichnet. Beim einloggen erhält der User ein JWT Token in dem seine ID, Name und die Rolle User gespeichert ist.

Das erreichen des Endpoints ist auch kein Problem. Der User kann sich problemlos seine Daten abholen.

**Jetzt kommt das Problem: **
Da nun alle eingeloggten User Zugriff auf diesen Endpoint haben, könnten diese ja theoretisch eine GraphQL Querys gegen die API feuern (wenn die User vielleicht technischer versiert sind oder was auch immer). Diese User haben z.B. ihren Token und Ihre ID auswendig machen können, und wollen nun dem Betreiber schaden.

Somit könnte ja jeder User, problemlos einfach alle Daten wie z.B. private Nachrichten von anderen Usern abfragen, ohne auch nur an ein Problem zu stoßen.

Jetzt kommt die Frage:
Wie schützt man vor solchen Dingen seine API?

Ich hatte mir folgendes überlegt:
Da ich z.B. mit JWT Tokens arbeite, könnte man sobald ein User die GraphQL Schnittstelle erreicht, die UserID abfragen mit der UserID die im Token steht (wobei das Problem weiterhin besteht das der "Hacker" auch dies Problemlos umgehen könnte. Weiter dachte ich dann, dass nur die User Daten abgefragt werden können, mit der ID die mitgesendet wurde. Da ID's GUID/UUID sind, könnte der angebliche "Hacker", nicht so einfach an ID's der anderen User kommen. Aber es gibt auch Szenarien wo ein User auch mal Daten von anderen Braucht, wie z.B. einen Status oder irgendwie sowas. Somit müsste es die Option geben das in ausnahmen dann doch ohne ID Abfrage die Query genehmigt wird.

(Token Claims Abfragen, z.B. so:)


var test = User.FindFirst(ClaimTypes.Role).Value;

Und man fragt dann vielleicht noch immer zusätzlich ab welche Felder den überhaupt versucht werden zu query'n, was dann aber wieder so viele Ausnahmen mit sich bringen würde.

In dieser Vorgehensweise sehe ich so viele Lücken, dass mir das ganze nicht geheuer ist.

Wie würdet ihr bei sowas vorgehen? Oder gibt es da etwas wie eine best practice?
Ich bin auch schon sehr lange auf Suche im Internet gegangen aber zu so einem Problem konnte ich noch nichts finden, oder ich habe nach den falschen Stichwörtern gesucht, was ich nicht ausschließen möchte.

Wenn jemand Ideen hat oder Links etc. von Beiträgen hat in diese Themen diskutiert wurden, würde ich mich sehr freuen 😃)

Ich bin noch recht frisch und möchte mich für alle Hilfe bedanken 😃

16.806 Beiträge seit 2008
vor 4 Jahren

Wie schützt man vor solchen Dingen seine API?

Durch Authorization - und das ist an der Stelle nicht anders, als bei jeder anderen API auch. Dazu braucht man auch kein "Hacker" sein.
Das Authroize-Attribut macht aber in der Form nichts anderes als die Prüfung eines generellen Zugriffs - basierend darauf, dass der Nutzer überhaupt authentifiziert ist und optional die angegebene Policy erfüllt.
Es ist aber keine Authorisierung auf Ressourcen!

Geht es um Resource Based Authorization, dann kommst Du nicht um ein Resource Requirement drum herum.
In ASP.NET Core ist das bereits eingebaut: Resource-based authorization in ASP.NET Core
Das hilft Dir an für sich aber nicht in Deiner Applikationslogik.
Authorisierung ist immer eine spezifische Implementierung - das kann Dir kein Attribut so pauschal abnehmen.

wobei das Problem weiterhin besteht das der "Hacker" auch dies Problemlos umgehen könnte.

Nein, kann er prinzipiell nicht. Token haben eine Signierung. Änderungen machen den Token durch Checksummen ungültig.
Anatomy of a JWT request

a ID's GUID/UUID sind, könnte der angebliche "Hacker", nicht so einfach an ID's der anderen User kommen.

Doch kann er: erraten. Aufwändig, aber möglich. Wird das nicht abgesichert, ist das eine Sicherheitslücke.

Wie würdet ihr bei sowas vorgehen?

Authorisierung als Teil der Logik.

Bezogen auf Mediator in den anderen Threads: der CommandHandler muss die Authorisierung durchführen und ggfls. eine Exception werfen.
Um es einfacher zu machen kannst Du im Falle von MediatR für viele Fälle die eingebaute Pipeline verwenden:
Mediatr 3.0 Using Pipeline behaviors for authentication

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 4 Jahren

Achso okey, ich verstehe. Zumindest zu 80% 😄.

Ich habe mir die Beiträge angesehen und auch in die Dokumentation von der Version 3.

https://github.com/jbogard/MediatR/wiki/Behaviors

und auch ein paar weitere Blogposts dazu

mediatr-behaviors
examples

Dazu hätte ich noch eine Frage:

Es ist nun ja also so das man speziell eine Pipeline nutzen kann, die vor dem Handler ausgeführt wird.
Wie auch das Beispiel von Stackoberflow das du geschickt hast zeigt. Der letzte Post in der Frage zeigt aber auch noch etwas anderes als die Pipeline, und zwar mascht der User da folgendes:

Die Pipeline: (fast das gleiche wie von der angenommenen Antwort)

namespace MediatR.Extensions.FluentValidation
{
    public class ValidationPipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    {
        private readonly IValidator<TRequest>[] _validators;

        public ValidationPipelineBehavior(IValidator<TRequest>[] validators)
        {
            _validators = validators;
        }

        public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next)
        {
            var context = new ValidationContext(request);

            var failures =
                _validators.Select(v => v.Validate(context)).SelectMany(r => r.Errors).Where(f => f != null).ToList();

            if (failures.Any())
            {
                throw new ValidationException(failures);
            }

            return await next();
        }
    }
}

Was ich jetzt zusätzlich bei ihm noch sehe ist folgendes:


public classs SaveCommand: IRequest<int>
 {
    public string FirstName { get; set; }

    public string Surname { get; set; }
 }

   public class SaveCommandValidator : AbstractValidator<SaveCommand>
    {
       public SaveCommandValidator()
       {
          RuleFor(x => x.FirstName).Length(0, 200);
          RuleFor(x => x.Surname).NotEmpty().Length(0, 200);
       }
    }

Hier macht er ja zusätzlich zu dem Command noch den SaveCommandValidator. Scheint erstmal eine nette Sache zu sein das man alle Felder noch prüfen kann, aber wie würde man das in verbingung mit Mediatr nutzen?

Wäre es dann so das man anstatt des Commands/Query einfach die Class SaveCommandValidator übergeben würde?

Und eine kleine zweite Frage:

Bezogen auf Mediator in den anderen Threads: der CommandHandler muss die Authorisierung

Aus den Beiträge konnte ich entnehmen, zumindest habe ich das so verstanden, dass es nicht unbedingt der CommandHandler sonder auch der QueryHandler (heißt der so) mit den Pipelines verwendet werden kann. Denn bei den Abfragen mit GraphQL würde es sich ja um Abfragen und Mutations handeln.

lg Olli

16.806 Beiträge seit 2008
vor 4 Jahren

Mediator ist eine vereinfachte Darstellung von CQRS.
Es gibt nur Commands und Notifications - keine Queries.
Queries werden identisch zu Commands implementiert; daher gibt es hier kein Unterschied.

Der Link zu StackOverflow war nur ein Beispiel, wie die Pipeline verwendet wird.
Das Beispiel zeigt eben die Validierung von Commands.

Ich authorisiere i.d.R. direkt im Command.

Die Validatoren werden in diesem Beispiel einfach über die Dependency registriert und werden dann in ValidationPipelineBehavior injiziert.
Heisst jetzt aber nicht, dass das 1:1 für Deine Authorisierung passt.

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 4 Jahren

Achso, dann hatte ich das nicht ganz richtig verstanden.

Ich dachte nämlich das z.B. die gezeigte Klasse für SaveCommandValidator, dann in den Handler übergben wird. das ist nicht richtig.

Ich spiele heut noch weiter damit rum und wenn ich etwas gutes raus bekomme, poste ich es hier auch für andere Leute die das Thema interessiert.

Danke dir Abt 😃

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 4 Jahren

Also ich habe jetzt etwas rum probiert.

Die Pipeline funktioniert einwandfrei und ist eine gute Sache.

Für die Zugriffskontrolle für die verschiedenen Ressourcen habe ich nach lange überlegen zwei Möglichkeiten "gefunden" bzw. mir überlegt.

Möglichkeit 1:

Die Erstellung von WhiteLists und Gruppen. Z.B.:

Die Gruppe User würde sich quasi noch in zwei "Untergruppen" aufteilen.

  • User der seine eigene privaten Daten haben möchte
  • User der allgemeine öffentlichen Daten eines anderen Users haben möchte

Somit müssten beide Untergruppen zwei verschiedene WhiteLists haben. Als Beispiel habe ich mal einfach hier eine List<string> erstellt mit allen Felder die die Gruppe User, der seine privaten Daten haben möchte, query'n kann:

(Die Liste muss nicht so aussehenm, das hier ist wie gesagt nur ein Beispiel mit Felder die der User z.B. Abfragen könnte dann)


User_Whitelist_Own_ID = new List<string>();
User_Whitelist_Own_ID.Add("id");
User_Whitelist_Own_ID.Add("login_name");
User_Whitelist_Own_ID.Add("games");
User_Whitelist_Own_ID.Add("game");
User_Whitelist_Own_ID.Add("name");
User_Whitelist_Own_ID.Add("user_relations");
User_Whitelist_Own_ID.Add("messages");
User_Whitelist_Own_ID.Add("message");
User_Whitelist_Own_ID.Add("fk_user_account_id_sender");

Was ich an der Sache gut finden würde ist, dass man eine gute Übersicht hätte von Felder etc. die eine Bestimmte User Gruppe haben bzw. query'n kann.

Was ich schlecht finde, ist das man all diese Listen irgendwo sicher speichern muss und eine Liste vom Typ String wahrscheinlich auch nicht soooooo clean ist.

Allerdings könnte ich mit dieser Methode für jede Gruppe alle Felder definieren und dann die vom User geschickte Query auf diese Felder überprüfen. Quasi prüfen ob alle gesendeten Felder in der Liste vorhanden sind, wenn nicht, dann Fehlermeldung. Das habe ich hier zum testen mal mit einer rekursieven Funktion gemacht, in die alle Felder gegeben wird und dann auch somit alle Child-Elemente überprüft:


public bool CheckGraphQLQueryFieldPermission(List<KeyValuePair<string, GraphQL.Language.AST.Field>> fieldSelection, bool boolUnEvenCount, List<string> fieldWhiteList)
    {
        int intMatchedWhiteListFields = fieldSelection.Where(x => fieldWhiteList.Contains(x.Key)).ToList().Count();
        var intAllFields = fieldSelection.Count();

        foreach(KeyValuePair<string, GraphQL.Language.AST.Field> field in fieldSelection)
        {
            if (intMatchedWhiteListFields != intAllFields || boolUnEvenCount)
            {
                return boolUnEvenCount = true;
            }

            int intChildrenCount = field.Value.SelectionSet.Selections.Count();
            if (intChildrenCount > 0)
            {
                boolUnEvenCount = CheckGraphQLQueryFieldPermission(field.Value.GetSelectedFields().ToList(), boolUnEvenCount, fieldWhiteList);
            }   

        }
        return false;
    }

Mit meinen Tests hat das soweit auch ganz gut funktioniert.
Da bleibt nur die Frage ob man die Listen vielleicht in der Datenbank/Json speichert oder vielleicht die ganzen Liste anhand von Klassen aufbaut bzw. DatenbanModels.

Möglichkeit zwei:
In der Dokumentation gibt es den Abschnitt Authorization (Authorization), bei der an den Feldern in den Klassen festgelegt wird, welche Gruppe, welche Felder abfragen darf. Dort würde einem aber dann im Falle wie mit den beiden unterschiedlichen UserGruppen, die Daten aus der eigentlichen Query fehlen, wie z.B. die mitgesendete ID, um zu überprüfen ob der User gerade seine eigenen Daten (gesendete ID mit der ID aus dem JWToken überprüfen) haben möchte, oder die eines anderen Nutzers.

Zudem scheint das ganze immer mehr unübersichtlicher zu werden, desto mehr Ausnahmefälle hat.

Möglichkeit drei:
Das dritte und letzte, wäre das man in der Pipe einfach mit sehr vielen Case's oder If's arbeitet, ohne irgendwelche Listen etc.

Das finde ich aber mit am schlechtesten und Übersicht ist damit ja auch so gut wie keine gegeben, dafür allerdings sehr flexible.

Ehrlich gesagt bin ich mit sowas sehr unerfahren und weiß nicht was davon vielleicht am besten wäre. Mir persönlich gefällte die WhiteList Sache aber ich weiß nicht wo ich die Speichern soll, bzw. müsste man dann auch ein paar Liste erstellen (was ich nicht schlimm fände. Damit wäre man pro Gruppe aber immer sehr flexible.

Ich habe kein Problem mit mehr Arbeit oder allgemein Arbeit die zu leisten ist. Da ich aber so unerfahren bin weiß ich einfach nicht mit welcher Möglichkeit ich an die Sache ran gehen soll.

16.806 Beiträge seit 2008
vor 4 Jahren

Generell: von den Gruppen geht man eigentlich weg zu Claims.

Claims besagen auf Basis von Policies, was gemacht werden darf, und was nicht.
Beispiel: users:read users:create
Ist eine modernere, granularere Art und Weise like Roles.

Was Du da ingesamt beschreibst ist Resource Based Authorization:
> Ein User darf nur sein eigenes Objekt bearbeiten, aber nicht das eines anderen Users.

Der Link zu GraphQL:
Ich kannte das so nicht - und neige auch dazu zu sagen, dass ich es für keine sooo gute Idee halte die Authorisierung auf diese Art und Weise zu lösen.
Authorization sollte ein Single Point sein - und Teil der Logik.

O
Olii Themenstarter:in
76 Beiträge seit 2017
vor 4 Jahren

Authorization sollte ein Single Point sein - und Teil der Logik.

Ich beschäftige mich immer noch mit dem Thema aber habe mal eine Frage zu einer Aussage die ich hier zitiert habe.

Mit Singel Point meinst du ja wahrscheinlich sowas wie, das alle Zugrifskontrollen z.B. in der Pipeline von MediatR durchgeführt werden. Aber in der Pipeline können garnicht alle Szenarien gehandelt werden.

Dazu mal ein Beispiel:

Um User Daten zu erhalten gibt es Das Repository mit der Funktion getUserAccount.

Nun könnte es ja drei Szenarien geben:

  1. User möchte seine eignene AccountDaten (also mit privaten Daten)
  2. User möchte Daten eines anderen Accounts (nur öffentliche Daten)
  3. Der Admin möchte egal von welchem User, immer die Daten einsehen können (bis zu einem gewissen Grad)

Jetzt könnte man natürliche das ganze auf verschiedene Arten implementieren:

Man könnte im Repository für alle drei Fälle eine eigene Funktion machen und diese dann im Mediater Handler je nach User Argument/User Role aufrufen.

Man könnte aber auch immer nur die Funktion getUserAccount verwenden aber in dieser Funktion prüfen welche Daten der User gerade abfragen darf.

Aber in beiden Fällen wäre das doch kein Singel Point, da die Prüfungen in verschiedenen Methoden bzw. Klassen durchgeführt werden müssen. Also Jede Entität hat ja meist auch ein eigenes Repository, in dem dann auch wieder irgendwelche Ausnahmen programmiert werden müssen.

Die Lösung mit den Handlern wäre aber auch kein Singelpoint, da es ja mehr als nur einen Handler gibt.

Und in der Pipeline die ganzen Überprüfungen zu haben, funktioniert nicht, da man in dieser ja nur die generischen Typen zur Verfügung hat und von dort aus ja auch keine Repositorys aufrufen kann (das wird ja dann in den Handlern selbst gemacht). Also ich meine quasi das in der Pipeline sowas gemacht wird:

Wenn UserRole = Admin Dann RepositoryUser.getUserAccount

sowas würde ja nicht funktionieren.

Oder meintest du mit Singel point etwas anderes bzw. weiter gefächertest? Sowas wie in etwa, ein Singel point können auch alle Handler zusammen sein. Quasi der Bereich in dem die Prüfungen stattfinden. Ich weiß gerade nicht wie ich es besser erklären soll 😄

Ich hoffe es ist verständlich was ich damit meine.
Im Handler könnte man je nach User Arguments und User Role

16.806 Beiträge seit 2008
vor 4 Jahren

Um User Daten zu erhalten gibt es Das Repository mit der Funktion getUserAccount.

Methode. Funktionen sind in C# was anderes.

Nun könnte es ja drei Szenarien geben:

Der Single Point wäre die Schicht; als die Verantwortlichkeit - nicht eine Stelle an Code.
Vermisch das mal nicht mit Repositories oder Handlern oder was auch immer. Die Art und Weise ist völlig egal an dieser Stelle.

Aber bezogen auf MediatR: Natürlich gibt es Queries und Commands, die keinerlei Berechtigungslevel kennen.
Für User-Abfragen versuche ich aber immer die Identität des Users mitzugeben, aus der die Sicht geladen werden soll.
Ob ich dies aber an ein Handler oder an ein Service oder an ein Repository mitgebe: völlig egal.

Es geht nur darum, dass ich versuche, dass die Authorisierung immer Teil der Logik ist - nicht Teil der UI Technologie. Das ist auch der empfohlene Weg.
Natürlich muss die UI für gewisse funktionale Dinge die Authorisierung ausführen - aber die Definition kommt aus der Logik.

Beispiel: die Hauptansicht dieses Forums (https://mycsharp.de/wbb2/forum.php) hat verschiedene Views.
View von Nicht-angemeldeten Benutzern, von angemeldeten Benutzern, von Nutzern mit bestimmtem Access Level.
Der User A sieht also evtl. mehr als User B aber weniger als User C.

Das kann man alles in einem Handler prüfen, in dem man dem Query den User mitgibt und darauf die Rückgabe basiert.

Wenn man aber jetzt in der UI nur einen Button bei bestimmten Usern anzeigen will, dann kann das die Logik natürlich nicht übernehmen, weil die Logik die UI aufgrund der Schichtentrennung [Artikel] Drei-Schichten-Architektur ) nicht kennen darf.
Die UI muss also gewisse Dinge selbst übernehmen, damit die UI an gewissen Stellen basierend auf der Authorisierung gewisse Dinge Anzeigen kann oder nicht.

Daher nochmal der Hinweis:
Authorisierung ist was absolut Anwendungsspezifisches.
Nur weil es generelle Lösungen für viele Dinge gibt heisst das nicht, dass alles auf Deine Anwendung passt.
Es kann sein - und die Wahrscheinlichkeit ist sehr hoch - dass Du gewisse Dinge selbst programmieren musst.