Anleitung für Onlinedienste
Diese Anleitung basiert auf dem Konzept OAuth für XBezahldienste mit DPoP.
Überblick
Diese Anleitung gliedert sich in zwei Teile:
- Registrierung beim OAuth-Server
- 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üsseljwk-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:
- Access Token beziehen
- Gültigkeit eines vorhandenen Tokens prüfen
- DPoP Proof JWT erzeugen
- Bearer Token erzeugen
- Access Token beziehen
- API Aufruf
- DPoP Proof JWT erzeugen
- Bearer Token erzeugen
- 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.
Header
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 JWTalg
: 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 IDaud
(Audience): URI des OAuth-Serversiss
(Issuer): Client IDexp
(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ältBearer
+ das Access TokenDPoP
: 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.