OBS/Kostenpflichtige Module/RESTServer/Zugaenge: Unterschied zwischen den Versionen
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 | | 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''. | 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 | 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
- A Preise aktualisieren
- C Personen übertragen
- E Kategorien verwalten
- G Kataloge verwalten
- I Merkliste übertragen
- K Varianten übertragen
- L Artikelvarianten übertragen
- M Referenzarten übertragen
- N Lagerbestände verwalten
- U Bestellungen einlesen
- V leere Passworte füllen
- W Update-Informationen zurücksetzen
- X Konfiguration
- Z Protokoll
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.
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:
- 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.
- 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.
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
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 |
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.
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: ....
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.
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;
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