Skip to Content

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

  1. Le client génère une paire de clés publique/privée (une fois, au démarrage ou par session)
  2. 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.
  3. Auris valide la proof, lie le token à la clé publique (via un claim jkt — JWK Thumbprint) et retourne un type de token DPoP au lieu de Bearer
  4. À chaque appel API, le client inclut à la fois l’access token (Authorization: DPoP <token>) et une nouvelle DPoP proof (DPoP: <proof>)
  5. Le resource server valide la proof par rapport au claim jkt dans 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ètreDescription
Activer DPoPAccepte les DPoP proofs. Les tokens demandés avec DPoP seront liés à l’émetteur. Les tokens sans DPoP sont toujours acceptés.
Exiger DPoPRefuse toutes les demandes de tokens sans une DPoP proof valide. Active seulement après que tous les clients ont terminé la migration.
Exiger un NonceNonces é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 :

  1. Active DPoP (mais ne l’exige pas) — les clients peuvent opter
  2. Mets à jour tous les clients pour envoyer des DPoP proofs
  3. Surveille que toutes les demandes de tokens incluent des DPoP proofs (vérifie les logs Auris)
  4. 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 :

  1. Le client envoie une demande de token avec une DPoP proof (pas de nonce au premier essai)
  2. Si un nonce est requis, Auris répond avec HTTP 400 et un header DPoP-Nonce contenant la valeur du nonce
  3. Le client crée une nouvelle DPoP proof incluant le nonce et réessaie la demande
  4. 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èmeCauseSolution
invalid_dpop_proofLe JWT de la proof est malformé ou la signature est invalideVé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êteAssure-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_nonceLe serveur requiert un nonce mais il n’a pas été fourniLis le header DPoP-Nonce depuis la réponse et inclus-le dans la prochaine proof
dpop_proof_replayLe même jti a été utilisé deux foisGénère un jti unique (UUID) pour chaque proof
Token rejeté par le resource serverLe jkt dans le token ne correspond pas à la clé de la proofAssure-toi d’utiliser la même paire de clés pour la demande de token et les appels API
iat trop ancienDécalage d’horloge entre client et serveurAssure-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érationPermission
Activer/configurer DPoP sur une applicationmanage:applications
Gérer la configuration DPoPmanage:dpop_config
Demander des tokens avec DPoPAucune permission spéciale (authentification client uniquement)

Guides Associés