Guida all'adozione di DPoP

Questa guida descrive in dettaglio come implementare DPoP (Demonstrating Proof-of-Possession) nelle integrazioni OAuth 2.0 con la piattaforma OAuth di Google. DPoP (definito in RFC 9449) protegge le applicazioni dal furto di token e dagli attacchi di replay associando crittograficamente i token a una coppia di chiavi asimmetriche generate 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 che utilizza 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 dell'URI di reindirizzamento viene utilizzato per la 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 DPoP direttamente a causa del requisito client_secret e delle limitazioni CORS sull'intestazione DPoP-Nonce. Per proteggere le SPA, indirizza il traffico tramite un backend per il frontend (BFF) che funge da client riservato, abilita 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. È possibile anche una coppia di chiavi RSA, ma non è consigliata a causa dei costi di calcolo più elevati.

Ecco un esempio di intestazione JOSE:

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

Per creare il corpo della prova sono necessari quattro valori.

Due rivendicazioni: htm: POST e htu: https://oauth2.googleapis.com/token sono valori fissi e non cambiano quando si effettua una richiesta all'endpoint del token di Google.

Le altre due rivendicazioni: 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 di aggiornamento, il valore di jti è l' hash SHA256 con codifica Base64 e URL del codice di autorizzazione, ad esempio jti = BASE64URL(SHA-256(authorization_code)).

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 come JWT (RFC7519) per l'utilizzo diretto nell'intestazione HTTP DPoP nella richiesta del token:

$ curl -X POST https://oauth2.googleapis.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6\
       IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZ\
       k93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3\
       c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhMSVkwbF9OOGl5NS\
       1pbG9uMWd1YWQ5TkZRSEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh\
       0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwiaWF0IjoxNzg0ODIy\
       MDI1fQ.OSdQCmqTng_uZmGK5UXf8hcEMtoOu7ucmYtl5mx4901RXnj6fJRJQmIeTq\
       fhprRBTG_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"
}

Il nonce generato dal server di autorizzazione di Google deve essere incluso in ogni richiesta di token successiva. Tieni presente che un valore nonce viene utilizzato una sola volta e un valore nonce mancante, non valido, scaduto o riutilizzato viene rifiutato con una risposta HTTP 400. In questo caso, viene restituito un nuovo nonce da utilizzare nei 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 del token con DPoP
Figura 2. La sequenza di eventi nel flusso di aggiornamento dei token con gestione degli errori e tentativi.

Creazione della prova DPoP

Il metodo per creare una prova per l'aggiornamento dei 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 del codice di autorizzazione. Il corpo della prova viene creato in modo simile, ma include una rivendicazione 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 nella rivendicazione nonce e il timestamp di emissione (iat) deve essere aggiornato per ogni richiesta. L'ID JWT (jti) è una stringa casuale univoca generata per ogni richiesta, utilizzando l'API WebCrypto integrata crypto.getRandomValues(new Uint8Array(24)) e codificando la stringa in Base64URL.

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

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

L'intestazione JOSE e il corpo del payload vengono 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: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6\
       IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiVkM5MXk5WllkZlNXYUR2OEphSTZneDVpZ\
       k93MnJuM1lkcWtBQjUxVXU2RSIsInkiOiJpa1BqT3RlYTRrN2ZXUFZyUll3YUE0V3\
       c2aVZZM3BPT0lDb3RId3dHVjNvIn19.eyJqdGkiOiJvMjlDTjhaSVkwbF9LOGl5NS\
       1pbG9uMWd1YWQ5TkY2SEZPZFhUenJCTmNrIiwiaHRtIjoiUE9TVCIsImh0dSI6Imh\
       0dHBzOi8vb2F1dGgyLmdvb2dsZWFwaXMuY29tL3Rva2VuIiwibm9uY2UiOiJBTjNY\
       d0pqWnNqbmIwWnVXa1JsZWs4UVU3d1ktWmhmLTVJUDZ0TzB0T1J6MEtndERUMUJvO\
       EZYLXc0bnozcjVsbmVwSSIsImlhdCI6MTc4NDgyMjAyNX0.MEQCIDm09AXo2c9sov\
       GrTUkrbEB_k9mra_Dkji-CQ9mSZVP1AiBxbiqkCE7Dt9RKyUT_3kj7q1vCvVggwnW\
       JNX3P3vO1mw" \
  --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, errato o riutilizzato o quando si esegue la transizione tra diversi flussi di lavoro OAuth (ad esempio, quando si passa dallo scambio iniziale del codice di autorizzazione a una richiesta di aggiornamento del token), il server di Google applica l'isolamento del flusso di lavoro. Ciò significa che il server rifiuta incondizionatamente il nonce con una sfida HTTP 400 use_dpop_nonce per stabilire uno spazio dei nomi nonce nuovo per il nuovo flusso di lavoro.

Ecco un esempio di risposta 400 che richiede un 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",
  "challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}

Salva il valore DPoP-Nonce per utilizzarlo 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.