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

Aus OBS Wiki
Zur Navigation springen Zur Suche springen
(Die Seite wurde neu angelegt: „{{Kostenpflichtige Module}} =Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz= Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.…“)
Keine Bearbeitungszusammenfassung
 
(Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt)
Zeile 3: Zeile 3:
=Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz=
=Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz=


Dieses Beispiel zeigt einen schreibenden Endpunkt fÜr eine mobile App: AuftrÄge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen Über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen Über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.
Dieses Beispiel zeigt einen schreibenden Endpunkt für eine mobile App: Aufträge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.


==Einrichtung in OBS==
==Einrichtung in OBS==
Zeile 9: Zeile 9:
* '''Server-Profil:''' Standard-TLS-Profil ''Public-API'' (Port 443)
* '''Server-Profil:''' Standard-TLS-Profil ''Public-API'' (Port 443)
* '''Zugang:''' ''Mobile-App''
* '''Zugang:''' ''Mobile-App''
** API-Key: zufÄllig generiert
** API-Key: zufällig generiert
** JWT aktiv, JWT-Endpunkt ''auth'', JWT-Key zufÄllig, JWT-Exp 60 (Minuten)
** JWT aktiv, JWT-Endpunkt ''auth'', JWT-Key zufällig, JWT-Exp 60 (Minuten)
* '''Endpunkte''' (beide dem Profil ''Public-API'' zugeordnet):
* '''Endpunkte''' (beide dem Profil ''Public-API'' zugeordnet):
** ''orders/{uid}'' - Auftrag lesen/Ändern
** ''orders/{uid}'' - Auftrag lesen/Ändern
** ''orders/{uid}/material'' - Material erfassen
** ''orders/{uid}/material'' - Material erfassen
* '''Berechtigung:''' Zugang ''Mobile-App'' fÜr beide Endpunkte freigeschaltet
* '''Berechtigung:''' Zugang ''Mobile-App'' für beide Endpunkte freigeschaltet


==JWT-Authentifizierungs-Skript (Zugang)==
==JWT-Authentifizierungs-Skript (Zugang)==


Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode ''Refresh'' auf. Die Hilfsfunktionen (''PasswortPasst'', ''RefreshJti...'') sind illustrativ und projektabhÄngig.
Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode ''Refresh'' auf. Die Hilfsfunktionen (''PasswortPasst'', ''RefreshJti...'') sind illustrativ und projektabhängig.


<syntaxhighlight lang="pascal" line>
<syntaxhighlight lang="pascal" line>
Zeile 91: Zeile 91:
==Endpunkt ''orders/{uid}'' - ändern mit ETag / If-Match==
==Endpunkt ''orders/{uid}'' - ändern mit ETag / If-Match==


''GET'' liefert den Auftrag samt ''ETag'' (Version), ''PUT'' prÜft Rolle und ''If-Match''.
''GET'' liefert den Auftrag samt ''ETag'' (Version), ''PUT'' prüft Rolle und ''If-Match''.


<syntaxhighlight lang="pascal" line>
<syntaxhighlight lang="pascal" line>
Zeile 126: Zeile 126:
     oRes := TJSONObject.Create();
     oRes := TJSONObject.Create();
     try
     try
         // nur Rolle "lead" darf Ändern - Rolle kommt aus dem Token, nicht aus dem Body
         // nur Rolle "lead" darf ändern - Rolle kommt aus dem Token, nicht aus dem Body
         if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
         if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
             oRes.AddPair('_OBS_HTTP_STATUS', 403);
             oRes.AddPair('_OBS_HTTP_STATUS', 403);
Zeile 161: Zeile 161:
==Endpunkt ''orders/{uid}/material'' - idempotentes Anlegen==
==Endpunkt ''orders/{uid}/material'' - idempotentes Anlegen==


''POST'' erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201. Ein wiederholter Aufruf mit gleichem ''Idempotency-Key'' liefert dieselbe Antwort ohne Zweitbuchung. Der Idempotenz-Eintrag wird in derselben Transaktion wie die Buchung gespeichert.
''POST'' erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201.
 
Gegen doppelte Sendungen schickt der Client den Header ''Idempotency-Key''. '''Das
Skript muss dafür nichts tun''' - der Server erkennt den Header, merkt sich das
Ergebnis und liefert bei einer Wiederholung mit demselben Schlüssel die
gespeicherte Antwort zurück, ohne das Skript erneut zu starten (siehe
[[OBS/Kostenpflichtige Module/RESTServer/Endpunkte|Endpunkte]], Abschnitt
Idempotenz). Das Skript kümmert sich nur um seine Fachlogik:


<syntaxhighlight lang="pascal" line>
<syntaxhighlight lang="pascal" line>
Zeile 167: Zeile 174:
var oRes, oErr: TJSONObject;
var oRes, oErr: TJSONObject;
     oVal: TJSONValue;
     oVal: TJSONValue;
     cKey, cUid, cArtikel, cGespeichert, cNeueUid: string;
     cUid, cArtikel, cNeueUid: string;
     nMenge: integer;
     nMenge: integer;
begin
begin
Zeile 173: Zeile 180:
     try
     try
         cUid := oParams.Values['_OBS_PATH_uid'];
         cUid := oParams.Values['_OBS_PATH_uid'];
        cKey := oParams.Values['idempotency-key'];
        // 1) Idempotenz: gleicher Key bereits verarbeitet?
        if ((not Empty(cKey)) and IdempotenzAntwort(cKey, cGespeichert)) then begin
            result := cGespeichert;  // identische Antwort, keine Zweitbuchung
            exit;
        end;


         // 2) Validierung -> 422 mit traceId
         // 1) Validierung -> 422 mit traceId.
        //    Wichtig: fachliche Ablehnung als 4xx melden, nicht als 200 mit
        //    Fehlertext - nur dann gibt der Server den Idempotency-Key wieder
        //    frei und der Client darf ihn nach Korrektur erneut verwenden.
         cArtikel := '';
         cArtikel := '';
         nMenge  := 0;
         nMenge  := 0;
Zeile 199: Zeile 202:
         end;
         end;


         // 3) In EINER Transaktion: Position anlegen + Idempotenz-Eintrag committen
         // 2) Buchen. Die Transaktionssteuerung bleibt beim Skript; der
         //    (Transaktionssteuerung Über die OBS-Skript-DB-API)
        //    Idempotenz-Speicher des Servers läuft getrennt davon und
         //    umschliesst diesen Block nicht.
         cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);
         cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);


        // 3) Antwort vollständig aufbauen - genau sie wird eingefroren und bei
        //    einer Wiederholung erneut ausgeliefert.
         oRes.AddPair('_OBS_HTTP_STATUS', 201);
         oRes.AddPair('_OBS_HTTP_STATUS', 201);
         oRes.AddPair('uid', cNeueUid);
         oRes.AddPair('uid', cNeueUid);
         result := oRes.ToJSON();
         result := oRes.ToJSON();
        if (not Empty(cKey)) then begin
            IdempotenzSpeichern(cKey, result);  // in derselben Transaktion
        end;
     finally
     finally
         MyFreeAndNil(oRes);
         MyFreeAndNil(oRes);
Zeile 218: Zeile 220:
==Test mit curl==
==Test mit curl==


Token holen (Antwort enthÄlt token, refreshToken und serverTime):
Token holen:


  curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
  curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
Zeile 224: Zeile 226:
       https://api.meinserver.de/auth/
       https://api.meinserver.de/auth/


  {"token":"eyJ...","refreshToken":"eyJ...","serverTime":"2026-06-29T15:30:12+02:00"}
  {"token":"eyJ...","accessToken":"eyJ...","refreshToken":"eyJ...",
  "expiresIn":3600,"serverTime":"2026-06-29T15:30:12+02:00"}
 
Bei falschen Zugangsdaten antwortet der Server mit '''401''' und dem Text aus dem
Authenticate-Skript als ''error.message''.


Auftrag lesen (liefert den ETag-Header):
Auftrag lesen (liefert den ETag-Header):
Zeile 245: Zeile 251:
       -d "{\"artikel\":\"A100\",\"menge\":3}" ^
       -d "{\"artikel\":\"A100\",\"menge\":3}" ^
       https://api.meinserver.de/orders/4711/material
       https://api.meinserver.de/orders/4711/material
Die Antwort des zweiten Aufrufs trägt zusätzlich den Header
''Idempotent-Replay: true'' - daran ist erkennbar, dass sie aus dem Speicher kam
und nichts erneut gebucht wurde.


Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):
Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):
Zeile 254: Zeile 264:


* Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
* Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
* Optimistic Concurrency Über ''ETag'' (GET) und ''If-Match'' (PUT) -> 409 VERSION_CONFLICT.
* Optimistic Concurrency über ''ETag'' (GET) und ''If-Match'' (PUT) -> 409 VERSION_CONFLICT.
* Idempotente Schreibzugriffe Über den ''Idempotency-Key'' (Schutz gegen Doppelbuchung).
* Idempotente Schreibzugriffe über den ''Idempotency-Key'' - '''vom Server erledigt''', das Skript enthält dafür keine Zeile Code.
* RollenprÜfung aus dem Token-Claim ''_OBS_JWT_CLAIM_roles'', nicht aus dem Body.
* Rollenprüfung aus dem Token-Claim ''_OBS_JWT_CLAIM_roles'', nicht aus dem Body.
* JWT mit Custom-Claims (''tenant''/''roles'') und Refresh-Token mit Rotation.
* JWT mit Custom-Claims (''tenant''/''roles'') und Refresh-Token mit Rotation.
* ''traceId'' im Fehler-Body fÜr die Support-Nachverfolgung (auch als Header ''X-Trace-Id'').
* ''traceId'' im Fehler-Body für die Support-Nachverfolgung (auch als Header ''X-Trace-Id'').

Aktuelle Version vom 17. August 2026, 07:53 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



Beispiel 5: Schreibzugriff mit Statuscodes, ETag und Idempotenz

Dieses Beispiel zeigt einen schreibenden Endpunkt für eine mobile App: Aufträge werden mit echten HTTP-Statuscodes aktualisiert, konkurrierende Änderungen über ETag/If-Match abgesichert (Optimistic Concurrency) und doppelte Sendungen über einen Idempotency-Key abgefangen. Die Anmeldung nutzt JWT mit Custom-Claims (Mandant, Rollen) und einem Refresh-Token.

Einrichtung in OBS

  • Server-Profil: Standard-TLS-Profil Public-API (Port 443)
  • Zugang: Mobile-App
    • API-Key: zufällig generiert
    • JWT aktiv, JWT-Endpunkt auth, JWT-Key zufällig, JWT-Exp 60 (Minuten)
  • Endpunkte (beide dem Profil Public-API zugeordnet):
    • orders/{uid} - Auftrag lesen/Ändern
    • orders/{uid}/material - Material erfassen
  • Berechtigung: Zugang Mobile-App für beide Endpunkte freigeschaltet

JWT-Authentifizierungs-Skript (Zugang)

Stellt Mandant und Rollen als Custom-Claims aus und legt ein Refresh-Token an (das jti wird in einer eigenen Sperrtabelle abgelegt). Bei einem Refresh ruft der Server im selben Skript die Methode Refresh auf. Die Hilfsfunktionen (PasswortPasst, RefreshJti...) sind illustrativ und projektabhängig.

function Authenticate(oParams: TStrings; oBody: TJSONObject): string;
var oRes : TJSONObject;
    oVal : TJSONValue;
    cUser, cPass, cRefreshJti: string;
begin
    oRes := TJSONObject.Create();
    try
        cUser := ''; cPass := '';
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('username'); if (Assigned(oVal)) then cUser := oVal.Value;
            oVal := oBody.GetValue('password'); if (Assigned(oVal)) then cPass := oVal.Value;
        end;

        if (PasswortPasst(cUser, cPass)) then begin
            cRefreshJti := GlobalUID();
            RefreshJtiAblegen(cUser, cRefreshJti);   // eigene Sperrtabelle

            oRes.AddPair('status'               , 1);
            oRes.AddPair('_OBS_JWT_ID'          , cUser);
            oRes.AddPair('_OBS_JWT_SUBJECT'     , cUser);
            oRes.AddPair('_OBS_JWT_CLAIM_tenant', TechnikerMandant(cUser));
            oRes.AddPair('_OBS_JWT_CLAIM_roles' , TechnikerRollen(cUser));   // z.B. "tech,lead"
            oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cRefreshJti);
            // _OBS_JWT_REFRESH_EXP weggelassen -> Default 90 Tage
        end else begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'Login fehlgeschlagen');
        end;
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

//------------------------------------------------------------------------------

function Refresh(oParams: TStrings; oBody: TJSONObject): string;
var oRes: TJSONObject;
    cAltesJti, cNeuesJti, cUser: string;
begin
    oRes := TJSONObject.Create();
    try
        cAltesJti := oParams.Values['_OBS_JWT_ID'];      // altes Refresh-jti
        cUser     := oParams.Values['_OBS_JWT_SUBJECT'];

        if (not RefreshJtiGueltig(cAltesJti)) then begin
            oRes.AddPair('status', 9);
            oRes.AddPair('error' , 'AUTH_EXPIRED');
            result := oRes.ToJSON();
            exit;
        end;

        cNeuesJti := GlobalUID();
        RefreshJtiRotieren(cAltesJti, cNeuesJti);         // altes sperren, neues ablegen

        oRes.AddPair('status'               , 1);
        oRes.AddPair('_OBS_JWT_ID'          , cUser);
        oRes.AddPair('_OBS_JWT_SUBJECT'     , cUser);
        oRes.AddPair('_OBS_JWT_CLAIM_tenant', oParams.Values['_OBS_JWT_CLAIM_tenant']);
        oRes.AddPair('_OBS_JWT_CLAIM_roles' , oParams.Values['_OBS_JWT_CLAIM_roles']);
        oRes.AddPair('_OBS_JWT_REFRESH_ID'  , cNeuesJti);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Endpunkt orders/{uid} - ändern mit ETag / If-Match

GET liefert den Auftrag samt ETag (Version), PUT prüft Rolle und If-Match.

function Get(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nVer: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];
        if (not AuftragLesen(oParams.Values['_OBS_JWT_CLAIM_tenant'], cUid, oRes, nVer)) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 404);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'NOT_FOUND'));
            result := oRes.ToJSON();
            exit;
        end;
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nVer));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

//------------------------------------------------------------------------------

function Put(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oHdr: TJSONObject;
    cUid: string;
    nAktuell, nIfMatch: integer;
begin
    oRes := TJSONObject.Create();
    try
        // nur Rolle "lead" darf ändern - Rolle kommt aus dem Token, nicht aus dem Body
        if (Pos('lead', oParams.Values['_OBS_JWT_CLAIM_roles']) = 0) then begin
            oRes.AddPair('_OBS_HTTP_STATUS', 403);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'FORBIDDEN_ROLE'));
            result := oRes.ToJSON();
            exit;
        end;

        cUid     := oParams.Values['_OBS_PATH_uid'];
        nAktuell := AuftragVersion(cUid);
        nIfMatch := iVal(oParams.Values['if-match']);

        if (nIfMatch <> nAktuell) then begin
            oHdr := TJSONObject.Create();
            oHdr.AddPair('ETag', xStr(nAktuell));
            oRes.AddPair('_OBS_HTTP_STATUS', 409);
            oRes.AddPair('_OBS_HEADERS', oHdr);
            oRes.AddPair('error', TJSONObject.Create.AddPair('code', 'VERSION_CONFLICT'));
            result := oRes.ToJSON();
            exit;
        end;

        AuftragSpeichern(cUid, oBody);   // setzt Version auf nAktuell + 1
        oHdr := TJSONObject.Create();
        oHdr.AddPair('ETag', xStr(nAktuell + 1));
        oRes.AddPair('_OBS_HEADERS', oHdr);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Endpunkt orders/{uid}/material - idempotentes Anlegen

POST erfasst eine Materialposition: Validierungsfehler -> 422, erfolgreiches Anlegen -> 201.

Gegen doppelte Sendungen schickt der Client den Header Idempotency-Key. Das Skript muss dafür nichts tun - der Server erkennt den Header, merkt sich das Ergebnis und liefert bei einer Wiederholung mit demselben Schlüssel die gespeicherte Antwort zurück, ohne das Skript erneut zu starten (siehe Endpunkte, Abschnitt Idempotenz). Das Skript kümmert sich nur um seine Fachlogik:

function Post(oParams: TStrings; oBody: TJSONObject): string;
var oRes, oErr: TJSONObject;
    oVal: TJSONValue;
    cUid, cArtikel, cNeueUid: string;
    nMenge: integer;
begin
    oRes := TJSONObject.Create();
    try
        cUid := oParams.Values['_OBS_PATH_uid'];

        // 1) Validierung -> 422 mit traceId.
        //    Wichtig: fachliche Ablehnung als 4xx melden, nicht als 200 mit
        //    Fehlertext - nur dann gibt der Server den Idempotency-Key wieder
        //    frei und der Client darf ihn nach Korrektur erneut verwenden.
        cArtikel := '';
        nMenge   := 0;
        if (Assigned(oBody)) then begin
            oVal := oBody.GetValue('artikel'); if (Assigned(oVal)) then cArtikel := oVal.Value;
            oVal := oBody.GetValue('menge');   if (Assigned(oVal)) then nMenge   := iVal(oVal.Value);
        end;
        if ((Empty(cArtikel)) or (nMenge <= 0)) then begin
            oErr := TJSONObject.Create();
            oErr.AddPair('code'   , 'VALIDATION_FAILED');
            oErr.AddPair('message', 'artikel und menge sind Pflicht');
            oErr.AddPair('traceId', oParams.Values['_OBS_TRACE_ID']);
            oRes.AddPair('_OBS_HTTP_STATUS', 422);
            oRes.AddPair('error', oErr);
            result := oRes.ToJSON();
            exit;
        end;

        // 2) Buchen. Die Transaktionssteuerung bleibt beim Skript; der
        //    Idempotenz-Speicher des Servers läuft getrennt davon und
        //    umschliesst diesen Block nicht.
        cNeueUid := MaterialAnlegen(cUid, cArtikel, nMenge);

        // 3) Antwort vollständig aufbauen - genau sie wird eingefroren und bei
        //    einer Wiederholung erneut ausgeliefert.
        oRes.AddPair('_OBS_HTTP_STATUS', 201);
        oRes.AddPair('uid', cNeueUid);
        result := oRes.ToJSON();
    finally
        MyFreeAndNil(oRes);
    end;
end;

Test mit curl

Token holen:

curl -X POST -H "apikey: [API-KEY]" -H "Content-Type: application/json" ^
     -d "{\"username\":\"tech1\",\"password\":\"geheim\"}" ^
     https://api.meinserver.de/auth/
{"token":"eyJ...","accessToken":"eyJ...","refreshToken":"eyJ...",
 "expiresIn":3600,"serverTime":"2026-06-29T15:30:12+02:00"}

Bei falschen Zugangsdaten antwortet der Server mit 401 und dem Text aus dem Authenticate-Skript als error.message.

Auftrag lesen (liefert den ETag-Header):

curl -i -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     https://api.meinserver.de/orders/4711
... ETag: 7

ändern mit korrektem If-Match -> 200, mit veraltetem If-Match -> 409:

curl -i -X PUT -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     -H "If-Match: 7" -H "Content-Type: application/json" ^
     -d "{\"status\":\"erledigt\"}" ^
     https://api.meinserver.de/orders/4711

Material idempotent erfassen (zweiter Aufruf mit gleichem Key -> gleiche Antwort, keine Doppelbuchung):

curl -i -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer eyJ..." ^
     -H "Idempotency-Key: 9c84-7f2a-..." -H "Content-Type: application/json" ^
     -d "{\"artikel\":\"A100\",\"menge\":3}" ^
     https://api.meinserver.de/orders/4711/material

Die Antwort des zweiten Aufrufs trägt zusätzlich den Header Idempotent-Replay: true - daran ist erkennbar, dass sie aus dem Speicher kam und nichts erneut gebucht wurde.

Token erneuern (Refresh-Token im Authorization-Header an denselben JWT-Endpunkt):

curl -X POST -H "apikey: [API-KEY]" -H "Authorization: Bearer <refreshToken>" ^
     https://api.meinserver.de/auth/

Was zeigt das Beispiel?

  • Echte HTTP-Statuscodes aus dem Skript: 201, 403, 404, 409, 422.
  • Optimistic Concurrency über ETag (GET) und If-Match (PUT) -> 409 VERSION_CONFLICT.
  • Idempotente Schreibzugriffe über den Idempotency-Key - vom Server erledigt, das Skript enthält dafür keine Zeile Code.
  • Rollenprüfung aus dem Token-Claim _OBS_JWT_CLAIM_roles, nicht aus dem Body.
  • JWT mit Custom-Claims (tenant/roles) und Refresh-Token mit Rotation.
  • traceId im Fehler-Body für die Support-Nachverfolgung (auch als Header X-Trace-Id).