Skip to Content

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 :

  1. Le client génère une paire de clés éphémère (EC P-256 ou RSA-PSS)
  2. Avant chaque requête, le client crée un DPoP proof JWT signé avec la clé privée
  3. Le serveur lie l’access token à l’empreinte de clé publique du client
  4. 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ête
  • htu : 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 :

  1. La signature du DPoP proof JWT avec la clé publique inline
  2. Que htm correspond à la méthode HTTP de la requête
  3. Que htu correspond à l’URL de la requête
  4. Que iat est récent (fenêtre de 30 secondes par défaut)
  5. Que jti n’a pas été vu récemment (protection anti-replay)
  6. Que l’empreinte de la clé publique correspond au claim cnf.jkt du 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 :

  1. La première requête sans nonce échoue avec error: use_dpop_nonce
  2. La réponse inclut un header DPoP-Nonce: <valeur>
  3. Le client inclut le nonce dans le claim nonce du proof suivant
  4. 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 :

DPoPmTLS
NiveauApplication (JWT)Transport (TLS)
Fonctionne dans les browsers✅ Oui❌ Non (Web Crypto API manque l’accès aux certificats client)
PKI requiseNonOui
Overhead de configurationFaibleÉlevé
GranularitéPar requêtePar connexion
Support protocoleRFC 9449RFC 8705
Recommandé pourSPAs, mobile, CLIService-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