Skip to Content

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:

AttaccoMeccanismo
Esfiltrazione token via XSSJavaScript malevolo legge il token dal localStorage e lo invia a un server dell’attaccante
Fuga token tramite logToken di accesso accidentalmente registrati da proxy, CDN o server applicativi
Replay del tokenUn token intercettato viene riusato da un dispositivo o IP diverso
Furto token tramite middleware compromessoUn 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:

  1. Il client genera una coppia di chiavi effimera (RSA o EC)
  2. Quando richiede un token, il client crea una DPoP proof — un JWT firmato con la chiave privata
  3. 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)
  4. Ad ogni chiamata API, il client invia sia il token di accesso legato che una DPoP proof fresca firmata con la stessa chiave privata
  5. Il resource server verifica che la chiave pubblica della proof corrisponda al binding jkt del 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" }
ClaimDescrizione
jtiIdentificatore unico per questa proof (previene il replay)
htmIl metodo HTTP della richiesta che accompagna questa proof
htuL’URL HTTP della richiesta (senza query/frammento)
iatQuando la proof è stata creata (deve essere recente)
nonceNonce 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 proof

La 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

DimensioneDPoP (RFC 9449)mTLS (RFC 8705)
Meccanismo di bindingJWK thumbprint nel token + proof JWT firmataThumbprint del certificato client nel token
Requisiti infrastrutturaliNessuno (livello applicativo puro)La terminazione TLS deve preservare il certificato client
Supporto browserFunziona nei browser via Web Crypto APINon supportato nei browser
Gestione certificatiNessun certificato necessarioRichiede PKI o autorità certificatrice
Compatibilità proxy/CDNEccellente (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:

ImpostazioneDescrizione
Abilita DPoPToggle principale per il supporto DPoP su questa applicazione
Richiedi DPoPQuando abilitato, l’applicazione rifiuta le richieste token non-DPoP
Richiedi NonceQuando 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