OBS/Kostenpflichtige Module/RESTServer/Zugaenge: Unterschied zwischen den Versionen

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
(2 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 22: Zeile 22:
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt
| Host          || Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt
|-
|-
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch '';'' getrennt
| CORS-Origins  || Liste erlaubter Origins für Browser-Aufrufe, mehrere durch '';'' getrennt. '''Leer heisst nicht „keine Einschränkung", sondern „kein Browser-Zugriff"''' - siehe unten
|-
|-
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)
| mTLS-Subject  || Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)
Zeile 28: Zeile 28:
| JWT          || Schalter, ob für diesen Zugang ein JWT-Token nötig ist
| JWT          || Schalter, ob für diesen Zugang ein JWT-Token nötig ist
|-
|-
| JWT-Endpunkt  || Pfad, über den der JWT-Token bezogen wird (z.B. ''oauth'' oder ''meineapp/dev/auth/login''). Verglichen wird der '''vollständige Pfad''', mehrstufige Angaben mit ''/'' sind also erlaubt
| JWT-Endpunkt  || Pfad, über den '''alle Token-Vorgänge''' laufen: Anmelden, Erneuern und Abmelden (z.B. ''oauth'' oder ''meineapp/dev/auth''). Verglichen wird der '''vollständige Pfad''', mehrstufige Angaben mit ''/'' sind also erlaubt
|-
|-
| JWT-Key      || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar
| JWT-Key      || Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar
Zeile 59: Zeile 59:
Ablauf:
Ablauf:


# Browser sendet einen Preflight-Request (''OPTIONS''). Der REST-Server prüft das Origin gegen die globale Origin-Liste aller Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.
# Browser sendet einen Preflight-Request (''OPTIONS''). Der REST-Server prüft das Origin gegen die globale Origin-Liste '''aller''' Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.
# Der eigentliche Request wird gegen die Origin-Liste des aktuell verwendeten Zugangs geprüft.
# Der eigentliche Request wird gegen die Origin-Liste '''des aktuell verwendeten Zugangs''' geprüft.
# Stimmt das Origin nicht, wird mit 403 ''Origin nicht erlaubt für Account ...'' abgelehnt.
# Stimmt das Origin nicht, wird mit 403 ''Origin nicht erlaubt für Account ...'' abgelehnt.


Erlaubte Methoden: ''GET, POST, PUT, DELETE, PATCH''. Erlaubte Header: ''Content-Type, apikey, Authorization, Accept''. Diese Werte sind systemseitig festgelegt und werden in der Preflight-Antwort zurückgegeben.
Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen
''apikey'' mit, der Zugang ist zu diesem Zeitpunkt unbekannt. '''Die Durchsetzung
liegt deshalb auf Schritt 2''', nicht auf dem Preflight. Praktische Folge: trägt
'''ein''' Zugang auf dem Server ein ''*'' in seiner Liste, antwortet jeder
Preflight mit ''Access-Control-Allow-Origin: *'' - auch für Zugänge mit engerer
Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.
 
Erlaubte Methoden: ''GET, POST, PUT, DELETE, PATCH'' (systemseitig festgelegt).
 
Bei den '''Anfrage-Headern''' spiegelt der Server im Preflight zurück, was der
Browser in ''Access-Control-Request-Headers'' angefragt hat. Damit sind eigene
Header eines Projekts - ''Idempotency-Key'', ''If-Match'', später
''Upload-Offset''/''Upload-Length'' - ohne Änderung am Server nutzbar. Fragt der
Preflight keine Header an, antwortet der Server mit der Liste
''Content-Type, apikey, Authorization, Accept''.
 
{{Hinweis|Die Header-Liste ist '''keine Schutzgrenze''' - das ist das Origin. Sie
sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die
Zugangsdaten dieser Schnittstelle in Headern stehen (''apikey'',
''Authorization'') und nicht in Cookies: es gibt also keine automatisch
mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.}}
 
===Was „leer" bedeutet===
 
{{Achtung|Ein '''leeres''' Feld CORS-Origins heisst '''nicht''' „keine
Einschränkung". Es heisst: '''jeder Aufruf mit ''Origin''-Header wird mit 403
abgelehnt'''. Für Clients '''ohne''' ''Origin''-Header - mobile Apps, Server,
curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.}}
 
Daraus ergeben sich drei sinnvolle Einstellungen:
 
{| class="wikitable"
! Feld !! Wirkung
|-
| leer || Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden
|-
| konkrete Origins || Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen
|-
| ''*'' || Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden
|}
 
Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key
zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste
verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt.
Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat,
umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.


==JWT-Authentifizierung==
==JWT-Authentifizierung==


Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh):
Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).
 
'''Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt''' - den im
Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist
nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift '''vor''' der
Adressauflösung und braucht deshalb weder eine Zeile in ''RESTSRV_ENDPOINTS''
noch eine Berechtigung in ''RESTSRV_ACCESS''.
 
Unterschieden wird nach Verb und Token-Art:
 
{| class="wikitable"
! Aufruf !! Ablauf
|-
| ''POST'' ohne ''Authorization'' || Anmelden (Skript-Methode ''Authenticate'')
|-
| ''POST'' mit Bearer, Refresh-Token || Erneuern (Skript-Methode ''Refresh'')
|-
| ''DELETE'' mit Bearer, Access-Token || Abmelden (kein Skript nötig)
|-
| ''POST'' mit Bearer, Access-Token || '''401''' - unverändert
|}
 
{{Hinweis|Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist
Absicht. Würde ein vorgelegtes Access-Token beim ''POST'' als Abmeldung gedeutet,
würde ein Client, der seine beiden Token verwechselt, den Anwender still
abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.}}


===Phase 1: Token-Bezug===
===Phase 1: Token-Bezug===
Zeile 103: Zeile 173:
   "token": "eyJ...", "accessToken": "eyJ...", "refreshToken": "eyJ...",
   "token": "eyJ...", "accessToken": "eyJ...", "refreshToken": "eyJ...",
   "expiresIn": 28800, "serverTime": "2026-06-29T15:30:12+02:00"}
   "expiresIn": 28800, "serverTime": "2026-06-29T15:30:12+02:00"}
Mit der Ausstellung legt der Server zu dem Token-Paar eine '''Sitzungszeile''' in
''RESTSRV_TOKEN'' an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt,
Datenbankproblem), wird '''kein Token ausgeliefert''': der Server antwortet mit
'''503''' und ''Retry-After''. Das ist bewusst von der abgelehnten Anmeldung
unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt
werden.


Wird die Anmeldung abgelehnt (''status'' = 9), antwortet der Server mit
Wird die Anmeldung abgelehnt (''status'' = 9), antwortet der Server mit
Zeile 121: Zeile 198:
         Authorization: Bearer eyJhbGciOi...
         Authorization: Bearer eyJhbGciOi...


Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew). Im Skript stehen die Claims als Parameter ''_OBS_JWT_ID'', ''_OBS_JWT_SUBJECT'' und ''_OBS_JWT_AUDIENCE'' zur Verfügung.
Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die '''Sitzung''' in ''RESTSRV_TOKEN''. Im Skript stehen die Claims als Parameter ''_OBS_JWT_ID'', ''_OBS_JWT_SUBJECT'' und ''_OBS_JWT_AUDIENCE'' zur Verfügung.
 
Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen
Sitzung gesperrt wurde - beides mit '''401''' und dem Code ''AUTH_EXPIRED''. Der
Grund steht jeweils im Protokoll '''RESTSRV_PROTO''' unter ''TokenStore: ...''.
 
{{Hinweis|Ein Token, das '''vor''' der Einführung dieser Prüfung ausgestellt
wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich
angemeldete Konsumenten also einmalig neu anmelden.}}


===Phase 3: Token erneuern (Refresh)===
===Phase 3: Token erneuern (Refresh)===
Zeile 131: Zeile 216:
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)
         Authorization: Bearer eyJhbGciOi... (Refresh-Token)


Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, ''token_use=refresh'') und ruft im selben Skript die Methode '''Refresh''' auf. Das Skript prüft seine Sperrtabelle, rotiert das Refresh-jti und liefert neue Claims. Der Server antwortet mit einem rotierten Paar ''{"token","refreshToken","serverTime"}''. Ein bereits benutztes (rotiertes) Refresh-Token wird mit 401 abgelehnt.
Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, ''token_use=refresh''), '''entwertet es in der Sitzung''' und ruft erst dann im selben Skript die Methode '''Refresh''' auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar ''{"token","accessToken","refreshToken","expiresIn","serverTime"}'' in '''derselben Sitzung'''.
 
{{Achtung|Eine '''eigene Sperrtabelle im Skript ist nicht mehr nötig''' und sollte
entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im
Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die
Sitzung nicht.}}
 
Verhalten bei einem bereits benutzten Refresh-Token:
 
{| class="wikitable"
! Lage !! Antwort !! Wirkung auf die Sitzung
|-
| Erneute Vorlage '''innerhalb von 60 Sekunden''' || 401 ''AUTH_EXPIRED'' || Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen
|-
| Erneute Vorlage '''später''' || 401 ''AUTH_EXPIRED'' || Die '''gesamte Sitzung wird gesperrt'''. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert
|}
 
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].
Details und ein vollständiges Beispiel siehe [[OBS/Kostenpflichtige Module/RESTServer/Scripting|Scripting]].
===Phase 4: Abmelden (Logout)===
Abgemeldet wird mit einem '''DELETE''' auf denselben JWT-Endpunkt, das
Access-Token im ''Authorization''-Header:
DELETE https://api.meinserver.de/oauth/
Header: apikey: [API-KEY]
        Authorization: Bearer eyJhbGciOi... (Access-Token)
HTTP/1.1 204 No Content
Der Server sperrt die '''ganze Sitzung''' - also auch die noch nicht
abgelaufenen Access-Token aller vorherigen Erneuerungen. '''Ein Skript ist dafür
nicht nötig''': es gibt keine Methode ''Logout'', der Vorgang läuft komplett in
der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver
JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.
* Ein wiederholter Aufruf ist unschädlich und bleibt '''204''' - auch wenn die Sitzung längst gesperrt ist.
* Ein abgelaufenes oder ungültiges Token ergibt '''401''' (es wird schon bei der Prüfung abgewiesen).
* Ein Refresh-Token statt des Access-Tokens ergibt '''401'''.
* Lässt sich die Sitzung nicht sperren, antwortet der Server '''503''' mit ''Retry-After'' - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.
====Alle Geräte abmelden====
Der Fall „Gerät verloren" ist bewusst '''nicht''' am JWT-Endpunkt untergebracht:
er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das
Antwortfeld ''_OBS_JWT_REVOKE'', das jeder gewöhnliche Endpunkt setzen kann:
<syntaxhighlight lang="pascal" line>
function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // 'session' = nur diese Sitzung, 'all' = alle Sitzungen des Subjects
        oRes.AddPair('_OBS_JWT_REVOKE' , 'all');
        oRes.AddPair('_OBS_HTTP_STATUS', 204);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
</syntaxhighlight>
{{Hinweis|''all'' setzt voraus, dass das Authenticate-Skript ''_OBS_JWT_SUBJECT''
gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und
der Aufruf antwortet mit 503.}}
==Sitzungen (RESTSRV_TOKEN)==
Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die
Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung
sowie die Kennzeichen „verbraucht" und „gesperrt".
* '''Rotation und Widerruf sind getrennt.''' Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.
* '''Die Sitzung überlebt die Rotation.''' Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.
* '''Aufräumen läuft automatisch''' beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- '''und''' Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.
* '''Kein Eingriff nötig.''' Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als ''TokenStore: ...'' in '''RESTSRV_PROTO'''.


==mTLS-Subject-Prüfung==
==mTLS-Subject-Prüfung==

Aktuelle Version vom 20. August 2026, 08:30 Uhr

Kostenpflichtige Module

Internet-Shop
UPS
IMS Professional
SMS
Mehrlager-Verwaltung
Mehrsprachen Modul
Multilanguage Modul
EVA Marketing Tool
Termin-Projekte
Edifact-Schnittstelle
Backup Überwachung Email
OBS Geo Daten
DeliSprint / DPD
Filialen
Cashback
Moebelschnittstelle
Dokumenten Manager
DocuWare-Schnittstelle
OFML-Kalkulation
Versicherungsschaden
Gutschriftsanzeigen
Kameraverwaltung
DataInOut
OpenMasterData / IDS
Sammelpositionen



Zugänge

Ein Zugang (Account) ist der Kanal, über den ein externer Konsument den REST-Server anspricht. Er bündelt API-Key, Host-Beschränkung, CORS-Konfiguration, optionale JWT-Authentifizierung und optionale mTLS-Subject-Prüfung.

Aufruf

Stammdaten -> Z Weitere Stammdaten -> REST-Server -> Zugänge

Felder

Feld Beschreibung
Name Sprechender Name, erscheint im Protokoll und der Statistik
Aktiv Inaktive Zugänge werden bei der Authentifizierung mit 403 abgewiesen
API-Key Eindeutiger Schlüssel, den der Konsument im HTTP-Header apikey senden muss. Per Schlüsselsymbol generierbar
Host Optionaler Hostname oder IP-Adresse des Konsumenten. Stimmt die anfragende IP nicht überein, wird mit 403 abgelehnt
CORS-Origins Liste erlaubter Origins für Browser-Aufrufe, mehrere durch ; getrennt. Leer heisst nicht „keine Einschränkung", sondern „kein Browser-Zugriff" - siehe unten
mTLS-Subject Erwartete RDN-Komponente(n) im Client-Zertifikat-Subject, Komma-/Semikolon-getrennt (alle müssen vorkommen; nur sinnvoll bei mTLS-Servern)
JWT Schalter, ob für diesen Zugang ein JWT-Token nötig ist
JWT-Endpunkt Pfad, über den alle Token-Vorgänge laufen: Anmelden, Erneuern und Abmelden (z.B. oauth oder meineapp/dev/auth). Verglichen wird der vollständige Pfad, mehrstufige Angaben mit / sind also erlaubt
JWT-Key Geheimer Schlüssel zur Signierung/Verifikation, per Schlüsselsymbol generierbar
JWT-Exp Gültigkeitsdauer eines ausgestellten Access-Tokens in Minuten. Derselbe Wert wird dem Konsumenten in der Token-Antwort als expiresIn in Sekunden mitgeteilt und steht dem Endpunkt-Skript als _OBS_JWT_EXP_SEC zur Verfügung

API-Key

Der API-Key ist die Basis-Authentifizierung. Der Client übergibt ihn im HTTP-Header:

apikey: abcd-efgh-ijkl-mnop-qrstuvwxyz12

Wird ein Aufruf ohne apikey-Header gestellt, sucht der Server intern nach einem Zugang mit API-Key * - damit lassen sich (mit Vorsicht!) auch öffentliche Endpunkte ohne Authentifizierung realisieren.

HINWEIS: Ein Zugang mit API-Key * sollte nur für bewusst freigegebene Endpunkte verwendet werden und immer in Kombination mit einer Host-Beschränkung oder CORS-Beschränkung betrieben werden.

Host-Beschränkung

Ist im Feld Host ein Wert hinterlegt, akzeptiert der Server Anfragen unter diesem Zugang nur dann, wenn die Remote-IP exakt diesem Host entspricht oder durch DNS-Auflösung auf diese IP zeigt. DNS-Auflösungen werden 5 Minuten gecached.

CORS

Soll ein Endpunkt aus einer Browser-Anwendung (Single-Page-App, JavaScript) heraus aufgerufen werden, muss das Origin der Anwendung in der CORS-Liste des Zugangs eingetragen sein.

Format der Liste:

https://app.kunde.de;https://test.kunde.de;https://localhost:8080

Ablauf:

  1. Browser sendet einen Preflight-Request (OPTIONS). Der REST-Server prüft das Origin gegen die globale Origin-Liste aller Accounts (Cache, TTL 60s) und antwortet mit 204 + CORS-Header, falls erlaubt.
  2. Der eigentliche Request wird gegen die Origin-Liste des aktuell verwendeten Zugangs geprüft.
  3. Stimmt das Origin nicht, wird mit 403 Origin nicht erlaubt für Account ... abgelehnt.

Der Preflight kann nicht zugangsbezogen prüfen: der Browser schickt dabei keinen apikey mit, der Zugang ist zu diesem Zeitpunkt unbekannt. Die Durchsetzung liegt deshalb auf Schritt 2, nicht auf dem Preflight. Praktische Folge: trägt ein Zugang auf dem Server ein * in seiner Liste, antwortet jeder Preflight mit Access-Control-Allow-Origin: * - auch für Zugänge mit engerer Liste. Deren eigentliche Anfrage wird trotzdem gegen die eigene Liste geprüft.

Erlaubte Methoden: GET, POST, PUT, DELETE, PATCH (systemseitig festgelegt).

Bei den Anfrage-Headern spiegelt der Server im Preflight zurück, was der Browser in Access-Control-Request-Headers angefragt hat. Damit sind eigene Header eines Projekts - Idempotency-Key, If-Match, später Upload-Offset/Upload-Length - ohne Änderung am Server nutzbar. Fragt der Preflight keine Header an, antwortet der Server mit der Liste Content-Type, apikey, Authorization, Accept.

HINWEIS: Die Header-Liste ist keine Schutzgrenze - das ist das Origin. Sie

sagt dem Browser nur, welche Header der Server versteht. Dazu kommt, dass die Zugangsdaten dieser Schnittstelle in Headern stehen (apikey, Authorization) und nicht in Cookies: es gibt also keine automatisch

mitgeschickte Berechtigung, die eine fremde Seite ausnutzen könnte.

Was „leer" bedeutet

ACHTUNG: Ein leeres Feld CORS-Origins heisst nicht „keine

Einschränkung". Es heisst: jeder Aufruf mit Origin-Header wird mit 403 abgelehnt. Für Clients ohne Origin-Header - mobile Apps, Server,

curl - ändert das Feld gar nichts, sie werden nie gegen CORS geprüft.

Daraus ergeben sich drei sinnvolle Einstellungen:

Feld Wirkung
leer Kein Browser-Zugriff. Richtig für Zugänge, die nur von Apps oder Servern benutzt werden
konkrete Origins Browser-Zugriff nur von diesen Seiten. Der Regelfall für Web-Anwendungen
* Browser-Zugriff von überall. Wirkt zusätzlich auf die Preflight-Antwort aller anderen Zugänge - bewusst und sparsam verwenden

Für eine Browser-Anwendung ist dieses Feld besonders wichtig, weil ihr API-Key zwangsläufig im JavaScript liegt und damit öffentlich ist. Die Origin-Liste verhindert dann, dass jemand den Key nimmt und von seiner eigenen Seite benutzt. Sie ist allerdings kein Ersatz für eine Zugriffskontrolle: wer den Key hat, umgeht CORS mit einem einzigen Aufruf ausserhalb des Browsers.

JWT-Authentifizierung

Wird ein Zugang mit aktiver JWT-Pflicht aufgerufen, sind die folgenden Phasen zu durchlaufen (Phase 3 nur bei genutztem Refresh).

Anmelden, Erneuern und Abmelden laufen über denselben Endpunkt - den im Feld JWT-Endpunkt hinterlegten Pfad. Ein eigener Endpunkt fürs Abmelden ist nicht nötig und wäre auch umständlicher: der JWT-Endpunkt greift vor der Adressauflösung und braucht deshalb weder eine Zeile in RESTSRV_ENDPOINTS noch eine Berechtigung in RESTSRV_ACCESS.

Unterschieden wird nach Verb und Token-Art:

Aufruf Ablauf
POST ohne Authorization Anmelden (Skript-Methode Authenticate)
POST mit Bearer, Refresh-Token Erneuern (Skript-Methode Refresh)
DELETE mit Bearer, Access-Token Abmelden (kein Skript nötig)
POST mit Bearer, Access-Token 401 - unverändert
HINWEIS: Dass das Abmelden am Verb hängt und nicht allein an der Token-Art, ist

Absicht. Würde ein vorgelegtes Access-Token beim POST als Abmeldung gedeutet, würde ein Client, der seine beiden Token verwechselt, den Anwender still

abmelden - statt das klare 401 zu bekommen, an dem der Fehler auffällt.

Phase 1: Token-Bezug

Der Konsument ruft den JWT-Endpunkt des Zugangs auf, z.B.:

POST https://api.meinserver.de/oauth/
Header: apikey: [API-KEY]
Body:   {"username":"...", "password":"..."}

Der Server führt das im Zugang hinterlegte Authentifizierungs-Skript aus (F7 in der Zugangs-Liste). Das Skript muss eine Funktion Authenticate bereitstellen, die ein JSON-Objekt mit:

  • status = 1 bei Erfolg, 9 bei Misserfolg
  • error = Fehlertext (bei status=9)
  • _OBS_JWT_ID, _OBS_JWT_SUBJECT, _OBS_JWT_AUDIENCE für die JWT-Claims (optional)
  • _OBS_JWT_CLAIM_<name> für beliebige Custom-Claims (z.B. Mandant tenant, Rollen roles); im Folge-Skript lesbar
  • _OBS_JWT_REFRESH_ID (optional) löst die Ausstellung eines Refresh-Tokens aus; _OBS_JWT_REFRESH_EXP setzt dessen Lebensdauer in Minuten (Default 90 Tage)

zurückliefert. Bei Erfolg signiert der Server einen Token (HS256, Issuer OBS REST-Server, Lebenszeit gemäß JWT-Exp) und antwortet:

{"token":        "eyJhbGciOi...",
 "accessToken":  "eyJhbGciOi...",
 "expiresIn":    28800,
 "serverTime":   "2026-06-29T15:30:12+02:00"}

Zum Aufbau der Antwort:

  • token und accessToken enthalten denselben Token. accessToken ist der Name, den die meisten Client-Bibliotheken erwarten; token bleibt für bestehende Konsumenten erhalten.
  • expiresIn ist die Gültigkeit in Sekunden (JWT-Exp × 60).
  • Alle weiteren Felder, die das Authenticate-Skript zurückgibt, werden mit ausgeliefert - mit Ausnahme von status und den _OBS_-Steuerfeldern. So kann das Skript z.B. eine Benutzer-Id, den Mandanten oder Rollen direkt in die Anmelde-Antwort legen, ohne dass der Client dafür einen zweiten Aufruf braucht.

Beispiel mit Zusatzfeldern aus dem Skript:

{"benutzerId": "4711", "tenant": "nord", "roles": ["TECHNIKER"],
 "token": "eyJ...", "accessToken": "eyJ...", "refreshToken": "eyJ...",
 "expiresIn": 28800, "serverTime": "2026-06-29T15:30:12+02:00"}

Mit der Ausstellung legt der Server zu dem Token-Paar eine Sitzungszeile in RESTSRV_TOKEN an. Lässt sich diese Zeile nicht schreiben (Tabelle fehlt, Datenbankproblem), wird kein Token ausgeliefert: der Server antwortet mit 503 und Retry-After. Das ist bewusst von der abgelehnten Anmeldung unterschieden - die Zugangsdaten waren in Ordnung, der Versuch darf wiederholt werden.

Wird die Anmeldung abgelehnt (status = 9), antwortet der Server mit 401 Unauthorized und reicht den Text aus dem Feld error als error.message an den Client durch - der Konsument kann ihn also direkt anzeigen. Die Formulierung liegt damit beim Skript.

HINWEIS: {{{1}}}

Phase 2: Token-Verwendung

Bei allen folgenden Aufrufen sendet der Client den Token im Authorization-Header:

GET https://api.meinserver.de/kalender/v1
Header: apikey: [API-KEY]
        Authorization: Bearer eyJhbGciOi...

Der Server verifiziert Signatur, Issuer, Ausstellzeitpunkt und Ablauf (Toleranz: 20 Sekunden Clock-Skew) und prüft anschliessend die Sitzung in RESTSRV_TOKEN. Im Skript stehen die Claims als Parameter _OBS_JWT_ID, _OBS_JWT_SUBJECT und _OBS_JWT_AUDIENCE zur Verfügung.

Die Sitzungsprüfung weist ein Token ab, das zu keiner Zeile gehört oder dessen Sitzung gesperrt wurde - beides mit 401 und dem Code AUTH_EXPIRED. Der Grund steht jeweils im Protokoll RESTSRV_PROTO unter TokenStore: ....

HINWEIS: Ein Token, das vor der Einführung dieser Prüfung ausgestellt

wurde, gehört zu keiner Sitzung und wird abgewiesen. Nach dem Update müssen sich

angemeldete Konsumenten also einmalig neu anmelden.

Phase 3: Token erneuern (Refresh)

Soll die App lange ohne Neuanmeldung arbeiten, stellt das Authenticate-Skript zusätzlich ein Refresh-Token aus (Feld _OBS_JWT_REFRESH_ID). Zum Erneuern ruft der Client denselben JWT-Endpunkt auf, aber mit dem Refresh-Token im Authorization-Header:

POST https://api.meinserver.de/oauth/
Header: apikey: [API-KEY]
        Authorization: Bearer eyJhbGciOi... (Refresh-Token)

Der Server erkennt am Bearer-Token den Refresh-Fall, verifiziert das Token (Signatur, Ablauf, token_use=refresh), entwertet es in der Sitzung und ruft erst dann im selben Skript die Methode Refresh auf. Das Skript liefert nur noch die aktuellen Claims zurück - Rotation, Einmalgebrauch und Sperrliste erledigt der Server. Die Antwort ist ein rotiertes Paar {"token","accessToken","refreshToken","expiresIn","serverTime"} in derselben Sitzung.

ACHTUNG: Eine eigene Sperrtabelle im Skript ist nicht mehr nötig und sollte

entfernt werden. Wer sie weiterführt, rotiert zweimal: einmal im Skript, einmal im Server. Der Server sieht die Skript-Tabelle nicht, und das Skript sieht die

Sitzung nicht.

Verhalten bei einem bereits benutzten Refresh-Token:

Lage Antwort Wirkung auf die Sitzung
Erneute Vorlage innerhalb von 60 Sekunden 401 AUTH_EXPIRED Keine. Das ist der Normalfall einer App, deren parallele Anfragen gleichzeitig in den Refresh laufen - eine gewinnt, die anderen sollen die vom Gewinner gespeicherten Token benutzen
Erneute Vorlage später 401 AUTH_EXPIRED Die gesamte Sitzung wird gesperrt. Das Token wurde kopiert oder wiederholt, ohne dass ein Gewinner es noch brauchen könnte; der Vorfall wird mit IP protokolliert

Details und ein vollständiges Beispiel siehe Scripting.

Phase 4: Abmelden (Logout)

Abgemeldet wird mit einem DELETE auf denselben JWT-Endpunkt, das Access-Token im Authorization-Header:

DELETE https://api.meinserver.de/oauth/
Header: apikey: [API-KEY]
        Authorization: Bearer eyJhbGciOi... (Access-Token)
HTTP/1.1 204 No Content

Der Server sperrt die ganze Sitzung - also auch die noch nicht abgelaufenen Access-Token aller vorherigen Erneuerungen. Ein Skript ist dafür nicht nötig: es gibt keine Methode Logout, der Vorgang läuft komplett in der Engine. Damit funktioniert das Abmelden für jeden Zugang mit aktiver JWT-Pflicht, ohne dass am Authentifizierungs-Skript etwas geändert wird.

  • Ein wiederholter Aufruf ist unschädlich und bleibt 204 - auch wenn die Sitzung längst gesperrt ist.
  • Ein abgelaufenes oder ungültiges Token ergibt 401 (es wird schon bei der Prüfung abgewiesen).
  • Ein Refresh-Token statt des Access-Tokens ergibt 401.
  • Lässt sich die Sitzung nicht sperren, antwortet der Server 503 mit Retry-After - ein Client darf nicht glauben, er sei abgemeldet, während sein Token weiterläuft.

Alle Geräte abmelden

Der Fall „Gerät verloren" ist bewusst nicht am JWT-Endpunkt untergebracht: er braucht eine fachliche Entscheidung darüber, wer das darf. Dafür gibt es das Antwortfeld _OBS_JWT_REVOKE, das jeder gewöhnliche Endpunkt setzen kann:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
begin
    oRes := TJSONObject.Create();
    try
        // 'session' = nur diese Sitzung, 'all' = alle Sitzungen des Subjects
        oRes.AddPair('_OBS_JWT_REVOKE' , 'all');
        oRes.AddPair('_OBS_HTTP_STATUS', 204);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;
HINWEIS: all setzt voraus, dass das Authenticate-Skript _OBS_JWT_SUBJECT

gesetzt hat. Ohne Subject lässt sich die Menge der Sitzungen nicht bestimmen, und

der Aufruf antwortet mit 503.

Sitzungen (RESTSRV_TOKEN)

Je ausgestelltem Token-Paar entsteht eine Zeile. Sie enthält die Sitzung, die Zeilenkennung, das Subject, die beiden Laufzeiten, IP und Trace-ID der Ausstellung sowie die Kennzeichen „verbraucht" und „gesperrt".

  • Rotation und Widerruf sind getrennt. Ein Refresh entwertet nur das Refresh-Token; der zugehörige Access-Token bleibt bis zu seinem Ablauf gültig. Sonst bekämen parallel laufende Anfragen ein 401, obwohl niemand abgemeldet ist.
  • Die Sitzung überlebt die Rotation. Alle Paare einer Anmeldung tragen dieselbe Sitzungskennung - deshalb kann ein Logout sie gemeinsam sperren.
  • Aufräumen läuft automatisch beim Serverstart und danach höchstens stündlich. Gelöscht wird nur, was nichts mehr entscheiden kann: Zeilen, deren Access- und Refresh-Laufzeit vorbei sind. Eine verbrauchte Zeile bleibt bis dahin stehen, weil sonst die Erkennung eines später vorgelegten gestohlenen Refresh-Tokens verloren ginge.
  • Kein Eingriff nötig. Die Tabelle wird nicht gepflegt; sie ist Diagnose-Material. Alle Abweisungen stehen als TokenStore: ... in RESTSRV_PROTO.

mTLS-Subject-Prüfung

Wird im Feld mTLS-Subject ein Wert hinterlegt, prüft der Server zusätzlich zur Standard-mTLS-Validierung, ob der Subject-DN des (CA-validierten) Client-Zertifikats die hinterlegten RDN-Komponenten enthält. Verglichen wird jede Komponente vollständig (kein Substring-Match) und case-insensitiv; mehrere Komponenten werden durch Komma oder Semikolon getrennt und müssen alle im Subject vorkommen.

Beispiele für Subject-Strings im Cert (OneLine-DN):

/C=DE/O=Kunde GmbH/CN=client-prod.kunde.de
/C=DE/O=Kunde GmbH/CN=client-test.kunde.de

Mit CN=client-prod.kunde.de im Feld wird genau das prod-Cert akzeptiert, das test-Cert mit 401 abgewiesen. Da jede RDN-Komponente vollständig verglichen wird, matcht z.B. CN=client1 nicht fälschlich auch CN=client10. Mehrere Bedingungen lassen sich mit Komma/Semikolon kombinieren, z.B. O=Kunde GmbH;CN=client-prod.kunde.de.

Listen-Funktionen

In der Zugänge-Liste (allein):

  • Einfg - neuer Zugang
  • Return - Zugang bearbeiten
  • F4 - Sortierung
  • F7 - JWT-Authentifizierungs-Skript bearbeiten
  • F8 - Statistik für den Zugang

Aus der Endpunkt-Liste via F6 geöffnet (Auswahl für Berechtigung):

  • F2 - markierte Zugänge dem Endpunkt zuordnen
  • F5 - Zugang in der Liste markieren / Markierung aufheben