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:
-
die Antwort meldet einen Status 401 Unauthorized
-
die Antwort meldet einen Status 403 Forbidden
-
die Antwort ist nicht 401 und nicht 403
Der folgende Code implementiert diese Prüfung in JavaScript.
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:
<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:
// 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.
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.
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:
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:
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:
-
eine zentrale Funktion zum Senden von HTTP-Anfragen mit Access Token und zum Verarbeiten der Authentifizierungs-Aufforderungen des Servers
-
eine Funktion für den Login
-
eine Funktion zum Erneuern eines Access Token
-
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 |
eine |
Refresh |
einen |
Anfragen mit Token, die mit HTTP-Statuscode |
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
[3] Modul neon-auth
[5] BearerLoginService des Moduls neon-auth
[6] BearerRefreshService des Moduls neon-auth
[7] Artikel zu Mustache auf heise online