Zum Hauptinhalt springen
Version: Next

Anleitung für Onlinedienste

Diese Anleitung basiert auf dem Konzept OAuth für XBezahldienste mit DPoP.

Überblick

Diese Anleitung gliedert sich in zwei Teile:

  1. Registrierung beim OAuth-Server
  2. Implementierung von OAuth mit DPoP im Onlinedienst

Registrierung

Vorbereitung

Zur Erzeugung des Schlüssels werden mit dieser Anleitung drei Skripte zur Verfügung gestellt:

  • create-client.sh führen Sie aus, um einen Client anzulegen. Dieses Skript ruft die anderen beiden Skripte auf.
  • jwk-generate.py generiert einen privaten Schlüssel
  • jwk-2public.py konvertiert einen privaten zu einem öffentlichen Schlüssel

Zur Ausführung benötigen Sie python und idealerweise auch poetry auf Ihrem System. Wenn Sie kein poetry installiert haben, müssen Sie die jwcrypto Bibliothek installieren und die Aufrufe im ersten Skript anpassen.

Sofern Sie poetry nutzen, führen Sie poetry install aus, um die benötigten Bibliotheken (jwcrypto) zu installieren.

$ poetry install

Schlüssel erzeugen

Führen Sie das Skript create-client.sh aus. Es benötigt als Parameter den Namen des Clients. Dieser wird als Verzeichnisname verwendet, nutzen Sie also ein kurzes Wort ohne Leerzeichen.

Das Skript legt das Verzeichnis clients/example an, wobei statt "example" ihr Client-Name eingesetzt wird.

$ ./scripts/create-client.sh example
$ ls -al clients/example
total 16
drwxr-xr-x@ 4 anh staff 128 Jun 20 17:46 .
drwxr-xr-x@ 3 anh staff 96 Jun 20 17:46 ..
-rw-r--r--@ 1 anh staff 3198 Jun 20 17:46 private_key.jwk
-rw-r--r--@ 1 anh staff 760 Jun 20 17:46 public_key.jwk

Das Skript hat im Verzeichnis clients/example zwei Dateien erstellt:

  • private_key.jwk: Der private Schlüssel. Diesen benötigt der Onlinedienst.
  • public_key.jwk: Der öffentliche Schlüssel. Diesen benötigt der Administrator des OAuth-Servers, um Ihren Client anzulegen.

Client registrieren

Nun muss der Client im OAuth-Server angelegt werden. Senden Sie den public_key.jwk an den Administrator des OAuth-Servers, damit Ihr Client angelegt wird.

Hinweis: Die Adresse, an die Sie den Schlüssel senden, sollten Sie mit dieser Anleitung erhalten haben.

Implementierung

Die Implementierung der Authentifikation wird in dieser Anleitung anhand von Python Skripten gezeigt. Benutzen Sie eine geeignete Bibliothek, um die Funktionalität nachzubauen.

Der Ablauf gestaltet sich wie folgt:

  1. Access Token beziehen
    1. Gültigkeit eines vorhandenen Tokens prüfen
    2. DPoP Proof JWT erzeugen
    3. Bearer Token erzeugen
    4. Access Token beziehen
  2. API Aufruf
    1. DPoP Proof JWT erzeugen
    2. Bearer Token erzeugen
    3. API-Aufruf durchführen

DPoP Proof JWT erzeugen

Um ein Access Token zu beziehen und für jeden API-Aufruf wird ein DPoP Proof JWT benötigt. Da DPoP Proof JWTs die HTTP-Methode (z.B. POST) und die URI enthalten, könnten sie nur bei gleichartigen Aufrufen wiederverwendet werden. Zusätzlich sollte zur Sicherheit der Gültigkeitszeitraum (exp/expires) möglichst kurz gewählt werden. Auch kann der Server die Wiederverwendung eines Tokens anhand der Token ID (jti) erkennen und die Wiederverwendung ablehnen. Daher müssen DPoP Proof JWT für jeden Aufruf neu generiert werden.

Für das DPoP Proof JWT wird folgender Header verwendet. Details zu den Parametern sind im RFC 9449, Sec. 4.2 beschrieben.

  • typ: Fester Wert "dpop+jwt" für ein DPoP Proof JWT
  • alg: Wir nutzen den Algorithmus "RS256" für "RSASSA-PKCS1-v1_5 using SHA-256" (siehe Liste bei der IANA)
  • jwk: Der JSON Web Key. Achtung: Hier darf nur der öffentliche Schlüssel mitgesendet werden!
{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {{client_key}}
}

Anstelle von {{client_key}} muss der öffentliche Schlüssel eingesetzt werden. Hier ein Beispiel:

{
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": {
"e": "AQAB",
"kid": "c2e0278b-eeb7-4aeb-a670-62f2477d784a",
"kty": "RSA",
"n": "k-vXxZ0WC_1ZqxEhIvEQulD59kCgfe41-SNAIziFpsyu7ovEyShNN9YEvNiNPf_HfAadJEI-kGuz88gwnmK9ANxrGkzVqmIB_JLcKgFqmXuoAlGZ05v8OIUonhXxMmPselppg-ujf-HAX98HmxGN9OnAHBgkn1Ka9HHhdotMN3lG8HYh3-Y12wuglm3VpauP_Sxl8svkkLWG6cKylq16n4rcVKEo9qwSjxToUO08efgMGykAmHAatvWcTc5zt3JmHCQovbF0_Vryo7fCeMbn6BtLckUHkDZnBg0joOd3WuD2Z1elcwKAKQGH0f5gEHmiIxj0J3CGVGcAwTzqr_YzDQYfQwIFI1TPmRAyR32L-tGWsWVBFN40JFi8cuU91sSUTb9Vyvicw0CLVA1ERJ7CQcDWPKf0fjbNmQPHH4vMl7iVYXJ2egJmZS7Oee-zmmKSSXHd7N9q0tM68gU8AZ5zOm0CERsvnv5uUBP6M7IgyZth9i9_3bVSaHMOUKCZ95cABgEFKMKUk9zonF5srvmpSsV3TaN_NqLnueAiu9apr3MmxvLL68-MSBgnN1krX0rW3JRRFJigp58kW4_uZwWOwUjLCP8-abNN2Ejh20YncLFcGjriJPYrP98ZhufkEIgAeI2vjP7RcdktUV_MNYMdvOdVDb4dhLynu-2Fq9hW9UE"
}
}

Payload (Claims)

Der Payload (Claims) ist wie folgt aufgebaut. Details zu den Parametern sind im RFC 9449, Sec. 4.2 beschrieben.

  • jti: Eine eindeutige ID für den DPoP Proof JWT. Eine zufällige UUID wird empfohlen.
  • htm: HTTP Methode des Aufrufs, hier "POST".
  • htu: HTTP URI des Aufrufs, hier der Token-Endpoint des OAuth-Servers.
  • iat: Ausstellungszeitpunk (issued at) des DPoP Proof JWT.
{    
"jti": "{{random_uuid}}",
"htm": "POST",
"htu": "{{oauth_server_token_url}}",
"iat": {{now}}
}

Auch hier müssen die entsprechenden Werte eingesetzt werden:

  • {{random_uuid}}: Eine zufällig erzeugte UUID
  • {{oauth_server_token_url}}: Die Adresse des Token Endpoints des OAuth-Servers
  • {{now}}: Die aktuelle Zeit im UNIX Format (Anzahl der Sekunden seit dem 1.1.1970)

Hier ein Beispiel:

{
"jti": "aa73024f-fe5f-41ef-ba2c-cab8e7600827",
"htm": "POST",
"htu": "http://localhost:8081/token",
"iat": 1718276594
}

Signieren

Nun wird ein DPoP Proof JWT mit diesen Werten erzeugt wurde und mit dem privaten Schlüssel signiert. Der private Schlüssel muss zu dem öffentlichen Schlüssel im Header gehören.

Beispiel-Skript

Das Skript jwt-generate-dpop-token.py zeigt beispielhaft, wie der Token generiert wird. Die entsprechende Funktionalität muss in den Onlinedienst eingebaut werden.

Bearer Token erzeugen

Für die Access Token Anfrage wird zusätzlich zum DPoP Proof JWT ein Bearer Token benötigt.

Header

Im Header stehen folgende Informationen:

  • alg: Wir nutzen den Algorithmus "RS256" für "RSASSA-PKCS1-v1_5 using SHA-256" (siehe Liste bei der IANA)
  • kid: Verwendeter Schlüssel
{
"alg": "RS256",
"kid": "c2e0278b-eeb7-4aeb-a670-62f2477d784a"
}

Payload (CLaims)

Im Payload (Claims) stehen folgende Informationen:

  • sub (Subject): Clients ID
  • aud (Audience): URI des OAuth-Servers
  • iss (Issuer): Client ID
  • exp (expires): Ablaufzeitpunkt im UNIX Format (Anzahl der Sekunden seit dem 1.1.1970)
  • jti (JWT ID): Eindeutige ID des Tokens
{
"sub": "example",
"aud": "http://localhost:8081",
"iss": "example",
"exp": 1718276653.722606,
"jti": "1b9657b7-15c3-4ea1-9209-9601d009dcac"
}

Signieren

Nun wird ein Bearer Token mit diesen Werten erzeugt wurde und mit dem privaten Schlüssel signiert. Das Bearer Token wird mit dem gleichen Schlüssel wie das DPoP Proof JWT signiert.

Beispiel-Skript

Das Skript jwt-generate-bearer-token.py zeigt beispielhaft, wie der Token generiert wird. Die entsprechende Funktionalität muss in den Onlinedienst eingebaut werden.

Access Token beziehen

Gültigkeit eines vorhandenen Tokens prüfen

Das Access Token kann und sollte bis zu seinem Ablauf wiederverwendet werden. Die Gültigkeitsdauer finden Sie als expires_in Angabe in Sekunden in der Antwort des OAuth-Servers und als exp (expires) Claim im Token selbst als Unix-Zeitstempel.

Prüfen Sie daher vor dem Bezug eines Access Tokens, ob dieses noch eine ausreichende Restgültigkeit hat und verwenden Sie es ggf. wieder. Nur wenn kein wiederverwendbares Access Token vorliegt, wird ein neues abgerufen.

Header

  • Content-Type: application/x-www-form-urlencoded
  • DPoP: Enthält das DPoP Proof JWT für diesen Aufruf

Body

Der Body enthält einen URL-encodierten String mit folgenden Parametern:

  • grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
  • assertion: Der Bearer Token

Beispiel

POST /token HTTP/1.1
Accept: application/json, */*;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 1007
Content-Type: application/x-www-form-urlencoded
DPoP: eyJhbGciOiJSUzI1NiIsImp3ayI6eyJlIjoiQVFBQiIsImtpZCI6ImMyZTAyNzhiLWVlYjctNGFlYi1hNjcwLTYyZjI0NzdkNzg0YSIsImt0eSI6IlJTQSIsIm4iOiJrLXZYeFowV0NfMVpxeEVoSXZFUXVsRDU5a0NnZmU0MS1TTkFJemlGcHN5dTdvdkV5U2hOTjlZRXZOaU5QZl9IZkFhZEpFSS1rR3V6ODhnd25tSzlBTnhyR2t6VnFtSUJfSkxjS2dGcW1YdW9BbEdaMDV2OE9JVW9uaFh4TW1Qc2VscHBnLXVqZi1IQVg5OEhteEdOOU9uQUhCZ2tuMUthOUhIaGRvdE1OM2xHOEhZaDMtWTEyd3VnbG0zVnBhdVBfU3hsOHN2a2tMV0c2Y0t5bHExNm40cmNWS0VvOXF3U2p4VG9VTzA4ZWZnTUd5a0FtSEFhdHZXY1RjNXp0M0ptSENRb3ZiRjBfVnJ5bzdmQ2VNYm42QnRMY2tVSGtEWm5CZzBqb09kM1d1RDJaMWVsY3dLQUtRR0gwZjVnRUhtaUl4ajBKM0NHVkdjQXdUenFyX1l6RFFZZlF3SUZJMVRQbVJBeVIzMkwtdEdXc1dWQkZONDBKRmk4Y3VVOTFzU1VUYjlWeXZpY3cwQ0xWQTFFUko3Q1FjRFdQS2YwZmpiTm1RUEhINHZNbDdpVllYSjJlZ0ptWlM3T2VlLXptbUtTU1hIZDdOOXEwdE02OGdVOEFaNXpPbTBDRVJzdm52NXVVQlA2TTdJZ3ladGg5aTlfM2JWU2FITU9VS0NaOTVjQUJnRUZLTUtVazl6b25GNXNydm1wU3NWM1RhTl9OcUxudWVBaXU5YXByM01teHZMTDY4LU1TQmduTjFrclgwclczSlJSRkppZ3A1OGtXNF91WndXT3dVakxDUDgtYWJOTjJFamgyMFluY0xGY0dqcmlKUFlyUDk4Wmh1ZmtFSWdBZUkydmpQN1JjZGt0VVZfTU5ZTWR2T2RWRGI0ZGhMeW51LTJGcTloVzlVRSJ9LCJ0eXAiOiJkcG9wK2p3dCJ9.eyJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxL3Rva2VuIiwiaWF0IjoxNzE4Mjc2NTk0LjY0MDQ3MSwianRpIjoiYWE3MzAyNGYtZmU1Zi00MWVmLWJhMmMtY2FiOGU3NjAwODI3In0.OxIWF61QWYwFCozmALMCepb_V5MsT7BUYVMtI0RmyNAjZXz4iS1dvtDkDk009DXCTSrMWl8g9_evbJLqZ-PryZ8UNzo67SfmiHt0KOrGpNP8naEQpyaRFzNRvBMYoO-H03c_aRy0AmzEi5aGqzDlBTdxpllGwH-jVg3nkiSZnRQG7Z8xeXUEMjaP3YAxrHFZGI8FUF_8PeENlnyGP6wpSD-qEqNLZWqG5Hy4Av2C-p04-r_XMVpe3NfwTJU7XCrhlmq8yiSd_fc4qRONWE57TIpcnSWc5gbV4FzzoKsHWH5zerITJSQlhjXdiJqeamy_Y0lGp0i1kAtKK25-3om1_Ezu-LTfqsyWHgh7tGOAr01fFKIjn34tMDbsaKKL1IKWB76DzMDctbqaosuE2bxx96aexTwW32VDfn_YjDuYT0U-hd0TXDZLvGPraCuaZqgXGzhYd4zXFQIGOdvVmMpLWd_l613RIhg6w0hmUgDi-Jf2syuEC5e_vDLk66X0i7IA8n6-4zP01LQN0UeictcqyizSlCEyg83wVwUyIutajjM3OhTxxUgxBsHPnap3XtTouuJhmo08dIVnRqp3-lhdm1deuiruy91Ha_hXfCuxdH--TFjnoh2A-l1XnEBtOqqAz8RkNpXXtRtHjSHWZVNfheaOX8XiaJSXQDVkJ-UsYyQ
Host: localhost:8081
User-Agent: HTTPie/3.2.2

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6ImMyZTAyNzhiLWVlYjctNGFlYi1hNjcwLTYyZjI0NzdkNzg0YSJ9.eyJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjgwODEiLCJleHAiOjE3MTgyNzY2NTMuNzIyNjA2LCJpc3MiOiJleGFtcGxlIiwianRpIjoiMWI5NjU3YjctMTVjMy00ZWExLTkyMDktOTYwMWQwMDlkY2FjIiwic3ViIjoiZXhhbXBsZSJ9.MOPd6O6dgSZc79BeBG2Phgl3x3c1E40SM0s9PrlKE3LyEWZmSgoz8FfJVx76Vn6EE4LwblAgmKiFlmskX5uS2jwxcJ-sSWgtCCdbaTrOaAcsf_dSO_D7FvR4Jcdt5kAaUlk8Iwk7IBvStKjw0e1YAzuKmy7Tf1S6vHMRbEVqktBd4tgfwk_c_U3CsnK3m-1hJL7_z2s7FORnpkjuPPpgnwBcZOsGsTvNnS6dUdN5Ft6-vpq35qdPgw8DoA9WcP9_SXtlxww-oRGVRlXtSSfbmh1Fky15n-AKOkukDgExtUqlVIXMKSBaun2nEvdhQCCrG242omAwSTfNH2XgH5Y0s11dDPVVNnu8550eSgSq6d6szkCz2fB-QZ9PNZcKbPy4CgsRNw_vjvfjTk9HbI11H1qbWZwZWlmYsYcrtbTbB4qZ1SJCJyskJyFg76kQR_RKivRt_fcMVupm5ARjpeyDxnUU3DmZxhDx9ZMOGs_q2tVOBRv0tLb2DtxOM63FO3jKT21Yq9W2fYnKw3YMkIyL-xzdW77XqZQW-5ezxtRCkSmbts8aR292TSiUJ9XDPtSaiOXcdnx9UWfobQBz2kGrTgxOkFu9Do4DUGYt_4aPXa6e-XQOCRj6BaykcfLIU4P0PpBQRhXlXr5ZoDOUoN5gDFORjCKCW2iq5wdHXdGA4bQ

Antwort

Im Erfolgsfall ist die Antwort ein JSON-Dokument, das unter "access_token" den Access Token enthält.

HTTP/1.1 200 Cache-Control: no-store
Connection: keep-alive
Content-Length: 552
Content-Type: application/json;charset=UTF-8
Date: Thu, 13 Jun 2024 11:03:15 GMT
Keep-Alive: timeout=20
Pragma: no-cache

{"access_token": "eyJraWQiOiJuZnFjIiwidHlwIjoiYXQrand0IiwiYWxnIjoiRWREU0EifQ.eyJzdWIiOiJleGFtcGxlIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgxIiwic2NvcGUiOiJzdGFydF90cmFuc2FjdGlvbiIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MSIsImNuZiI6eyJqa3QiOiJMM3hhR0M1QnJxR2RLVkN1OVVaRTFFM19TZXVKTzJYTDRUV2NmY0lnRS00In0sImV4cCI6MTcxODI3Njg5NSwiaWF0IjoxNzE4Mjc2NTk1LCJqdGkiOiJVVkpibThGcjBpdyIsImNsaWVudF9pZCI6ImV4YW1wbGUifQ.c1bpTDl8jlx0XZJZYLr4c8HD7ZNpW04dZhmNEq9MAICxLd7ZrO07TeOEahfSSBeNiLKoRnW_NF4drJv3Vk2DDw", "expires_in": 300, "scope": "post read", "token_type": "DPoP"}

API-Aufruf durchführen

Bei einem API-Aufruf müssen zwei Header mitgesendet werden:

  • Authorization: Enthält Bearer + das Access Token
  • DPoP: Enthält das DPoP Proof JWT für diesen Aufruf

Alle weiteren Header und der Body werden gemäß dem API-Aufruf verwendet.

Hinweis: DPoP Proof JWTs enthalten die HTTP-Methode (z.B. POST) und die URI und müssen daher für jeden Aufruf individuell generiert werden.