Mit dem Implicit Flow von OAuth 2.0 kann ein Benutzer einer JavaScript-basierten Anwendung das Recht geben, in seinem Namen auf Services zuzugreifen. Das kann zur Realisierung von Single-Sign-On bei Single Page Applications (SPA) verwendet werden. Dazu wird wie folgt vorgegangen:
- Die SPA leitet den Benutzer auf einen OAuth-2.0-Authorization-Server um
- Der Benutzer meldet sich beim Authorization Server an und gibt an, der SPA bestimmte Rechte zukommen lassen zu wollen.
- Der Authorization Server leitet den Benutzer auf die SPA um und übergibt im Zuge dessen im Hash-Fragment der URL ein Access-Token
- Die SPA authentifiziert sich mit diesem Access-Token gegenüber Backend-Services und zeigt somit an, dass sie Namen des aktuellen Benutzers handelt.
Das nachfolgende Listing zeigt eine TypeScript-Klasse, welche eine AngularJS-Anwendung mit einem OAuth-2.0-Authorization-Server verbindet. Jene, die lieber pures JavaScript verwenden, finden das dazugehörige JavaScript-Kompilat zum Download am Ende dieses Textes.
Der Konstruktor nimmt den AngularJS-Service $window, welcher ein Wrapper für das gleichnamige Browser-Objekt ist, entgegen. Dieser Service wird verwendet, um den Benutzer auf den Authorization-Server umzuleiten sowie um ein Access-Token aus dem Hash-Fragment der URL zu entnehmen. Die Eigenschaft isLoggedIn gibt Auskunft darüber, ob für den Benutzer ein Access-Token vorliegt. Diese Information kann in einer Eigenschaft verwaltet werden, da die gezeigte Klasse in weiterer Folge als AngularJS-Service zum Einsatz kommt und es somit lediglich eine Instanz davon geben wird.
Die Funktion initImplicitFlow initiiert den Implicit Flow, indem sie den Benutzer auf die Login-Seite des Authorization-Servers umleitet. Zuvor geniert sie jedoch eine zufällige Zeichenfolge und legt diese im Local-Storage ab. Darüber hinaus übergibt sie diese Zeichenfolge als URL-Parameter state an den Authorization-Server. Der Authorization Sever sendet diesen Parameter im Zuge seiner Antwort wieder an die SPA retour. Auf diese Weise kann die SPA sicherstellen, dass eine solche Antwort eine direkte Folge des zuvor initiierten Implicit Flows ist. Dies verhindert, dass Angreifer der Anwendung eine gefälschte Antwort unterschieben.
Die Funktion tryLogin prüft, ob die aktuelle URL ein Access-Token sowie einen Parameter state beinhaltet. Wurde beides gefunden, prüft tryLogin, ob der übergebene State dem zuvor gespeicherten State entspricht. Nur wenn dies der Fall ist, akzeptiert die gezeigte Funktion das Access-Token, legt es im Local-Store ab und setzt die Eigenschaft isLoggedIn auf true. Der Rückgabewert von tryLogin gibt an, ob ein Access-Token gefunden und akzeptiert wurde. Ist dem so, weist er den Wert true auf; ansonsten false.
Das von tryLogin akzeptierte Access-Token kann vom Entwickler später über getAccessToken abgerufen werden.
module oauth {
export interface IOAuthService {
getIsLoggedIn(): boolean;
initImplicitFlow(): void;
tryLogin(): boolean;
getAccessToken(): string;
}
export class OAuthService implements IOAuthService {
private isLoggedIn: boolean = false;
private $window: ng.IWindowService;
public constructor($window: ng.IWindowService) {
this.$window = $window;
}
public getIsLoggedIn(): boolean {
return this.isLoggedIn;
}
public initImplicitFlow(): void {
var state = this.createAndSaveState();
var url = "http://localhost:59978/oauth/authorize?response_type=token&client_id=myClient&state=" + state + "&redirect_uri=http://localhost:59978/";
this.$window.location.href = url;
}
public tryLogin(): boolean {
this.isLoggedIn = false;
var parts = this.getFragment();
var accessToken = parts["access_token"];
var state = parts["state"];
if (!accessToken || !state) return false;
var savedState = this.$window.localStorage.getItem("state");
if (savedState == state) {
this.$window.localStorage.setItem("access_token", accessToken);
this.$window.localStorage.removeItem("state");
this.isLoggedIn = true;
return true;
}
return false;
}
public getAccessToken(): string {
return this.$window.localStorage.getItem("access_token");
}
private getFragment() {
if (this.$window.location.hash.indexOf("#") === 0) {
var hash = this.$window.location.hash.substr(1);
if (hash.indexOf("/") == 0) hash = hash.substr(1);
return this.parseQueryString(hash);
} else {
return {};
}
}
private createAndSaveState(): string {
var state = this.createState();
this.$window.localStorage.setItem("state", state);
return state;
}
private createState() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 20; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
private parseQueryString(queryString) {
var data = {}, pairs, pair, separatorIndex, escapedKey, escapedValue, key, value;
if (queryString === null) {
return data;
}
pairs = queryString.split("&");
for (var i = 0; i < pairs.length; i++) {
pair = pairs[i];
separatorIndex = pair.indexOf("=");
if (separatorIndex === -1) {
escapedKey = pair;
escapedValue = null;
} else {
escapedKey = pair.substr(0, separatorIndex);
escapedValue = pair.substr(separatorIndex + 1);
}
key = decodeURIComponent(escapedKey);
value = decodeURIComponent(escapedValue);
data[key] = value;
}
return data;
}
}
}
Das nachfolgende Listing zeigt, wie die zuvor betrachtete Implementierung als AngularJS-Service registriert werden kann.
module oauth {
var oauthApp = angular.module("comp.oauth", []);
oauthApp.factory("oauthService", function ($location: ng.ILocationService, $window: ng.IWindowService) {
return new OAuthService($window);
});
}
Um diesen Service zu nutzen, fordert ihn der Entwickler über Dependency Injection an. Das nachfolgende Listing zeigt zum Beispiel einen Controller, in den der Service injiziert wird. Dieser Controller ruft tryLogin auf. War tryLogin erfolgreich, legt der Controller einen Eintrag mit dem Namen Authorization unter $http.defaults.headers.common an. Dies bewirkt, dass AngularJS bei jedem AJAX-Aufruf den festgelegten Wert im Rahmen des Kopfzeileneintrages Authorization überträgt. Dieser Wert besteht aus zwei Teilen, welche durch ein Leerzeichen getrennt werden. Der erste Teil beinhaltet den String Bearer. Damit zeigt die SPA dem Service an, dass sie sich über ein Access-Token (Bearer-Token = Token, welches dem Überbringer bestimmte Rechte einräumt) authentifiziert. Der zweite Teil beinhaltet das vom Authorization-Server erhaltene Access-Token.
Möchte der Entwickler das Access-Token nicht bei jedem Aufruf übergeben, kann er die hier betrachtete Zeile auch weglassen und diesen Header bei Bedarf vor dem Aufruf eines Services festlegen. War tryLogin nicht erfolgreich, ruft der hier gezeigte Controller die Funktion initImplicitFlow, welche den Benutzer zum Authorization-Server weiterleitet, auf.
app.controller("MainCtrl", function ($scope, oauthService, $http) {
if (oauthService.tryLogin()) {
$http.defaults.headers.common[Authorization] = Bearer + oauthService.getAccessToken();
} else {
oauthService.initImplicitFlow();
}
$scope.isLoggedIn = oauthService.getIsLoggedIn();
});
Hierbei ist zu beachten, dass sich der Benutzer nicht direkt bei der SPA anemeldet. Möchte die SPA herausfinden, um wem es sich beim aktuellen Benutzer handelt (z. B. um ihn direkt zu begrüßen) muss sie diese Info bei einem Backend-Service in Erfahrung bringen.