Neon [1] ist ein ultrakompakter, modular aufgebauter HTTP-Server auf der Grundlage des Java-Moduls jdk.httpserver zum Einbetten in Apps und Microservices. In diesem Dokument ist beschrieben, wie die Erweiterung zur Authentifizierung neon-auth eingesetzt wird.

Authentifizierung

Authentifizierung ist der Nachweis der Indentität eines Menschen, eines Geräts, einem Dokument oder einer Information [3].

Im Rahmen der Authentifizierung wird geprüft, ob eine Anfrage von einem authentifizierten Nutzer kommt. Es wird nicht geprüft, ob der Nutzer autorisiert ist, die Anfrage auszuführen.

Neon [1] implementiert die Authentifizierung mit Hilfe eines Authenticator. Abgeleitet von com.sun.net.httpserver.Authenticator werden mit Verwendung eines Authenticators alle HTTP-Anfragen nur verarbeitet, wenn sie von authentifizierten Nutzern stammen. Das Modul neon-auth erweitert Neon [1] hierbei um eine Möglichkeit der Authentifizierung nach RFC 6750 Bearer Token Authentication [5].

In diesem Dokument ist die Verwendung des Moduls neon-auth auf der Seite des Servers beschrieben. Im Dokument Authentifizierung für Webclients [10] wird zudem betrachtet, wie die Authentifizierung auf der Seite des Clients implementiert werden kann.

Authentifizierung einschalten

Zum Einschalten der Authentifizierung für einen bestimmten HTTP-Service-Endpunkt kann die Klasse BearerAuthenticator dienen. Sollen ein oder mehrere HTTP-Kontexte eine Authentifizierung erfordern, wird in der Serverbeschreibung [4] ein Eintrag für den Authenticator angelegt.

Beispiel für die Konfiguration eines BearerAuthenticator
  "authenticator": {
    "className": "de.uhilger.neon.auth.BearerAuthenticator",
    "attributes": {
      "loginTL": 10000,
      "expireSeconds": 7200,
      "refreshExpireSeconds": 86400,
      "refreshSeconds": 3600,
      "loginRoute": "/login",
      "refreshRoute": "/refresh",
      "authenticatorName": "myAuth"
    }
  }

Das Element authenticator wird nur einmal in einer Serverbeschreibung angegeben und besitzt die folgenden Eigenschaften.

className

Die Klasse des Authenticators

loginTL

Die Wartezeit in Millisekunden zwischen zwei beliebigen Logins. Nach jedem Aufruf eines Login werden weitere Login-Versuche über alle Nutzer hinweg für die hier angegebene Wartezeit pausiert. Dies bremst Brute Force Attacken.

expireSeconds

Die Gültigkeit eines Authentifizerungs-Token in Sekunden

refreshExpireSeconds

Die Gültigkeit eines Refresh-Tokens in Sekunden

refreshSeconds

Die Anzahl von Sekunden, bis ein Authentifizierungs-Token zum Refresh aufgefordert wird.

loginRoute

Die Route, die für die Anmeldung über diesen Authenticator genutzt wird. Aufrufe dieser Route sind zur Bindung an einen BearerLoginService vorgesehen und werden vom Authenticator ohne gültigen Authentifizierungs-Token zur Verarbeitung weitergegeben.

refreshRoute

Die Route, die für die Erneuerung eines Authentifizierungs-Token genutzt wird. Aufrufe dieser Route sind zur Bindung an einen BearerRefreshService vorgesehen und werden vom Authenticator ohne gültigen Authentifizierungs-Token zur Verarbeitung weitergegeben.

authenticatorName

Der Name, über den der Authenticator an HTTP-Kontexte gebunden wird.

HTTP-Kontexte, für deren Nutzung eine Authentifizierung vorgesehen ist, erhalten in der Serverbeschreibung [4] einen Verweis auf diesen Authenticator.

Beispiel für das Einschalten der Authentifizierung für einen HTTP-Kontext
"server": [
  {
    "name": "External Server",
    "port":7000,
    "contexts": [
      {
        "className": "de.uhilger.neon.Handler",
        "sharedHandler": "true",
        "contextPath": "/pfad/zum/kontext",
        "authenticator": "myAuth",
        "attributes": {
          "contextName": "admin"
        }
      }
    ]
  }
]

Die Eigenschaft "authenticator": "myAuth" im Element contexts verweist auf die zuvor dargestellte Konfiguration des Authenticators und bewirkt, dass alle Anfragen, die an diesen HTTP-Kontext gerichtet sind, nur nach erfolgreicher Authentifizierung gelingen.

Benutzerverzeichnis

Ein beliebig beschaffenes Benutzerverzeichnis enthält in der Regel die Angaben über Bereiche, die eine Authentifizierung erfordern, autorisierte Benutzer und deren Rechte. Für die Verwendung von neon-auth ist es vorgesehen, über die Schnittstelle Directory derartige Benutzerverzeichnisse in den Ablauf der Authentifizierung einzubinden.

die Schnittselle Directory
public interface Directory {
  public boolean isValid(String userId, String password);
  public boolean hasRole(String userId, String roleId);
}

Mit der Klasse SimpleDirectory ist eine sehr einfache Implementierung eines solchen Benutzerverzeichnisses enthalten, dessen Inhalt aus einer Datei gelesen wird.

Beispiel für die Nutzung einer Datei mit Nutzerdaten über die Klasse SimpleDirectory
SimpleDirectory directory = new SimpleDirectory();
directory.readFromFile(new File("conf", "my.directory"));
directory.setName("/meine-app");

In der Datei my.directory erwartet die Klasse SimpleDirectory Einträge in folgender Struktur.

Struktur der Einträge im Benutzerverzeichnis für die Klasse SimpleDirectory
# Benutzer meiner App
# [userId]=[password],[roleId],[roleId],etc.
test=test,testRolle
ulrich=geheim,testRolle,andereRolle
fred=undisclosed,nutzerRolle,adminRolle

Der Ausdruck vor dem Gleichheitszeichen ist die Nutzer-Kennung, die zur Anmeldung dient. Der erste Eintrag nach dem Gleichheitszeichen ist das Kennwort, gefolgt von einer Liste mit Komma getrennter Rollen-IDs. Eine Autorisierung auf der Grundlage von Rollen kann mit diesen Angaben erfolgen, ist aber nicht Teil der Implementierung von neon-auth.

neon-auth liefert mit der Schnittstelle Directory und dem Beispiel SimpleDirectory den Ausgangspunkt für andere Implementierungen wie beispielsweise gegen eine Datenbank oder einen Web-Service. Erweiterungen oder Abwandlungen der Klasse SimpleDirectory sollten eine Form der Verschlüsselung für das Kennwort einbauen, damit es nicht im Klartext für andere lesbar vorliegt.

Login

Ist ein HTTP-Kontext mit der Klasse BearerAuthenticator verbunden, wird gemäß Spezifikation [5] eine Antwort mit Status 401 Unauthorized gesendet, wenn eine HTTP-Anfrage ohne Authentifizerung für diese Ressource eingeht.

Antwort auf eine nicht authentifizierte Anfrage einer Ressouce, die eine Authentifizerung erfordert
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"

Der Client muss dann einen HTTP-Service-Endpunkt aufrufen, der eine Authentifizierung ermöglicht. Ein solcher Endpunkt zum Login kann mit der Klasse BearerLoginService mit Hilfe eines Actors wie in folgendem Beispiel implementiert werden.

Beispiel für einen Service-Endpunkt zur Authentifizierung
@Actor(name = "loginActor")
public class LoginActor extends ConsumerActor {

  @Action(handler = {"admin"}, route = "/login", type = Action.Type.POST, handlesResponse = true)
  public void run(HttpExchange exchange) throws IOException {
    Object o = getData("user.directory");
    if (o instanceof Directory) {
      new BearerLoginService((Directory) o).login(exchange);
    }
  }

}

Ein solcher Login-Actor muss einem HTTP-Kontext zugeordnet sein, der eine Authentifizierung erfordert und dem zu diesem Zweck ein BearerAuthenticator zugeordnet wurde.

Wichtig

Der Eintrag route="/login" der Action-Annotation muss mit dem Eintrag im Attribut loginRoute der Konfiguration des Authenticators übereinstimmen.

Über /pfad/zum/kontext/login kann eine Benutzeranmeldung erfolgen. Der BearerLoginService erwartet den Aufruf per HTTP POST mit {"name": "fred", "password": "secret"} im Body der Anfrage. Ist die Anmeldung erfolgreich, sendet der BearerLoginService die Antwort selbsttätig gemäß der Bearer Token Spezifikation [5]. Deshalb muss der Actor handlesResponse=true setzen und als Parameter den HttpExchange verlangen wie im Beispiel gezeigt.

die Antwort auf eine erfolgreiche Authentifizierung
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"mF_9.B5f-4.1JqM",
  "token_type":"Bearer",
  "expires_in":3600,
  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA"
}

Ein Authenticator kann auf diese Weise an mehrere HTTP-Kontexte gebunden sein, woraufhin diese jeweils eine Authentifizierung verlangen. Es genügt, einen der betreffenden HTTP-Kontexte mit einem Actor für das Login zu versehen. Auf diese Weise durchgeführte Anmeldungen gelten für alle HTTP-Kontexte, die diesen Authenticator verwenden.

Der BearerLoginService überprüft Nutzerangaben gegen ein Benutzerverzeichnis, das die Schnittstelle Directory implementiert. Eine Anwendung muss selbst dafür sorgen, ein solches Benutzerverzeichnis bereitzuhalten. Im Beispiel ist dies mit der Methode getData der Basisklasse ConsumerActor [9] realisiert, die die Schnittstelle DataProvider [8] implementiert.

Refresh

Ist ein Authentifizierungs-Token abgelaufen, erhält der Client bei Anfrage einer Ressource, die eine Authentifizierung erfordert, eine Ablehnung.

Antwort auf die Anfrage einer Ressource, die eine Authentifzierung erfordert, wenn die Authentifizierung nicht mehr gültig ist
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
                    error="invalid_token",
                    error_description="The access token expired"

Bei der Bearer Token Authentication muss der Client den Refresh Token aus der letzten Authentifizierung verwenden, um die Authentifizierung zu erneuern, was dem Benutzer während einer längeren Sitzung erspart, sein Kennwort erneut einzugeben.

Zum Erneuern der Authentifizierung muss der Client einen HTTP-Service-Endpunkt aufrufen, der die Erneuerung der Authentifizierung ermöglicht. Ein solcher Endpunkt kann mit der Klasse BearerRefreshService mit Hilfe eines Actors wie in folgendem Beispiel implementiert werden.

Beispiel für einen Service-Endpunkt zur Erneuerung der Authentifizierung
@Actor(name = "refreshActor")
public class RefreshActor {

  @Action(handler = {"admin"}, route = "/refresh", type = Action.Type.POST, handlesResponse = true)
  public void run(HttpExchange exchange) throws IOException {
    new BearerRefreshService().refresh(exchange);
  }

}

Ein solcher Refresh-Actor muss einem HTTP-Kontext zugeordnet sein, der eine Authentifizierung erfordert und dem zu diesem Zweck ein BearerAuthenticator zugeordnet wurde.

Wichtig

Der Eintrag route="/refresh" der Action-Annotation muss mit dem Eintrag im Attribut refreshRoute der Konfiguration des Authenticators übereinstimmen.

Über /pfad/zum/kontext/refresh kann eine Erneuerung der Authentifizierung erfolgen. Der BearerRefreshService erwartet darüber eine HTTP-Anfrage im spezifikationsgemäßen Format [5] wie folgt:

Anfrage zum Erneuern einer Authentifizierung
POST /meine-app/refresh HTTP/1.1
Host: example.com

grant_type=refresh_token
&refresh_token=xxxxxxxxxxx
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx

Ist der Refresh Token gültig, antwortet der BearerRefreshService so wie beim Login mit einem neuen Satz Tokens.

Voraussetzungen, Lizenz

neon-auth wird unter den Bedingungen der GNU Affero General Public License [11] bereitgestellt.

Das Modul basiert auf JJWT [6] sowie Gson [7] und erfordert neben der Datei neon-auth.jar die Dateien jjwt-api.jar, jjwt-impl.jar, jjwt-gson.jar sowie gson.jar.

Verweise

[5] RFC 6750 Bearer Token Authentication

[6] JJWT JSON Web Token für Java

[7] Gson: Java Objekte von JSON deserialisieren und nach JSON serialisieren

Änderungsverlauf

Version 1

Vom 18. Juni 2021

http-base - Dateien und Streams ausliefern
http-realm - Nutzerverzeichnis zur Authentifizierung
http-oauth - Bearer Authentication für Neon
http-up - Dateien zu Neon heraufladen
http-adoc - Asciidoctor mit Neon transformieren
http-image - Bilder mit Neon verwenden

Version 2

Vom 2. Januar 2022

http-cm - Dateien mit Neon verwalten
http-template - Mustache-Vorlagen mit Neon verarbeiten

Version 3

Vom 20. Februar 2024

Authentifizierung - Bearer Token Authentication für Neon 2