Implémenter DPoP
DPoP (Demonstrating Proof of Possession, RFC 9449) lie les access tokens à une paire de clés cryptographiques spécifiques au client. Contrairement aux tokens Bearer standard qui peuvent être utilisés par quiconque les possède, les tokens liés avec DPoP sont inutiles pour un attaquant qui les vole — il ne peut pas produire une proof valide sans la clé privée.
Pourquoi utiliser DPoP :
- Prévenir le vol de tokens : les attaques XSS qui exfiltrent des tokens depuis localStorage ou les cookies ne peuvent pas utiliser les tokens volés sans la clé privée correspondante
- Prévenir les fuites dans les logs : les tokens accidentellement enregistrés dans les logs serveur ou les systèmes de suivi d’erreurs ne peuvent pas être rejoués
- Prévenir les attaques replay : chaque DPoP proof inclut la méthode HTTP et l’URL, la liant à une requête spécifique
- Conformité : certains standards de sécurité et APIs financières exigent des tokens liés à l’émetteur
Comment fonctionne DPoP
- Le client génère une paire de clés publique/privée (une fois, au démarrage ou par session)
- Lors de la demande d’un token, le client inclut une DPoP proof JWT dans le header
DPoP. La proof contient la clé publique, la méthode HTTP et l’URL, et un identifiant unique. - Auris valide la proof, lie le token à la clé publique (via un claim
jkt— JWK Thumbprint) et retourne un type de tokenDPoPau lieu deBearer - À chaque appel API, le client inclut à la fois l’access token (
Authorization: DPoP <token>) et une nouvelle DPoP proof (DPoP: <proof>) - Le resource server valide la proof par rapport au claim
jktdans le token
Configuration dans la Console
Active DPoP sur l’Application
Dans la Console Auris, va sur Applications et sélectionne ton application. Dans l’onglet Paramètres, trouve la section DPoP :
| Paramètre | Description |
|---|---|
| Activer DPoP | Accepte les DPoP proofs. Les tokens demandés avec DPoP seront liés à l’émetteur. Les tokens sans DPoP sont toujours acceptés. |
| Exiger DPoP | Refuse toutes les demandes de tokens sans une DPoP proof valide. Active seulement après que tous les clients ont terminé la migration. |
| Exiger un Nonce | Nonces émis par le serveur dans les DPoP proofs. Ajoute une protection replay au coût d’un aller-retour supplémentaire. |
Planifie la Migration
Si tu as des clients existants utilisant des tokens Bearer, utilise une approche de migration progressive :
- Active DPoP (mais ne l’exige pas) — les clients peuvent opter
- Mets à jour tous les clients pour envoyer des DPoP proofs
- Surveille que toutes les demandes de tokens incluent des DPoP proofs (vérifie les logs Auris)
- Active “Exiger DPoP” pour refuser les demandes sans proof
Implémentation pour SPA
Génère une Paire de Clés
Génère une paire de clés ECDSA P-256 en utilisant la Web Crypto API. Fais-le une fois par session navigateur et stocke la paire en mémoire (pas dans localStorage — elle est non-extractible par conception) :
const dpopKeyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // non-extractible — la clé privée ne peut pas être exportée
['sign', 'verify'],
)Crée une DPoP Proof
Une DPoP proof est un JWT signé avec la clé privée. Elle contient la clé publique (comme jwk dans le header), la méthode HTTP et l’URL de destination, un jti unique et le timestamp actuel :
async function createDpopProof(keyPair, method, url, nonce) {
// Exporte la clé publique comme JWK
const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
// Crée le header de la proof
const header = {
typ: 'dpop+jwt',
alg: 'ES256',
jwk: {
kty: publicKeyJwk.kty,
crv: publicKeyJwk.crv,
x: publicKeyJwk.x,
y: publicKeyJwk.y,
},
}
// Crée le payload de la proof
const payload = {
jti: crypto.randomUUID(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
...(nonce && { nonce }),
}
// Signe la proof (fonction helper pour créer un JWT compact)
return await signJwt(header, payload, keyPair.privateKey)
}Demande un Token avec DPoP
Inclus la DPoP proof dans le header DPoP lors de la demande d’un token :
const tokenUrl = 'https://auth.votredomaine.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://votre-app.com/callback',
client_id: 'votre-client-id',
code_verifier: pkceCodeVerifier,
}),
})
const token = await response.json()
// token.token_type sera "DPoP" au lieu de "Bearer"Effectue des Appels API avec DPoP
Chaque appel API doit inclure à la fois le token lié avec DPoP et une nouvelle proof :
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,
},
})
}
// Utilisation
const users = await dpopFetch(
'https://auth.votredomaine.com/api/users',
'GET',
dpopKeyPair,
token.access_token,
)Implémentation pour Node.js
Pour les applications Node.js côté serveur, utilise la bibliothèque jose pour la génération de clés et la signature JWT :
import * as jose from 'jose'
// Génère une paire de clés au démarrage du service
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
}
// Demande un token avec DPoP
const tokenUrl = 'https://auth.votredomaine.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,
}),
})Support SDK
Le SDK @auris/js inclut un helper createDpopProof qui gère la génération de clés, la création de proofs et la gestion des nonces :
import { AurisClient, createDpopProof } from '@auris/js'
// Le SDK peut générer et gérer les clés DPoP automatiquement
const auris = new AurisClient({
domain: 'auth.votredomaine.com',
clientId: 'votre-client-id',
useDpop: true, // Active la génération automatique des DPoP proofs
})
// loginWithRedirect() et handleRedirectCallback() incluront
// automatiquement les DPoP proofs dans les demandes de tokens
await auris.loginWithRedirect({ scope: 'openid profile' })Gestion des Nonces
Quand “Exiger un Nonce” est activé sur l’application, Auris émet un nonce côté serveur qui doit être inclus dans la DPoP proof. Cela fournit une protection replay — chaque proof ne peut être utilisée qu’une seule fois.
Le flux pour la gestion des nonces :
- Le client envoie une demande de token avec une DPoP proof (pas de nonce au premier essai)
- Si un nonce est requis, Auris répond avec
HTTP 400et un headerDPoP-Noncecontenant la valeur du nonce - Le client crée une nouvelle DPoP proof incluant le nonce et réessaie la demande
- Auris accepte la proof et retourne le token
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),
})
// Vérifie si un nonce est requis
const newNonce = response.headers.get('DPoP-Nonce')
if (response.status === 400 && newNonce) {
nonce = newNonce
continue // réessaie avec le nonce
}
return await response.json()
}
throw new Error('Impossible d\'obtenir le token après le retry avec nonce')
}Vérifie toujours le header DPoP-Nonce dans chaque réponse — pas seulement dans les réponses d’erreur. Le serveur peut faire tourner le nonce même dans les réponses de succès, et tu dois utiliser le nonce le plus récent dans les requêtes suivantes.
Résolution des Problèmes
Problèmes courants dans l’implémentation de DPoP :
| Problème | Cause | Solution |
|---|---|---|
invalid_dpop_proof | Le JWT de la proof est malformé ou la signature est invalide | Vérifie que la proof est un JWT valide signé avec la même paire de clés |
invalid_dpop_proof (mismatch htm/htu) | htm ou htu dans la proof ne correspond pas à la méthode/URL effective de la requête | Assure-toi que htm correspond à la méthode HTTP et htu correspond à l’URL complète (incluant schéma et host, excluant query/fragment) |
use_dpop_nonce | Le serveur requiert un nonce mais il n’a pas été fourni | Lis le header DPoP-Nonce depuis la réponse et inclus-le dans la prochaine proof |
dpop_proof_replay | Le même jti a été utilisé deux fois | Génère un jti unique (UUID) pour chaque proof |
| Token rejeté par le resource server | Le jkt dans le token ne correspond pas à la clé de la proof | Assure-toi d’utiliser la même paire de clés pour la demande de token et les appels API |
iat trop ancien | Décalage d’horloge entre client et serveur | Assure-toi que l’horloge du client est exacte. Auris tolère jusqu’à 60 secondes de décalage. |
Test avec curl
DPoP est difficile à tester directement avec curl car chaque requête nécessite un JWT proof signé unique. À des fins de test, tu peux générer des proofs avec un script :
# Génère une DPoP proof avec Node.js et l'envoie à curl
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.votredomaine.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.votredomaine.com/api/auth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-H "DPoP: $PROOF" \
-d "grant_type=client_credentials" \
-d "client_id=votre-client-id" \
-d "client_secret=votre-client-secret"Format du Token DPoP
Quand DPoP est utilisé, l’access token émis inclut un claim jkt (JWK Thumbprint) qui le lie à la clé publique du client :
{
"sub": "user-id",
"iss": "https://auth.votredomaine.com",
"aud": "https://auth.votredomaine.com",
"exp": 1735000000,
"iat": 1734996400,
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I",
"cnf": {
"jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I"
}
}Le token_type dans la réponse du token sera "DPoP" au lieu de "Bearer", indiquant que le token doit être présenté avec une DPoP proof.
Permissions Requises
| Opération | Permission |
|---|---|
| Activer/configurer DPoP sur une application | manage:applications |
| Gérer la configuration DPoP | manage:dpop_config |
| Demander des tokens avec DPoP | Aucune permission spéciale (authentification client uniquement) |
Guides Associés
- Credentials M2M Client — Section DPoP pour les tokens M2M
- Hosted Login (PKCE) — DPoP peut être combiné avec PKCE pour une sécurité maximale
- Protection contre les Attaques — Autres couches de sécurité qui complètent DPoP