Zum Hauptinhalt springen
Version: Next

OAuth für XBezahldienste mit DPoP

Überblick

Kontext

Für zu bezahlende Anträge integrieren Onlinedienste den Bezahldienst des jeweiligen Bundeslandes.
Der Bezahldienst vermittelt die Bezahltransaktion an weitere Dienste.
Da es hier nur um das Auslösen der Bezahltransaktion geht, wurden diese in der Grafik ausgespart.
Der Zugriff auf die API des Bezahldienstes wird durch das OAuth 2 Protokoll abgesichert.
Daher muss der Onlinedienst erst ein Zugriffstoken bei einem OAuth-Server beziehen.
Mit diesem Token kann er auf die XBezahldienste API zugreifen, um den Bezahlvorgang auszulösen und den Status abzufragen.

Kontext

Use Cases

Damit der Onlinedienst auf die XBezahldienste API zugreifen kann, müssen entsprechende Vorbedingungen geschaffen werden, die in diesem Dokument beschriebene werden.

Use Cases

Ablauf

Ablauf

JWK Schlüsselpaar generieren

Mit dem Skript jwk-generate.py kann ein privater Schlüssel generiert werden. Alternativ kann dies auch als administrative Funktion in den Onlinedienst eingebaut werden.

#!/usr/bin/env python3  

import uuid

from jwcrypto import jwk

kid = str(uuid.uuid4())
key = jwk.JWK.generate(kid=kid, kty='RSA', size=4096)
print(key.export())

Das Ergebnis sieht dann so aus:

{
"d": "AXfvzVLPCVkvatT5k93aDUl4w862MS3cXA_zJu5cmFBzo6NgVaFoa9ecdi39r_oCNltf88BTG-6vy4mnHulqyC-IAiu1WtLMg4AHPsUDFIk_XJ-IBAtqRHrg8XnVCkum6TuMzN4Z4k8A0IrXqdzRoJbx_O1p7ql8p5TaWMmO_QV_lGtN0InZ6sXppxipzHJgtHIchH2s9BlTQK6ZxIhuHVec8pujexhv-jrqSARk626QmzPr4TeKslqLndOg_zcb56ddvq1cdJfyrPip7fZGjt23gOfJyI8btHoMqqAlpZGybIn4luTVViLhYO6yMDOu7gOFR_90W_HmVmTHDS5n6RMxLWcKEZn9FZ4OwhTD1VX2n2vEK8QlGeNchKXwJeKc7PWRFJr3jWw1qwSdlbBJlzZyTDZvNbEj2B6L3iNyC0GYeCLG5ZtqInk9usCxap8dOKyqczvk4AkjL8WBwV3P8W_Rc53-b2UtxakSRPf485faxDE75_pL1rzEhvh2qntmLXy95mgNUb6yh44rOas78z4eu_SC5vys8Vi-V0mVdo-ZGy2IoZSroHd_-YrdxRCsdMbf3Aw9avyqsAeeLBk7qcWxFxuwFRAEbVqUQ3mlo-3e3pKTe_7n-otQK_1wosgpUA3H9rly4y-dFlAuRXDGwra2hJzg7rmaSQGUUzq_LAE",
"dp": "pTkDcxo1tKlINxhypURmRrpRimLx-M7fFaBYACEnHtnp8-eRIYt9v8shLehPIMA4itT0hoVIxwNwZbeK7E-koXKcaK8pOMlRnAn9iugT9SnvNJ9cWMvd7nHlu1JxCmSRr56k2hPkpRV9EQjCbMzuPWijP0kMiiC1LgkEnssoupOOPwq8ZqPanMPqV8GuBfrQZCupanssLf67cpMoNkA3SKCOnuAhtfAQQC0pafCK_H3H7l6VUI7gX6sPCUuMz4O3Hbtzj6e_RyJFyv2WlNd01Jo-8B8_BD6RG6BnLFEN3JCv2LLNIHevw4jK6AmdCKQFCjB1b3hjIDMnQ2XIDGaxAQ",
"dq": "SVZfEreMh2t6f6YTATIpo6IvlqLUFs25Cu8zOBjCZKkkpQsLKYogLprXJxSWhYZ-ye6l51wwW3F0-wrUsctsOoqmbM7aS0s0ogaPp1r9FBpKy5646_PyUgqSCx4nCF-69Cqg4uU3mS7hSrJXD44lOt8v1PtJm2bxJuuMI5ltft2a73SoypVGobrg9y8joKDcEukO_aHh088EmmOatBjDCEPh2nx8Zl6FPn5cs8hr9CbcqC92RtGBrbW8i2Y-phlYNMIQFgJ1X3HzM_ghIyPmch6NnGTVGEYzRlGyIbQF6VUu2Zp3BplzKv9dX4xsXF0gEw3sEIcf5yFlCO7aPqnUAQ",
"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",
"p": "0FCTajDH6KucKbAvRLpq8kWo_TR7dTz4eiLvl6K8r33d1GRWLDqzwgdLMxbRS_qv-XKDI7mMvMj5rpmLRwfdaZFk8FEYF7suLudI0LBZB6ciuUpA7nzkGL4BmgjEqwXKyqJ3nwp0UBw4iAlk6RX69-XXfyT-2nVKKfOuUscfw8di9x7EutW_-noOXA7DlCq9oEdNO-4RBsV6jzYXBDK8GUEk0PhhCDk9K1jBfOMf4SdcyYXCuqhH79LXa4PXjTEQz6ZA8jTonFiOOD14rHuGyXJL5Ucr44qdc8ieIm-_m-rdMnbl1b9sx2esPXxj7c1wTNtMGa2Mj8LjaIhhK6sewQ",
"q": "tcgoDnisQerLg3PTdFCpYVMk4M7DSdXcc_UjUHC54As-fcoXSCo7Q0Nd-cMYL_7NbH6QwB35Qe9F6Tm0NognGMac2RbrQw4E7jcqqM6zsdagZWhhZB-WGEq1nhgwFl-dqmKTdPGd7BQV750uGyktPtyM3Bbp_TeBGjn376H54jVi4stUp5nr7bwEgzctd0B9rIyuz5jR96SGjxFSkVhy9Uzb907_LPRC5Sgnhkj2mCfSSlKORkCuvRH9_0CewRqNRAfK8RM-EAb_NIyPGOm1FBwxNvEQoCNuzywAMBVzxlLOBw-66tvEPsQNAKMWeHZSat-JUZg1vT5RHxk0ak_2gQ",
"qi": "vi2yN9S6GxqD4DwNVm792QIAo5dFhfu0nLGwFoOJllA2dWIuVzz3WIDq1ZlmTKCoHDHxTJfKTJdPhW0TQJg842rpxk9hF_K5Ci3XNYqXbvD3aH6LwqoWF1EZOfqzl5NnD1vXKdE0FtLMgABcYOvYySM1qoYB7RSHRbt6KIwx7dcX06SVImfANjArSTT6n20R-1gkcPXQQOENEP35n8LXuB1cUAf7m8IUVuBDNsErKORRIGUz-aLCSrejALhcwrvpGBM6pI8re6HX7q1zDLa5T0XGIKaA475GpZ7uxHx5sTbTBJ2fRHznLPKPi6598cJorxcDY3ViJgDEmBkZQQGp0w"
}

Aus dem privaten Schlüssel kann mit dem Skript jwk-2public.py ein öffentlicher Schlüssel abgeleitet werden.

#!/usr/bin/env python3

import sys

from jwcrypto import jwk

key_json = sys.stdin.read()
key = jwk.JWK.from_json(key_json)
print(key.export_public())

Dieser sieht dann so aus:

{
"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"
}

Client registrieren

Der Client wird über den Client Registration Endpoint registriert. Dabei wird der öffentliche Schlüssel im Array jwks.keys angegeben.

Dies erfolgt während des Proof of Concept (PoC) durch das FIT-Connect Team. Im Produktivbetrieb wird die Registrierung über das Self-Service-Portal von FIT-Connect erfolgen.

POST /clients HTTP/1.1
Host: demo.c2id.com
Content-Type: application/json

{
"dpop_bound_access_tokens": true,
"grant_types": [
"urn:ietf:params:oauth:grant-type:jwt-bearer"
],
"preferred_client_id": "example",
"scope": "post read",
"token_endpoint_auth_method": "private_key_jwt",
"token_endpoint_auth_signing_alg": "RS256",
"jwks": {
"keys": [
{
"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"
}
]
}
}

DPoP Proof JWT generieren

Für jeden API-Aufruf und für das Beziehen eines Access Tokens, wird ein DPoP Proof JWT benötigt.

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 und mit dem privaten Schlüssel signiert. Der private Schlüssel muss zu dem öffentlichen Schlüssel im Header gehören.

Beispiel-Skript

Das folgende Skript zeigt beispielhaft, wie der Token generiert wird. Die entsprechende Funktionalität muss in den Onlinedienst eingebaut werden.

#!/usr/bin/env python3

import datetime
import json
import os
import sys
import uuid

from jwcrypto import jwk, jwt


def create_token(header_data, claims_data, private_key):
token = jwt.JWT(header=header_data, claims=claims_data)
token.make_signed_token(private_key)
return token.serialize(compact=True)


def get_config_dir():
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, "config")
return config_dir


def get_client_dir(client_name):
client_dir = os.path.join(get_config_dir(), client_name)
return client_dir


def load_config():
config_file = os.path.join(get_config_dir(), "config.json")
with open(config_file) as config_input:
config = json.load(config_input)
return config


def load_client(client_name):
client_file = os.path.join(get_client_dir(client_name), "client.json")
with open(client_file) as client_input:
client_data = json.load(client_input)
return client_data


def load_key(key_file):
with open(key_file) as key_input:
key_str = key_input.read()
key_dict = json.loads(key_str)
key = jwk.JWK(**key_dict)
return key


def load_private_key(client_name):
key_file = os.path.join(get_client_dir(client_name), "private_key.jwk")
private_key = load_key(key_file)
return private_key


def create_header(client_data):
client_key = client_data["jwks"]["keys"][0]
header_data = {
"typ": "dpop+jwt",
"alg": "RS256",
"jwk": client_key
}
return header_data


def create_claims(config, client_data):
c2id_url = config["c2id"]["url"]
unix_now = datetime.datetime.now(datetime.UTC).timestamp()
claims_data = {
"jti": str(uuid.uuid4()),
"htm": "POST",
"htu": f"{c2id_url}/token",
"iat": unix_now
}
return claims_data


# read client_name as first argument or use "dummy" as default
if len(sys.argv) >= 2:
client_name = sys.argv[1]
else:
client_name = "dummy"

config = load_config()
client_data = load_client(client_name)
private_key = load_private_key(client_name)
header_data = create_header(client_data)
claims_data = create_claims(config, client_data)
dpop_token = create_token(header_data, claims_data, private_key)
print(dpop_token)

Bearer Token generieren

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

Header

Im Header stehen folgende Informationen:

  • kid: Key-ID des registrierten Schlüssels, mit dem das Bearer Token signiert wird.
  • alg: Wir nutzen den Algorithmus "RS256" für "RSASSA-PKCS1-v1_5 using SHA-256" (siehe Liste bei der IANA)
{
"alg": "RS256",
"kid": "c2e0278b-eeb7-4aeb-a670-62f2477d784a"
}

Payload (CLaims)

Im Payload (Claims) stehen folgende Informationen:

  • sub (Subject): Client-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, z.B. eine UUID.

Hinweis: Da sich der Client das Token selbst ausstellt, steht die Client-ID sowohl im Claim "Issuer" (iss) als aus im "Subject" (sub).

{
"aud": "http://localhost:8081",
"exp": 1718276653.722606,
"iss": "example",
"jti": "1b9657b7-15c3-4ea1-9209-9601d009dcac",
"sub": "example"
}

Signieren

Nun wird ein Bearer Token mit diesen Werten erzeugt 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 folgende Skript zeigt beispielhaft, wie der Token generiert wird. Die entsprechende Funktionalität muss in den Onlinedienst eingebaut werden.

#!/usr/bin/env python3

import datetime
import json
import os
import sys
import uuid

from jwcrypto import jwk, jwt


def create_token(header_data, claims_data, private_key):
token = jwt.JWT(header=header_data, claims=claims_data)
token.make_signed_token(private_key)
return token.serialize(compact=True)


def get_config_dir():
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, "config")
return config_dir


def get_client_dir(client_name):
client_dir = os.path.join(get_config_dir(), client_name)
return client_dir


def load_config():
config_file = os.path.join(get_config_dir(), "config.json")
with open(config_file) as config_input:
config = json.load(config_input)
return config


def load_client(client_name):
client_file = os.path.join(get_client_dir(client_name), "client.json")
with open(client_file) as client_input:
client_data = json.load(client_input)
return client_data


def load_key(key_file):
with open(key_file) as key_input:
key_str = key_input.read()
key_dict = json.loads(key_str)
key = jwk.JWK(**key_dict)
return key


def load_private_key(client_name):
key_file = os.path.join(get_client_dir(client_name), "private_key.jwk")
private_key = load_key(key_file)
return private_key


def create_header(client_data):
client_key = client_data["jwks"]["keys"][0]
kid = client_key["kid"]
header_data = {"alg": "RS256", "kid": kid}
return header_data


def create_claims(config, client_data):
c2id_url = config["c2id"]["url"]
client_id = client_data["client_id"]
claims_data = {
"iss": client_id,
"sub": client_id,
"aud": "http://example.com",
"exp": datetime.datetime.now(datetime.UTC).timestamp() + 60, # now + 60 seconds
"jti": str(uuid.uuid4())
}
return claims_data


if len(sys.argv) >= 2:
client_name = sys.argv[1]
else:
client_name = "dummy"

config = load_config()
client_data = load_client(client_name)
private_key = load_private_key(client_name)
header_data = create_header(client_data)
claims_data = create_claims(config, client_data)
bearer_token = create_token(header_data, claims_data, private_key)
print(bearer_token)

Access Token Anfrage

Access Token Abruf

Nun wird das Access Token abgerufen. Dazu wird der DPoP Poof JWT im HTTP-Header "DPoP" und das Access Token im Body als Parameter "assertion" übertragen.

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
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"}

Access Token

Wird das Access Token decodiert, sehen wir im Header folgende Informationen:

  • kid: Verwendeter Schlüssel des Servers
  • typ: Kennzeichnet den DPoP Proof JWT gemäß RFC 9068, Sec. 2.1 als Access Token im JWT Format (application/at+jwt)
  • alg: Verwendeter Signaturalgorithmus
{
"kid": "nfqc",
"typ": "at+jwt",
"alg": "EdDSA"
}

Im Payload (Claims) stehen folgende Informationen:

  • sub (Subject): Client-ID, für den das Token ausgestellt wurde
  • aud (Audience): Dummy-Wert (Ausstellender OAuth-Server)
  • scope: Der Scope für den Zugriff, hier post read
  • iss (Issuer): Ausstellender OAuth-Server
  • cnf (Confirmation): Legt fest, wie der Besitz des Schlüssels nachgewiesen wird. (siehe RFC 7800, Sec. 3.1)
    • jkt (JWK Thumbprint Confirmation Method): Prüfmethode gemäß RFC 9449, Sec. 6.1. Es handelt sich um einen base64url-codierten SHA-256 des Zertifikats.
  • exp (expires): Ablaufzeitpunkt im UNIX Format (Anzahl der Sekunden seit dem 1.1.1970)
  • iat (issued at): Ausstellungszeitpunkt im UNIX Format (Anzahl der Sekunden seit dem 1.1.1970)
  • jti (JWT ID): Eindeutige ID des Tokens
  • client_id: Client-ID, für den das Token ausgestellt wurde; identisch mit Subject oben

Hinweis: In der "Audience" (aud) sollte die Adresse des Servers stehen, für den das Token ausgestellt wurde. Da die Token an keinen bestimmten Bezahldienst gebunden sind, wird dieser Claim nicht verwendet. Der OAuth-Server füllt ihn daher mit seiner eigenen Adresse.

Hinweis 2: 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.

{
"sub": "example",
"aud": "http://localhost:8081",
"scope": "post read",
"iss": "http://localhost:8081",
"cnf": {
"jkt": "L3xaGC5BrqGdKVCu9UZE1E3_SeuJO2XL4TWcfcIgE-4"
},
"exp": 1718276895,
"iat": 1718276595,
"jti": "UVJbm8Fr0iw",
"client_id": "example"
}

API-Aufruf durchführen

Beim API-Aufruf werden beide Token in zwei HTTP-Headern gesendet:

  • Authorization: Enthält Bearer + das Access Token
  • DPoP: Enthält das DPoP Proof JWT

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

DPoP Proof JWT prüfen

Die Prüfung eines DPoP Proof JWTs wird in RFC 9499, Sec. 4.3 beschrieben: To validate a DPoP proof, the receiving server MUST ensure the following:

  1. There is not more than one DPoP HTTP request header field.
  2. The DPoP HTTP request header field value is a single and well-formed JWT.
  3. All required claims per Section 4.2 are contained in the JWT.
  4. The typ JOSE Header Parameter has the value dpop+jwt.
  5. The alg JOSE Header Parameter indicates a registered asymmetric digital signature algorithm [IANA.JOSE.ALGS], is not none, is supported by the application, and is acceptable per local policy.
  6. The JWT signature verifies with the public key contained in the jwk JOSE Header Parameter.
  7. The jwk JOSE Header Parameter does not contain a private key.
  8. The htm claim matches the HTTP method of the current request.
  9. The htu claim matches the HTTP URI value for the HTTP request in which the JWT was received, ignoring any query and fragment parts.
  10. If the server provided a nonce value to the client, the nonce claim matches the server-provided nonce value.
  11. The creation time of the JWT, as determined by either the iat claim or a server managed timestamp via the nonce claim, is within an acceptable window (see Section 11.1).
  12. If presented to a protected resource in conjunction with an access token,
    • ensure that the value of the ath claim equals the hash of that access token, and
    • confirm that the public key to which the access token is bound matches the public key from the DPoP proof.
      To reduce the likelihood of false negatives, servers SHOULD employ syntax-based normalization (Section 6.2.2 of [RFC3986]) and scheme-based normalization (Section 6.2.3 of [RFC3986]) before comparing the htu claim.
      These checks may be performed in any order.

Es wird empfohlen, die Prüfung einer geeigneten Bibliothek zu überlassen.

Access Token prüfen

Das Access Token ist vom OAuth-Server signiert. Das JSON Web Key Set (siehe RFC 7517, Sec. 5) mit allen öffentlichen Schlüsseln des OAuth-Servers finden Sie als Datei jwks.json im obersten Verzeichnis des Servers (z.B. https://xbezahldienste-poc.fit-connect.dev/jwks.json). Das JWK Set kann mehrere Schlüssel enthalten. Es muss der Schlüssel aus dem JWK Set ausgewählt werden, dessen Key-ID (kid) mit der aus dem Header des zu prüfenden Tokens übereinstimmt.

Die Prüfung des Access Tokens wird in RFC 9449, Sec. 6 beschrieben.

Es wird empfohlen, die Prüfung einer geeigneten Bibliothek zu überlassen.

Zugriffsprüfung

Prüfung des Access Token und DPoP Proof JWT

Hinweise zur Prüfung des Zugriffs sind in RFC 9449, Sec. 7 beschrieben.

Es wird empfohlen, die Prüfung einer geeigneten Bibliothek zu überlassen.

Berechtigungsprüfung

Sind die Token korrekt, muss noch geprüft werden, ob die im Access Token angegebenen Scopes den API-Aufruf gestatten.

In der XBezahldienste OpenAPI Spezifikation ist für jeden Endpunkt spezifiziert, welcher Scope erforderlich ist.

Anlagen

Referenzen

Weiterführende Informationen