DPoP (Demonstrating Proof of Possession)
Les Bearer tokens ont un problème fondamental : quiconque possède le token peut l’utiliser. Si un access token est exfiltré via XSS, fuité dans des logs ou intercepté par un middleware compromis, l’attaquant peut effectuer des appels API authentifiés indétectables jusqu’à l’expiration du token.
DPoP (Demonstrating Proof of Possession) résout ce problème en liant cryptographiquement l’access token à une paire de clés détenus par le client. Même si le token est volé, il est inutilisable sans la clé privée correspondante.
Le Problème Bearer Token
Les attaques courantes contre les bearer tokens :
- Exfiltration XSS : JavaScript malveillant lit le token depuis le localStorage
- Fuite dans les logs : Les access logs du serveur capturent les headers
Authorization - Replay d’attaque : Token intercepté utilisé plus tard depuis un autre endroit
- Middleware compromis : Proxy ou CDN qui lit et réutilise le token
Le Mécanisme DPoP
DPoP ajoute une couche de proof of possession au-dessus des bearer tokens standard :
- Le client génère une paire de clés éphémère (EC P-256 ou RSA-PSS)
- Avant chaque requête, le client crée un DPoP proof JWT signé avec la clé privée
- Le serveur lie l’access token à l’empreinte de clé publique du client
- Chaque appel API doit inclure le token ET une nouvelle proof signée
Sans la clé privée, un token volé est inutilisable.
Flux Détaillé
Étape 1 : Générer la Paire de Clés Éphémère
import { generateKeyPair, exportJWK, SignJWT } from 'jose'
const { privateKey, publicKey } = await generateKeyPair('ES256', {
extractable: true
})
const publicKeyJwk = await exportJWK(publicKey)Étape 2 : Créer un DPoP Proof JWT
Le proof JWT a une structure spécifique :
Header :
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": { /* clé publique inline */ }
}Payload :
{
"jti": "unique-random-id",
"htm": "POST",
"htu": "https://auth.yourdomain.com/api/auth/token",
"iat": 1700000000,
"nonce": "server-provided-nonce"
}htm: Méthode HTTP de la requêtehtu: URL de la requête (sans query string ni fragment)jti: ID unique pour prévenir les replays (le serveur garde un cache des JTIs vus)nonce: Nonce fourni par le serveur pour prévenir les pre-play attacks
Étape 3 : Token Request avec DPoP
POST /api/auth/token
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Li4ufX0...
grant_type=authorization_code&code=AUTH_CODE&...Étape 4 : Le Serveur Lie le Token à la Clé
Le serveur vérifie le proof et émet un access token contenant le claim cnf (confirmation) :
{
"sub": "user_01234",
"cnf": {
"jkt": "JWK_thumbprint_SHA256"
},
"token_type": "DPoP"
}La valeur jkt est l’empreinte SHA-256 de la clé publique du client (RFC 7638). Elle lie le token à cette clé spécifique.
Étape 5 : Appels API avec DPoP
Chaque appel API nécessite le token ET un nouveau proof signé pour cette requête spécifique :
GET /api/user/profile
Authorization: DPoP eyJhbGciOiJFUzI1NiJ9... (access token)
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Li4ufX0...Note le schéma Authorization: DPoP — pas Authorization: Bearer. La présence d’un header DPoP valide avec le bon htm/htu est obligatoire pour chaque requête.
Étape 6 : Le Resource Server Vérifie
Le resource server vérifie :
- La signature du DPoP proof JWT avec la clé publique inline
- Que
htmcorrespond à la méthode HTTP de la requête - Que
htucorrespond à l’URL de la requête - Que
iatest récent (fenêtre de 30 secondes par défaut) - Que
jtin’a pas été vu récemment (protection anti-replay) - Que l’empreinte de la clé publique correspond au claim
cnf.jktdu token
Si n’importe quelle vérification échoue, la requête est rejetée avec 401 Unauthorized.
Gestion du Nonce
Pour prévenir les pre-play attacks (créer des proofs à l’avance), Auris peut exiger des nonces fournis par le serveur. Quand les nonces sont requis :
- La première requête sans nonce échoue avec
error: use_dpop_nonce - La réponse inclut un header
DPoP-Nonce: <valeur> - Le client inclut le nonce dans le claim
noncedu proof suivant - Les nonces expirent après 10 minutes
// Exemple de gestion automatique du nonce
async function createDpopProof(
method: string,
url: string,
nonce?: string
): Promise<string> {
const payload: Record<string, unknown> = {
jti: crypto.randomUUID(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
}
if (nonce) payload.nonce = nonce
return new SignJWT(payload)
.setProtectedHeader({
typ: 'dpop+jwt',
alg: 'ES256',
jwk: publicKeyJwk,
})
.sign(privateKey)
}DPoP vs mTLS
DPoP et mTLS (mutual TLS) résolvent tous les deux le problème de possession de token, mais avec des tradeoffs différents :
| DPoP | mTLS | |
|---|---|---|
| Niveau | Application (JWT) | Transport (TLS) |
| Fonctionne dans les browsers | ✅ Oui | ❌ Non (Web Crypto API manque l’accès aux certificats client) |
| PKI requise | Non | Oui |
| Overhead de configuration | Faible | Élevé |
| Granularité | Par requête | Par connexion |
| Support protocole | RFC 9449 | RFC 8705 |
| Recommandé pour | SPAs, mobile, CLI | Service-to-service M2M |
Configuration dans la Console
Dans Administration → Applications → [Application] → Sécurité Avancée :
- Activer DPoP : Les clients peuvent optionnellement utiliser DPoP
- Exiger DPoP : Seuls les clients DPoP sont acceptés (rejette les requêtes bearer standard)
- Exiger le Nonce : Force l’utilisation des nonces fournis par le serveur
Concepts Associés
- Tokens — Access tokens, refresh tokens et claims
- OAuth 2.0 & OIDC — Le cadre d’autorisation sous-jacent
- Credentials M2M — DPoP pour les services machine-to-machine
- Applications — Configurer les paramètres DPoP dans la Console