Skip to Content

DPoP implementieren

DPoP (Demonstrating Proof of Possession, RFC 9449) bindet Access Tokens an ein spezifisches kryptografisches Schlüsselpaar des Clients. Im Gegensatz zu Standard-Bearer-Tokens, die von jedem verwendet werden können, der sie besitzt, sind DPoP-gebundene Tokens für einen Angreifer, der sie stiehlt, nutzlos — er kann ohne den privaten Schlüssel keinen gültigen Proof erstellen.

Gründe für DPoP:

  • Token-Diebstahl verhindern: XSS-Angriffe, die Tokens aus localStorage oder Cookies exfiltrieren, können die gestohlenen Tokens ohne den entsprechenden privaten Schlüssel nicht verwenden
  • Log-Leakage verhindern: Tokens, die versehentlich in Server-Logs oder Fehler-Tracking-Systemen protokolliert wurden, können nicht wiedergegeben werden
  • Replay-Angriffe verhindern: Jeder DPoP-Proof enthält die HTTP-Methode und URL und bindet ihn an eine spezifische Anfrage
  • Compliance: Einige Sicherheitsstandards und Finanz-APIs erfordern Sender-eingeschränkte Tokens

Wie DPoP funktioniert

  1. Der Client generiert ein öffentliches/privates Schlüsselpaar (einmal beim Start oder pro Sitzung)
  2. Beim Anfordern eines Tokens schließt der Client einen DPoP-Proof-JWT im DPoP-Header ein. Der Proof enthält den öffentlichen Schlüssel, die HTTP-Methode und URL sowie einen eindeutigen Bezeichner.
  3. Auris validiert den Proof, bindet das Token an den öffentlichen Schlüssel (über einen jkt-Claim — JWK-Thumbprint) und gibt einen DPoP-Token-Typ anstelle von Bearer zurück
  4. Bei jedem API-Aufruf schließt der Client sowohl das Access Token (Authorization: DPoP <token>) als auch einen frischen DPoP-Proof (DPoP: <proof>) ein
  5. Der Ressourcen-Server validiert den Proof gegen den jkt-Claim im Token

Console-Einrichtung

DPoP in der Anwendung aktivieren

In der Auris Console zu Applications navigieren und deine Anwendung auswählen. Im Tab Settings den DPoP-Bereich finden:

EinstellungBeschreibung
DPoP aktivierenDPoP-Proofs akzeptieren. Mit DPoP angeforderte Tokens werden sender-eingeschränkt. Tokens ohne DPoP werden weiterhin akzeptiert.
DPoP erforderlichAlle Token-Anfragen ohne gültigen DPoP-Proof ablehnen. Nur aktivieren, nachdem alle Clients migriert wurden.
Nonces erforderlichServerseitig ausgestellte Nonces in DPoP-Proofs. Fügt Replay-Schutz auf Kosten eines zusätzlichen Round-Trips hinzu.

Migration planen

Wenn du bestehende Clients mit Bearer-Tokens hast, einen schrittweisen Migrationsansatz verwenden:

  1. DPoP aktivieren (aber nicht erforderlich machen) — Clients können sich einwählen
  2. Alle Clients aktualisieren, um DPoP-Proofs zu senden
  3. Überwachen, dass alle Token-Anfragen DPoP-Proofs enthalten (Auris-Logs prüfen)
  4. “DPoP erforderlich” aktivieren, um Anfragen ohne Proofs abzulehnen

Implementierung für SPAs

Schlüsselpaar generieren

Ein ECDSA P-256-Schlüsselpaar mit der Web Crypto API generieren. Dies einmal pro Browser-Sitzung tun und das Schlüsselpaar im Speicher aufbewahren (nicht in localStorage — es ist absichtlich nicht extrahierbar):

const dpopKeyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, false, // nicht-extrahierbar — der private Schlüssel kann nicht exportiert werden ['sign', 'verify'], )

DPoP-Proof erstellen

Ein DPoP-Proof ist ein JWT, der mit dem privaten Schlüssel signiert ist. Er enthält den öffentlichen Schlüssel (als jwk im Header), die Ziel-HTTP-Methode und URL, eine eindeutige jti und den aktuellen Zeitstempel:

async function createDpopProof(keyPair, method, url, nonce) { // Öffentlichen Schlüssel als JWK exportieren const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey) // Proof-Header erstellen const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: { kty: publicKeyJwk.kty, crv: publicKeyJwk.crv, x: publicKeyJwk.x, y: publicKeyJwk.y, }, } // Proof-Payload erstellen const payload = { jti: crypto.randomUUID(), htm: method, htu: url, iat: Math.floor(Date.now() / 1000), ...(nonce && { nonce }), } // Proof signieren (Hilfsfunktion zum Erstellen eines kompakten JWT) return await signJwt(header, payload, keyPair.privateKey) }

Token mit DPoP anfordern

Den DPoP-Proof im DPoP-Header bei der Token-Anforderung einschließen:

const tokenUrl = 'https://auth.yourdomain.com/api/auth/token' const proof = await createDpopProof(dpopKeyPair, 'POST', tokenUrl) const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'DPoP': proof, }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, redirect_uri: 'https://your-app.com/callback', client_id: 'your-client-id', code_verifier: pkceCodeVerifier, }), }) const token = await response.json() // token.token_type wird "DPoP" statt "Bearer" sein

API-Aufrufe mit DPoP durchführen

Jeder API-Aufruf muss sowohl das DPoP-gebundene Token als auch einen frischen Proof einschließen:

async function dpopFetch(url, method, dpopKeyPair, accessToken, options = {}) { const proof = await createDpopProof(dpopKeyPair, method, url) return fetch(url, { ...options, method, headers: { ...options.headers, 'Authorization': `DPoP ${accessToken}`, 'DPoP': proof, }, }) } // Verwendung const users = await dpopFetch( 'https://auth.yourdomain.com/api/users', 'GET', dpopKeyPair, token.access_token, )

Implementierung für Node.js

Für serverseitige Node.js-Anwendungen die jose-Bibliothek für Schlüsselgenerierung und JWT-Signierung verwenden:

import * as jose from 'jose' // Schlüsselpaar beim Dienst-Start generieren const { publicKey, privateKey } = await jose.generateKeyPair('ES256') async function createDpopProof(method: string, url: string, nonce?: string) { const publicJwk = await jose.exportJWK(publicKey) const proof = await new jose.SignJWT({ htm: method, htu: url, ...(nonce && { nonce }), }) .setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk: publicJwk, }) .setJti(crypto.randomUUID()) .setIssuedAt() .sign(privateKey) return proof } // Token mit DPoP anfordern const tokenUrl = 'https://auth.yourdomain.com/api/auth/token' const proof = await createDpopProof('POST', tokenUrl) const tokenResponse = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'DPoP': proof, }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.AURIS_CLIENT_ID, client_secret: process.env.AURIS_CLIENT_SECRET, }), })

SDK-Unterstützung

Das @auris/js SDK enthält einen createDpopProof-Helper, der Schlüsselgenerierung, Proof-Erstellung und Nonce-Verwaltung übernimmt:

import { AurisClient, createDpopProof } from '@auris/js' // Das SDK kann DPoP-Schlüssel automatisch generieren und verwalten const auris = new AurisClient({ domain: 'auth.yourdomain.com', clientId: 'your-client-id', useDpop: true, // Aktiviert automatische DPoP-Proof-Generierung }) // loginWithRedirect() und handleRedirectCallback() werden // automatisch DPoP-Proofs in Token-Anfragen einschließen await auris.loginWithRedirect({ scope: 'openid profile' })

Nonce-Behandlung

Wenn “Nonces erforderlich” für die Anwendung aktiviert ist, stellt Auris eine serverseitige Nonce aus, die im DPoP-Proof enthalten sein muss. Dies bietet Replay-Schutz — jeder Proof kann nur einmal verwendet werden.

Der Ablauf für die Nonce-Behandlung:

  1. Client sendet eine Token-Anfrage mit einem DPoP-Proof (keine Nonce beim ersten Versuch)
  2. Wenn eine Nonce erforderlich ist, antwortet Auris mit HTTP 400 und einem DPoP-Nonce-Header, der den Nonce-Wert enthält
  3. Client erstellt einen neuen DPoP-Proof mit der Nonce und wiederholt die Anfrage
  4. Auris akzeptiert den Proof und gibt das Token zurück
async function requestTokenWithNonce(dpopKeyPair, tokenUrl, body) { let nonce = undefined for (let attempt = 0; attempt < 2; attempt++) { const proof = await createDpopProof(dpopKeyPair, 'POST', tokenUrl, nonce) const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'DPoP': proof, }, body: new URLSearchParams(body), }) // Prüfen, ob eine Nonce erforderlich ist const newNonce = response.headers.get('DPoP-Nonce') if (response.status === 400 && newNonce) { nonce = newNonce continue // Mit Nonce wiederholen } return await response.json() } throw new Error('Token nach Nonce-Wiederholung nicht erhältlich') }

Prüfe immer auf einen DPoP-Nonce-Header bei jeder Antwort — nicht nur bei Fehlerantworten. Der Server kann die Nonce auch bei erfolgreichen Antworten rotieren, und du solltest die neueste Nonce bei nachfolgenden Anfragen verwenden.


Fehlerbehebung

Häufige Probleme bei der DPoP-Implementierung:

ProblemUrsacheLösung
invalid_dpop_proofProof-JWT ist fehlerhaft oder Signatur ist ungültigSicherstellen, dass der Proof ein gültiger JWT ist, der mit demselben Schlüsselpaar signiert wurde
invalid_dpop_proof (htm/htu-Abweichung)htm oder htu im Proof stimmt nicht mit der tatsächlichen Anfrage-Methode/URL übereinSicherstellen, dass htm der HTTP-Methode und htu der vollständigen URL entspricht (einschließlich Schema und Host, ohne Query/Fragment)
use_dpop_nonceServer erfordert eine Nonce, aber keine wurde angegebenDen DPoP-Nonce-Header aus der Antwort lesen und in den nächsten Proof einschließen
dpop_proof_replayDieselbe jti wurde zweimal verwendetEine eindeutige jti (UUID) für jeden Proof generieren
Token vom Ressourcen-Server abgelehntjkt im Token stimmt nicht mit dem Schlüssel des Proofs übereinSicherstellen, dass dasselbe Schlüsselpaar für die Token-Anfrage und API-Aufrufe verwendet wird
iat zu altUhr-Abweichung zwischen Client und ServerSicherstellen, dass die Uhr des Clients korrekt ist. Auris erlaubt bis zu 60 Sekunden Abweichung.

Mit curl testen

DPoP ist schwierig mit curl direkt zu testen, da jede Anfrage einen eindeutigen signierten JWT-Proof erfordert. Zu Testzwecken können Proofs mit einem Skript generiert werden:

# DPoP-Proof mit Node.js generieren und an curl weiterleiten PROOF=$(node -e " const jose = require('jose'); (async () => { const { privateKey, publicKey } = await jose.generateKeyPair('ES256'); const jwk = await jose.exportJWK(publicKey); const proof = await new jose.SignJWT({ htm: 'POST', htu: 'https://auth.yourdomain.com/api/auth/token' }) .setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk }) .setJti(require('crypto').randomUUID()) .setIssuedAt() .sign(privateKey); process.stdout.write(proof); })(); ") curl -X POST https://auth.yourdomain.com/api/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "DPoP: $PROOF" \ -d "grant_type=client_credentials" \ -d "client_id=your-client-id" \ -d "client_secret=your-client-secret"

DPoP-Token-Format

Wenn DPoP verwendet wird, enthält der ausgestellte Access Token einen jkt-Claim (JWK-Thumbprint), der ihn an den öffentlichen Schlüssel des Clients bindet:

{ "sub": "user-id", "iss": "https://auth.yourdomain.com", "aud": "https://auth.yourdomain.com", "exp": 1735000000, "iat": 1734996400, "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I", "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" } }

Der token_type in der Token-Antwort wird "DPoP" anstelle von "Bearer" sein, was darauf hinweist, dass das Token mit einem DPoP-Proof präsentiert werden muss.


Erforderliche Berechtigungen

OperationBerechtigung
DPoP für eine Anwendung aktivieren/konfigurierenmanage:applications
DPoP-Konfiguration verwaltenmanage:dpop_config
Tokens mit DPoP anfordernKeine besondere Berechtigung (nur Client-Authentifizierung)

Verwandte Anleitungen