Single Sign-On und OAuth2 mit Java (PDF)

Autor:
Steffen Jacobs
Orientation in Objects GmbH
Steffen Jacobs
Steffen Jacobs
Datum:Juli 2019

Single Sign-On und OAuth2 mit Java - Ein Tutorial

Single Sign-On (SSO) und OAuth2 sind zwei Stichwörter, die oft in einem Atemzug genannt werden. Dabei werden hierbei zwei völlig unterschiedliche Konzepte beschrieben.

Beim Single Sign-On geht es darum, dem Benutzer die mehrfache manuelle Anmeldung mit Benutzername/Passwort zu erleichtern, indem mit nur einem Login alle Dienste verwendet werden können. Dagegen steht bei OAuth2 die Einschränkung von Rechten einer fremden Anwendung auf unterschiedliche Benutzerdaten eines Benutzers im Vordergrund. So kann ein Twitter-Benutzer etwa entscheiden, ob eine fremde Anwendung nur Rechte auf seinen Twitternamen erhält, oder zusätzlich auch auf seine Emailadresse oder sogar auf alle Tweets.

In diesem Artikel soll es um die Konzepte hinter SSO und OAuth2 gehen und wie diese beiden Technologien kombiniert werden können. Dazu gibt es in diesem Artikel neben einem Twitter-Use-Case auch eine einfache Anwendung mit Spring Boot und Spring Security OAuth, um SSO am Beispiel des Social Login via GitHub oder Facebook zu veranschaulichen.

Überblick

Dieser Artikel gliedert sich in die folgenden Teile:

Tabelle 1: Kapitelübersicht

Single Sign-On - Eine Einführung

Viele Benutzer verwenden täglich eine Menge unterschiedlicher Systeme. Bei jedem dieser Systeme müssen sich die Benutzer separat anmelden, im Optimalfall mit unterschiedlichen Passwörtern. Daher muss man sich als User eine große Menge an Zugangsdaten für all diese Systeme merken. Dies führt dann bei der Mehrzahl der Nutzer dazu, dass sie ständig eines der Passwörter vergessen. Als Resultat wird der Workload am Service Desk durch all die “Passwort-vergessen”-Anfragen signifikant erhöht. Wer sich die vielen verschiedenen Passwörter nicht merken möchte, verwendet überall ein ähnliches oder sogar das gleiche Passwort. Darunter leidet dann die allgemeine Sicherheit der Benutzerkonten.

Eine viel bessere Alternative bietet hier Single Sign-On (SSO). Dabei muss ein Nutzer sich im Optimalfall nur noch einmal anmelden und kann dann auf alle seine Systeme und Services zugreifen.

In diesem Kapitel soll eine Einführung in die Konzepte hinter Single Sign-On gegeben werden. Nach einer allgemeinen Einführung folgt eine kurze Diskussion der Vor- und Nachteile, sowie eine Zusammenfassung der wichtigsten konzeptionellen Ansätze. Dazu treten wir zunächst einmal einen Schritt zurück und betrachten das Authentifizierungsproblem etwas abstrakter. Worum geht es bei der Authentifizierung eigentlich?

Identitäten und Vor-/Nachteile von SSO

Im Prinzip hat jeder Mensch ja eine physische Identität. Im Internet wird diese dann über mehrere logische Identitäten abgebildet, welche dann auf die Services gemappt werden. Dies ist in der folgenden Abbildung visualisiert:

SSO: Physische und Logische Identitäten

Abbildung 1: SSO: Physische und Logische Identitäten

Die physische Identität von Max Mustermann lässt sich beispielsweise auf seine logischen Identitäten auf Facebook, Twitter und LinkedIn übertragen. Hierbei muss jeder Anbieter (Facebook, Twitter, etc.) eine logische Identität von Max Mustermann redundant aufbewahren, um diesen dann bei der Verwendung des Dienstes identifizieren zu können.

Abstrakt betrachtet geschieht das Mapping von der physischen Identität Max Mustermanns auf seine logischen Identitäten (sein Twitter Account, etc.) über einen Authentifizierungsprozess. Dieser kann beispielsweise die Eingabe einer Emailadresse und eines Passwortes beinhalten. Durch diese Kombination kann der Benutzer dann eindeutig identifiziert werden und seiner logischen Identität zugeordnet werden. Diesen Authentifizierungsprozess für jeden einzelnen Dienst durchzuführen, ist zeitaufwändig, möglicherweise unsicher und unterscheidet sich von Dienst zu Dienst.

Für dieses Problem gibt es das Konzept des Single Sign-On. Einmal anmelden, auf alles zugreifen. Das hat einige Vorteile. So kann beispielsweise Zeit beim Login-Prozess gespart werden, da dieser für jeden Dienst redundante Prozess wegfällt. Auch ist in der Theorie die allgemeine Sicherheit höher, da Identifizierungsdetails, wie etwa ein Passwort, nur einmal abgefragt und übertragen werden. Außerdem ist die Stelle, an der die Authentifizierung stattfindet, bekannt und kann besonders abgesichert werden. Wenn doch mal eine Identität entwendet oder von einem unauthorisierten Benutzer verwendet wird, müssen die Authentifizierungsdetails nur an einer zentralen Stelle einmalig geändert werden, um alle Dienste für den betroffenen Nutzer wieder sicher zu machen. Der Nutzer muss dann nicht etwa an zwanzig Stellen sein Passwort ändern.

Dies kann allerdings gleichzeitig auch zu einem Nachteil werden. Wenn eine Identität einmal gestohlen ist, kann der Angreifer direkt auf sämtliche verbundene Services zugreifen und damit weitaus mehr Schaden anrichten, als wenn nur ein isolierter Dienst kompromittiert wurde. Auch kann es, je nach Implementierung des SSO, zu einem Single-Point-of-Failure im System kommen. Somit ließe sich beim Ausfall des hochkritischen Authentifizierungssystems plötzlich keiner der Services mehr verwenden.

Dies kann außerdem relevant werden, sobald SSO-Lösungen auf externe Server angewiesen sind, etwa bei einem Social-Login via Google-Account oder Facebook-Account, so müssen diese Server vom lokalen Netz aus auch verfügbar sein. Dies kann zu Problemen führen, wenn etwa ein Land die Google-Server blockiert oder an einer Schule die Facebook-Nutzung technisch unterbunden wurde. Hier könnten derartige SSO-Dienste dann nicht mehr verwendet werden.

Insbesondere in Bezug auf den Social-Login mit Facebook drängt sich aus aktuellem Anlass auch gleich die Frage nach dem Datenschutz auf. Dieser kann natürlich zu einem Problem werden, da der Identity-Provider oder SSO-Server je nach Implementierung mitbekommt, welche Services ein User wann nutzt. Diese Daten könnten natürlich zweckentfremdet und etwa zu Werbezwecken eingesetzt werden.

All diese Vor- und Nachteile sind abhängig vom unterliegenden Ansatz und den verwendeten Technologien unterschiedlich stark ausgeprägt. Um diese unterschiedlichen Ansätze soll es jetzt gehen.

Unterschiedliche Ansätze zur Authentifizierung

Da es sich bei Single Sign-On eher um ein allgemeines Konzept handelt, gibt es einige unterschiedliche Ansätze zur Implementierung. Im Folgenden werden die wichtigsten und relevantesten Ansätze kurz erläutert. Konkrete Implementierungstechnologien, wie etwa SAML oder OAuth2 werden in einem folgenden Kapitel besprochen. Hier soll es erst einmal um die grundlegenden Konzepte gehen [SAML] [OAUTH].

Medienlösung

Bei der Medienlösung wird zur Authentifizierung ein elektronisches Token verwendet. Dieses kann beispielsweise ein RSA SecurId Hardware Token sein, welches einen Code auf einem Display anzeigt, der dann im System eingegeben wird. Alternativ kann es einen Hardwareschlüssel geben, etwa als dediziertes Gerät, welches beispielsweise über USB oder Bluetooth mit dem Computer verbunden wird. Außerdem gibt es inzwischen einige Smartphone-Apps, welche die gleiche Funktionalität beinhalten und einem das zusätzliche Gerät ersparen. [RSAT] [PGID]

Portalbasierte Lösung

Der nächste wichtige Ansatz ist die portalbasierte Lösung. Dazu die folgende Abbildung:

SSO: Portalbasierte Lösung

Abbildung 2: SSO: Portalbasierte Lösung

Wie in der Abbildung zu erkennen ist, wird beim portalbasierten Ansatz ein SSO-Server als Portal den einzelnen Services vorgeschaltet. In unserem Beispiel authentifiziert sich der Benutzer also beim SSO-Server. Die nachgeschalteten Services vertrauen dem SSO-Server. So kann der SSO-Server beispielsweise ein Session-Cookie oder ein Token an den Client zurückgeben oder ihm ein Zertifikat ausstellen. Mit diesem Cookie/Token/Zertifikat kann sich der User dann bei den einzelnen Services direkt einloggen. Alternativ kann ein Ticket ausgestellt werden, welches dann von den Diensten über den SSO-Server geprüft werden kann.

Ein klassisches Beispiel für ein stark integriertes portalbasiertes SSO ist Microsoft Passport. Ein recht weit verbreitetes Authentifizierungssystem basierend auf dem Ticket-System ist Kerberos. [MPASS] [KERB]

Circle of Trust

Beim Circle of Trust gibt es ein Netzwerk aus vertrauenswürdigen Diensten, die sich gegenseitig vertrauen. Diese Lösung wird auch "Ticketbasierte Lösung" genannt. Hierbei gibt es keinen zentralen SSO-Server, vielmehr kann jedes System als SSO-Server auftreten. Dies ist in der folgenden Abbildung visualisiert:

SSO: Circle of Trust

Abbildung 3: SSO: Circle of Trust

Sobald sich ein Benutzer bei einem der Systeme im “Circle of Trust” authentifiziert hat, erhält er ein Ticket von diesem System. Dieses Ticket kann dann zur Authentifizierung an allen anderen Systemen innerhalb des “Circle of Trust” verwendet werden. Die anderen Systeme “glauben” also, dass das System, welches das Ticket ausgestellt hat, die Identität des Benutzers korrekt festgestellt hat. Im obigen Beispiel authentifiziert sich der Benutzer beim “Atlassian Jira”-Service und erhält ein Ticket. Mit diesem Ticket kann er sich dann beim “Atlassian Bitbucket”-Service anmelden.

Ein Beispiel hierfür ist das Liberty Alliance Project. [LIBAL]

Lokale Lösung

Die lokale Lösung ist momentan besonders weit verbreitet, da hierbei keine zusätzlichen Server seitens des Anbieters benötigt werden. Bei diesem Ansatz wird die Authentifizierung direkt beim Client in einer lokal installierten Anwendung abgehandelt. Diese Anwendung trägt dann automatisch die korrekten Daten für den Benutzer in die Felder ein. Ein Beispiel hierfür sind Passwort-Manager Produkte, wie etwa KeePass [KPAS].

SSO: Passwort Manager

Abbildung 4: SSO: Passwort Manager

Wie in der Abbildung oben zu sehen ist, greift der Benutzer zunächst auf seinen lokal installierten Passwort-Manager zu. Dieser verwahrt alle Zugangsdaten für den Benutzer, meist geschützt durch etwa ein Master-Passwort. Der Passwort-Manager meldet den Benutzer dann direkt bei dem ausgewählten System an. Natürlich kann der Passwort-Manager auch in Kombination mit einem SSO-Server verwendet werden und sich für den Benutzer dort anmelden.

So viel erst mal zu Single Sign On. Nun zum nächsten Teil des Artikels: OAuth2.

OAuth2 im Überblick

Bei OAuth2 handelt es sich um ein weitverbreitetes Autorisierungsprotokoll, laut eigenen Angaben ein “Industry Standard”. Die neue Version 2 ist der Nachfolger der ersten, nun obsoleten Version von OAuth. Entwickelt wird OAuth von der gleichnamigen IETF OAuth Working Group. OAuth2 ist in RFC 6749 spezifiziert. [OAW] [RFC6]

OAuth2 soll den automatisierten Zugriff von Anwendungen, die weder dem Benutzer selbst noch dem Serviceanbieter, sondern stattdessen einer dritten Partei gehören, verbessern. Dazu soll der Zugriff auf HTTP-Services und -Ressourcen durch die dritte Partei mittels einer Bestätigung durch den Benutzer und einem feingranularen Berechtigungssystem vereinfacht werden.

Die Kernidee ist dabei, das übliche Loginverfahren mit Username/Passwort durch ein besseres System zu ersetzen, da es beim Username/Passwort-Ansatz einige Mängel gibt. So müssen etwa die Klartextpasswörter auf einem Server der dritten Partei gespeichert werden, da sie ja für jeden Login benötigt werden. Außerdem kann der Zugriff durch die dritte Partei nicht reguliert werden. Das Resultat ist in dieser Abbildung zu sehen:

OAuth2: Kapselung des Ressourcenzugriffs: Zugriff ohne Kapselung (oben) und gekapselter Zugriff (unten)

Abbildung 5: OAuth2: Kapselung des Ressourcenzugriffs: Zugriff ohne Kapselung (oben) und gekapselter Zugriff (unten)

Mit Username/Passwort erhält das Client-Gerät direkten Zugriff auf das gesamte Benutzerkonto und alle verbundenen Ressourcen (in der Abbildung oben). Es ist nicht möglich, der dritten Partei nur Zugang zu ausgewählten Teilen oder Ressourcen zu gewähren. Ein viel praktischer und sichererer Ansatz ist dagegen die Kapselung des Zugriffs auf einzelne Ressourcen, wie in der Abbildung unten vorgeschlagen. Hierbei würde die Anwendung der dritten Partei nur auf jene Ressourcen zugreifen können, deren Zugriff der Benutzer ihr explizit erlaubt hat.

Bei OAuth2 werden diese oben genantnen und für die Sicherheit eher problematischen Login-Credentials abstrahiert. Dazu gibt es mehrere Rollen, die von einem Dienst, Benutzer oder Server eingenommen werden können. Um das Zugriffsproblem zu lösen, gibt es außerdem ein Berechtigungssystem mit Einzelberechtigungen. Zusätzlich gibt es zwei grundlegende Client-Typen: Der Confidential Client und der Public Client. Aber zuerst einmal zurück zu den Rollen und dem Berechtigungssystem.

Rollen in OAuth2

Insgesamt gibt es die folgenden vier Rollen:

Tabelle 2: Rollen in OAuth2

In der Praxis laufen Authorization Server und Resource Server insbesondere bei kleineren Services häufig auf der gleichen Maschine oder sind sogar in der gleichen Binary enthalten.

Im nächsten Schritt geht es um die Funktion von Berechtigungen, die den Zugriff des Clients auf die Resourcen auf dem Resource Server einschränken. Diese Berechtigungen werden in der Regel vom Authorization Server verwaltet.

Feingranulare Berechtigungen

Mit OAuth2 ist es möglich, detaillierte Berechtigungen zu definieren, um Anwendungen feingranularen Zugriff auf bestimmte Funktionen und Daten der Benutzer zu geben. Damit kann der Umfang des Zugangs von Anwendungen zu den persönlichen Daten der Benutzer minimiert werden. Außerdem sieht der Benutzer detailliert, welche Anwendung auf welche seiner persönlichen Daten zugreifen kann.

Dieses Berechtigungssystem funktioniert über sogenannte Scopes. Ein Scope definiert dabei eine fixierte Menge an Berechtigungen. Beispielsweise könnte ein fiktiver Scope user:profile lauten. Dieser Scope könnte dann etwa den Zugriff auf alle Profildaten eines Benutzers ermöglichen. Auch ist damit ein Vererbungssystem umsetzbar. So könnte user:profile:email etwa in user:profile enhalten sein. Da im OAuth2-Standard jedoch nur ein einzelner String als Scope spezifiziert ist, ist dieses Berechtigungssystem anwendungsspezifisch. Ein Beispiel für ein derartiges Scope-System ist das System von GitHub. Die dazugehörige Dokumentation ist auf den Websites von GitHub Developers zu finden. [OAS]

Für ein effektives Berechtigungssystem wird außerdem weiteres Wissen zum Client und dessen sicherheitsrelevante Implikationen benötigt. Dabei gibt es die beiden Client-Typen Confidential Client und Public Client.

Confidential Client und Public Client

Die Anwendungen, welche über OAuth2 auf die Daten ihrer Benutzer zugreifen möchten, lassen sich allgemein in zwei Gruppen unterteilen.

Ein Confidential Client ist ein Clientprogramm, welches in der Lage ist, Geheimnisse für sich zu behalten. Dies bedeutet, dass der Quellcode und die verwendeten Ressourcen des Clients nicht öffentlich verfügbar sind und der Datenverkehr sich nicht ohne Weiteres beliebig abfangen lässt. Dem gegenüber steht der Public Client, welcher komplett öffentlich ist und keine Geheimnisse bewahren kann.

In OAuth2 erhält jeder Client zunächst einmal eine individuelle ClientId, um sich gegenüber dem Authorization Server identifizieren zu können. Beide Client-Typen enthalten diese ClientId. Zusätzlich erhält der Confidential Client noch ein geheimes Client-Secret, welches nur von Client und Authorization Server geteilt wird. Da der Public Client ja keine Geheimnisse bewahren kann, ergibt ein derartiges Secret hier keinen Sinn.

Eine Browser-Anwendung, welche komplett in JavaScript implementiert ist und im Browser läuft, ist ein Public Client. Hier ist nämlich der gesamte Quellcode inklusive eines möglicherweise integrierten kryptographischen Secrets von jedem lesbar.

Ein Beispiel für einen Confidential Client ist dagegen eine klassische Webserver-Backend-Anwendung. Hier sind Quellcode und verwendete Ressourcen (etwa eine Datei mit dem kryptographischen Secret) nicht öffentlich zugänglich und befinden sich auf einem abgesicherten Server.

Diese Unterscheidung zwischen Confidential Client und Public Client ist für OAuth2 relevant, da die beiden Typen unterschiedliche Use-Cases implizieren, für die unterschiedliche Workflows vorgesehen sind. Diese Workflows werden im Folgenden beschrieben.

Workflows

Wie oben beschrieben, gibt es für die beiden unterschiedlichen Client-Typen unterschiedliche Methoden, über welche die Authentifizierung via OAuth2 erfolgen kann. Diese Methoden werden auch Workflows genannt. In OAuth2 werden diese über unterschiedliche Authorization Grant Types, quasi Authorizierungstypen, abgebildet. So gibt es etwa den Authorization Code, die Password Authentication oder die Client Credentials.

Vor der Verwendung eines der Workflows muss der Authorization Server zunächst mit der Third-Party-Anwendung bekannt gemacht werden. Dabei weist der Authorization Server der Anwendung eine eindeutige ClientId zu und teilt mit ihr ein Client Secret, sofern es sich bei der Third-Party-Anwendung um einen Confidential Client handelt. Wie dieser Prozess in der Praxis funktioniert, ist unten im Abschnitt “Setup einer OAuth2-Anwendung” beschrieben. Zunächst einmal aber zu den Workflows.

Authorization Code

Beginnen wir mit dem Authorization Code. Bei dem Authorization-Code-Verfahren handelt es sich um einen zweistufigen Prozess, bei dem zunächst ein Authorization-Code erhalten wird, der dann im nächsten Schritt gegen ein Access Token getauscht wird. Hierbei werden die Zugangsdaten des Benutzers nicht benötigt, daher muss der Third-Party Dienst, der sich im Namen des Benutzer einloggen möchte, auch kein Klartextpasswort kennen und speichern. Der in der Praxis sehr häufig anzutreffende Authorization-Code-Workflow wird im Folgenden kurz beschrieben und anschließend erläutert. Der Authorization-Code-Workflow ist in der folgenden Abbildung visualisiert:

OAuth2 Authorization-Code Workflow

Abbildung 6: OAuth2 Authorization-Code Workflow

Zunächst wird eine Anfrage vom Client an den Server geschickt, welche den Scope (siehe Kapitel "Berechtigungen" oben) und die ClientId der Anwendung enthält. Der Authorization Server leitet den Client mit einem HTTP-Redirect weiter und gibt als Parameter den Authorization-Code mit. Dieser Authorization-Code kann anschließend in Kombination mit der ClientId und dem Client Secret gegen ein Access-Token umgetauscht werden. Mit diesem Access-Token kann schließlich der Dienst verwendet und Benutzerinformationen abgerufen und bearbeitet werden.

Bei der ersten Anfrage nach einem Authorization Code wird der betreffende Benutzer gefragt, ob er der anfragenden Anwendung die benötigten Berechtigungen erteilen möchte (siehe Abbildung "OAuth Notification" im Praxiskapitel weiter unten). Wenn der Benutzer die Anfrage bestätigt hat, speichert der Authorization Server die zugehörige ClientId in Bezug auf den aktuellen Benutzer. Er weiß also, welche Third Party Anwendung (ClientId) auf welche Benutzerprofile zugreifen darf. Beim nächsten Mal muss der Benutzer die Anfrage daher nicht erneut bestätigen. Durch die ClientId kann der Authorization Server dann die anfragende Anwendung mit den ihm bekannten Anwendungen abgleichen und verlangt im nächsten Schritt das Client Secret zur Authentifizierung der Anwendung. Es ist also sichergestellt, dass:

  • Der Client identifizierbar ist (über die ClientId)

  • Der Benutzer dem Client irgendwann einmal erlaubt hat, auf bestimmte Berechtigungen zuzugreifen (Eintrag auf dem Authorization Server mit ClientId und Benutzerkontext)

  • Der Client der ist, für den er sich ausgibt (vorher zwischen Authorization Server und Client-Anwendung geteiltes Client Secret)

Da diese Authorisierungsmethode via Authorization Code ja auf einem kryptographischen Secret basiert, setzt sie einen Confidential Client voraus. Für Public Clients kommt daher ein etwas abgewandelter Workflow zum Einsatz. Dabei wird auf den Versand des Client-Secrets im letzten Schritt verzichtet. Da dies zu möglichen Schwachstellen führen kann, da nun nicht mehr so einfach nachgewiesen werden kann, ob der Client der ist, für den er sich ausgibt, gibt es die Proof-Key-for-Code-Exchance-Extension (PKCE). Damit wird dann wieder der zweistufige Prozess verwendet, allerdings in abgewandelter Form. PKCE ist jedoch nicht ganz trivial und wird daher hier aus Platzgründen ausgeklammert. Kurz gesagt stellt PKCE sicher, dass der Client, der im zweiten Schritt das Token erhalten soll, der gleiche ist, welcher im ersten Schritt den Authorization Code angefragt hat. Hierdurch sollen Man-In-The-Middle-Attacken ausgeschlossen werden. Weitere Details zu PKCE können dem Blogartikel in den Referenzen entnommen werden. [PKCE] [PKCE2]

Password Authentication

Ein weiteres, relevantes Verfahren ist die Password Authentication. Wie der Name schon sagt, wird hierbei statt einem Authorization Code ein Passwort zum Abruf des Tokens verwendet. Dieses Verfahren hebelt allerdings das gesamte Konzept der Kapselung in Scopes und der minimalen Berechtigungen aus. Er existiert hauptsächlich aus Kompatibilitätsgründen und funktioniert wie folgt:

OAuth2 Passwort Workflow

Abbildung 7: OAuth2 Passwort Workflow

Dieser Workflow ist einfacher als der Authorization-Code-Workflow und enthält nur eine POST-Anfrage an /token. Hierbei wird neben der ClientId der Benutzername und das Passwort angegeben. Zurück kommt direkt ein Access-Token.

Das Password-Authentication-Verfahren eignet sich gut für Public Clients, da hierbei kein Secret gespeichert werden muss und der Benutzer einfach seinen Benutzernamen und das Passwort eingeben kann. Dieses Verfahren wird üblicherweise auch von den zugehörigen First-Party-Apps des Dienstes selbst verwendet. So könnte etwa eine offizielle Twitter-App diese Schnittstelle der Twitter-API verwenden. Theoretisch können auch hier die Berechtigungen auf die ClientId gescoped werden, was aber in der Praxis wenig Sinn ergibt. Schließlich könnte sich die Third-Party Anwendung auch gleich mit Benutzername und Passwort einloggen und die Scopes damit umgehen.

Client Credentials

Schließlich gibt es noch den Client Credentials Workflow, welcher für den Abruf von Meta-Informationen unabhängig vom Benutzer gedacht ist. Dazu gehören Daten, die keinem User sondern direkt der Anwendung gehören. Also Daten und Ressourcen, welche der Client selbst abgelegt hat und die keinem Benutzer gehören. Der Client Credentials Workflow ist nur für Confidential Clients vorgesehen und ist eine verkürzte Version des Authorization-Code-Workflows, nur eben ohne Authorization Code.

OAuth2 Client-Credentials Workflow

Abbildung 8: OAuth2 Client-Credentials Workflow

Wie aus dieser Abbildung hervorgeht, werden in diesem Workflow zunächst ClientId und Client Secret zusammen mit einem optionalen Scope an den Server gesendet, welcher gleich ein Access-Token zurückschickt. Der Umtausch eines Authorization Codes in ein solches Token entfällt damit. Natürlich kann mit diesem Token dann auch nur auf die dem Client selbst gehörenden Ressourcen zugegriffen werden, nicht etwa auf die Ressourcen eines Benutzers.

Nachdem wir nun also umfangreiche Kenntnisse zu den Grundlagen von Single Sign-On und OAuth2 erworben haben, möchten wir uns mal ansehen, wie OAuth2 in der Praxis funktioniert und welche Schritte für eine erfolgreiche Implementierung und den Betrieb nötig sind.

OAuth2 in der Praxis

Um via OAuth2 auf anwendungsspezifische Benutzerdaten zugreifen zu können, werden zwei Dinge benötigt: Eine OAuth2-App beim Anwendungsbetreiber (z.B. GitHub oder Twitter) und ein OAuth2 Client, welcher über diese App auf die Benutzerdaten zugreifen kann.

Setup einer OAuth2 Anwendung

Um eine OAuth2-Anwendung aufzusetzen, muss diese zunächst beim betreffenden Service (in diesem Beispiel GitHub [GITH]) registriert werden. Dabei wird neben einem Namen für die Anwendung und einer Website auch eine Callback-URL angegeben. Anschließend wird dann die öffentliche ClientId und ein privates Client Secret generiert. ClientId und Client Secret müssen der entsprechenden Client-Anwendung natürlich mitgeteilt werden, damit diese sich authentifizieren kann. Ein Beispiel, wie dieses Setup aussehen könnte, ist im folgenden, reduzierten Screenshot von GitHub zu sehen:

GitHub: Setup einer OAuth2-App

Abbildung 9: GitHub: Setup einer OAuth2-App

Auf GitHub gibt es Felder für den Anwendungsnamen, die Website, die Beschreibung, etc. Außerdem kann die Callback-URL im Feld ‘Authorization Callback URL’ angegeben werden. Diese ist die URL, an welche ein Client, beispielsweise während des Authorization-Code-Workflows, mit dem Authorization-Code weitergeleitet wird. Diese Callback-URL ist in unserem Fall zu Testzwecken auf https://localhost:8080/auth/code/ gesetzt.

Eine OAuth2 App für GitHub

GitHub unterstützt bei seiner OAuth2-Schnittstelle die Authentifizierungsmethode mit Authorization Code. Wie der Informationsaustausch bei der Verwendung dieser Methode im Fall von GitHub im Detail funktioniert, zeigt die folgende Grafik:

OAuth2 mit GitHub via Authorization Code

Abbildung 10: OAuth2 mit GitHub via Authorization Code

Im ersten Schritt wird eine GET-Anfrage vom Client an den Authorization Server von GitHub geschickt. Diese geht an die /authorize -URL. Für GitHub lautet diese https://github.com/login/oauth/authorize. Dieser GET-Aufruf enthält in unserem Bespiel aus der Abbildung einige Parameter. Der Parameter client_id enthält die während des Aufsetzens erhaltene, öffentliche ClientId der Anwendung aus dem vorherigen Schritt. Außerdem wird der Permission-Scope, in diesem Fall user:email der angeforderten Berechtigungen mitgesendet. Hier soll nur auf die Emailadresse des Benutzers zugegriffen werden. Dieser Scope kann anschließend vom Benutzer bestätigt werden. Zusätzlich gibt es noch einige optionale Parameter, auf die in diesem Beispiel verzichtet wird, da die GitHub-OAuth2-Schnittstelle für diese implizite Standardwerte vorhält, sofern sie nicht explizit spezifiziert werden. Dazu gehört etwa eine Redirect-URL, an welche der Browser anschließend weitergeleitet wird. Sie muss gleich der im Setup definierten Callback-URL sein oder diese erweitern. Auch könnte noch mittels response_type=code der Authorization-Code-Workflow ausgewählt werden. Die GitHub-API verwendet diesen Workflow allerdings standardmäßig, daher muss er nicht explizit angegeben werden. Des Weiteren kann ein zuvor generierter Zustand (beispielsweise state=test1337) als Parameter angehängt werden. Dieser dient dazu, die Transaktion später wieder zu identifizieren und zuordnen zu können. Dieser Zustand wird üblicherweise in einem Cookie gespeichert. In unserem einfachen Fall verzichten wir auf diesen Zustand.

Nachdem der GitHub Authorization Server die Anfrage an /authorize erhalten hat, muss der Benutzer die Berechtigungsanfrage bestätigen. Bei GitHub öffnet sich beispielsweise, sofern man bereits eingeloggt ist, ein Popup, welches in etwa so aussieht:

GitHub OAuth2 Notification

Abbildung 11: GitHub OAuth2 Notification

Sobald der Benutzer auf den grünen Button mit der Aufschrift ‘Authorize steffenjacobs’ geklickt hat, wird der Browser mit seinem Authorization Code an die beim Aufsetzen hinterlegte Callback-URL weitergeleitet. Wenn in der vorherigen GET-Anfrage eine Redirect-URL angegeben wurde, muss diese mit der beim Aufsetzen festgelegten Basis-URL (Callback-URL) übereinstimmen, darf aber Unterordner der angegebenen Basis-URL enthalten. Bei diesem HTTP-Redirect wird der Authorization Code dem Browser als URL-Parameter mitgegeben. Die Idee ist, die Redirect-URL so einzustellen, dass diese entweder gleich auf den Applicationserver des Anwendungsanbieters weiterleitet, oder auf eine lokale Adresse, auf welcher ein Server wartet, der den Authorization Code dann in Empfang nimmt.

Im nächsten Schritt wird nun der Authorization Code in ein Access-Token umgetauscht. Dazu wird eine POST-Anfrage an die /token -URL (bei GitHub /access_token -URL) des Dienstes geschickt. Diese POST-Anfrage enthält, wie aus der obigen Abbildung zu entnehmen ist, wieder die ClientId und außerdem den soeben erhaltenen Authorization Code. Zusätzlich wird das Client-Secret, welches beim Aufsetzen der Anwendung erstellt wurde, mitgeschickt. Zurück kommt vom Authorization Server ein Access-Token und je nach Implementierung eine Verfallszeit und der Umfang der Berechtigung (Scope, im Beispiel oben user:email ).

In unserem GitHub-Beispiel kommt die folgende Antwort zurück:

access_token=083e0adba93e78bde9d02292cd7ce60492679d53
&scope=user%3Aemail
&token_type=bearer

Beispiel 1: Antwort von GitHub

Es handelt sich also um ein Bearer-Token, welches auf alle API-Funktionalitäten zugreifen kann, für die die Berechtigung user:email nötig ist.

Alternativ kann bei der HTTP-POST-Anfrage auch im Header das Feld Accept auf application/json gesetzt werden. Dann kommt nach dem Aufruf der /token -Methode ein JSON-Objekt zurück:

{
    "access_token": "b194dc8c2dc6e26ff1d4847250e1b7ae87321863",
    "token_type": "bearer",
    "scope": "user:email"
}

Beispiel 2: Antwort von GitHub im JSON-Format

Dieses lässt sich von der eigenen Client-Anwendung möglicherweise einfacher verarbeiten.

Anschließend lässt sich mit Hilfe des Access-Tokens auf die API des Dienstes zugreifen. In diesem Beispiel kann nun neben dem öffentlichen Profil des Benutzers die Emailadresse abgerufen werden. Um zum Beispiel die öffentlichen Profildaten abzurufen, wird eine GET-Anfrage an den entsprechenden User-Endpunkt der GitHub-API geschickt, welche als URL-Parameter das Access-Token enthält:

https://api.github.com/user
  ?access_token=b194dc8c2dc6e26ff1d4847250e1b7ae87321863

Beispiel 3: Anfrage an /user -Endpunkt der GitHub-API

Zurück kommt ein größeres JSON-Objekt mit allen Profildaten.

{
    "login": "steffenjacobs",
    "id": xxxxxxx,
    ...
    "created_at": "2014-08-18T14:49:17Z",
    "updated_at": "2019-01-08T15:37:24Z"
}

Beispiel 4: Antwort der GitHub-API

Alternativ kann auch eine GET-Anfrage an denselben Endpunkt (https://api.github.com/user) geschickt werden und im HTTP-Header das Feld Authorization auf den Wert token b194dc8c2dc6e26ff1d4847250e1b7ae87321863 gesetzt werden. Zurück kommt das gleiche JSON-Objekt.

So viel zu GitHub. Um ein wenig Diversität hinein zu bringen, soll das gelernte im nächsten Schritt auf die Twitter-aPI angewendet werden, welche ganz ähnlich funktioniert. Dieses Mal soll die Authentifizierung vollautomatisch von einem Java-Programm durchgeführt werden.

Automatisches Versenden eines Tweets über die Twitter-API

Das beispielhafte Java-Programm in diesem Artikel soll sich automatisch bei Twitter authentifizieren und anschließend einen Tweet versenden können. Der Benutzer kann die Authentifizierungs-Anfrage bestätigen und den Authorization Code an das Programm weitergeben, welches diesen dann ein Access-Token umtauschen kann.

Abhängigkeiten

Da hier das Rad nicht neu erfunden werden soll, werden wir auf einige Bibliotheken zurückgreifen. Dazu gehören Apache Commons IO, Apache HTTP-Components und SignPost.

Apache Commons IO [APACI] wird verwendet, um mittels der IOUtils-Klasse den Inputstream, der als Antwort auf die Anfragen vom Webserver zurückkommt, in einen String zu aggregieren. Apache HTTP-Components [APAHC] wird für die Kommunikation über HTTP verwendet und Signpost [SIGNP] unterstützt allgemein den OAuth2-Prozess. SignPost soll dabei helfen, zunächst den Authorization-Code anzufordern, ihn in ein Access-Token umzutauschen und dieses dann an die API-Calls anzuhängen.

Ein erster Tweet

Zunächst versuchen wir einmal, überhaupt einen Tweet über die Twitter-API zu verschicken. Nachdem wir im Twitter Developer Dashboard eine OAuth2-App aufgesetzt haben (genau wie im letzten Kapitel bei GitHub), können wir uns dort auch gleich ein fertiges Access-Token mit einem Secret holen (siehe Abbildung unten).

Twitter-Java-Connector App im App-Dashboard von Twitter Developers

Abbildung 12: Twitter-Java-Connector App im App-Dashboard von Twitter Developers

Dieses Token mit Secret können wir dann für unseren ersten Test-Tweet verwenden. Der Java-Code dazu:

public static void tweet(String tweetText, String accessToken, 
  String accessTokenSecret) throws Exception { 
			
  // OAuthConsumer mit KEY und SECRET erstellen
  final OAuthConsumer oAuthConsumer = 
    new CommonsHttpOAuthConsumer(API_KEY, API_KEY_SECRET); 
			
  // Access-Token und Secret setzen
  oAuthConsumer.setTokenWithSecret(accessToken, accessTokenSecret); 
		
  // API-Call erstellen
  final HttpPost httpPost = new HttpPost(
    "https://api.twitter.com/1.1/statuses/update.json?status=" + tweetText); 
			
  // API-Call mit Access-Token versehen
  oAuthConsumer.sign(httpPost);
  try (final CloseableHttpClient 
    httpClient = HttpClientBuilder.create().build()) { 
		
  // API-Call ausführen
    final HttpResponse httpResponse = httpClient.execute(httpPost);
    System.out.println(IOUtils.toString(httpResponse.getEntity().getContent(), 
      Charset.forName("UTF-8")));
  }
}

Beispiel 5: tweet()-Methode zum Versenden eines Tweets

Die tweet -Methode erhält als Parameter insgesamt drei Textstrings. Der erste beinhaltet den Tweet, der zweite das Access-Token und der dritte das Secret zum Access-Token. Im ersten Block wird zunächst ein Objekt vom Typ OAuthConsumer erstellt, welches bei der Initialisierung den API-Key und das API-Secret (siehe Twitter-Developers-Screenshot oben) erhält. Dieser OAuthConsumer bekommt anschließend im zweiten Block das Access-Token mit Secret gesetzt, welches vorher an die Methode übergeben wurde. Danach wird ein Objekt vom Typ HttpPost erstellt, welches die POST-Anfrage an den Twitter-API-Endpunkt zum Tweeten enthält. Dieses Anfrageobjekt erhält dann vom OAuthConsumer das Token (vierter Block) und wird über einen HttpClient abgeschickt (letzter Block). Die Antwort kommt als Input-Stream und wird mit Hilfe der Klasse IOUtils zum Schluss in einen String transformiert. Der Methodenaufruf sieht wie folgt aus:

tweet("Test%20Tweet", ACCESS_TOKEN, ACCESS_TOKEN_SECRET);

Beispiel 6: Anfrage an die GitHub-API

Hier das Resultat:

Test Tweet

Abbildung 13: Test Tweet

Und die Antwort:

{"created_at":"Mon Jan 14 15:26:40 +0000 2019","id":108483418... }

Beispiel 7: Antwort der GitHub-API

Das war jetzt natürlich etwas einfach, da wir dem Programm ja bereits alles gegeben haben, was es zur Authentifizierung bei der Twitter-API benötigt. Im nächsten Schritt soll das Programm daher selbstständig einen Authorization Code anfordern und in ein Access-Token umtauschen.

Tweet mit automatischer Authentifizierung

Jetzt soll also zunächst einmal ein Authorization Code vom entsprechenden Endpunkt der Twitter-API nach Bestätigung durch den Benutzer angefordert werden. Dieser Code soll dann beim Access-Token-Endpunkt umgetauscht werden. Schließlich soll mit dem dadurch erhaltenen Access-Token ein Tweet gesendet werden. Der entsprechende Java-Code der all dies kann:

final String AUTH_URL = "https://api.twitter.com/oauth/authenticate";
final String ACC_TOKEN_URL = "https://api.twitter.com/oauth/access_token";
final String REQ_TOKEN_URL = "https://api.twitter.com/oauth/request_token"; 

// OAuthConsumer mit KEY und SECRET erstellen 
final OAuthConsumer oAuthConsumer = 
  new DefaultOAuthConsumer(API_KEY, API_KEY_SECRET); 

// OAuthProvider mit den korrekten URLs definieren 
final OAuthProvider provider = 
    new DefaultOAuthProvider(REQ_TOKEN_URL, ACC_TOKEN_URL, AUTH_URL); 

// Authorize-URL anfordern
final String authUrl = 
  provider.retrieveRequestToken(oAuthConsumer, OAuth.OUT_OF_BAND); 

// Authorize-URL im Webbrowser anzeigen
Desktop.getDesktop().browse(URI.create(authUrl)); 

// PIN/Authorization Code per Konsole anfordern 
System.out.print("Authorization Code eingeben: ");
final BufferedReader b = new BufferedReader(new InputStreamReader(System.in));
final String pin = b.readLine(); 

// PIN in Access-Token umtauschen 
provider.retrieveAccessToken(oAuthConsumer, pin); 

// API-Call mit erhaltenem Token und Secret 
tweet("Test%20Tweet", oAuthConsumer.getToken(), 
  oAuthConsumer.getTokenSecret());

Beispiel 8: Code zur automatischen Authentifizierung via OAuth2

Im ersten Block nach den URLs wird wieder ein OAuthConsumer mit API-Key und API-Secret erstellt. Zusätzlich wird dieses Mal noch ein Objekt vom Typ OAuthProvider erstellt, welches später die Authorize-URL anfordern und den Authorization Code in ein Access-Token umtauschen soll. Dafür benötigt der OAuthProvider die URLs, um Tokens anzufordern, Tokens umzutauschen und sich zu authentifizieren.

Zunächst lässt der OAuthProvider einen Link generieren, auf dem der Benutzer die App autorisieren und einen Authorization Code generieren kann (zweiter Block). Dieser wird dann gleich im Webbrowser geöffnet:

Der Benutzer muss nun die App autorisieren.

Abbildung 14: Der Benutzer muss nun die App autorisieren.

Nachdem die Autorisierung durch den Benutzer mittels eines Klicks auf die blaue “Autorisiere App“-Schaltfläche geschehen ist, wird der Benutzer auf eine Seite weitergeleitet, auf welcher der Authorization Code angezeigt wird. Dieser kann dann manuell vom Benutzer kopiert werden. Im Java-Code wartet im dritten Block von unten ein Scanner auf diesen Authorization Code.

Nachdem der Scanner den Code eingelesen hat, wird dieser vom OAuthProvider in ein Access-Token mit Secret umgetauscht, welches dann gleich im OAuthConsumer abgelegt wird (vorletzter Block). Abschließend kann mit der tweet -Methode aus dem vorherigen Codebeispiel der Tweet wie gehabt verschickt werden.

So verwendet man also OAuth2 mit einer fremden API. Aber wie könnte das ganze in einer eigenen Anwendung aussehen?

SSO via OAuth2 in der Praxis mit Spring

Nachdem wir in den letzten Kapiteln zu OAuth2 umfangreiche Grundlagen geschaffen und bereits erste praktische Erfahrungen gesammelt haben, soll es hier darum gehen, wie in einer eigenen Spring-Boot-Anwendung das Login via OAuth2 Single-Sign-On über einen fremden Authorization Server geregelt werden kann.

Dazu werden wir eine kleine CRM-artige Webanwendung mit Spring Boot entwickeln, bei der man Kundenobjekte ansehen und hinzufügen kann. Das Ganze soll allerdings erst nach einer Anmeldung möglich sein. Diese Anmeldung soll entweder über einen GitHub-Account oder über einen Facebook-Account geschehen können.

Das Tutorial gliedert sich in sieben Teile:

  • Kurze Beschreibung der Abhängigkeiten

  • Die grundlegende Basis-CRM-Anwendung

  • Hauptanwendung mit SecurityConfiguration in Spring Boot

  • OAuth2 Konfiguration in der application.yml

  • Integration der Konfiguration mit der Hauptanwendung

  • Definition der SSO-Filter und OAuth2 Endpunkte

  • Front End in HTML und JavaScript

Abhängigkeiten

Zunächst die JavaScript Abhängigkeiten. Da wären JQuery, Bootstrap und JS-Cookie. Wofür diese benötigt werden, kann weiter unten im Kapitel “Das Front-End” nachgelesen werden. Anschließend folgen die Abhängigkeiten von Spring Boot und Spring Security. Der Einfachheit halber haben wir hier die Starter-Dependencies für Spring Boot und Spring Security verwendet. Außerdem wird eine Abhängigkeit für die Verwendung von OAuth2 mit Spring Security verwendet.

Zum Schluss folgen noch einige Abhängigkeiten für JAXB, da dieses ja seit Java 9 nicht mehr im JDK enthalten ist. Da die hier verwendete Version von Spring Boot davon allerdings noch nichts zu wissen scheint, liefern wir es manuell nach.

Die Basisanwendung

Beginnen wir mit der Entwicklung einer einfachen Spring-Boot-App für unsere Webanwendung. Zunächst das Customer-Objekt. Dieses soll die zwei Parameter id und companyName haben, sowie die entsprechenden Getter und Setter und equals/hashCode-Methoden.

public class Customer {
    private long id;
    private String companyName;	
    ...
}

Beispiel 9: Auszug aus der Customer-Klasse

Außerdem ein einfaches CustomerDao-Objekt, welches die Customer-Objekte in einem nicht-persistenten Speicher (eine lokale Liste) ablegt:

@Component
public class CustomerDao {
    private final List<Customer> customers = new ArrayList<Customer>();
 
    public List<Customer> getCustomers() { ... }
 
    public void addCustomer(final Customer customer) { ... }
 
    public Customer getCustomerById(final long id) { ... }
 
    @PostConstruct
    public void init() { ... }
}

Beispiel 10: Auszug aus der CustomerDao-Klasse

Die Customer-Objekte können also ausgelesen (getCustomer() -Methode), sowie nach ID durchsucht werden (getCustomerById() -Methode). Neue Customer-Objekte können mit addCustomer() hinzugefügt werden. Initial gibt es die beiden Customer “Apple” und “Google” mit den CustomerIDs 0 und 1.

In Spring Boot werden sogenannte Controller -Objekte verwendet, um REST-artige Web-Endpunkte anzubieten. Dazu wird die Controller-Klasse mit @Controller annotiert. Für unsere Anwendung erstellen wir einen POST-Endpunkt, um ein neues Customer-Objekt hinzuzufügen (addCustomer() -Methode). Außerdem erstellen wir zwei GET-Endpunkte, um alle Customer (listCustomers() -Methode) oder nur den Customer korrespondierend zu einer bestimmten CustomerID (getCustomer() -Methode) zurückzugeben. Der entsprechende Code im Controller dazu:

@Controller
public class CustomerController {
    @Autowired
    CustomerDao customerDao;
 
    @RequestMapping(value = "/customers", method = RequestMethod.GET)
    public ResponseEntity<List<Customer>> listCustomers() {
        return new ResponseEntity<List<Customer>>(
			customerDao.getCustomers(), HttpStatus.OK);
    }
 
    @RequestMapping(value = "/customer", method = RequestMethod.GET)
    public ResponseEntity<Customer> getCustomer(
		@RequestParam("customerId") Long customerId) {
        return new ResponseEntity<>(customerDao.getCustomerById(
			customerId), HttpStatus.OK);
    }
 
    @RequestMapping(value = "/customer", method = RequestMethod.POST, 
		consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity<String> addCustomer(Customer customer) {
        customerDao.addCustomer(customer);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

Beispiel 11: Der Customer-Controller

So viel zu unserer Basis-Anwendung. All diese Funktionalität soll nur für eingeloggte Benutzer verfügbar sein. Diese Anforderung wird als nächstes angegangen. Außerdem soll der Login nicht über ein einfaches User/Passwort-Verfahren in der lokalen Anwendung geschehen, sondern über die Authorization-Server von GitHub oder Facebook abgewickelt werden.

Die Hauptanwendung

Beginnen wir mit der Hauptklasse der Anwendung, die den Haupteinstiegspunkt des Java-Programms enthält:

@EnableOAuth2Client
@EnableAuthorizationServer
@SpringBootApplication
@RestController
public class App extends WebSecurityConfigurerAdapter {
 
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**").
        authorizeRequests().
        antMatchers("/", "/login**", "/webjars/**", "/error**")
        .permitAll()
        .anyRequest()
        .authenticated()
        .and().exceptionHandling()
        .authenticationEntryPoint(
            new LoginUrlAuthenticationEntryPoint("/"))
        .and().logout()
        .logoutSuccessUrl("/")
        .permitAll()
        .and().csrf()
        .csrfTokenRepository(
            CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and().addFilterBefore(
            ssoFilter(), BasicAuthenticationFilter.class);
    }
 
    private Filter ssoFilter() { ... }
}

Beispiel 12: Auszug aus der Hauptklasse der Anwendung

Zunächst ein paar kurze Worte zu den Annotationen. @EnableOAuth2Client aktiviert die grundlegende Funktionalität für einen OAuth2 Client mit Spring Security. @EnableAuthorizationServer aktiviert den Authorization-Endpunkt und den Token-Endpunkt für den Authorization Server in Spring Security. Die beiden Annotationen @SpringBootApplication und @RestController sind nicht spezifisch für die Security und annotieren die Klasse nur als Hauptklasse und als Klasse mit integrierten REST-Funktionalitäten, damit die @RequestMappings im nächsten Schritt funktionieren.

Nun zur komplizierteren configure() -Methode. Zunächst stellen wir eine allgemeine Regel für das URL-Pattern “/**” ein (Zeile 13). Für alle URLs wird zunächst einmal via authorizeRequests() die Möglichkeit der Autorisierung aktiviert (Zeile 14). Als nächstes wird der Zugriff auf die URLs “/”, “/login”, “/webjars/” und “/error” konfiguriert, damit später überhaupt eine Loginseite angezeigt werden kann. Dies geschieht über die permitAll() -Methode (Zeile 16). Außerdem wird der Zugriff für alle angemeldeten Benutzer erlaubt (Zeilen 17 und 18). Für die nicht angemeldeten Benutzer wird ein exceptionHandling() konfiguriert, welches auf die Hauptseite (“/”) mit dem Login weiterleitet (bis Zeile 21). Anschließend wird die Logout-URL als die gleiche Hauptseite unter “/” konfiguriert (bis Zeile 24). Zum Schluss wird noch das Cross-Site-Request-Forgery (CSRF)-Token aktiviert (Zeile 25-27) und der SSO-Filter für unsere Single-Sign-On-Provider gesetzt (Zeilen 28 und 29). Die ssoFilter() -Methode definieren wir später im Kapitel “Definition der SSO-Filter”.

OAuth2-Konfiguration in der application.yml

Für die Definition des SSO-Filters müssen zunächst die Authorization-Server der verknüpften Identity-Provider (Facebook und GitHub), sowie die grundlegende OAuth2-Konfiguration in der application.yml im Resourcenverzeichnis eingetragen werden:

facebook:
  client:
    clientId: client-id-of-your-oauth2-facebook-app
    clientSecret: client-secret-of-your-oauth2-facebook-app
    accessTokenUri: https://graph.facebook.com/oauth/access_token
    userAuthorizationUri: https://www.facebook.com/dialog/oauth
    tokenName: oauth_token
    authenticationScheme: query
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://graph.facebook.com/me
     
github:
  client:
    clientId: client-id-of-your-oauth2-github-app
    clientSecret: client-secret-of-your-oauth2-github-app
    accessTokenUri: https://github.com/login/oauth/access_token
    userAuthorizationUri: https://github.com/login/oauth/authorize
    clientAuthenticationScheme: form
  resource:
    userInfoUri: https://api.github.com/user
     
security:
  oauth2:
    client:
      client-id: oauth2-testapp
      client-secret: oauth2-testsecret
      scope: read,write
      auto-approve-scopes: '.*'

Beispiel 13: Die application.yml Konfigurationsdatei

Für einen erfolgreichen Anmeldeversuch bei einem fremden Authorization Server wird eine OAuth2-App bei dem entsprechenden Anbieter benötigt. Wie man diese erstellen kann, ist in den vorherigen Kapiteln exemplarisch für GitHub und kurz für Twitter erklärt. Der Prozess bei Facebook hat allerdings ganz ähnlich funktioniert.

In den oben gelisteten OAuth2-Konfigurationen müssen nun für jeden SSO-Provider, in unserem Fall also GitHub und Facebook, einige Daten hinterlegt werden. Dazu gehören Client-ID und ClientSecret der OAuth2-App. Außerdem werden die bereits bekannten OAuth2-URLs für den Erhalt des Authorization Codes und den Umtausch desselben in ein Access Token benötigt. Wenn das Access-Token im zu empfangenden JSON-Objekt (siehe OAuth2-Setup-Beispiel) nicht “token” oder “access_token” heißt, muss dies hier spezifiziert werden. So heißt dass Access Token beispielsweise bei Facebook oauth_token, daher muss in Zeile 7 tokenName: oauth_token spezifiziert werden. Dann muss noch die User-Info-Uri angegeben werden. Hier können später Benutzerdaten, wie etwa der Benutzername des angemeldeten Benutzers, abgerufen werden. Bei Facebook lautet diese …/me, während sie bei GitHub …/user lautet. Diese Schemata sind beide valide und üblich.

Der letzte Teil (security) der application.yml definiert die eigene OAuth2-Schnittstelle der Spring-Boot-Anwendung. Dazu wird unter client die OAuth2-App mit der Client-ID oauth2-testapp und dem Client-Secret oauth2-testsecret definiert. Sie hat als Permission-Scope read und write für alle Scopes (.*).

Die Verknüpfung mit der Hauptanwendung

Nun muss die soeben erstellte Konfiguration noch mit der Hauptanwendung verknüpft werden. Dazu werden einige Methoden in die Hauptklasse eingefügt. Zunächst einmal die innere Klasse ClientResources:

class ClientResources {
    @NestedConfigurationProperty
    private AuthorizationCodeResourceDetails client = 
		new AuthorizationCodeResourceDetails();
 
    @NestedConfigurationProperty
    private ResourceServerProperties resource = 
		new ResourceServerProperties(); 
    ...
}

Beispiel 14: Auszug aus der innere Klasse ClientResources

Die Klasse ClientResources soll per @Bean jeweils einmal für GitHub und für Facebook befüllt werden. Sie wrappt einfach nur ein leeres AuthorizationCodeResourceDetails -Objekt und ein leeres ResourceServerProperties-Objekt. Hier die beiden dazugehörigen @Beans:

@Bean
@ConfigurationProperties("github")
public ClientResources github() {
    return new ClientResources();
}
 
@Bean
@ConfigurationProperties("facebook")
public ClientResources facebook() {
    return new ClientResources();
}

Beispiel 15: Die beiden @Beans in der Hauptklasse

Definition der SSO-Filter

Die beiden ClientResources-Beans von oben werden für die im Kapitel “Die Hauptanwendung” ausgelassene ssoFilter() -Methode benötigt, welche für jede der beiden Beans einen SSO-Filter erstellt. Das folgende Snippet wird in die Hauptklasse eingefügt:

@Autowired
OAuth2ClientContext oauth2ClientContext;
 
private Filter ssoFilter() {
    CompositeFilter filter = new CompositeFilter();
    List<Filter> filters = new ArrayList<>();
    filters.add(ssoFilter(facebook(), "/login/facebook"));
    filters.add(ssoFilter(github(), "/login/github"));
    filter.setFilters(filters);
    return filter;
}
 
private Filter ssoFilter(ClientResources client, String path) {
    OAuth2ClientAuthenticationProcessingFilter filter = 
        new OAuth2ClientAuthenticationProcessingFilter(path);
    OAuth2RestTemplate template = 
        new OAuth2RestTemplate(client.getClient(), 
        oauth2ClientContext);
    filter.setRestTemplate(template);
    UserInfoTokenServices tokenServices = 
        new UserInfoTokenServices(client.getResource().
        getUserInfoUri(), client.getClient().getClientId());
    tokenServices.setRestTemplate(template);
    filter.setTokenServices(tokenServices);
    return filter;
}

Beispiel 16: Die SSO-Filter-Methoden

In Zeile 5-9 wird ein neues CompositeFilter -Objekt erstellt, welches die beiden anschließend erstellten Objekte für Facebook und GitHub kombiniert. Die private ssoFilter() -Methode erzeugt nun aus dem übergebenen ClientResources-Objekt einen OAuth2ClientAuthenticationProcessingFilter, indem es die notwendigen Daten aus dem ClientResources-Objekt auspackt.

Schließlich werden noch die beiden Request-Methoden für die Ressourcenabfrage (/me und /user) benötigt (Zeilen 13-23), sowie ein weiteres ResourceServerConfiguration -Objekt, welches den Zugriff auf /me erlaubt (Zeile 8-9). Diese Methoden werden später vom Front-End aufgerufen und sollen Daten zum Benutzer, welche vorher von GitHub oder Facebook geladen wurden, zurückgeben. Der Inhalt der Methoden ist denkbar einfach:

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends
    ResourceServerConfigurerAdapter {
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/me").authorizeRequests()
            .anyRequest().authenticated();
    }
}
 
@RequestMapping({ "/me" })
public Map<String, String> users(Principal principal) {
    Map<String, String> map = new LinkedHashMap<>();
    map.put("name", principal.getName());
    return map;
}
 
@RequestMapping({ "/user" })
public Principal user(Principal principal) {
    return principal;
}

Beispiel 17: Die beiden Request-Methoden

Das Front-End

Da es in diesem Artikel in erster Linie um OAuth2 und das Backend geht, sind Front-End und die dazugehörigen Erläuterungen kur und bündig gehalten. Zunächst der Head der index.html:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Auth2 SpringBoot Example</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width" />
<base href="/" />
<link rel="stylesheet" type="text/css"
    href="/webjars/bootstrap/css/bootstrap.min.css" />
<a href="/webjars/jquery/jquery.min.js">/webjars/jquery/jquery.min.js</a>
<a href="/webjars/bootstrap/js/bootstrap.min.js">
/webjars/bootstrap/js/bootstrap.min.js</a>
<a href="/webjars/js-cookie/js.cookie.js">
/webjars/js-cookie/js.cookie.js</a>
</head>

Beispiel 18: Head der index.html

Im Head definieren wir den Titel der Seite, sowie einige Metainformationen. Außerdem lassen wir Skripte für Bootstrap, JQuery und JSCookie laden.

Als Nächstes beginnen wir mit dem Body.

<body>
    <h1 style="padding-left: 10px;">OAuth2 SpringBoot Example</h1>
    <div class="container"></div>
 
    <div style="padding-left: 20px">
        <h2>Social Login</h2>
        <div class="container unauthenticated">
            <div>
                Mit Facebook: <a href="/login/facebook">Hier Klicken</a>
            </div>
 
            <div>
                Mit GitHub: <a href="/login/github">Hier Klicken</a>
            </div>
        </div>
        <div class="container authenticated">
            <div class="container">
                Eingeloggt als: <span id="user"></span>
                <button class="btn btn-primary">Logout</button>
            </div>
            <br />
            <div class="container">
                <div>
                    <a href="/customers">Alle Kunden zeigen</a>
                </div>
                <div>
                    <a href="/customer?customerId=0">Kunde mit ID 0 zeigen</a>
                </div>
 
                <div>
                    <a id="createCustomer" href="void(0);">
					Facebook-Testkunde erstellen</a>
                </div>
            </div>
        </div>
    </div>

Beispiel 19: Body der index.html

Es fällt auf, dass den div-Elementen unterschiedliche Klassen angehängt sind. Die Klasse authenticated markiert divs, welche nur angemeldeten Benutzern angezeigt werden. Divs mit unauthenticated werden dagegen nur nicht angemeldeten Benutzern angezeigt. So ist beispielsweise der Container mit den Anmeldelinks nur für unangemeldete Benutzer sichtbar, während der Container mit dem Logout-Knopf und den Links für “Alle Kunden anzeigen“, etc. nur für angemeldete Benutzer sichtbar wird.

Nun schließen wir den Body mit dem folgenden Javascript ab:

$.ajaxSetup({
    beforeSend : function(xhr, settings) {
        if (settings.type == 'POST' || settings.type == 'PUT'
                || settings.type == 'DELETE') {
            if (!(/^http:.*/.test(settings.url) || /^https:.*/
                    .test(settings.url))) {
                // Only send the token to relative URLs i.e. locally.
                xhr.setRequestHeader("X-XSRF-TOKEN", Cookies
                        .get('XSRF-TOKEN'));
            }
        }
    }
});
 
$.get("/user", function(data) {
    if (data.userAuthentication != null) {
        $("#user").html(data.userAuthentication.details.name);
        $(".unauthenticated").hide()
        $(".authenticated").show()
    }
});

var logout = function() {
    $.post("/logout", function() {
        $("#user").html('');
        $(".unauthenticated").show();
        $(".authenticated").hide();
    })
    return true;
};
 
var customer = function() {
    $.ajax({
            type : "POST",
            contentType : "application/x-www-form-urlencoded; charset=utf-8",
            url : "/customer?id=3&companyName=Facebook",
            data : "",
            dataType : "x-www-form-urlencoded"
        });
    $("#createCustomer").hide();
};

Beispiel 20: JavaScript am Ende der index.html

Die oberste Methode beforeSend ist Teil der JQuery-API und spezifiert einen Default-Call für alle Ajax-Calls. In diesem Fall sollen alle Calls mit einem XSRF-Token ausgestattet werden, um Cross-Site-Request-Forgery zu unterbinden.

Anschließend wird der oben in Spring Boot definierte /user -Endpunkt aufgerufen. Sofern ein sinnvolles Resultat zurückkommt, wird der Name des Benutzers in das HTML-Element mit der id user eingefügt und die Container mit der Annotation authenticated und unauthenticated angezeigt bzw. ausgeblendet.

Als Nächstes wird die logout -Methode definiert. Sie wird vom Logout-Button (siehe HTML oben) aufgerufen und schickt eine POST-Anfrage an den /logout -Endpunkt des Spring-Boot-Backends. Anschließend werden wie nach dem Aufruf des /user -Endpunktes oben die entsprechenden Container aktualisiert.

Der letzte Script-Block enthält die clientseitige Logik, die nach einem Klick auf den “Facebook-Testkunde-erstellen”-Link eine POST-Anfrage ans Backend schickt, um den Kunden zu erstellen. Außerdem wird anschließend der Link ausgeblendet.

Die fertige Seite sieht zunächst wie folgt aus:

Loginseite der Beispielanwendung

Abbildung 15: Loginseite der Beispielanwendung

Das Feld für den Namen des Benutzers sowie die Links zum Anschauen und Hinzufügen von Kunden sind wie erwartet zunächst ausgeblendet. Nach einem Klick auf “Hier Klicken” neben GitHub erscheint, sofern der Benutzer noch nicht auf GitHub angemeldet ist, die folgende Abfrage:

Anmelde-Prompt für GitHub

Abbildung 16: Anmelde-Prompt für GitHub

Nachdem der Benutzer hier seine Daten eingegeben und auf die grüne Schaltfläche mit der Aufschrift "Sign in" gedrückt hat, wird er mit seinem Authorization-Code automatisch an unseren Server weitergeleitet. Der Authorization Code wird dann vom OAuth2-Modul in Spring Security automatisch im Hintergrund entgegengenommen und in ein Access-Token umgetauscht. Schließlich landet der Benutzer auf der folgenden Seite:

Seite nach erfolgreicher Anmeldung

Abbildung 17: Seite nach erfolgreicher Anmeldung

Wie in der Abbildung erkennbar, wurde der Klarname des Benutzers (im Beispiel “Steffen Jacobs”) von der GitHub-API abgefragt und hier angezeigt. Außerdem werden die drei Optionen zum Anzeigen und Erstellen der Kunden für unsere Basisanwendung angezeigt. Die Login-Links wurden ausgeblendet.

Ein Klick auf “Alle Kunden zeigen” leitet auf /customers um, wo dann die aktuell existierenden Kunden angezeigt werden:

/customers Endpunkt nach Klick auf “Alle Kunden anzeigen”

Abbildung 18: /customers Endpunkt nach Klick auf “Alle Kunden anzeigen”

Mit einem Klick auf “Logout” kann sich der Benutzer wieder aus der Testanwendung ausloggen. Seine Login-Session beim Authorization Provider, etwa GitHub, bleibt jedoch bestehen. Nach einem erneuten Klick auf den Anmeldelink wird der Benutzer nicht erneut nach Passwort und Benutzername gefragt, sondern wird gleich an die Hauptseite zurückgeleitet und angemeldet.

Abschluss

Wir haben uns am Anfang des Artikels umfangreich mit den Hintergründen und Konzepten zu Single Sign-On auseinandergesetzt. Anschließend haben wir uns in OAuth2 eingearbeitet, haben angeschaut welche Rollen es gibt, wie das Berechtigungssystem gedacht ist und welche Client-Typen welche OAuth2-spezifischen Workflows implizieren. Im darauf folgenden Praxiskapitel haben wir unser neu erlerntes Wissen gleich einmal am Beispiel von GitHub ausprobiert und dort eine OAuth2-App aufgesetzt.

Danach ging es ans Programmieren: Wir haben eine Anwendung entwickelt und implementiert, die automatisch Tweets verschicken kann. Dazu haben wir ihr in der ersten Interation ein generiertes Access-Token gegeben, welches sie dann in der nächsten Iteration auch selbstständig abrufen konnte.

In der finalen Anwendung haben wir das gesamte in den vorherigen Kapiteln aufgebaute Wissen zu SSO und OAuth2 noch einmal umfassend eingesetzt. Dazu haben wir eine Spring-Boot Anwendung in Java programmiert, welche wir über Spring Security abgesichert und mittels SSO und OAuth2 per Social Login an die Identity Provider von GitHub und Facebook angebunden haben. Die Benutzer können sich bei unserer Anwendung einfach mit ihrem Facebook- oder GitHub-Benutzerkonto anmelden und müssen sich nicht separat registrieren.

Bibliographie

[SAML] OASIS Security Services (SAML) TC | OASIS
(https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=security)
OASIS; 2019-01-28

[OAUTH] OAuth 2 Community Site
(https://oauth.net/2/)
OAuth; 2019-01-28

[RSAT] RSA SecurID Hardware Tokens
(https://www.rsa.com/content/dam/en/data-sheet/rsa-securid-hardware-tokens.pdf)
Meo, Melissa, RSA SECURID® Hardware Tokens; 10/15

[PGID] PingID
(https://www.pingidentity.com)
, , Ping Identity; 2019-04-03

[KERB] The Kerberos Network Authentication Service (V5)
(https://tools.ietf.org/html/rfc4120)
Neuman, Clifford, Kerberos; Hartman, Sam, Kerberos; Yu, Tom, Kerberos; Raeburn, Kenneth, Kerberos; 2019-01-28

[LIBAL] Liberty Alliance
(http://www.projectliberty.org/)
Liberty Alliance, , Liberty Alliance; 2019-01-28

[G_SSO] Single Sign-On – Eine Einführung
(https://blog.oio.de/2018/06/19/single-sign-on-eine-einfuhrung/)
Jacobs, Steffen, Orientation in Objects GmbH; 2018-06-19

[G_OA2] Eine Einführung in OAuth 2
(https://blog.oio.de/2018/08/20/eine-einfuhrung-in-oauth-2/)
Jacobs, Steffen, Orientation in Objects GmbH; 2018-08-20

[GH_OA2] OAuth2 in der Praxis: Anwendungssetup mit GitHub
(https://blog.oio.de/2019/01/14/oauth2-in-der-praxis-anwendungssetup-mit-github/)
Jacobs, Steffen, Orientation in Objects GmbH; 2019-01-14

[PKCE] OAuth2 - PKCE
(https://blog.oio.de/2019/01/15/oauth2-pkce/)
Jacobs, Steffen, Orientation in Objects GmbH; 2018-01-15

[TW_OA2] OAuth2 mit Java: Versenden eines Tweets über die Twitter-API
(https://blog.oio.de/2019/01/16/oauth2-mit-java-versenden-eines-tweets-uber-die-twitter-api/)
Jacobs, Steffen, Orientation in Objects GmbH; 2019-01-16

[SB_OA2] Spring Boot: SSO mit OAuth2 via GitHub oder Facebook
(https://blog.oio.de/2019/01/22/spring-boot-sso-mit-oauth2-via-github-oder-facebook/)
Jacobs, Steffen, Orientation in Objects GmbH; 2019-01-22

[OAW] Web Authorization Protocol (oauth) - Working Group
(https://datatracker.ietf.org/wg/oauth/about/)
Working Group, , ; 2009-05-12

[RFC6] The OAuth 2.0 Authorization Framework
(https://tools.ietf.org/html/rfc6749)
Hardt, Dick, ; 2010-10

[PKCE2] Proof Key for Code Exchange by OAuth Public Clients
(https://tools.ietf.org/html/rfc7636)
Agarwal, Naveen, Google; Sakimura, Nat, Nomura Research Institute; Bradley, J, Ping Identity; 2015-09-01

[GITH] GitHub Developer
(https://developer.github.com/v3/)
GitHub, , GitHub; 2019-02-06

[SIGNP] Signpost
(https://developer.github.com/v3/)
Käppler, Matthias, SoundCloud Ltd.; 2016-08-18

Zum Geschaeftsbreich Competence Center
Schulung
Haben Sie Interesse an der Schulung Einführung in das Spring Framework?
Schulung
Sicherheit in der Cloud zeigen wir Ihnen in unserer Cloud Native mit Spring Schulung.

Service

Competence Center

Veröffentlichungen

Artikelübersicht