Stefan Zörner
29.03.2019
Micro Moves, Bauteil 7. Spieler Registrieren und Single-Sign on.
Mittlerweile haben wir mit den Folgen der Micro-Moves-Serie schon einiges an Funktionalität zusammen. Benutzer von FLEXess können Partien starten und im Browser gegeneinander und gegen einen Computer-Gegner (seit Folge 6) antreten. Das Thema Sicherheit haben wir bisher allerdings komplett ausgespart. Wir ändern das in dieser Folge.
Aktuell können anonyme Benutzer über den games-Service (siehe Blog-Folge Bauteil 1) Partien mit zwei beliebigen Spielernamen (Texteingabe) einleiten. Anschließend können sie über die play-Oberfläche (siehe Blog-Folge Bauteil 3) Züge absetzen. Mittlerweile prüft der rules-Service (siehe Blogfolge Bauteil 5) zwar, ob der Zug regelkonform ist. Wer da am anderen Ende des Browsers sitzt spielte aber keine Rolle bisher. Jeder Benutzer kann bei jeder Partie mitziehen, nicht nur in seinen eigenen.
Mit dieser Folge führen wir einen neuen Service ein: players. Er ist in Python implementiert und ermöglicht es Benutzern, sich als Spieler zu registrieren und am System anzumelden. Die Web-Oberfläche hierzu bauen wir mit Flask (analog zu Bauteil 2 chess-diagrams, aber mit HTML-basierter Oberfläche).
Bei erfolgreicher Anmeldung stellt players ein JWT-Token (siehe unten) aus, mit dem sich der Benutzer auch bei anderen Services authentifizieren kann. Hier ist insbesondere games zu nennen, das ab dieser Folge unter anderem sicherstellt, dass nur angemeldete Benutzer, die mit von der Partie sind, tatsächlich ziehen können.
Eine Warnung: Unsere Implementierung ist eine einfache Lösung zu Illustrationszwecken. In echten Microservices-Vorhaben ist der Einsatz von JWT zwar ebenfalls üblich, nicht aber das Selberstricken der Authentifizierung wie hier in “players”. Ich diskutiere am Ende gängige Alternativen hierzu.
JSON Web Token, kurz JWT (ausgesprochen: “jot”), ist ein Standard für die sichere Übermittlung von Behauptungen (engl. Claims) bei gleichzeitig geringem Platzbedarf (also Ressourcen-Verbrauch). JWT genießt breite Unterstützung durch (Web-)Frameworks und Bibliotheken in allen nur erdenklichen Programmiersprachen. Unter anderem auch in den in Micro Moves bisher verwéndeten: Java, Python und JavaScript.
Ein JWT-Token sieht so aus (die Zeilenumbrüche gehören nicht zum Token):
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJwcGFudGhlciIsIm5hbWUiOiJQYXVsIFBhbnRoZXIiLCJyb2xlcyI6InVzZXIifQ.
cM93rF0CUXk5PxH8elR0paJ4PFqtbr0RbcFNFBocsmQ
Auf den ersten Blick also wüst, tatsächlich aber lediglich drei mit Base64-kodierte Bestandteile, mit Punkten voneinander getrennt:
Die Seite jwt.io stellt ein schönes Online-Werkzeug zum Token-Kodieren und -dekodieren zur Verfügung. Kopiert man das obige Token in diese Seite (siehe auch Screenshot unten), kann man Header und Payload lesen.
Header: {
"alg": "HS256",
"typ": "JWT"
}
Payload: {
"sub": "ppanther",
"name": "Paul Panther",
"roles": "user"
}
Das Token ist nicht verschlüsselt, sondern lediglich mit dem Verfahren HMAC / SHA-256 (kurz HS256, Angabe im Header) digital signiert (der dritte Teil des Tokens). Hierbei handelt es sich um ein symmetrisches Verfahren. Derselbe Schlüssel wird server-seitig zum Signieren benutzt und auch zum Überprüfen der Signatur (ebenfalls server-seitig, ggf. durch andere Module, der Client kriegt den Schlüssel nicht zu sehen). Auch dieses Überprüfen kann der Debugger auf jwt.io zeigen. Einfach den Schlüssel bei “Verify Token” eingeben. In unserem Beispiel lautete er “secret123”.
Die Payload sind die eigentlichen Daten innerhalb des Tokens, deren “Echtheit” die Signatur sicherstellt. Konkret handelt es sich hier um den Anmeldenamen (sub, kurz für subject), den Namen eines Benutzer und seine Rolle(n). Bei uns gibt es die Rollen user, admin und engine.
Wie stellen wir nun solche Token aus? Wie übermittelt ein Client das Token an andere Services? Und wie überprüfen diese die digitale Signatur? Schauen wir es uns der Reihe nach an!
Der Spieler ist als Konzept bisher nicht präsent. Spielernamen tauchen in Form von Zeichenketten im games-Subsystem auf, das wars. Unser neuer Service players ist eine simple Verwaltung von Spielern in Python. Der Quelltext von players ist wie immer auf GitHub verfügbar (unter modules/players). Über Webseiten ermöglicht players …
Beim letzten Punkt kommt JWT zum Zuge, um die Information, dass ein Spieler sich erfolgreich am System angemeldet hat, inkl. weiterer Claims sicher an andere Teile des Systems (allen voran games) zu tragen.
Technologisch ähnelt players dem Python-Service chess-diagrams aus Folge 2 dieser Blog-Serie. Es kommen ebenfalls Flask als Web-Framework und gunicorn als Web-Server zum Einsatz. Mit Jinja2 kommt eine Template-Engine hinzu und mit TinyDB eine einfache Persistenzlösung. Für das für JWT nötige Kodieren und Dekodieren nutzt players die Python Bibliothek PyJWT.
players beinhaltet folgende Seiten und Funktionalität.
Seite (URL) | Funktionalität |
/ | Startseite. Listet alle Spieler in einer Tabelle auf. |
/register | Formularbasierte Registrierung eines neuen Spielers. |
/profile_userid | Profilseite für den Spieler mit dem Benutzernamen _userid_. |
/login | Anmeldung durch Benutzername und Kennwort. |
/logoff | Abmelden des angemeldeten Benutzers. |
/about | Über das Modul players. |
Für die Seiten sind unter den URLs Funktionen für Flask im Python-File players.py hinterlegt, die auf entsprechende HTTP-Requests reagieren. Sie delegieren für die Darstellung der resultierenden (einfach gehaltenen) Seiten auf mit Jinja2 realisierte HTML-Templates.
Der neue Service lässt sich separat starten (z.B. via Kommando python3 player.py, Tests analog zu chess-diagrams via tox), ich hab ihm auch wieder ein Dockerfile spendiert und ihn in die Konfiguration von docker-compose mit aufgenommen. Die Seiten sind unter dem Pfad _/players/ im Reverse-Proxy _eingeklinkt, d.h. _http://localhost:9000/players/about _liefert in der Konfiguration und bei lokalem Start die About-Seite zum Modul.
players macht sich das Speicherns der registrierten Benutzer und ihrer Rollen sehr einfach und nutzt dazu TinyDB. Da Micro Moves in erster Linie Konzepte erläutert reicht das zur Illustration. Zu Alternativen und ernstzunehmenden Lösungsansätzen weiter unten.
Konsequenterweise – den Spielzeugcharakter mit TinyDb im Blick – speichert players keine Kennwörter ab. Auch nicht verschlüsselt oder als Hash. Es erhebt nicht einmal Kennwörter. Die Authentifizierung simulieren wir durch Überprüfen, ob der Benutzer als Kennwort überhaupt etwas eingibt. Dieses “Sicherheitskonzept” ist natürlich nicht viel besser als eine Admin-Party (alle Partygäste haben Admin-Rechte), wie wir sie vor dieser Folge hatten. Aber zur Illustration reicht es uns hier. – Liebe Kinder, falls Ihr das hier seht: Probiert das nicht zuhause an Eurem Produktionssystem aus!! ;-)
Im Falle der erfolgreichen Anmeldung stellt das players-Modul ein JWT-Token aus und sendet es als HTTP-Cookie an den Browser. Da die Micro Moves Module hinter einem gemeinsamen Reverse Proxy stehen, teilen sie sich für den Client von außen gesehen Hostname und Port. Das Cookie wird daher vom Browser bei Aufrufen – und das ist entscheidend – an alle beteiligte Module gesendet, insbesondere auch an games.
Das Erzeugen (“Kodieren”) und Auslesen (“Dekodieren”) des JWT-Tokens ist in players in web_token.py implementiert. Die Herausforderung dabei ist die digitale Signatur (Signieren beim Ausstellen, Überprüfen beim “Wiederkommen”). Die eigentliche Arbeit übernimmt die Python-Bibliothek PyJWT.
Für das hier gewählte Signierverfahren HS256 benötigen wir einen geheimen Schlüssel (“Shared Secret”). Wir geben ihn der Einfachheit halber von außen über eine Umgebungsvariable in den Docker-Prozess. Das Ganze lässt sich mit docker-compose recht einfach konfigurieren. Hier ein Ausschnitt aus der Datei docker-compose.yml
...
players:
build: ./players/
environment:
- JWT_COOKIE_NAME
- JWT_SECRET
...
Die Werte der beiden Variablen sind in der Konfigurationsdatei .env abgelegt. Unter dem Cookie-Namen legt players das JWT-Token im HTTP-Response ab und sendet es an den Browser zurück. Umgekehrt versucht players mit diesem Namen den JWT-Token als Cookie-Wert aus einem HTTP-Request auszulesen.
In Abhängigkeit vom empfangenen Cookie ändert sich der rechte Teil der Navigation. Falls keines da ist, stehen Links zum Registrieren und Anmelden zur Verfügung. Ansonsten ein Dropdown für Benutzerprofil und Abmelden (siehe Abbildung unten). Name und User ID des Benutzers werden dazu aus dem JWT-Token entnommen.
Der Konfiguationsparameter JWT_SECRET ist das “shared secret” für die digitale Signatur. Wer dieses hat, kann Signaturen aus players überprüfen (siehe z.B. unten im games-Modul). Ein Angreifer könnte damit auch selbst “gültige” JWT Tokens ausstellen. Der Wert sollte daher nur vertrauenswürdigen Beteiligten zugänglich gemacht werden.
Um dieses Sicherheitsrisiko zu umgehen werden statt HS256 gerne asymmetrische Verfahren mit einem öffentlichen Schlüssel zum Überprüfen (erhalten alle) und einem geheimen Schlüssel zum Signieren (erhält nur der Aussteller) gewählt. Mehr dazu etwa im schönen Beitrag “The Importance of Using Strong Keys in Signing JWTs”.
Das games-Modul interessiert sich sehr für den Benutzer hinter einem Request. Nur ein angemeldeter Benutzer sollte …
Für letzteres sollte er zugleich nicht nur Spieler in der zugehörigen Partie sein, sondern auch am Zug.
Gerade das letzte Beispiel zeigt, das die Autorisierung, d.h. die Entscheidung über die Berechtigung, im Modul games gut aufgehoben ist. players kann die Authentifizierung klären, und relativ statische Rollenzugehörigkeiten liefern. Ob der Benutzer irgendwo am Zug ist weiß es nicht.
Anonyme Benutzer lassen wir Partien auflisten und anzeigen, auch das zusehen von Partien anderer Spieler lassen wir zu. Weiterhin ist es angemeldeten Benutzern erlaubt, gegen sich selbst zu spielen. Das aber eher aus didaktischen Gründen – es lässt sich so weiterhin leichter ein wenig herumspielen über das play UI. Administratoren (d.h. Benutzer mit Rolle admin) können den Computer-Spieler Stockfish (vgl. Bauteil 6) gegen sich selbst spielen lassen – “normale” Benutzer (also nur Rolle user) können nur selbst gegen Stockfish antreten.
Das Auslesen des JWT-Tokens ist in games als Servlet-Filter realisiert (Klasse JwtCookieFilter im Package org.flexess.games.user). Der Prozess erhält über Umgebungsvariablen den Namen des Cookies und das Shared Secret. Er “fischt” den Cookie, falls vorhanden, aus dem Request und verifiziert die Signatur des enthaltenen JWT-Tokens mit Hilfe des Shared Secrets. Hier kommt die JWT-Java-Bibliothek von Auth0 zum Einsatz. Im Erfolgsfall baut das Servlet ein User-Objekt mit User ID (z.B. ppanther), Namen (z.B. Paul Panther) und Rollen (z.B. user) und legt es im Request als Attribut ab.
Die Spring Web MVC-Controller können dann darauf zugreifen, und die oben beschriebenen Überprüfungen durchführen (Beispiel: Ist der Benutzer angemeldet? Ist er am Zug? …). Und sie können Werte automatisch setzen. Wenn zum Beispiel der angemeldete Benutzer einer Partie als 2. Spieler beitritt, kann games dessen User ID in die Partie-Entität eintragen – je nachdem als schwarz oder weiß. Und den Namen im Menu anzeigen und Links aufs Profil gehen auch …
Für das Spielen im Browser hatten wir in Bauteil 3 eine Single-Page-Application play mit Vue.js gebaut. Hier ist für das Berücksichtigen der neuen Funktionalität vergleichsweise wenig zu tun. Wenn ein Benutzer angemeldet ist, und in die SPA wechselt, sendet der Browser auch von dort unseren Cookie mit dem JWT-Token an die hinter dem Reverse Proxy liegenden Web-Applikationen.
Die Interaktion zwischen SPA und games erfolgt über REST – unser ServletFilter greift auch bei diesen Anfragen, fischt den Cookie aus dem HTTP-Request validiert das enthaltene User-Objekt gegen das Shared Secret und legt es als Attribut im Request-Objekt ab.
Die Überprüfung, ob der Benutzer, der einen Zug sendet, tatsächlich am Zug ist, erfolgt in der Java-Klasse GameRestController (Package org.flexess.games.rest) in games. Falls nein gibt es eine Fehlermeldung ( HTTP 403, Forbidden) zurück, die in play fachlich angezeigt wird, siehe Screenshot.
In der SPA play selbst habe ich auf Sicherheitsabfragen verzichtet. Das hätte zwar die Benutzbarkeit erhöht (Beispiel: Man darf nur Züge absenden, wenn man am Zug ist), allerdings könnte ich dann genau solche Überprüfungen nicht gut demonstrieren.
In play ist somit eigentlich gar nichts zu tun. Insbesondere brauchen wir die digitale Signatur des JWT-Tokens nicht verifizieren. Der Browser sendet es einfach unverändert zurück – games verifiziert es auf dem Server. Sollte ein Angreifer auf dem Client ein anderes Token unterschieben, sollte games das merken, da die Signatur nicht stimmt.
Das einzige, was wir in play tun, ist den Benutzer mit User ID und Namen für das Menu anzuzeigen. Da die SPA die ganze Browser-Seite belegt, müssen wir die Funktionalität doppeln (mehr zu diesem Kompromiss in der nächsten Folge dieser Blog-Serie). Dazu müssen wir das JWT-Token allerdings nur auslesen und dekodieren, nicht verifizieren. Hierzu brauchen wir den Schlüssel nicht. Insbesondere muss der Shared Key nicht in die SPA gelangen – was auch fatal wäre. Wegen des symmetrischen Verfahren wäre das Fälschen eines Anmelde-Tokens damit ein Leichtes.
Das Dekodieren eines JWT Tokens in play ist in JavaScript ziemlich leicht. Anders als beim digitalen Signieren in Python (players) und Überprüfen der Signatur in Java (games) und Python (players) kommen wir hier mit Bordmitteln, also ohne Fremdbibliothek durch. Siehe Funktionen getPayloadFromJWT und getUserFromJWT in der Datei players.js in play.
Die Benutzerdaten speichert players aktuell lokal über TinyDB in einer Datei ab. Das macht den Docker-Container zustandsbehaftet und damit nicht redundant betreibbar. Eine Persistenz-Lösung außerhalb des Prozesses wäre daher sinnvoll, naheliegend ein Verzeichnisdienst wie Active Directory oder OpenLDAP. Das sind auch seriöse Orte, um Kennwörter abzulegen, die Authentifizierung durchzuführen oder Gruppenzugehörigkeiten zu speichern. Vielleicht zeige ich das nochmal in einer späteren Folge dieser Blog-Serie, und erinnere mich an meine LDAP-Vergangenheit.
Eine andere Option wäre gleich eine Lösung, die Authentifizierung und Single Sign-on für Webapplikationen “hauptberuflich” implementiert, beispielsweise CAS oder Keycloak. Auch die Möglichkeit, sich via Google-Account etc. anzumelden, ist denkbar. Beim Betrieb in Cloud-Umgebungen oder Container-Orchestrierungen steht Authentifizierung oftmals als Dienst zur Verfügung und wäre auch eine Option.
In den Modulen rules und chess-diagrams habe ich das JWT-Token bisher nicht überprüft. Bei chess-diagrams ist es aktuell nicht auch nötig. Auch nicht angemeldete Benutzer können eine Partie verfolgen. rules ist aktuell von außen nicht erreichbar, bisher greift nur games darauf zu. Hier könnte man das Token bereits jetzt durchschleifen und in rules überprüfen. Den Einbau hole ich mach, wenn wir rules direkt von außen nutzen (kommt noch).
Unsere Schachplattform wächst und gedeiht – das Menu weist nun zwei Bereiche auf. Einen für die Partien (Games), einen für die Spieler (Players). Da die beiden Bereiche separate Webanwendungen sind, und in games auch noch die SPA play steckt, ist das Menu aktuell 3x implementiert. Inkl. der Logik für das Anzeigen des Benutzers bzw. des Login-Links. Das Hinzufügen eines weiteren Bereiches mit eigenem Menu macht das Anfassen an allen anderen Teilen erforderlich, um die Menüpunkt aufzunehmen.
Wenn Ihr mehr über JWT erfahren wollt lege Euch das schöne E-Book von Sebastián Peyrott ans Herz: JWT Handbook (Cover siehe rechts).
Aktuell wird die Startseite statisch vom Reverse Proxy geliefert. Im nächsten Beitrag in dieser Serie fügen wir mit der Homepage ein weiteres Modul mit UI-Anteil hinzu und diskutieren dabei auch den obigen Kompromiss im Menu (ein neuer Punkt muss in allen Modulen eingefügt werden) und Alternativen dazu.
Ach ja: Fragen und Anregungen sind natürlich jederzeit willkommen. Per Kommentar hier im Blog oder gerne auch per Mail direkt an mich …