zurück zum Artikel

Tipps und Tricks für AngularJS, Teil 3: OAuth 2.0

Manfred Steyer

Der Standard OAuth 2.0 ermöglicht die Implementierung von Single Sign-On. Durch den Einsatz von Umleitungen und die Nutzung der von UI-Router angebotenen Ereignisse können Entwickler damit zeitgemäße Log-in-Szenarien umsetzen.

Tipps und Tricks für AngularJS, Teil 3: OAuth 2.0

Der Standard OAuth 2.0 ermöglicht die Implementierung von Single Sign-On. Durch den Einsatz von Umleitungen und die Nutzung der von UI-Router angebotenen Ereignisse können Entwickler damit zeitgemäße Log-in-Szenarien umsetzen.

Webnutzer sehen sich heutzutage mit zahlreichen Benutzerkonten konfrontiert. Das legt den Wunsch nahe, ihnen bei Bedarf die Möglichkeit anbieten zu können, sich mit nur einem Konto bei mehreren Webangeboten anzumelden. Ein Werkzeug, mit dem sich diese Aufgabe angehen lässt, ist der populäre Standard OAuth 2.0 [1]. Er lässt sich nutzen, um einer Webanwendung das Recht zu geben, im Namen des Benutzers auf Dienste zuzugreifen. Der Benutzer muss sich dafür lediglich bei einem zentralen Log-in-Provider anmelden.

Während der Artikel "Flexible und sichere Internetdienste mit OAuth 2.0 [2]" allgemeine Informationen zu OAuth 2.0 liefert, zeigt der vorliegende Beitrag, wie man den Standard in einer AngularJS-Anwendung nutzen kann. Dafür kommt die Implementierung eines sogenannten Implicit Flows zum Einsatz. Diese Spielart von OAuth 2.0 wurde speziell für JavaScript-basierte Clients geschaffen. Zur Abrundung streift der Artikel die in "OpenID Connect: Login mit OAuth, Teil 1 – Grundlagen [3]" beschriebene OAuth-2.0-Erweiterung OpenID Connect.

Um die Implementierung des Implicit Flow mit AngularJS zu erklären, kommt die mit Abbildung 1 veranschaulichte Demoanwendung zum Einsatz. Sie gibt dem Benutzer die Möglichkeit, einen Gutschein zu kaufen. Dazu nutzt die App eine Web API, die den Betrag des Gutscheins im Rahmen einer POST-Anfrage über einen URL-Parameter entgegennimmt und einen JSON-String mit einem Gutscheincode zurückliefert. Die Autorisierung findet mit einem OAuth-2.0-Token statt.

In der Demoanwendung kommt OAuth 2.0 für die Authentifizierung zum Einsatz (Abb. 1).

In der Demoanwendung kommt OAuth 2.0 für die Authentifizierung zum Einsatz (Abb. 1).

Die Demoanwendung besteht aus vier Menüpunkten, wobei hinter jedem eine mit dem UI-Router von AngularJS [4] realisierte Route steht. Deren Konfiguration sieht wie folgt aus:

var app = angular.module("demo", ["oauth2", "ui.router"]);

app.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {

$urlRouterProvider.otherwise("/home");

$stateProvider.state('home', {
url: '/home',
templateUrl: '/app/routing-demo/home.html',
}).state('gutschein', {
url: '/gutschein',
templateUrl: '/app/routing-demo/gutschein.html',
controller: "GutscheinCtrl",
restricted: true
}).state('login', {
url: '/login?requestedUrl',
templateUrl: '/app/routing-demo/login.html',
controller: "LoginCtrl"
}).state('logout', {
url: '/logout',
templateUrl: '/app/routing-demo/logout.html',
controller: "LogoutCtrl"
});

});

Sie befindet sich in einem Modul demo, das die Module oauth2 und ui.router referenziert. Während Letzteres den UI-Router beheimatet, handelt es sich bei ersterem um ein Modul, das Teil des betrachteten Beispiels ist und wiederverwendbare Aspekte in Hinblick auf OAuth 2.0 kapselt.

Bei genauerer Betrachtung fällt die Eigenschaft restricted bei der Route gutschein auf. UI-Router kennt sie nicht und ignoriert sie deswegen. Die in weiterer Folge gezeigten Beispiele können diese Information jedoch auslesen. Sie gehen davon aus, dass Routen, bei denen diese Eigenschaft den Wert "true" aufweist, angemeldeten Benutzern vorbehalten sind.

Um den von OAuth 2.0 beschriebenen Implicit Flow zu implementieren, muss eine Single-Page-Anwendung eigentlich nur den Benutzer unter Verwendung der definierten Parameter auf die Log-in-Seite des Autorisierunsservers umleiten. Wenn Letzterer den Benutzer wieder zurück zur Anwendung sendet, findet diese das Access-Token im Hash-Fragment der URL.

Dreh- und Angelpunkt der betrachteten Komponente [5] ist die Konstruktorfunktion für den vom Modul oauth2 veröffentlichten Angular-Service OAuthService, dessen Grundgerüst wie folgt aussieht:

function OAuthService($document, $window, $timeout, $q) {

this.clientId = "";
this.redirectUri = "";
this.loginUrl = "";
this.scope = "";
this.state = "";

this.createLoginUrl = function (additionalState) { };
this.tryLogin = function () { };
this.getAccessToken = function () { };
this.getIsLoggedIn = function () { };
this.logOut = function () { };

}

Die eingangs beschriebenen Eigenschaften konfigurieren den Service. Bei clientId handelt es sich um die Kennung (ID), die der Autorisierungsserver für die Anwendung vergeben hat. Die Eigenschaft redirectUri enthält die URL, an die er den Benutzer nach erfolgter Anmeldung sendet. Sie entspricht der URL der Anwendung, die beim Autorisierungsserver registriert sein muss. Als loginUrl bezeichnet der Service hingegen die URL des Servers, bei der sich der Benutzer anmeldet, und scope umfasst die von der Anwendung benötigten Rechte. Die Eigenschaft state enthält einen String, den der Client dem Autorisierungsserver sendet, der ihn dann wieder zurückschickt. Auf die Weise bleiben Zustandsinformationen trotz der vorgesehenen Umleitungen erhalten.

Neben diesen Eigenschaften findet man im OAuthService auch einige in der folgenden Tabelle beschriebenen Funktionen, deren Implementierung der Autor auf GitHub [6] zur Verfügung stellt.

Funktion Beschreibung
createLoginUrl Die Funktion erzeugt die durch OAuth 2 beschriebene URL, die zur Log-in-Seite des Autorisierungsservers führt. Sie setzt sich aus den Werten der Eigenschaften clientId, redirectUri, loginUrl und scope zusammen. Zusätzlich enthält sie einen Parameter state, der neben den definierten Zustandsdaten eine Nonce enthält. Sie wird für spätere Überprüfungen im lokalen Speicher hinterlegt.
tryLogin
Versucht, im Hash-Fragment der aktuellen URL ein Access-Token zu finden. Sie ist aufzurufen, nachdem der Autorisierungsserver den Benutzer zur SPA zurückgesendet hat. Die Funktion prüft auch, ob die Nachricht des Autorisierungsservers den von createLoginUrl übersendeten Nonce enthält. Nur in dem Fall akzeptiert sie das Access-Token. War die Überprüfung erfolgreich, verstaut die Funktion es im lokalen Speicher und liefert true zurück.
getAccessToken Liefert das Access-Token aus dem Local Storage des Browsers.
getIsLoggedIn Informiert darüber, ob für den Benutzer ein Access-Token vorliegt.
logOut Löscht das Access-Token im Local Store des Browsers.

Der folgende Quelltextauszug zeigt, wie Entwickler die zuvor beschriebene OAuth-Implementierung nutzen können.

app.run(function (oauthService, $http, $state, $rootScope, $location) {

oauthService.redirectUri = "http://localhost:42344/index.html";
oauthService.loginUrl = "http://localhost:42344/authorization";
oauthService.clientId = "myClient";
oauthService.scope = "voucher";

$rootScope.$on("$stateChangeStart", function (event, toState,
toParams, fromState, fromParams) {

if (toState.restricted && !oauthService.getIsLoggedIn()) {
event.preventDefault();
var requestedUrl = $state.href(toState, toParams);
$state.transitionTo("login", { requestedUrl: requestedUrl });
}

});

if (oauthService.getIsLoggedIn() || oauthService.tryLogin()) {
$http.defaults.headers.common['Authorization'] = 'Bearer ' +
oauthService.getAccessToken();

if (oauthService.state) {
$location.url(oauthService.state.substr(1)); // führendes #
// abschneiden
}
}

});

Dazu ist zunächst das eingangs genutzte Beispiel um den Aufruf der Modulfunktion run zu erweitern. Nach dem Konfigurieren des Moduls ruft die an sie übergebene Funktion AngularJS auf. Ihr lässt sich unter anderem der zuvor beschriebenen oauthService injizieren, den man unter Verwendung der hierfür bereitgestellten Eigenschaften konfiguriert. Da das betrachtete Beispiel dem Benutzer die Möglichkeit bietet, Gutscheine zu kaufen, kommt als Scope der Wert voucher zum Einsatz, der das dazu nötige Recht ausdrückt.

Wie eingangs beschrieben liegt das Definieren von Scopes im Aufgabenbereich der einzelnen Autorisierungsserver. Da es für die hier zu findenden Erklärungen nicht erheblich ist, verzichten die gezeigten Beispiele zur Vereinfachung auf die Nutzung von HTTPS. Im Produktiveinsatz sollte OAuth 2.0 jedoch generell nur gemeinsam mit einer sicheren HTTPS-Verbindung Verwendung finden.

Nach dem Konfigurieren von oauthService definiert die betrachtete Funktion eine Ereignisbehandlungsroutine für das Ereignis $stateChangeStart, das UI-Router vor dem Wechsel zu einer neuen Route auslöst. Sie prüft, ob die gewünschte Route lediglich angemeldeten Benutzern vorbehalten ist. Dazu zieht sie die Eigenschaft restricted heran. Handelt es sich um eine Route für angemeldete Benutzer und informiert getIsLoggedIn darüber, dass der Benutzer nicht angemeldet ist, bricht die Routine den Wechsel zur gewünschten Route mit preventDefault ab und veranlasst mit der Funktion transitionTo des $state-Services einen Wechsel zum Zustand login. Als Routing-Parameter erhält der Zustand die URL, die zur gewünschten Route führt. Das betrachtete Beispiel erhält sie durch Aufruf der Funktion href des $state-Services. Der Controller des Zustands login kann die URL nach erfolgter Anmeldung verwenden, um den Benutzer zu seinem ursprünglichen Ziel zu senden.

Bei alldem ist zu beachten, dass das hier gezeigte Sperren von Routen für nicht autorisierte Benutzer mehr der Benutzerfreundlichkeit und weniger der Sicherheit dient, zumal bei Single-Page-Applikationen der hierfür verantwortliche Quellcode clientseitig vorliegt und der Benutzer ihn somit abändern kann. Die eigentlichen Sicherheitsprüfungen müssen somit durch die aufgerufenen Services erfolgen.

Am Ende prüft die betrachtete Funktion, ob für den Benutzer entweder schon ein Access-Token im Local Storage des Browsers vorliegt oder ob zumindest in der URL eines zu finden ist. Letzteres trifft in den Fällen zu, in denen der Benutzer nach erfolgter Anmeldung vom Autorisierungsserver zurück zur Anwendung gesendet wurde. Gibt es ein solches Token, erhält der Service $http einen Standard-Header. Dabei handelt es sich um einen Authorization-Header, der den aufgerufenen Web APIs das Access-Token präsentiert. Der Wert des Headers setzt sich laut OAuth-2-Dokumentation [7] aus dem String-Bearer und dem Access-Token zusammen. Ein Leerzeichen trennt beide Teile.

Sofern der zusammen mit dem Access-Token erhaltene Zustand neben dem Zufallswert weitere Informationen enthält, geht die Anwendung davon aus, dass es sich dabei um die URL der Route handelt, die der Benutzer ursprünglich ansteuern wollte. In dem Fall leitet sie den Benutzer mit dem $location-Service auf diese URL um.

Um Routen oder Optionen in Hinblick auf die tatsächlichen Rechte des Benutzers zu aktivieren beziehungsweise zu deaktivieren, könnte sich die Single-Page-Anwendung unter Angabe des Access-Tokens an einen Service wenden, der Profilinformationen für den Benutzer liefert. Anhand dieser könnte die Anwendung entscheiden, welche Möglichkeiten sie dem Benutzer bietet. Der für den Zustand login zuständige Controller hat lediglich die Aufgabe, den Benutzer auf die Log-in-Seite des Autorisierungsservers umzuleiten.

app.controller("LoginCtrl", function ($scope, $stateParams, 
oauthService, $http) {

location.href =
oauthService.createLoginUrl($stateParams.requestedUrl);

});

Der obige Quelltextauszug nimmt sich der Aufgabe auf einfache Weise an, indem er mit der Funktion createLoginUrl die URL der Log-in-Seite samt der nötigen Parameter ermittelt. Durch das Zuweisen der URL an location.href veranlasst der Browser die gewünschte Umleitung. Damit der Anwendung nach dem Anmelden die ursprünglich angefragte Route vorliegt, übergibt sie ihre URL an createLoginUrl, die sie als Teil des state-Parameters übersendet.

Obwohl die gezeigte Implementierung geradlinig und folglich vergleichsweise einfach ist, sollte vor dem Hintergrund der Testbarkeit das Umleiten des Benutzers in einen eigenen Service ausgelagert werden. Alternativ dazu könnte die Anwendung eine Direktive zum Rendern einer Schaltfläche nutzen, die den Benutzer auf die Log-in-Seite umleitet. Eine Umsetzung dieser Ideen findet man im vom Autor zur Verfügung gestellten Repository [8].

Mehr Infos

Die OAuth-2-Erweiterung OpenID Connect wurde geschaffen, um Authentifizierungszenarien mit OAuth 2.0 zu standardisieren. Sie sieht vor, dass der Client ein ID-Token bekommt, das Informationen über den Benutzer enthält. Daneben umfasst sie zusätzliche Sicherheitsmerkmale wie die Möglichkeit, das ID-Token zu signieren. Um einen Missbrauch zu verhindern, listet das Token den Aussteller sowie den Client, für den es ausgestellt wurde. Eine Erweiterung des hier betrachteten Beispiels für die Nutzung des aufstrebenden Protokolls steht in einem eigenen Repository [18] bereit.

Während AngularJS keine Bordmittel für Log-in-Szenarien via OAuth 2.0 beziehungsweise OpenID Connect bietet, lassen sie sich durch entsprechende Umleitungen selbst schreiben. Auf die Weise kann der Benutzer ein bei einem Log-in-Provider hinterlegtes Konto zur Anmeldung nutzen. Um zu verhindern, dass Benutzer Routen verwenden, für die sie nicht autorisiert sind, kann eine Single-Page-Anwendung die von UI-Route angebotenen Ereignisse nutzen. Das dient jedoch in erster Linie der Benutzerfreundlichkeit, denn Sicherheitsprüfungen haben am Server zu erfolgen.

Manfred Steyer
ist Trainer und Berater bei IT-Visions [19] und an der FH CAMPUS 02 tätig. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten von Googles SPA-Framework.
(jul [20])


URL dieses Artikels:
https://www.heise.de/-2620374

Links in diesem Artikel:
[1] http://oauth.net/2/
[2] https://www.heise.de/ratgeber/Flexible-und-sichere-Internetdienste-mit-OAuth-2-0-2068404.html
[3] https://www.heise.de/hintergrund/OpenID-Connect-Login-mit-OAuth-Teil-1-Grundlagen-2218446.html
[4] https://github.com/angular-ui/ui-router
[5] https://github.com/manfredsteyer/AnguarJS-with-OAuth2
[6] https://github.com/manfredsteyer/AnguarJS-with-OAuth2
[7] http://oauth.net/2/
[8] https://github.com/manfredsteyer/AnguarJS-with-OAuth2
[9] https://www.heise.de/hintergrund/Tipps-und-Tricks-fuer-AngularJS-Teil-1-Internationalisierung-2516976.html
[10] https://www.heise.de/hintergrund/Tipps-und-Tricks-fuer-AngularJS-Teil-2-ES2015-2555723.html
[11] https://www.heise.de/hintergrund/Tipps-und-Tricks-fuer-AngularJS-Teil-3-OAuth-2-0-2620374.html
[12] https://www.heise.de/ratgeber/Tipps-und-Tricks-fuer-AngularJS-Teil-4-Animationen-in-AngularJS-1-4-2774580.html
[13] https://www.heise.de/ratgeber/Tipps-und-Tricks-fuer-AngularJS-Teil-5-Transformationen-und-Interceptors-2821088.html
[14] https://www.heise.de/hintergrund/Backend-lose-Entwicklung-mit-AngularJS-und-ngMockE2E-3086974.html
[15] https://www.heise.de/ratgeber/Tipps-und-Tricks-mit-AngularJS-Teil-7-GUIs-mit-Angular-2-und-Redux-Implementierung-ngrx-store-I-3192046.html
[16] https://www.heise.de/ratgeber/test-3194264.html
[17] https://www.heise.de/ratgeber/AngularJS-1-x-und-2-0-mit-dem-Component-Router-parallel-betreiben-2679282.html
[18] https://github.com/manfredsteyer/angular-with-openid-connect
[19] http://www.it-visions.at
[20] mailto:jul@heise.de