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
- Der Client generiert ein öffentliches/privates Schlüsselpaar (einmal beim Start oder pro Sitzung)
- 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. - Auris validiert den Proof, bindet das Token an den öffentlichen Schlüssel (über einen
jkt-Claim — JWK-Thumbprint) und gibt einenDPoP-Token-Typ anstelle vonBearerzurück - Bei jedem API-Aufruf schließt der Client sowohl das Access Token (
Authorization: DPoP <token>) als auch einen frischen DPoP-Proof (DPoP: <proof>) ein - 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:
| Einstellung | Beschreibung |
|---|---|
| DPoP aktivieren | DPoP-Proofs akzeptieren. Mit DPoP angeforderte Tokens werden sender-eingeschränkt. Tokens ohne DPoP werden weiterhin akzeptiert. |
| DPoP erforderlich | Alle Token-Anfragen ohne gültigen DPoP-Proof ablehnen. Nur aktivieren, nachdem alle Clients migriert wurden. |
| Nonces erforderlich | Serverseitig 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:
- DPoP aktivieren (aber nicht erforderlich machen) — Clients können sich einwählen
- Alle Clients aktualisieren, um DPoP-Proofs zu senden
- Überwachen, dass alle Token-Anfragen DPoP-Proofs enthalten (Auris-Logs prüfen)
- “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" seinAPI-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:
- Client sendet eine Token-Anfrage mit einem DPoP-Proof (keine Nonce beim ersten Versuch)
- Wenn eine Nonce erforderlich ist, antwortet Auris mit
HTTP 400und einemDPoP-Nonce-Header, der den Nonce-Wert enthält - Client erstellt einen neuen DPoP-Proof mit der Nonce und wiederholt die Anfrage
- 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:
| Problem | Ursache | Lösung |
|---|---|---|
invalid_dpop_proof | Proof-JWT ist fehlerhaft oder Signatur ist ungültig | Sicherstellen, 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 überein | Sicherstellen, dass htm der HTTP-Methode und htu der vollständigen URL entspricht (einschließlich Schema und Host, ohne Query/Fragment) |
use_dpop_nonce | Server erfordert eine Nonce, aber keine wurde angegeben | Den DPoP-Nonce-Header aus der Antwort lesen und in den nächsten Proof einschließen |
dpop_proof_replay | Dieselbe jti wurde zweimal verwendet | Eine eindeutige jti (UUID) für jeden Proof generieren |
| Token vom Ressourcen-Server abgelehnt | jkt im Token stimmt nicht mit dem Schlüssel des Proofs überein | Sicherstellen, dass dasselbe Schlüsselpaar für die Token-Anfrage und API-Aufrufe verwendet wird |
iat zu alt | Uhr-Abweichung zwischen Client und Server | Sicherstellen, 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
| Operation | Berechtigung |
|---|---|
| DPoP für eine Anwendung aktivieren/konfigurieren | manage:applications |
| DPoP-Konfiguration verwalten | manage:dpop_config |
| Tokens mit DPoP anfordern | Keine besondere Berechtigung (nur Client-Authentifizierung) |
Verwandte Anleitungen
- M2M Client Credentials — DPoP-Abschnitt für M2M-Tokens
- Hosted Login (PKCE) — DPoP kann mit PKCE für maximale Sicherheit kombiniert werden
- Angriffsschutz — Andere Sicherheitsebenen, die DPoP ergänzen