Inhaltsverzeichnis


Nichts verpassen?

angularjs ÔÇó security ÔÇó authentication | Marius Soutier ÔÇó | 9 Minuten

Authentifizierung ist ein Thema, das so gut wie jede gr├Â├čere Webanwendung betrifft. Da HTTP ein zustandsloses Protokoll ist, und damit keine Sessions kennt, muss bei jeder Anfrage eine Authentifizierungsinformation mitgeschickt werden. Damit sich ein Benutzer nur ein mal pro Session anmelden muss, soll diese Information erhalten bleiben und automatisch bei jedem Request mitgeschickt werden.

Konkret hei├čt das: Der Benutzer meldet sich bei der Webanwendung an und der Server ordnet dem authentifizierten Benutzer ein Sicherheitstoken zu (z.B. eine UUID), das auf Serverseite gespeichert wird. Das Token wird dem Client ├╝bermittelt, der sich dieses nun ÔÇťmerkenÔÇŁ muss, um es bei jeder weiteren Anfrage mitzuschicken. Der Server kann dann ├╝berpr├╝fen, ob das Token g├╝ltig ist.

Web-Frameworks bieten hier zwei klassische Vorgehensweisen: Sticky Sessions oder Cookies.

Sticky Sessions werden auf der Serverseite gehalten und ├╝ber einen URL-Parameter zugeordnet. Das f├╝hrt bei Anwendungen mit vielen parallelen Nutzern schnell zu Skalierbarkeitsproblemen, weil der Server f├╝r jeden parallelen Nutzer eine Zustandsinformation verwalten muss, die u.a. die Abbildung von der Session-ID auf die Sessiondaten enth├Ąlt. Das Problem wird dadurch noch versch├Ąrft, dass neben dem Token oft auch andere Daten in der Session gespeichert werden. Zudem sind die daraus entstehenden URLs in der Regel nicht ÔÇťbookmarkableÔÇŁ und der Zur├╝ck-Button des Browsers ├╝berf├╝hrt die Session eines Benutzers in den meisten F├Ąllen in einen inkonsistenten Zustand.

Alternativ kann der Server ein Cookie ausstellen, das das Sicherheitstoken enth├Ąlt. Der Browser schickt dieses Cookie dann automatisch bei jedem Request mit. Au├čer dem Skalierbarkeitsproblem entfallen damit zwar die anderen Probleme von Sticky Sessions, jedoch entsteht dabei ein Sicherheitsproblem: Wird das Cookie vom Server dazu genutzt, um Anfragen zu erlauben, ist es m├Âglich das Cookie zu stehlen und b├Âswillige Anfragen auszuf├╝hren.

Wie ist das m├Âglich? Angenommen ein Benutzer unserer Webanwendung landet beim Surfen auf einer Seite mit Bildern von s├╝├čen Hundewelpen. Doch die Macher der Seite haben hinterh├Ąltige Absichten und starten beim Laden der Seite mithilfe eines unsichtbaren Bildes Requests auf unseren Webservice. Da der Browser das Cookie mit dem Sicherheitstoken mitschickt, akzeptiert unsere Server-Anwendung diese Anfragen. Solche Attacken nennen wir auch Cross-Site-Request-Forgery (kurz: CSRF oder XSRF).

Single Page Web Applications to the Rescue!

Single Page Applications (SPA), wie man sie vorzugsweise mit AngularJS entwickelt, k├Ânnen diese Probleme komplett umgehen. Da unsere SPA nach dem initialen Ladevorgang die ganze Zeit im Speicher bleibt, k├Ânnen wir das vom Server erhaltene Token ohne Probleme bei jedem Request mitschicken. Dazu nutzen wir keine anf├Ąlligen Cookies, sondern einen eigenen HTTP-Header.

Damit sich der Benutzer beim Neuladen der Seite (neuer Tab o.├Ą.) nicht erneut anmelden muss, k├Ânnen wir das Token nach wie vor im Cookie oder Local Storage ablegen. Dieses Cookie wird zwar auch an den Server geschickt. Dieser ignoriert es aber und somit entsteht keine Sicherheitsl├╝cke.

Auch f├╝r diesen Ansatz gilt nat├╝rlich, dass man eine verschl├╝sselte Verbindung ├╝ber HTTPS bevorzugen sollte.

Die folgende Abbildung verdeutlicht nochmals den Ablauf.

Security Workflow

Auth-Token in AngularJS

Das klingt doch schon mal recht vielversprechend, aber wie geht das nun genau im Kontext von AngularJS? M├╝ssen wir uns etwa bei jedem Request selbst darum k├╝mmern, dass der Header mitgeschickt wird? Es w├Ąre doch super, wenn das Framework uns diese Arbeit abnimmt. Und das tut AngularJS nat├╝rlich!

Zun├Ąchst bietet uns der $http-Service die M├Âglichkeit, f├╝r jeden Request einen Header zu setzen. Der $httpProvider (das Modul, das den $http-Service injiziert) hat ein defaults.headers-Objekt, das wiederum Unterobjekte f├╝r die ├╝blichen HTTP-Verben bietet. M├Âchten wir den Header an alle Requests h├Ąngen, gibt es daf├╝r das Unterobjekt common:

$http.post("/login", credentials).then(function(response) {
  $httpProvider.defaults.headers.common["X-AUTH-TOKEN"] = response.data.token;
});

Da es sich aber um eine recht ├╝bliche Anforderung handelt, bringt $http einen Auth-Token-Mechanismus von Hause aus mit. Dabei muss der Server einfach nur ein Cookie namens XSRF-TOKEN ausliefern. AngularJS liest dieses Cookie automatisch aus und setzt den Header X-XSRF-TOKEN. Wird das Cookie vom Server oder Client entfernt, setzt AngularJS den Header nicht mehr. Damit ist dieser Teil erledigt.

Eine kleine Anmerkung am Rande: Die Dokumentation erw├Ąhnt, dass das Cookie nach dem ersten GET-Request gesetzt werden soll. Das ist nicht ganz korrekt, da das Cookie auch nach allen anderen HTTP-Anfragen (also POST, PUT, etc.) erkannt wird. Das stammt daher, dass klassische Round-Trip-Webanwendungen nach dem Login erst mal ein Session-Cookie ausstellen und zus├Ątzlich das XSRF-Cookie nutzen, um Missbrauch zu verhindern. Wir beschr├Ąnken uns hier jedoch auf ein einzelnes Cookie.

Auf abgelaufene Token reagieren

Solange das Token g├╝ltig ist, klappt alles wunderbar. Eine ordentliche Backend-Implementierung sollte eine bestehende Nutzer-Session jedoch nach einer bestimmten inaktiven Zeit invalidieren. Was passiert also, wenn das Token abl├Ąuft?

$http bietet einige Methoden um GET-, POST-, PUT-, DELETE- und HEAD-Requests bequemer auszuf├╝hren. Diese Methoden liefern nicht nur eine Promise zur├╝ck, sondern eine erweiterte Promise, die die Methoden success(callbackFn) und error(callbackFn) bietet und somit Method-Chaining (bekannt aus jQuery) erlaubt. Au├čerdem wird die Response schon destrukturiert.

$http.get("/users/3")
.success(function(data, status, headers, response) {
  $scope.user = data;
})
.error(function(data, status) {
  if (status == 401)
    // Zur Login-Seite
  else
    // Fehlermeldung anzeigen
});

Nicht schlecht, aber nat├╝rlich wollen wir nicht bei jeder Anfrage abfragen, ob der Server mit 401 geantwortet hat. Auch hier bietet uns AngularJS eine Hilfestellung. Wir k├Ânnen beim $httpProvider einen so genannten HTTP-Interceptor anmelden. Ein Interceptor f├Ąngt jede Response ab und entscheidet, ob die Response an die aufrufende Funktion weitergeleitet wird oder nicht. Ein Interceptor ist dabei nichts anderes als eine Funktion, die eine Promise ├╝bermittelt bekommt. Status-Codes im 200er-Bereich werden dabei als erfolgreiche (resolved) Promise ├╝bergeben, alle anderen Codes sind nicht-erfolgreich (rejected). Auf Basis unserer eignenen Logik k├Ânnen wir darauf reagieren oder sogar die Promise ├Ąndern.

var interceptor = function() {
  // Die Promise enth├Ąlt eine Response; wir m├╝ssen wieder eine Promise zur├╝ckliefern
  return function(promise) {
    return promise.then(
      function(response) { return response;}, // alles ok, dabei belassen wir es
      function(response) {
        if (response.status == 401) {
          // Zur Login-Seite
        }
        return $q.reject(response);
      }
    );
  };
};
$httpProvider.responseInterceptors.push(interceptor);

Man kann mithilfe von HTTP-Interceptoren jede Menge Nettigkeiten einbauen, um innerhalb unserer Anwendung intelligent mit Fehlern umzugehen. Beispielsweise k├Ânnten wir bei einem 401 direkt ein Login-Fenster anzeigen und nach get├Ątigtem Login den urspr├╝nglichen Request erneut abschicken (dies wird mit Angular 1.2 und around-interceptors noch einfacher). Ein weiterer Anwendungsfall k├Ânnte sich dadurch ├Ąu├čern, dass wir bei einem 404 mithilfe des Exponential Backoff-Algorithmus Timeout-Zeiten berechnen und den Request nach Ablauf des jeweiligen Timeouts erneut stellen und im Erfolgsfall die Daten nachladen.

Routing

Wenn man mit Routen arbeitet, bietet es sich an, schon vor dem Laden der Route abzufragen, ob der Nutzer autorisiert ist, die angeforderte Seite anzuschauen. Beim Konfigurieren der Routen kann man dazu einen weiteren Parameter resolve ├╝bergeben. Dieser Parameter muss mit einem Objekt gef├╝llt werden, das pro selbst gew├Ąhltem Key eine Funktion anbietet, die beim Laden der Route aufgerufen wird. Gibt die Funktion eine Promise zur├╝ck, so entscheidet das Ergebnis (resolve oder reject) der Promise, ob die Route geladen wird. Im folgenden Code-Beispiel pingen wir den Server einfach an, der wiederum ├╝berpr├╝ft, ob ein Token gesetzt ist und wie gehabt mit 200 oder 401 antwortet.

$routeProvider.when("/users/:id", { templateUrl:'/user.html', controller:UserCtrl, resolve:{
    authorize:function($http) {
      return $http.get("/ping"); // $http.get liefert eine Promise zur├╝ck
    }
  }
})

Antwortet der Server nun mit 401, wird das Event $routeChangeError gefeuert, auf das wir nun reagieren k├Ânnen. Da uns mit nextRoute die angeforderte Route ├╝bergeben wird, k├Ânnen wir uns diese merken und nach get├Ątigtem Login wieder ansteuern.

$scope.$on("$routeChangeError", function(event, nextRoute, currentRoute) {
  // Zur Login-Seite
  $rootScope.nextRoute = nextRoute; // oder in einem Service speichern
});

Datei-Uploads

Eine letzte Herausforderung ist das Hochladen von Dateien. Der XMLHttpRequest unterst├╝tzt das Hochladen von Dateien nicht, bzw. erst in Version 2 des Protokolls, das aber erst ab IE10 zur Verf├╝gung steht. Ein klassischer Trick ist daher das Posten in ein iFrame (f├╝r AngularJS gibt es daf├╝r z.B. das recht einfach gehaltene Modul ngUpload).

Wie k├Ânnen wir aber nun unseren Upload autorisieren? Einen Header k├Ânnen wir nicht mitschicken, also sollten wir unseren Webservice so erweitern, dass das Auth-Token auch in der URL oder als Formular-Wert mitgeschickt werden kann. Der URL-Ansatz empfiehlt sich hier, da der Service dann nicht erst den HTTP-Body parsen muss, um zu entscheiden, ob der Request erlaubt ist oder nicht.

Zusammenfassung

Sicherheit und Benutzerverwaltung sind in AngularJS auch nicht komplizierter als in klassischen Webanwendungen. Wie wir gesehen haben, sind doch einige nette Tricks m├Âglich, ohne dass man einen absurd gro├čen Programmieraufwand h├Ątte. Auch die guten alten HTTP-Statuscodes sind in modernen SPAs noch gut zu gebrauchen.

Schaut euch einfach das Beispielprojekt an und bei weiteren Fragen nehmt gerne Kontakt auf.