DPoP (Demonstrating Proof of Possession)
Il Problema dei Bearer Token
La specifica OAuth 2.0 Bearer Token (RFC 6750) definisce token che concedono accesso a chiunque li possegga. La parola “bearer” è letterale: chiunque “porti” il token può usarlo. Non c’è verifica che il presentatore del token sia la stessa parte a cui il token è stato emesso.
Questo crea una classe di attacchi:
| Attacco | Meccanismo |
|---|---|
| Esfiltrazione token via XSS | JavaScript malevolo legge il token dal localStorage e lo invia a un server dell’attaccante |
| Fuga token tramite log | Token di accesso accidentalmente registrati da proxy, CDN o server applicativi |
| Replay del token | Un token intercettato viene riusato da un dispositivo o IP diverso |
| Furto token tramite middleware compromesso | Un reverse proxy o API gateway memorizza i token per uso futuro |
In tutti questi casi, il token rubato funziona esattamente bene nelle mani dell’attaccante come in quelle del client legittimo.
Cosa Fa DPoP
DPoP (Demonstrating Proof of Possession), definito in RFC 9449, risolve questo problema legando i token alla coppia di chiavi crittografiche del client. Un token legato a DPoP è inutile senza la chiave privata corrispondente.
L’idea centrale:
- Il client genera una coppia di chiavi effimera (RSA o EC)
- Quando richiede un token, il client crea una DPoP proof — un JWT firmato con la chiave privata
- Il server di autorizzazione (Auris) estrae la chiave pubblica dalla proof e lega il token emesso a quella chiave tramite un JWK thumbprint (claim
jkt) - Ad ogni chiamata API, il client invia sia il token di accesso legato che una DPoP proof fresca firmata con la stessa chiave privata
- Il resource server verifica che la chiave pubblica della proof corrisponda al binding
jktdel token
Come Funziona: Passo per Passo
Passo 1: Il Client Genera una Coppia di Chiavi Effimera
// Genera una coppia di chiavi EC (P-256) usando la Web Crypto API
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
false, // non-estraibile: la chiave privata non può essere esportata
['sign', 'verify']
)Passo 2: Il Client Crea una DPoP Proof JWT
// Header della DPoP Proof JWT
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// Payload della DPoP Proof JWT
{
"jti": "unique-proof-id-abc123",
"htm": "POST",
"htu": "https://auth.example.com/api/auth/token",
"iat": 1739880000,
"nonce": "nonce-fornito-dal-server"
}| Claim | Descrizione |
|---|---|
jti | Identificatore unico per questa proof (previene il replay) |
htm | Il metodo HTTP della richiesta che accompagna questa proof |
htu | L’URL HTTP della richiesta (senza query/frammento) |
iat | Quando la proof è stata creata (deve essere recente) |
nonce | Nonce fornito dal server per la freschezza (opzionale) |
jwk (header) | La chiave pubblica corrispondente alla chiave privata usata per firmare la proof |
Passo 3: Il Client Invia la DPoP Proof con la Richiesta Token
POST /api/auth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...
grant_type=authorization_code&code=auth-code-here&...Passo 4: Il Server Lega il Token alla Chiave
Auris valida la DPoP proof e include il JWK thumbprint come claim jkt nel token di accesso emesso:
{
"sub": "usr_abc123",
"iss": "https://auth.example.com",
"cnf": {
"jkt": "JWK-thumbprint-della-chiave-pubblica-del-client"
},
"token_type": "DPoP"
}Passo 5: Il Client Invia il Token Legato + Proof Fresca nelle Chiamate API
GET /api/users/me HTTP/1.1
Authorization: DPoP eyJhbGciOiJSUzI1NiJ9...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...Nota: lo schema Authorization è DPoP, non Bearer.
Passo 6: Il Resource Server Verifica il Binding
Il resource server verifica che il thumbprint JWK della chiave pubblica nella proof corrisponda al claim cnf.jkt nel token di accesso.
Gestione dei Nonce
I nonce aggiungono un ulteriore livello di protezione contro il replay. Quando abilitati, il server di autorizzazione fornisce un nonce che il client deve includere nella prossima DPoP proof.
// Primo tentativo (senza nonce)
HTTP/1.1 400 Bad Request
DPoP-Nonce: eyJ2IjoiMSIsImlhdCI6MTczOTg4MDAwMH0
Content-Type: application/json
{"error": "use_dpop_nonce"}
// Secondo tentativo (con nonce) - includi il nonce nel claim "nonce" della proofLa gestione dei nonce aggiunge un round-trip extra alla prima richiesta in una sessione. Per la maggior parte delle applicazioni, il controllo di freschezza iat da solo fornisce protezione sufficiente contro il replay. Abilita i nonce solo quando si proteggono contro attaccanti sofisticati a livello di rete.
DPoP vs mTLS
| Dimensione | DPoP (RFC 9449) | mTLS (RFC 8705) |
|---|---|---|
| Meccanismo di binding | JWK thumbprint nel token + proof JWT firmata | Thumbprint del certificato client nel token |
| Requisiti infrastrutturali | Nessuno (livello applicativo puro) | La terminazione TLS deve preservare il certificato client |
| Supporto browser | Funziona nei browser via Web Crypto API | Non supportato nei browser |
| Gestione certificati | Nessun certificato necessario | Richiede PKI o autorità certificatrice |
| Compatibilità proxy/CDN | Eccellente (gli header passano attraverso) | Problematica (la terminazione TLS può rimuovere il cert client) |
Quando Usare DPoP
- Applicazioni browser (SPA): DPoP è l’unico meccanismo proof-of-possession che funziona nei browser
- Applicazioni mobile: Più facile da implementare rispetto alla gestione dei certificati mTLS
- Microservizi: Quando l’infrastruttura mTLS non è disponibile
Dettagli Implementativi di Auris
Abilitare DPoP
DPoP è configurato per applicazione nella Console Auris. Vai su Applicazioni → (seleziona applicazione) → Impostazioni:
| Impostazione | Descrizione |
|---|---|
| Abilita DPoP | Toggle principale per il supporto DPoP su questa applicazione |
| Richiedi DPoP | Quando abilitato, l’applicazione rifiuta le richieste token non-DPoP |
| Richiedi Nonce | Quando abilitato, tutte le DPoP proof devono includere un nonce fornito dal server |
Esempio di Codice: Generare DPoP Proof in TypeScript
import { SignJWT, exportJWK } from 'jose'
// Passo 1: Genera una coppia di chiavi EC effimera
const keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
)
// Passo 2: Esporta la chiave pubblica come JWK
const publicJwk = await exportJWK(keyPair.publicKey)
publicJwk.alg = 'ES256'
// Passo 3: Crea una DPoP proof
async function createDpopProof(
method: string,
url: string,
nonce?: string
): Promise<string> {
const builder = new SignJWT({
htm: method,
htu: url,
...(nonce ? { nonce } : {}),
})
.setProtectedHeader({ typ: 'dpop+jwt', alg: 'ES256', jwk: publicJwk })
.setJti(crypto.randomUUID())
.setIssuedAt()
return builder.sign(keyPair.privateKey)
}
// Passo 4: Usa il token DPoP nelle chiamate API
const apiUrl = 'https://api.example.com/users/me'
const apiProof = await createDpopProof('GET', apiUrl)
const apiResponse = await fetch(apiUrl, {
headers: {
'Authorization': `DPoP ${accessToken}`,
'DPoP': apiProof,
},
})Ogni DPoP proof deve avere un jti univoco e un iat fresco. Non riusare mai le proof tra richieste. I htm e htu devono corrispondere esattamente alla richiesta — una proof creata per GET /api/users non può essere usata per POST /api/users.
Concetti Correlati
- Token Spiegati — Struttura JWT, firma e verifica
- OAuth 2.0 & OIDC — Il framework di autorizzazione che DPoP estende
- M2M Client Credentials — Token di servizio legati a DPoP
- Applicazioni — Abilita DPoP per applicazione nella Console