Guida all'adozione di DPoP

Questa guida illustra in dettaglio come implementare DPoP (Demonstrating Proof-of-Possession) nelle tue integrazioni OAuth 2.0 con la piattaforma OAuth di Google. DPoP (definito in RFC 9449) protegge le tue applicazioni dal furto di token e dagli attacchi di replay associando i token in modo crittografico a una coppia di chiavi asimmetriche generata dal client.

Modifiche al flusso del codice di autorizzazione

L'aggiunta di DPoP a un flusso del codice di autorizzazione OAuth 2.0 esistente richiede la generazione e l'archiviazione di una coppia di chiavi, la creazione di un JWT di prova DPoP e l'inclusione della prova come intestazione HTTP quando il codice di autorizzazione viene scambiato con un token di aggiornamento, come mostrato nei passaggi 5 e 6 della Figura 1.

Flusso del codice di autorizzazione con DPoP
Figura 1. La sequenza di eventi nel flusso del codice di autorizzazione utilizzando DPoP.

Richiesta del codice di autorizzazione

La richiesta di autorizzazione viene creata normalmente. Ad esempio:

$ curl -G "https://accounts.google.com/o/oauth2/v2/auth" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8080" \
  --data-urlencode "response_type=code" \
  --data-urlencode "scope=calendar.readonly" \
  --data-urlencode "state=AI1Bvapj7E5SDmtW4gohcA" \
  --data-urlencode "code_challenge=PO4pPROl-31Wy9fVZ7uTW9Ga6CrjrSKsf4AAtx_JNM8" \
  --data-urlencode "code_challenge_method=S256" \
  --data-urlencode "nonce=PrMfmSNAvJFPQ7GnlEKUaw" \
  --data-urlencode "access_type=offline" \
  --data-urlencode "prompt=consent"

Il codice di autorizzazione restituito come parametro URI di reindirizzamento viene utilizzato nella creazione di una prova DPoP. Il token di aggiornamento è associato alla prova inclusa come intestazione HTTP in tutte le richieste successive all'endpoint del token.

Le SPA lato client pure e senza secret non possono utilizzare direttamente DPoP a causa del requisito client_secret e delle limitazioni CORS sull'intestazione DPoP-Nonce. Per proteggere le SPA, instrada il traffico attraverso un Backend-for-Frontend (BFF) che agisce come client confidenziale, attiva access_type=offline e utilizza DPoP per associare il token di aggiornamento lato server.

Crea la prova DPoP

Una prova contiene un'intestazione JOSE e un payload.

Per creare l'intestazione, genera una coppia di chiavi EC P-256 (ES256) e includi le coordinate della chiave pubblica (x e y) nel parametro jwk. È supportata anche una coppia di chiavi RSA (RS256), anche se è consigliata ES256 a causa delle dimensioni ridotte della firma e del costo di calcolo inferiore.

Ecco un esempio di intestazione JOSE:

{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
    "y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
  }
}

Per creare il payload della prova, sono necessarie quattro attestazioni (htm, htu, iat e jti), più un'attestazione nonce facoltativa.

Due claim: htm: POST e htu: https://oauth2.googleapis.com/token sono valori fissi e non cambiano quando viene effettuata una richiesta all'endpoint token di Google.

Le altre due attestazioni, iat e jti, devono essere generate per ogni richiesta. Il valore di iat è il timestamp di emissione e cambia per ogni richiesta. Il valore della rivendicazione dell'ID JWT (jti) dipende dal tipo di scambio. Quando un codice di autorizzazione viene scambiato con token di accesso e token di aggiornamento, il valore di jti è l'hash SHA256 con codifica Base-64 e URL del codice di autorizzazione, ad esempio jti = BASE64URL(SHA-256(authorization_code)). Una rivendicazione nonce è facoltativa nello scambio iniziale del codice di autorizzazione ed è necessaria solo se il tuo client dispone già di un DPoP-Nonce valido proveniente da una richiesta precedente.

Ecco un esempio di corpo del payload:

{
  "jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
  "htm": "POST",
  "htu": "https://oauth2.googleapis.com/token",
  "iat": 1784822025
}

L'intestazione JOSE e il corpo del payload vengono codificati e firmati come JWT (RFC 7519) per l'utilizzo diretto nell'intestazione HTTP DPoP nella richiesta del token. Per ES256, la firma JWS deve utilizzare il formato IEEE P1363 non elaborato (R || S, 64 byte per P-256):

$ curl -X POST https://oauth2.googleapis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZk93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhMSVkwbF9OOGl5NS1pbG9uMWd1YWQ5TkZRSEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiaWF0IjoxNzg0ODIyMDI1fQ.OSdQCmqTng_uZmGK5UXf8hcEMtoOu7ucmYtl5mx4901RXnj6fJRJQmIeTqfhprRBTG_RSJv2fPcWDqvQbDW7YA" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=4/0AXEQxIDNpLD-qpSIvjHb2Hl10uS_2sk2GBRpO8UJQ78YZF3hZ9LB9kTA1xYLD4xisi4C5w" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8080" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
  --data-urlencode "code_verifier=q8ZztyVv7HH8E2M-SEL8WaB-7CPs68rejN5UZ9OdYgo"

Viene restituito un token di aggiornamento associato a DPoP insieme a un'intestazione HTTP DPoP-Nonce, ad esempio:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI

{
  "access_token": "ya29.a0ARGnu0aebRL97B91dmvm14gTug5wpItFf9MVWq12Hja6yv09A_qxa4T73_z2gFbf32qR4RXispQ7vnOzv6gn0APLQrF51LVa6AOqCVPH2Tupocv8y0JHu4ByEbvgXEEhiHEU8Xa9_w3i-PKBPsKWiLi210RCZdqJjLXkcRrGnoPPjbGPzOPtm6KCJjPrNHG16caOWecaCgYKASESARASFQHGX2MiBn7ihbbk_n-buCbOfl2TDA0206",
  "expires_in": 3599,
  "refresh_token": "1//06dUPZ9FIBQm3CgYIARAAGAYSNwF-L9IrJwuIEKUA_zbBPU-xoCDGM0QrDu7-jv7cMQZ0kARPUK9WhwfFFfbOVEgXDQKmFh4w9GM",
  "scope": "https://www.googleapis.com/auth/calendar.readonly",
  "token_type": "Bearer"
}

Memorizza sempre nella cache e invia il DPoP-Nonce più recente restituito dal server di autorizzazione di Google nelle richieste di token successive. I nonce scadono periodicamente e, se in una prova manca un claim nonce richiesto o se contiene un nonce non valido o scaduto, il server restituisce una risposta HTTP 400 (use_dpop_nonce) con una nuova intestazione DPoP-Nonce da utilizzare nei nuovi tentativi.

Modifiche al flusso di aggiornamento dei token

L'aggiornamento di un flusso di aggiornamento dei token OAuth 2.0 esistente richiede la generazione e l'invio di una prova DPoP come intestazione HTTP quando un token di aggiornamento viene scambiato con nuovi token, come mostrato nei passaggi 2-5 della Figura 2.

Flusso di aggiornamento dei token con DPoP
Figura 2. La sequenza di eventi nel flusso di aggiornamento del token con gestione degli errori e nuovo tentativo.

Creazione della prova DPoP

Il metodo per creare una prova per l'aggiornamento del token è diverso dallo scenario del codice di autorizzazione. L'intestazione JOSE viene creata nello stesso modo descritto in precedenza durante la creazione di una richiesta di codice di autorizzazione. Il corpo della prova è costruito in modo simile, ma include un'attestazione nonce e jti contiene una stringa casuale univoca.

Per creare il corpo del payload, il valore dell'intestazione HTTP DPoP-Nonce restituito in precedenza deve essere incluso nell'attestazione nonce e il timestamp di emissione (iat) deve essere aggiornato per ogni richiesta. L'ID JWT (jti) è una stringa casuale univoca generata per richiesta, utilizzando l'API WebCrypto integrata crypto.getRandomValues(new Uint8Array(24)) e la codifica Base64URL della stringa.

Questo è un esempio di corpo del payload contenente jti, nonce e iat:

{
  "jti": "o29CN8ZIY0l_K8iy5-ilon1guad9NF6HFOdXTzrBNck",
  "htm": "POST",
  "htu": "https://oauth2.googleapis.com/token",
  "nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
  "iat": 1784822025
}

L'intestazione JOSE e il corpo del payload sono codificati come JWT (RFC7519) per l'utilizzo diretto nell'intestazione HTTP DPoP nella richiesta del token.

La prova viene aggiunta come intestazione DPoP alla richiesta di aggiornamento del token:

$ curl -X POST https://oauth2.googleapis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZk93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhaSVkwbF9LOGl5NS1pbG9uMWd1YWQ5TkY2SEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwibm9uY2UiOiJBTjNYd0pqWnNqbmIwWnVXa1JsZWs4UVU3d1ktWmhmLTVJUDZ0TzB0T1J6MEtndERUMUJvOEZYLXc0bnozcjVsbmVwSSIsImlhdCI6MTc4NDgyMjAyNX0.ObT0BejZz2yi8atNSStsQH-T2atr8OSOL4JD2ZJlU_VxbiqkCE7Dt9RKyUT_3kj7q1vCvVggwnWJNX3P3vO1mw" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=1//06dUPZ9FIBQm3CgYIARAAGAYSNwF-L9IrJwuIEKUA_zbBPU-xoCDGM0QrDu7-jv7cMQZ0kARPUK9WhwfFFfbOVEgXDQKmFh4w9GM" \
  --data-urlencode "client_id=YOUR_CLIENT_ID.apps.googleusercontent.com" \
  --data-urlencode "client_secret=YOUR_CLIENT_SECRET"

Quando viene utilizzato un nonce scaduto o non valido o quando si passa da un workflow OAuth a un altro (ad esempio, dallo scambio di codice di autorizzazione iniziale a una richiesta di aggiornamento del token), il server di Google applica l'isolamento del workflow. Ciò significa che il server rifiuta incondizionatamente il nonce con una verifica HTTP 400 use_dpop_nonce per stabilire un nuovo spazio dei nomi nonce per il nuovo workflow.

Questo è un esempio di risposta 400 che richiede un nuovo tentativo e la creazione di una nuova prova utilizzando il valore DPoP-Nonce:

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AO4t07Kf85RJXmltUhiAiELLPPrJ4zOi66zWxU1uDZbhRcahFBYvT0WlcjSSXULXknSA

{
  "error": "use_dpop_nonce",
  "error_description": "New DPoP nonce issued due to invalid or expired challenge."
}

In caso di esito positivo, vengono restituiti un nuovo nonce e un token di accesso di breve durata:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg=

{
  "access_token": "ya29.a0ARGnu0bDj9BAQYVbF5hi3vw-brBUZBZu1bnInk1hS7gueqEb6QPqUjDGb0MMj9A0QX5FRrJo3FDw-DEDtvVbRUdeCgjwsL_LVVFXz-p-MUyiFyRoufI4KC0Go9aq5cEjD_BWvOJLMSIY6_EnwnhqDgk0XxvzaaAxDnv8PXJAGev_UotcfApstqi0NCxbfi-6Kgull9QaCgYKAUQSARASFQHGX2MiZpMjRS6z4S0RjOkNxn2o1Q0206",
  "expires_in": 3599,
  "scope": "https://www.googleapis.com/auth/calendar.readonly",
  "token_type": "Bearer"
}

Salva il valore DPoP-Nonce da utilizzare nella richiesta successiva.

Per ulteriori dettagli e consigli, consulta l'articolo sull'utilizzo di OAuth 2.0 per applicazioni server web e le best practice.