Bearer Token Authentifizierung [1] in Webclients unterstützen.

UI und Authentifizierung

Eine im Browser verwendbare Bedienoberfläche (das User Interface, UI) liefert Bedienelemente zum Benutzer hin aus und stellt Zustand und Zustandswechsel dar. Sind Ressourcen des Servers nur für bestimmte Nutzer vorgesehen, ist vor Lieferung dieser Ressourcen eine Authentifizierung erforderlich, mit der die Identität der Nutzer festgestellt und entschieden werden kann, ob eine Ressource ausgeliefert werden darf.

Werden für jeden Zustandswechsel nicht jeweils ganze HTML-Seiten vom Server zum Client gesendet, gerät die Bedienung flüssiger und dynamischer. Ein solcher Webclient kommuniziert im Hintergrund asynchron mit dem Server, der Austausch mit dem Server erfolgt entkoppelt von den Client-Prozessen.

Damit dies auch für die Authentifizierung gelingt, ergeben sich zusätzliche Anforderungen, die von manchen Authentifizierungsmethoden nicht abgedeckt werden. Nachfolgend verschiedene Authentifizierungsmethoden.

Basic Authentication

Bei der Basic Authentication signalisiert der Server in seiner Antwort, dass der Browser eine Authentifizierung durchführen soll. Der Browser führt die Authentifizierung mit eigenen Mitteln durch, die von Browser zu Browser unterschiedlich implementiert sind und in der Regel einen einfachen modalen Dialog beinhalten, der vom Browser gezeigt wird. Die Authentifizierung bricht damit aus der Bedienoberfläche der Anwendung aus.

Form Based Authentication

Bei der Form Based Authentication erzeugt der Server ein Anmeldeformular, das anstelle der eigentlich gewünschten Seite an dem Browser geschickt wird. Ist die Anmeldung erfolgreich, sendet der Server die ursprünglich gewünschte Seite als Antwort auf die erfolgreiche Authentifizierung.

Diese Methode zur Authentifizierung bricht aus der Bedienoberfläche der Anwendung aus und sendet komplett neue Seiten zum Browser.

Bearer Authentication

Bei der Bearer Authentication, Token Authentication oder Bearer Token Authentication signalisiert der Server dem Client, dass eine Authentifizierung erforderlich ist. Der Server liefert lediglich Service-Endpunkte zur Authentifizierung in Form einer Programmschnittstelle und erwartet, dass diese Programmschnittstelle vom Client verwendet wird.

Der Client kann mit Hilfe der Programmschnittstelle eine Authentifizierung dynamisch im Hintergrund durchführen. Zum Benutzer hin kann der Client für die Authentifizierung eine Bedienoberfläche selbst erzeugen und so dasrstellen, dass sie zur Anwendung passt. Es müssen nicht ganze Seiten nachgeladen werden.

Die nachfolgenden Ausführungen beschreiben im Detail, wie ein Webclient die Bearer Token Authentication implementieren kann.

Client-Implementierung

Bei der Bearer Token Authentication [1] muss der Client bei Aufforderung des Servers eine Authentifizierung ausführen und erhält im Gegenzug einen Zugangscode (Token), den sich der Client 'merken' und jeder Anfrage als Beleg einer gültigen Authentifizierung mitgeben muss. Zusammen mit dem Zugangscode erhält der Client einen Erneuerungs-Code (Refresh Token), mit dem die Gültigkeit des Zugangscode während einer Sitzung ohne neuerliche Authentifizierung verlängert werden kann.

Ein Client, der die Bearer Token Authentifizierung unterstützt, muss Funktionen zur Erkennung von Aufforderungen zur Authentifizierung und zum Aufruf der Authentifizierungs-Funktionen besitzen wie sie nachfolgend beschrieben sind.

HTTP-Aufrufe prüfen

Alle HTTP-Aufrufe des Clients müssen prüfen, ob die Anwort eine Authentifizierung verlangt. Clients können das am Status der Antwort erkennen, folgende Ausprägungen sind möglich:

  1. die Antwort meldet einen Status 401 Unauthorized

  2. die Antwort meldet einen Status 403 Forbidden

  3. die Antwort ist nicht 401 und nicht 403

Der folgende Code implementiert diese Prüfung in JavaScript.

eine HTTP-Antwort im Hinblick auf Authentifizierung verarbeiten
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (this.readyState === 4) {
    if(this.status === 401) {
      // Unauthorized erfordert eine Anmeldung oder Erneuerung
      if(accessToken !== '') {
        refresh(xhr.getResponseHeader('WWW-Authenticate'));
      } else {
        login_form();
      }
    } else if(this.status === 403) {
      // wenn Forbidden, passt die Rolle nicht
      login_form();
    } else {
      // keine Anmeldung noetig, Antwort normal verarbeiten
      handle_response(this.responseText, this.status);
    }
  } else {
    // readyState !== 4 handhaben, wenn noetig
  }
};

Wenn die Antwort den Status 401 Unauthorized meldet, ist eine Authentifizierung erforderlich. Im obigen Code wird in diesem Fall geprüft, ob bereits eine Authentifizierung stattfand. Wenn noch keine Authentifizierung stattfand, muss diese mit Aufruf der Funktion login_form() ausgeführt werden. Ist dagegen bereits eine Authentifizierung erfolgt (accessToken !== ''), muss der vermutlich abgelaufene Access Token mit der Funktion refresh(..) erneuert werden.

Bei einem Status 403 Forbidden hat der Access Token nicht die erforderliche Rolle. Es wird ebenfalls login_form() aufgerufen und erwartet, dass eine Authentifizierung mit einem Benutzerkonto erfolgt, das die passende Rolle hat.

Login

Wird eine HTTP-Anfrage mit Status Unauthorized (401) beantwortet, muss der Client ein Login-Formular zeigen, in das der Nutzer eine Benutzerkennung und ein Kennwort zur Authentifizierung eintragen kann. Im vorangegangenen Beispiel wird dies durch Aufruf der Funktion login_form veranlasst.

Hierbei wird angenommen, dass die Funktion login_form das Anmeldeformular auf der Seite des Clients erzeugt. Webclients verwenden hierfür in der Regel einen Vorlagenmechanismus wie z.B. Mustache [7], mit dem der erforderliche HTML-Code dynamisch erzeugt und programmatisch via JavaScript in die Bedienoberfläche eingefügt wird.

Die Benutzerkennung und das Kennwort werden als Parameter name und password im Body einer HTTP POST Anfrage gesendet. Für die Beschreibung in diesem Dokument wird angenommen, dass die Funktion login_form ein Anmeldeforumlar erzeugt und darstellt, das die Eingabefelder wie folgt enthält:

Formularfelder für ein Anmeldeformular
<input name="name" type="text" class="form-control" placeholder="Benutzerkennung" required autofocus>
<input name="password" type="password" class="form-control" placeholder="Kennwort" required>

Der obige Inhalt kann mit einer Funktion wie der folgenden gelesen werden:

Benutzerkennung und Kennwort aus dem Anmeldeformular lesen und zum Server senden
  // Annahme: 'html' enthaelt den HTML-Code
  // fuer das Anmeldeformular, wie es
  // vom Vorlagen-Mechanismus erzeugt wurde
  document.querySelector('.zentraler-inhalt').innerHTML = html;
  var form = document.getElementById('loginform');
  form.addEventListener('submit', function(e) {
    e.preventDefault();
    var formData = new FormData(e.target);
    var value = Object.fromEntries(formData.entries());
    var daten = JSON.stringify(value);
    login(daten);
  });

Im obigen Code wird der HTTP POST Aufruf des Anmeldeformulars unterdrückt, damit nicht die gesamte Seite neu geladen wird. Der Inhalt des Anmeldeformulars wird stattdessen programmatisch ausgelesen und als JSON-Ausdruck der Client-Funktion login übergeben. Die Client-Funktion login ruft den Service-Endpunkt /login auf, der auf der Serverseite an den BearerLoginService [5] gebunden ist.

der Aufruf zur Authentifizierung
function login(daten) {
  //daten enthaelt etwas wie '{"name": "fred", "password": "geheim"}';
  http_post('mein-app-kontext/login', daten, function(antwort, status) {
    login_antwort(antwort, status);
  });
}

http_post im obigen Beispiel ist eine Hilfsfunktion, die den Aufruf von HTTP POST Anfragen in JavaScript vereinfacht und hier nicht näher beschrieben ist. Die Antwort der Authentifizierungsanfrage wird bei Erhalt an die Funktion login_antwort weitergereicht.

die Antwort der Authentifizierung verarbeiten
var accessToken;
var refreshToken;
function login_antwort(antwort, status) {
  if(status === 200) {
    var cred = JSON.parse(antwort);
    accessToken = cred.access_token;
    refreshToken = cred.refresh_token;
    folgefunktion();
  } else if(status === 406) {
    // wenn 406 Not Acceptable war das kein gueltiger Nutzer
    // oder kein gueltiges Kennwort
    login_form();
  } else {
    // etwas anderes handhaben
  }
}

Die oben dargestellte Funktion login_antwort prüft, ob der HTTP-Statuscode OK (200) vorliegt. In diesem Fall werden der Access-Token und der Refresh-Token aus der Antwort entnommen und zwischengespeichert. Anschließend wird die Funktion folgefunktion aufgerufen, die im Code-Beispiel als Stellvertreter einer Funktion steht, die mit erfolgreicher Authentifizierung als Nächstes aufgerufen werden soll.

Kommt die Antwort stattdessen mit dem HTTP-Statuscode Not Acceptable (406) zurück, wurde kein gültiger Nutzer oder kein gültiges Kennwort angegeben. In diesem Fall wird der Benutzer erneut aufgefordert, sich zu authentifizieren, indem die Funktion login_form erneut aufgerufen wird.

Authentifizierte Anfragen

Alle HTTP-Anfragen der Anwendung müssen nach erfolgreicher Authentifizierung den Access Token mitsenden, der beim Login erteilt wurde. Eine zentrale Funktion zum Senden von HTTP-Anfragen wie die folgende berücksichtigt den Access Token entsprechend:

HTTP-Anfragen mit Access Token senden
function http_call(method, callurl, data) {
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = auth_handler;
  xhr.open(method, callurl);
  if(accessToken !== '') {
    xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
  }
  if (method === 'GET') {
    xhr.send();
  } else if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
    xhr.send(data);
  }
}

Wurde auf der Serverseite ein Endpunkt mit einem BearerAuthenticator versehen, prüft der Server jede Anfrage auf gültigen Access Token. Im obigen Codebeispiel wird deshalb mit der Funktion setRequestHeader des XMLHttpRequest der Access Token spezifikationsgemäß gesetzt. Auf diese Weise kann der Server erkennen, welcher Nutzer die Anfrage sendet und entscheiden, ob eine Autorisierung für den Aufruf erteilt werden kann.

Die Funktion auth_handler im obigen Beispiel enthält die zuvor beschriebene Logik zur Prüfung der Antwort auf Authentifizerungsaufforderungen.

Authentifizierung erneuern

Eine Authentifizierung ist gewöhnlich mit einer Gültigkeitsdauer verbunden. Der Server prüft, ob ein Access Token noch gültig ist. Nach Ablauf der Gültigkeit sendet der Server Antworten mit Statuscode Unauthorized (401) und erwartet, dass der Client den Access Token erneuert.

Die Spezifikation der Bearer Token Authentication [1] sieht vor, dass Clients in einem solchen Fall eine HTTP-Anfrage zur Erneuerung des Access Token senden. Hierbei muss der Benutzer nicht erneut Benutzername und Kennwort angeben. Der Client sendet zur Authentifizierung der Erneuerungs-Anfrage den Refresh Token, der beim Login erteilt wurde.

Dieser Mechanismus ist bereits bei der Prüfung aller HTTP-Antworten der Anwendung berücksichtigt. Dort wird die Funktion refresh aufgerufen, wenn der Client eine Antwort mit Statuscode Unauthorized (401) erhält und bereits ein Access Token existiert.

Es wird in diesem Fall die Funktion refresh gerufen. Diese erhält als Parameter den Response Header WWW-Authenticate übergeben. Das folgende Beispiel zeigt die Verwendung:

eine Refresh-Anfrage senden
function refresh(authHeader) {
  if(authHeader !== null && authHeader.indexOf('invalid_token') > -1 && authHeader.indexOf('expired') > -1) {
    var daten = 'grant_type=refresh_token';
    daten += '&refresh_token=' + self.refreshToken;
    daten += '&client_id=';
    daten += '&client_secret=';
    http_post('mein-app-kontext/refresh', daten , function (antwort, status) {
      login_antwort(antwort, status);
    });
  }
}

Im obigen Beispiel wird geprüft, ob der WWW-Authenticate Header die Bestandteile enthält, die einen Refresh des Access Tokens erfordern. Ist das der Fall, wird eine HTTP POST Anfrage zusammengesetzt, die gemäß Spezifikation [1] zum Refresh dient und den erforderlichen Refresh Token enthält. Ist der Refresh erfolgreich, lautet die Antwort so wie beim Login und enthält einen neuen Access Token sowie einen neuen Refresh Token. Die Antwort kann wie schon beim Login mit der Funktion login_antwort verarbeitet werden.

Zusammenfassung

Webclients und Server müssen zusammenspielen, um eine Authentifizierung zu implementieren, die der Spezifikatikon für Bearer Token Authentication [1] entspricht. Auf der Seite des Clients sind die folgenden Bestandteile erforderlich:

  1. eine zentrale Funktion zum Senden von HTTP-Anfragen mit Access Token und zum Verarbeiten der Authentifizierungs-Aufforderungen des Servers

  2. eine Funktion für den Login

  3. eine Funktion zum Erneuern eines Access Token

  4. eine Funktion zum Verarbeiten der Login- und Refresh-Antwort

Nachfolgend eine Übersicht der entsprechenden Funktionen auf der Seite von Client und Server.

Aufgabe Server Client

Authentifizierung einschalten

In der Serverbeschreibung einen Authenticator konfigurieren [4]

Antworten aller HTTP-Anfragen prüfen wie im Beispiel

Login

einen BearerLoginService [5] an den login Endpunkt des Kontext binden

eine HTTP POST Anfrage mit name und password im Body an den login Endpunkt senden und die Antwort wie im Beispiel login_antwort verarbeiten

Refresh

einen BearerRefreshService [6] an den refresh Endpunkt des Kontext binden

Anfragen mit Token, die mit HTTP-Statuscode 401 beantwortet werden mit einer Refresh-Anfrage wie im Beispiel eine Refresh-Anfrage senden verarbeiten

In diesem Dokument ist beschrieben, wie die Client-Seite der Bearer Token Authentication gemäß Spezifikation implementiert werden kann. Soll ein Server diese Form der Authentifizierung einsetzen, können Neon [2] und dessen Modul neon-auth [3] zur Implementierung der Server-Seite dienen.

Fazit

Die Bearer Token Authentication ist die ideale Methode, um Webclients eine individuelle Authentifizierung zu ermöglichen, die sich nahtlos in eine Webanwendung einpasst. Mit Neon [2] und dessen Modul oauth [3] steht eine komplette serverseitige Implementierung dieser Authentifizierungsmethode bereit.

Gemeinsam bilden sie die Grundlage für schlanke und unabhängige Anwendungen auf der Basis von Webtechnologie.

Versionshistorie

Version 1 vom 27. November 2022: Initiale Fassung.

Version 2 vom 21. Februar 2024: Anpassung an Neon 2.

Verweise

[1] Spezifikation der Bearer Token Authentication

[2] Produktseite von Neon

[5] BearerLoginService des Moduls neon-auth

[6] BearerRefreshService des Moduls neon-auth

[7] Artikel zu Mustache auf heise online