Wäre es manchmal nicht verlockend, für einen Moment in jemandes Haut zu schlüpfen, um besser zu verstehen, wie sie/er die Welt sieht? Dieser Wunsch kommt auch immer wieder im Zusammenhang mit Software auf. Zum Beispiel, wenn ein Kunde den Kundendienst einer Tierhandlung anruft und meldet, er sei auf der Website eingeloggt, sehe aber keine Tiere mehr. Und die Mitarbeiterin sich auf der gleichen Webseite einloggt und einen ganzen Zoo von Tieren sieht und nicht versteht, was der Kunde hat. Gäbe es für die Mitarbeiterin eine Möglichkeit, per Mausklick auf die reduzierte Kundensicht zu wechseln, würde sie sehen, dass in dieser Darstellung tatsächlich keine Tiere mehr erscheinen: alle bereits verkauft – und deshalb nur noch für interne Mitarbeiter sichtbar.
Das Beispiel mag etwas an den (Hamster)haaren herbeigezogen sein. Eine kurzzeitige Beschränkung oder Filterung der eigenen Benutzerrechte aber, um eine Applikation temporär durch die Brille eines anderen Benutzers zu sehen, wäre oft von grossem Wert und würde zu mehr Verständnis für andere Anwendergruppen führen. Nicht zuletzt auch für Entwickler, die dann ihre Applikation mit verschiedenen Berechtigungen testen könnten, ohne unzählige Benutzerkonten anlegen und zwischen diesen hin- und herwechseln zu müssen.
Was braucht es, um so einen – sagen wir Autorisierungsfilter – umzusetzen? Im Folgenden soll eine mögliche Lösung skizziert werden am Beispiel einer Web-Applikation mit Angular-Frontend und ASP.NET-Core-Backend.
Lösungsskizze
Schauen wir uns an, wie unser Tierhandlungsbeispiel mit einem Autorisierungsfilter in groben Schritten ablaufen würde:
- Die Tierhandlungsmitarbeiterin loggt sich via einen Identity Provider in die Angular-Web-Applikation ein. Im JWT-Access-Token, der ihr dabei ausgestellt wird, sind (wie bisher) folgende Rollen enthalten:
"role": [ "ShowAvailableAnimals", "ShowSoldAnimals", "CreateAnimals" ],
- Die Angular-Applikation ruft neu vom REST-Backend zusätzlich die verfügbaren Autorisierungsfilter ab. Sie erhält folgende Liste:
[ { "Id": "Customer", "FilteredUserRoles": [ "ShowAvailableAnimals" ]}, ... ]
- Die Mitarbeiterin möchte auf die Kundensicht wechseln und wählt deshalb im Menü der Angular-Applikation den Autorisierungsfilter mit der ID «Customer» aus.
- Die Angular-Applikation speichert in einer Service-Variable die aktivierte Autorisierungsfilter-ID und leitet zurück auf die Hauptseite. Die Berechtigungsprüfung im Angular-Routing (eine Implementation vom Angular-Typ CanActivateFn) stellt fortan sicher, dass die Mitarbeiterin nur noch jene Pfade aufrufen darf, die für Rollen zugänglich sind, die sowohl in ihrem JWT-Access-Token als auch im aktiven Autorisierungsfilter «Customer» enthalten sind. In diesem Fall ist das die Rolle «ShowAvailableAnimals».
Die Angular-Routing-Definition könnte in etwa folgendermassen aussehen:const routes: Routes = [ { path: 'animals', component: AnimalsComponent, // Lambda-Ausdruck vom Typ CanActivateFn prüft, dass die // Rolle ShowAvailableAnimals gleichzeitig im JWT-Access-Token // enthalten ist UND im aktiven Autorisierungsfilter canActivate: [authorizedToShowAvailableAnimalsFn]}, }, ... ];
- Für den Aufbau der Übersichtsseite der Tiere setzt die Angular-Komponente nun zwei REST-Aufrufe ans Backend ab:
- GET /api/AvailableAnimals
- GET /api/SoldAnimals (die Angular-Applikation könnte diesen Aufruf auch direkt unterbinden, wenn sie weiss, dass er nicht erlaubt ist für die Rollen des Filters «Customer»)
- Bevor die Angular-Applikation die beiden REST-Anfragen abschickt, fügt eine registrierte Implementation des Angular-Interfaces HttpInterceptor automatisch zwei Werte in den Header des http-Requests ein. Erstens den JWT-Access-Token der Benutzerin. Und zweitens den Namen des aktiven Autorisierungsfilters (via ein selbst definiertes http-Header-Attribut X-Authorization-Filter). Der http-Header der REST-Anfragen sieht danach folgendermassen aus:
Authorization: Bearer eYJhb… X-Authorization-Filter: Customer ...
- Im Backend (ASP.NET Core) sind sämtliche REST-Methoden durch ein neu entwickeltes [AuthorizeWithRoleFilter]-Attribut gesichert. Dieses implementiert das ASP.NET-Core-Interface IAsyncAuthorizationFilter. Das Attribut [AuthorizeWithRoleFilter] berücksichtigt (im Gegensatz zum gewöhnlichen [Authorize]-Attribute) nur jene Rollen, die gleichzeitig dem Access-Token und dem im http-Header angegebenen X-Authorization-Filter zugeordnet sind. Das führt bei den beiden Aufrufen zu folgendem Ergebnis:
- [AuthorizeWithRoleFilter(Roles=“ShowAvailableAnimals“)]
GET /api/AvailableAnimals
Status-Code 200 (OK) – Liste der verfügbaren Tiere wird zurückgeliefert - [AuthorizeWithRoleFilter(Roles=“ShowSoldAnimals“)]
GET /api/SoldAnimals
Status-Code 401 (Unauthorized)
- [AuthorizeWithRoleFilter(Roles=“ShowAvailableAnimals“)]
- In der Angular-Applikation sieht die Tierhandlungsmitarbeiterin nun nur noch die verfügbaren Tiere, die auch der Kunde sehen würde. Die verkauften Tiere sind verschwunden.
Bis sie den Autorisierungsfilter wieder deaktiviert.
Wichtig bei der Umsetzung ist, dass immer nur die Schnittmenge der Rollen aus dem Access-Token und der Rollen, die dem aktiven Autorisierungsfilter zugeordnet sind, berücksichtigt werden darf. Es darf auf keinen Fall passieren, dass ein Benutzer durch einen Autorisierungsfilter implizit zusätzliche Rollen erhält, die nicht in seinem Access-Token enthalten sind. Andernfalls könnte sich ein Angreifer durch Setzen eines Autorisierungsfilters im http-Header sehr einfach Zugriff auf geschützte Daten verschaffen.
Fazit
Die Umsetzung eines Autorisierungsfilters erzeugt initial einen gewissen Aufwand, sowohl im Frontend, als auch im Backend. Dazu sind hauptsächlich ein paar Interfaces bzw. Lambdatypen zu implementieren, die danach aber immer wieder verwendet werden können. Ist diese Infrastruktur einmal implementiert, können Benutzer mit erweiterten Rechten die Applikation sehr bequem auf eine Benutzersicht mit limitierten Rechten umstellen, um Probleme nachzuvollziehen. Auch beim Testen ist eine solche Funktionalität sehr hilfreich. Besteht später der Bedarf, die Applikation weiterzuentwickeln, bleibt der Zusatzaufwand minimal.