Skip to Content

DPoP (Demonstrating Proof of Possession)

Das Problem mit Bearer-Tokens

Die OAuth 2.0 Bearer-Token-Spezifikation (RFC 6750) definiert Tokens, die jedem Zugriff gewähren, der sie besitzt. Das Wort „Bearer” ist wörtlich: Wer das Token trägt (hält), kann es verwenden. Es gibt keine Überprüfung, ob der Präsentierende des Tokens dieselbe Partei ist, für die das Token ausgestellt wurde.

Dies schafft eine Klasse von Angriffen:

AngriffMechanismus
Token-Exfiltration via XSSBösartiges JavaScript liest das Token aus localStorage und sendet es an den Server eines Angreifers
Token-Leck über LogsZugriffstoken werden versehentlich von Proxys, CDNs oder Anwendungsservern protokolliert
Token-ReplayEin abgefangenes Token wird von einem anderen Gerät oder einer anderen IP wiedergegeben
Token-Diebstahl über kompromittierte MiddlewareEin Reverse-Proxy oder API-Gateway speichert Tokens für späteren Missbrauch

In all diesen Fällen funktioniert das gestohlene Token in den Händen des Angreifers genauso gut wie in den Händen des legitimen Clients. Der Ressourcenserver kann nicht zwischen beiden unterscheiden, da Bearer-Tokens keinen Beweis darüber tragen, wer sie präsentiert.

Was DPoP tut

DPoP (Demonstrating Proof of Possession), definiert in RFC 9449, löst dieses Problem durch Bindung von Tokens an das kryptographische Schlüsselpaar des Clients. Ein DPoP-gebundenes Token ist ohne den entsprechenden privaten Schlüssel nutzlos.

Die Kernidee:

  1. Der Client generiert ein ephemeres Schlüsselpaar (RSA oder EC)
  2. Beim Anfordern eines Tokens erstellt der Client einen DPoP-Proof — ein JWT, das mit dem privaten Schlüssel signiert ist
  3. Der Autorisierungsserver (Auris) extrahiert den öffentlichen Schlüssel aus dem Proof und bindet das ausgestellte Token an diesen Schlüssel über einen JWK-Thumbprint (jkt-Claim)
  4. Bei jedem API-Aufruf sendet der Client sowohl das gebundene Zugriffstoken als auch einen frischen DPoP-Proof, der mit demselben privaten Schlüssel signiert ist
  5. Der Ressourcenserver verifiziert, dass der öffentliche Schlüssel des Proofs mit der jkt-Bindung des Tokens übereinstimmt

Wenn ein Angreifer das Zugriffstoken, aber nicht den privaten Schlüssel (der im Speicher gehalten und niemals übertragen wird) stiehlt, ist das Token wertlos — der Angreifer kann keine gültigen DPoP-Proofs erstellen.

Funktionsweise: Schritt für Schritt

Schritt 1: Client generiert ein ephemeres Schlüsselpaar

Wenn die Client-Anwendung initialisiert wird, generiert sie ein RSA- oder EC-Schlüsselpaar. Dieses Schlüsselpaar ist ephemer — es existiert nur im Speicher des Clients (oder sicherem Speicher) und wird nie an einen Server gesendet.

// Ein EC-Schlüsselpaar (P-256) über die Web Crypto API generieren const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, false, // nicht-extrahierbar: privater Schlüssel kann nicht exportiert werden ['sign', 'verify'] )

Der private Schlüssel ist als nicht-extrahierbar markiert, was bedeutet, dass selbst das eigene JavaScript des Clients das rohe Schlüsselmaterial nicht lesen kann. Es kann nur für Signieroperationen verwendet werden.

Schritt 2: Client erstellt ein DPoP-Proof-JWT

Vor einer Token-Anfrage erstellt der Client einen DPoP-Proof — ein JWT mit spezifischen Claims, signiert mit dem privaten Schlüssel:

// DPoP Proof JWT Header { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "base64url-kodierte-x-koordinate", "y": "base64url-kodierte-y-koordinate" } } // DPoP Proof JWT Payload { "jti": "eindeutige-proof-id-abc123", "htm": "POST", "htu": "https://auth.example.com/api/auth/token", "iat": 1739880000, "nonce": "server-bereitgestellte-nonce" }
ClaimBeschreibung
jtiEin eindeutiger Bezeichner für diesen Proof (verhindert Replay)
htmDie HTTP-Methode der Anfrage, die dieser Proof begleitet
htuDie HTTP-URL der Anfrage (ohne Query/Fragment)
iatWann der Proof erstellt wurde (muss aktuell sein)
nonceVom Server bereitgestellte Nonce für Frische (optional, siehe Nonce-Verwaltung)
jwk (Header)Der öffentliche Schlüssel entsprechend dem privaten Schlüssel, der zur Signierung des Proofs verwendet wurde

Der Proof wird mit dem privaten Schlüssel signiert. Der öffentliche Schlüssel ist im jwk-Header eingebettet, damit der Server die Signatur verifizieren kann.

Schritt 3: Client sendet DPoP-Proof mit Token-Anfrage

Der DPoP-Proof wird im DPoP-HTTP-Header zusammen mit der normalen Token-Anfrage gesendet:

POST /api/auth/token HTTP/1.1 Host: auth.example.com Content-Type: application/x-www-form-urlencoded DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7... grant_type=authorization_code &code=auth-code-hier &redirect_uri=https://app.example.com/callback &client_id=ihre-client-id &code_verifier=pkce-verifier

Schritt 4: Server bindet das Token an den Schlüssel

Auris validiert den DPoP-Proof:

  1. Verifiziert, dass der typ-Header dpop+jwt ist
  2. Verifiziert die Signatur mithilfe des eingebetteten jwk-öffentlichen Schlüssels
  3. Prüft, dass htm der Anfragemethode und htu der Anfrage-URL entspricht
  4. Prüft, dass iat aktuell ist (innerhalb akzeptabler Uhrabweichung)
  5. Prüft jti-Eindeutigkeit (verhindert Proof-Replay)
  6. Wenn eine Nonce erforderlich war, validiert den nonce-Claim

Wenn die Validierung erfolgreich ist, berechnet Auris den JWK-Thumbprint (einen Hash des öffentlichen Schlüssels gemäß RFC 7638) und fügt ihn als jkt-Claim in das ausgestellte Zugriffstoken ein:

// Zugriffstoken-Payload (DPoP-gebunden) { "sub": "usr_abc123", "iss": "https://auth.example.com", "aud": "ihre-client-id", "iat": 1739880000, "exp": 1739880900, "cnf": { "jkt": "JWK-thumbprint-des-oeffentlichen-schlüssels-des-clients" }, "token_type": "DPoP" }

Der cnf.jkt-(Confirmation / JWK-Thumbprint-)Claim bindet dieses Token kryptographisch an das Schlüsselpaar des Clients. Der token_type ist auf DPoP anstelle von Bearer gesetzt.

Schritt 5: Client sendet gebundenes Token + frischen Proof bei API-Aufrufen

Bei jedem API-Aufruf sendet der Client sowohl das DPoP-gebundene Zugriffstoken im Authorization-Header als auch einen frischen DPoP-Proof im DPoP-Header:

GET /api/users/me HTTP/1.1 Host: api.example.com Authorization: DPoP eyJhbGciOiJSUzI1NiJ9... DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...

Das Authorization-Schema ist DPoP, nicht Bearer. Jeder API-Aufruf erfordert einen frischen Proof mit einer eindeutigen jti, dem korrekten htm/htu für die aktuelle Anfrage und einem aktuellen iat.

Schritt 6: Ressourcenserver verifiziert die Bindung

Der Ressourcenserver:

  1. Extrahiert den DPoP-Proof aus dem DPoP-Header
  2. Validiert den Proof (Signatur, htm, htu, iat, jti)
  3. Berechnet den JWK-Thumbprint des jwk-öffentlichen Schlüssels des Proofs
  4. Vergleicht ihn mit dem cnf.jkt-Claim im Zugriffstoken
  5. Wenn sie übereinstimmen, hat der Präsentierende den privaten Schlüssel — Proof of Possession bestätigt

Wenn die Thumbprints nicht übereinstimmen, wird die Anfrage mit 401 Unauthorized abgelehnt — das Token wurde für ein anderes Schlüsselpaar ausgestellt, was auf Token-Diebstahl hinweist.

Nonce-Verwaltung

Nonces fügen eine zusätzliche Schicht von Replay-Schutz hinzu. Wenn aktiviert, stellt der Autorisierungsserver eine Nonce bereit, die der Client in den nächsten DPoP-Proof einschließen muss.

Funktionsweise von Nonces

  1. Client sendet eine Token-Anfrage ohne Nonce
  2. Server antwortet mit 400 und einem DPoP-Nonce-Header mit einem frischen Nonce-Wert
  3. Client wiederholt die Anfrage und schließt die Nonce im nonce-Claim des DPoP-Proofs ein
  4. Server validiert die Nonce und verarbeitet die Anfrage
  5. Optional schließt der Server einen neuen DPoP-Nonce-Header in der Antwort für die nächste Anfrage ein
// Erster Versuch (keine Nonce) POST /api/auth/token HTTP/1.1 DPoP: eyJ0eXAiOiJkcG9wK2p3dCJ9... // kein nonce-Claim // Server-Antwort HTTP/1.1 400 Bad Request DPoP-Nonce: eyJ2IjoiMSIsImlhdCI6MTczOTg4MDAwMH0 Content-Type: application/json {"error": "use_dpop_nonce"} // Zweiter Versuch (mit Nonce) POST /api/auth/token HTTP/1.1 DPoP: eyJ0eXAiOiJkcG9wK2p3dCJ9... // enthält "nonce": "eyJ2IjoiMSIs..."

Warum Nonces helfen

Ohne Nonces ist ein DPoP-Proof gültig, solange sein iat innerhalb des akzeptablen Zeitfensters liegt (typischerweise 60 Sekunden). Ein Angreifer, der den Proof während der Übertragung abfängt, hat ein kurzes Fenster, um ihn wiederzugeben. Nonces eliminieren dieses Fenster: Ein Proof ist nur gültig, wenn er die aktuellste Nonce des Servers enthält, die sich mit jeder Interaktion ändert.

Auris-Nonce-Implementierung

Auris speichert Nonces im DpopNonce-Prisma-Modell mit einer kurzen TTL. Nonces werden als signierte Tokens generiert, die eine Version und einen Zeitstempel enthalten, was es dem Server ermöglicht, sie ohne Datenbankabfrage für den üblichen Fall zu validieren.

Nonce-Verwaltung fügt der ersten Anfrage in einer Sitzung einen zusätzlichen Roundtrip hinzu. Für die meisten Anwendungen bietet die iat-Frischeprüfung allein ausreichenden Replay-Schutz. Aktiviere Nonces nur beim Schutz gegen ausgeklügelte Angreifer auf Netzwerkebene (z. B. in Zero-Trust-Umgebungen oder hochwertigen Finanz-APIs).

DPoP vs mTLS

Sowohl DPoP als auch mTLS (Mutual TLS, RFC 8705) lösen das gleiche grundlegende Problem: Tokens an die Identität des Clients binden. Sie verfolgen unterschiedliche Ansätze:

DimensionDPoP (RFC 9449)mTLS (RFC 8705)
BindungsmechanismusJWK-Thumbprint im Token + signiertes Proof-JWTClient-Zertifikat-Thumbprint im Token (cnf.x5t#S256)
InfrastrukturanforderungKeine (reine Anwendungsschicht)TLS-Terminierung muss Client-Zertifikat beibehalten
Client-KomplexitätSchlüsselpaar generieren + JWTs signierenX.509-Zertifikate beschaffen und verwalten
Browser-UnterstützungFunktioniert in Browsern via Web Crypto APINicht in Browsern unterstützt
ZertifikatsverwaltungKeine Zertifikate erforderlichErfordert PKI oder Zertifizierungsstelle
Proxy/CDN-KompatibilitätAusgezeichnet (Header passieren durch)Problematisch (TLS-Terminierung kann Client-Zertifikat entfernen)
LeistungProof-Generierung ist schnell (EC-Signierung ~1ms)TLS-Handshake ist aufwändiger, aber amortisiert
Token-DiebstahlschutzAusgezeichnetAusgezeichnet

Wann DPoP verwenden

  • Browser-Anwendungen (SPAs): DPoP ist der einzige Proof-of-Possession-Mechanismus, der in Browsern funktioniert
  • Mobile Anwendungen: Einfacher zu implementieren als mTLS-Zertifikatsverwaltung
  • Microservice-zu-Microservice: Wenn keine mTLS-Infrastruktur verfügbar ist
  • Hinter CDNs/Load-Balancern: DPoP funktioniert durch jeden Proxy, da es HTTP-Header verwendet

Wann mTLS verwenden

  • Server-zu-Server mit PKI: Wenn deine Organisation bereits Zertifikatsinfrastruktur hat
  • Zero-Trust-Netzwerke: Wo gegenseitige TLS-Authentifizierung auf der Transportschicht erforderlich ist
  • Regulatorische Anforderungen: Einige Finanzvorschriften verlangen speziell mTLS

Auris unterstützt DPoP. mTLS wird auf der Keycloak-Schicht für direkte Keycloak-Client-Integrationen unterstützt.

Auris-Implementierungsdetails

DPoP aktivieren

DPoP wird pro Anwendung in der Auris-Konsole konfiguriert. Gehe zu Anwendungen → (Anwendung auswählen) → Einstellungen:

EinstellungBeschreibung
DPoP aktivierenHaupt-Toggle für DPoP-Unterstützung dieser Anwendung
DPoP verlangenWenn aktiviert, lehnt die Anwendung Nicht-DPoP-Token-Anfragen ab
Nonces verlangenWenn aktiviert, müssen alle DPoP-Proofs eine server-bereitgestellte Nonce enthalten

Wenn DPoP aktiviert, aber nicht erforderlich ist, akzeptiert die Anwendung sowohl DPoP-gebundene Tokens (Authorization: DPoP ...) als auch einfache Bearer-Tokens (Authorization: Bearer ...). Dies ermöglicht eine schrittweise Migration.

DPoP-gebundene M2M-Tokens

DPoP wird auch für Machine-to-Machine (M2M)-Tokens unterstützt, die über den client_credentials-Grant ausgestellt werden. M2M-Tokens werden im M2MToken-Prisma-Modell mit jkt- (JWK-Thumbprint) und isDpopBound-Feldern gespeichert.

POST /api/auth/token Content-Type: application/x-www-form-urlencoded DPoP: eyJ0eXAiOiJkcG9wK2p3dCJ9... grant_type=client_credentials &client_id=m2m-client-id &client_secret=m2m-client-secret &scope=read:users

Das resultierende M2M-Token ist an das DPoP-Schlüsselpaar gebunden und bietet Proof-of-Possession für Service-zu-Service-Kommunikation.

DPoP-Proof-Validierung

Auris validiert DPoP-Proofs in der dpop-validator.ts-Middleware mit folgenden Prüfungen:

PrüfungFehlerantwort
typ-Header muss dpop+jwt sein400 invalid_dpop_proof
jwk-Header muss einen gültigen öffentlichen Schlüssel enthalten400 invalid_dpop_proof
Signatur muss mit dem eingebetteten jwk gültig sein400 invalid_dpop_proof
htm muss der HTTP-Methode der aktuellen Anfrage entsprechen400 invalid_dpop_proof
htu muss der URL der aktuellen Anfrage entsprechen (Schema + Host + Pfad)400 invalid_dpop_proof
iat muss innerhalb von 60 Sekunden der Server-Zeit liegen400 invalid_dpop_proof
jti darf nicht zuvor verwendet worden sein (Replay-Prüfung)400 invalid_dpop_proof
nonce muss der Nonce des Servers entsprechen (wenn Nonces erforderlich)400 use_dpop_nonce (mit DPoP-Nonce-Header)

Codebeispiel: DPoP-Proofs in TypeScript generieren

Das folgende Beispiel demonstriert den vollständigen DPoP-Flow über die Web Crypto API, geeignet für Browser- und Node.js-Umgebungen:

import { SignJWT, exportJWK, calculateJwkThumbprint } from 'jose' // Schritt 1: Ein ephemeres EC-Schlüsselpaar generieren const keyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'] ) // Schritt 2: Öffentlichen Schlüssel als JWK exportieren (für den Proof-Header) const publicJwk = await exportJWK(keyPair.publicKey) publicJwk.alg = 'ES256' // Schritt 3: DPoP-Proof für eine Token-Anfrage erstellen async function createDpopProof( method: string, url: string, nonce?: string ): Promise<string> { const builder = new SignJWT({ htm: method, htu: url, ...(nonce ? { nonce } : {}), }) .setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk: publicJwk, }) .setJti(crypto.randomUUID()) .setIssuedAt() return builder.sign(keyPair.privateKey) } // Schritt 4: DPoP-gebundenes Token anfordern const tokenUrl = 'https://auth.example.com/api/auth/token' const dpopProof = await createDpopProof('POST', tokenUrl) const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'DPoP': dpopProof, }, body: new URLSearchParams({ grant_type: 'authorization_code', code: 'auth-code', redirect_uri: 'https://app.example.com/callback', client_id: 'ihre-client-id', code_verifier: 'pkce-verifier', }), }) // Nonce-Anforderung behandeln if (response.status === 400) { const nonce = response.headers.get('DPoP-Nonce') if (nonce) { // Wiederholung mit Nonce const retryProof = await createDpopProof('POST', tokenUrl, nonce) // ... Anfrage mit retryProof wiederholen } } const { accessToken } = await response.json() // Schritt 5: DPoP-gebundenes Token bei API-Aufrufen verwenden const apiUrl = 'https://api.example.com/users/me' const apiProof = await createDpopProof('GET', apiUrl) const apiResponse = await fetch(apiUrl, { headers: { 'Authorization': `DPoP ${accessToken}`, 'DPoP': apiProof, }, })

Jeder DPoP-Proof muss eine eindeutige jti und ein frisches iat haben. Proofs niemals über Anfragen hinweg wiederverwenden. Das htm und htu müssen genau mit der Anfrage übereinstimmen — ein für GET /api/users erstellter Proof kann nicht für POST /api/users oder GET /api/roles verwendet werden.

Sicherheitseigenschaften

DPoP bietet folgende Sicherheitsgarantien:

EigenschaftWie sie erreicht wird
Token-Bindungcnf.jkt des Zugriffstokens stimmt mit dem jwk-Thumbprint des Proofs überein
Replay-SchutzJeder Proof hat eine eindeutige jti und aktuelles iat; Server-seitige Nonces fügen extra Frische hinzu
Methoden/URL-Bindunghtm und htu verhindern Proof-Wiederverwendung über verschiedene Endpunkte hinweg
Schlüssel-Nicht-ExfiltrierbarkeitPrivater Schlüssel wird mit extractable: false in Web Crypto generiert
Forward SecrecyEphemere Schlüsselpaare bedeuten, dass die Kompromittierung des Schlüssels einer Sitzung keine anderen Sitzungen betrifft

DPoP schützt nicht gegen:

  • XSS mit vollständiger Code-Ausführung: Wenn ein Angreifer beliebiges JavaScript im gleichen Origin ausführen kann, kann er crypto.subtle.sign() mit dem nicht-extrahierbaren Schlüssel aufrufen, um gültige Proofs zu erstellen. Er kann den Schlüssel jedoch nicht für die Verwendung außerhalb des Browsers exfiltrieren.
  • Kompromittierte Client-Binärdatei: Wenn die Client-Anwendung selbst mit einer Backdoor versehen ist, bietet DPoP keinen zusätzlichen Schutz.

Verwandte Konzepte