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
- Il client genera una coppia di chiavi pubblica/privata (una volta, all’avvio o per sessione)
- 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. - Auris valida la proof, lega il token alla chiave pubblica (tramite un claim
jkt— JWK Thumbprint) e restituisce un tipo di tokenDPoPinvece diBearer - Ad ogni chiamata API, il client include sia l’access token (
Authorization: DPoP <token>) che una nuova DPoP proof (DPoP: <proof>) - Il resource server valida la proof rispetto al claim
jktnel 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:
| Impostazione | Descrizione |
|---|---|
| Abilita DPoP | Accetta DPoP proof. I token richiesti con DPoP saranno vincolati al mittente. I token senza DPoP sono ancora accettati. |
| Richiedi DPoP | Rifiuta tutte le richieste di token senza una DPoP proof valida. Abilita solo dopo che tutti i client hanno completato la migrazione. |
| Richiedi Nonce | Nonce 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:
- Abilita DPoP (ma non richiederlo) — i client possono optare
- Aggiorna tutti i client per inviare DPoP proof
- Monitora che tutte le richieste di token includano DPoP proof (controlla i log Auris)
- 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:
- Il client invia una richiesta di token con una DPoP proof (nessun nonce al primo tentativo)
- Se è richiesto un nonce, Auris risponde con
HTTP 400e un headerDPoP-Noncecontenente il valore del nonce - Il client crea una nuova DPoP proof includendo il nonce e riprova la richiesta
- 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:
| Problema | Causa | Soluzione |
|---|---|---|
invalid_dpop_proof | Il JWT della proof è malformato o la firma non è valida | Verifica 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 richiesta | Assicurati che htm corrisponda al metodo HTTP e htu corrisponda all’URL completo (incluso schema e host, escluso query/fragment) |
use_dpop_nonce | Il server richiede un nonce ma non è stato fornito | Leggi l’header DPoP-Nonce dalla risposta e includilo nella prossima proof |
dpop_proof_replay | Lo stesso jti è stato usato due volte | Genera un jti univoco (UUID) per ogni proof |
| Token rifiutato dal resource server | Il jkt nel token non corrisponde alla chiave della proof | Assicurati di usare la stessa coppia di chiavi sia per la richiesta del token che per le chiamate API |
iat troppo vecchio | Sfasamento dell’orologio tra client e server | Assicurati 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
| Operazione | Permesso |
|---|---|
| Abilita/configura DPoP su un’applicazione | manage:applications |
| Gestisci la configurazione DPoP | manage:dpop_config |
| Richiedi token con DPoP | Nessun permesso speciale (solo autenticazione client) |
Guide Correlate
- Credenziali M2M Client — Sezione DPoP per token M2M
- Hosted Login (PKCE) — DPoP può essere combinato con PKCE per la massima sicurezza
- Protezione dagli Attacchi — Altri livelli di sicurezza che complementano DPoP