Skip to Content

Implementare DPoP

DPoP (Demonstrating Proof of Possession, RFC 9449) lega gli access token a una coppia di chiavi crittografiche specifiche del client. A differenza dei token Bearer standard che possono essere usati da chiunque li possieda, i token legati con DPoP sono inutili per un attaccante che li ruba — non può produrre una proof valida senza la chiave privata.

Perché usare DPoP:

  • Prevenire il furto di token: gli attacchi XSS che esfiltrano token da localStorage o cookie non possono usare i token rubati senza la chiave privata corrispondente
  • Prevenire fughe nei log: i token accidentalmente registrati nei log del server o nei sistemi di error tracking non possono essere riprodotti
  • Prevenire attacchi replay: ogni DPoP proof include il metodo HTTP e l’URL, vincolandola a una richiesta specifica
  • Conformità: alcuni standard di sicurezza e API finanziarie richiedono token vincolati al mittente

Come funziona DPoP

  1. Il client genera una coppia di chiavi pubblica/privata (una volta, all’avvio o per sessione)
  2. Quando richiede un token, il client include una DPoP proof JWT nell’header DPoP. La proof contiene la chiave pubblica, il metodo HTTP e l’URL, e un identificatore univoco.
  3. Auris valida la proof, lega il token alla chiave pubblica (tramite un claim jkt — JWK Thumbprint) e restituisce un tipo di token DPoP invece di Bearer
  4. Ad ogni chiamata API, il client include sia l’access token (Authorization: DPoP <token>) che una nuova DPoP proof (DPoP: <proof>)
  5. Il resource server valida la proof rispetto al claim jkt nel token

Configurazione nella Console

Abilita DPoP sull’Applicazione

Nella Console Auris, vai su Applicazioni e seleziona la tua applicazione. Nella scheda Impostazioni, trova la sezione DPoP:

ImpostazioneDescrizione
Abilita DPoPAccetta DPoP proof. I token richiesti con DPoP saranno vincolati al mittente. I token senza DPoP sono ancora accettati.
Richiedi DPoPRifiuta tutte le richieste di token senza una DPoP proof valida. Abilita solo dopo che tutti i client hanno completato la migrazione.
Richiedi NonceNonce emessi dal server nelle DPoP proof. Aggiunge protezione replay al costo di un round-trip aggiuntivo.

Pianifica la Migrazione

Se hai client esistenti che usano token Bearer, usa un approccio di migrazione graduale:

  1. Abilita DPoP (ma non richiederlo) — i client possono optare
  2. Aggiorna tutti i client per inviare DPoP proof
  3. Monitora che tutte le richieste di token includano DPoP proof (controlla i log Auris)
  4. Abilita “Richiedi DPoP” per rifiutare le richieste senza proof

Implementazione per SPA

Genera una Coppia di Chiavi

Genera una coppia di chiavi ECDSA P-256 usando la Web Crypto API. Fallo una volta per sessione browser e memorizza la coppia in memoria (non in localStorage — è non-estraibile per design):

const dpopKeyPair = await crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: 'P-256' }, false, // non-estraibile — la chiave privata non può essere esportata ['sign', 'verify'], )

Crea una DPoP Proof

Una DPoP proof è un JWT firmato con la chiave privata. Contiene la chiave pubblica (come jwk nell’header), il metodo HTTP e l’URL di destinazione, un jti univoco e il timestamp corrente:

async function createDpopProof(keyPair, method, url, nonce) { // Esporta la chiave pubblica come JWK const publicKeyJwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey) // Crea l'header della proof const header = { typ: 'dpop+jwt', alg: 'ES256', jwk: { kty: publicKeyJwk.kty, crv: publicKeyJwk.crv, x: publicKeyJwk.x, y: publicKeyJwk.y, }, } // Crea il payload della proof const payload = { jti: crypto.randomUUID(), htm: method, htu: url, iat: Math.floor(Date.now() / 1000), ...(nonce && { nonce }), } // Firma la proof (funzione helper per creare un JWT compatto) return await signJwt(header, payload, keyPair.privateKey) }

Richiedi un Token con DPoP

Includi la DPoP proof nell’header DPoP quando richiedi un token:

const tokenUrl = 'https://auth.tuodominio.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://tua-app.com/callback', client_id: 'il-tuo-client-id', code_verifier: pkceCodeVerifier, }), }) const token = await response.json() // token.token_type sarà "DPoP" invece di "Bearer"

Esegui Chiamate API con DPoP

Ogni chiamata API deve includere sia il token legato con DPoP che una nuova 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, }, }) } // Utilizzo const users = await dpopFetch( 'https://auth.tuodominio.com/api/users', 'GET', dpopKeyPair, token.access_token, )

Implementazione per Node.js

Per applicazioni Node.js lato server, usa la libreria jose per la generazione delle chiavi e la firma JWT:

import * as jose from 'jose' // Genera una coppia di chiavi all'avvio del servizio 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 } // Richiedi un token con DPoP const tokenUrl = 'https://auth.tuodominio.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, }), })

Supporto SDK

L’SDK @auris/js include un helper createDpopProof che gestisce la generazione delle chiavi, la creazione delle proof e la gestione dei nonce:

import { AurisClient, createDpopProof } from '@auris/js' // L'SDK può generare e gestire le chiavi DPoP automaticamente const auris = new AurisClient({ domain: 'auth.tuodominio.com', clientId: 'il-tuo-client-id', useDpop: true, // Abilita la generazione automatica delle DPoP proof }) // loginWithRedirect() e handleRedirectCallback() includeranno // automaticamente le DPoP proof nelle richieste di token await auris.loginWithRedirect({ scope: 'openid profile' })

Gestione dei Nonce

Quando “Richiedi Nonce” è abilitato sull’applicazione, Auris emette un nonce lato server che deve essere incluso nella DPoP proof. Questo fornisce protezione replay — ogni proof può essere usata solo una volta.

Il flusso per la gestione dei nonce:

  1. Il client invia una richiesta di token con una DPoP proof (nessun nonce al primo tentativo)
  2. Se è richiesto un nonce, Auris risponde con HTTP 400 e un header DPoP-Nonce contenente il valore del nonce
  3. Il client crea una nuova DPoP proof includendo il nonce e riprova la richiesta
  4. Auris accetta la proof e restituisce il 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), }) // Controlla se è richiesto un nonce const newNonce = response.headers.get('DPoP-Nonce') if (response.status === 400 && newNonce) { nonce = newNonce continue // riprova con il nonce } return await response.json() } throw new Error('Impossibile ottenere il token dopo il retry con nonce') }

Controlla sempre l’header DPoP-Nonce in ogni risposta — non solo nelle risposte di errore. Il server può ruotare il nonce anche nelle risposte di successo, e dovresti usare il nonce più recente nelle richieste successive.


Risoluzione dei Problemi

Problemi comuni nell’implementazione di DPoP:

ProblemaCausaSoluzione
invalid_dpop_proofIl JWT della proof è malformato o la firma non è validaVerifica che la proof sia un JWT valido firmato con la stessa coppia di chiavi
invalid_dpop_proof (mismatch htm/htu)htm o htu nella proof non corrisponde al metodo/URL effettivo della richiestaAssicurati che htm corrisponda al metodo HTTP e htu corrisponda all’URL completo (incluso schema e host, escluso query/fragment)
use_dpop_nonceIl server richiede un nonce ma non è stato fornitoLeggi l’header DPoP-Nonce dalla risposta e includilo nella prossima proof
dpop_proof_replayLo stesso jti è stato usato due volteGenera un jti univoco (UUID) per ogni proof
Token rifiutato dal resource serverIl jkt nel token non corrisponde alla chiave della proofAssicurati di usare la stessa coppia di chiavi sia per la richiesta del token che per le chiamate API
iat troppo vecchioSfasamento dell’orologio tra client e serverAssicurati che l’orologio del client sia accurato. Auris consente fino a 60 secondi di sfasamento.

Test con curl

DPoP è difficile da testare direttamente con curl perché ogni richiesta richiede un JWT proof firmato univoco. Per scopi di test, puoi generare le proof con uno script:

# Genera una DPoP proof con Node.js e invialala a 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.tuodominio.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.tuodominio.com/api/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "DPoP: $PROOF" \ -d "grant_type=client_credentials" \ -d "client_id=il-tuo-client-id" \ -d "client_secret=il-tuo-client-secret"

Formato del Token DPoP

Quando viene usato DPoP, l’access token emesso include un claim jkt (JWK Thumbprint) che lo lega alla chiave pubblica del client:

{ "sub": "user-id", "iss": "https://auth.tuodominio.com", "aud": "https://auth.tuodominio.com", "exp": 1735000000, "iat": 1734996400, "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I", "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" } }

Il token_type nella risposta del token sarà "DPoP" invece di "Bearer", indicando che il token deve essere presentato con una DPoP proof.


Permessi Richiesti

OperazionePermesso
Abilita/configura DPoP su un’applicazionemanage:applications
Gestisci la configurazione DPoPmanage:dpop_config
Richiedi token con DPoPNessun permesso speciale (solo autenticazione client)

Guide Correlate