Przewodnik po wdrażaniu DPoP

Ten przewodnik zawiera szczegółowe informacje o tym, jak wdrożyć DPoP (Demonstrating Proof-of-Possession) w integracjach OAuth 2.0 z platformą OAuth Google. DPoP (zdefiniowany w RFC 9449) zabezpiecza aplikacje przed kradzieżą tokenów i atakami typu replay, kryptograficznie wiążąc tokeny z asymetryczną parą kluczy wygenerowaną przez klienta.

Zmiany w przepływie kodu autoryzacji

Dodanie DPoP do istniejącego przepływu kodu autoryzacji OAuth 2.0 wymaga wygenerowania i przechowywania pary kluczy, utworzenia tokena JWT dowodu DPoP oraz dołączenia dowodu jako nagłówka HTTP, gdy kod autoryzacji jest wymieniany na token odświeżania, jak pokazano w krokach 5 i 6 na rysunku 1.

Przepływ kodu autoryzacji z DPoP
Rysunek 1. Sekwencja zdarzeń w przepływie kodu autoryzacji z użyciem DPoP.

Prośba o kod autoryzacji

Prośba o autoryzację jest tworzona w zwykły sposób. Na przykład:

$ 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"

Kod autoryzacji zwrócony jako parametr identyfikatora URI przekierowania jest używany do utworzenia dowodu DPoP. Token odświeżania jest powiązany z dowodem dołączonym jako nagłówek HTTP we wszystkich kolejnych żądaniach wysyłanych do punktu końcowego tokena.

Czyste, bezkluczowe aplikacje SPA po stronie klienta nie mogą używać DPoP bezpośrednio ze względu na wymaganie client_secret i ograniczenia CORS dotyczące nagłówka DPoP-Nonce. Aby zabezpieczyć aplikacje jednostronicowe, kieruj ruch przez Backend-for-Frontend (BFF), który działa jako poufny klient, umożliwia access_type=offline i wykorzystuje DPoP do powiązania tokena odświeżania po stronie serwera.

Tworzenie dowodu DPoP

Dowód zawiera nagłówek JOSE i ładunek.

Aby utworzyć nagłówek, wygeneruj parę kluczy EC P-256 (ES256) i uwzględnij współrzędne klucza publicznego (x i y) w parametrze jwk. Obsługiwana jest też para kluczy RSA (RS256), ale zalecamy ES256 ze względu na mniejszy rozmiar podpisu i niższy koszt obliczeniowy.

Oto przykładowy nagłówek JOSE:

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

Do utworzenia danych potwierdzających wymagane są 4 twierdzenia (htm, htu, iat i jti) oraz opcjonalne twierdzenie nonce.

Dwa oświadczenia: htm: POST i htu: https://oauth2.googleapis.com/token mają stałe wartości i nie zmieniają się podczas wysyłania żądania do punktu końcowego tokena Google.

Pozostałe 2 twierdzenia: iat i jti muszą być generowane dla każdego żądania. Wartość iat to sygnatura czasowa wydania, która zmienia się przy każdym żądaniu. Wartość deklaracji identyfikatora JWT (jti) zależy od typu wymiany. Gdy kod autoryzacji jest wymieniany na tokeny dostępu i tokeny odświeżania, wartość jti to zakodowany w Base-64 i adresie URL skrót SHA256 kodu autoryzacji, np. jti = BASE64URL(SHA-256(authorization_code)). Deklaracja nonce jest opcjonalna podczas początkowej wymiany kodu autoryzacji i jest wymagana tylko wtedy, gdy klient ma już prawidłowy token DPoP-Nonce z poprzedniego żądania.

Oto przykładowa treść ładunku:

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

Nagłówek JOSE i treść ładunku są kodowane i podpisywane jako JWT (RFC 7519) do bezpośredniego użycia w nagłówku HTTP DPoP w żądaniu tokena. W przypadku ES256 podpis JWS musi używać nieprzetworzonego formatu IEEE P1363 (R || S, 64 bajty dla 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"

Token odświeżania powiązany z DPoP jest zwracany wraz z nagłówkiem HTTP DPoP-Nonce, na przykład:

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"
}

Zawsze zapisuj w pamięci podręcznej i wysyłaj najnowszy element DPoP-Nonce zwrócony przez serwer autoryzacji Google w kolejnych żądaniach tokena. Liczby jednorazowe okresowo wygasają. Jeśli w dowodzie brakuje wymaganego oświadczenia nonce lub zawiera on nieprawidłową lub wygasłą liczbę jednorazową, serwer zwraca odpowiedź HTTP 400 (use_dpop_nonce) z nowym nagłówkiem DPoP-Nonce do użycia w ponownych próbach.

Zmiany w procesie odświeżania tokena

Aktualizacja istniejącego przepływu odświeżania tokena OAuth 2.0 wymaga wygenerowania i wysłania dowodu DPoP jako nagłówka HTTP, gdy token odświeżania jest wymieniany na nowe tokeny, jak pokazano w krokach 2–5 na rysunku 2.

Przepływ odświeżania tokena z DPoP
Rysunek 2. Sekwencja zdarzeń w procesie odświeżania tokena z obsługą błędów i ponowną próbą.

Tworzenie dowodu DPoP

Metoda tworzenia dowodu na potrzeby odświeżania tokena różni się od scenariusza z kodem autoryzacji. Nagłówek JOSE jest tworzony w taki sam sposób, jak opisano wcześniej podczas tworzenia żądania kodu autoryzacji. Treść potwierdzenia jest tworzona podobnie, ale zawiera deklarację nonce, a jti zawiera unikalny ciąg losowy.

Aby utworzyć treść ładunku, wcześniej zwrócona wartość nagłówka HTTP DPoP-Nonce musi być uwzględniona w roszczeniu nonce, a sygnatura czasowa wydania (iat) musi być aktualizowana przy każdym żądaniu. Identyfikator JWT (jti) to unikalny losowy ciąg znaków generowany na żądanie za pomocą wbudowanego interfejsu WebCrypto API crypto.getRandomValues(new Uint8Array(24)) i kodowany w formacie Base64URL.

Oto przykładowa treść ładunku zawierająca jti, nonce i iat:

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

Nagłówek JOSE i treść ładunku są kodowane jako JWT (RFC7519) do bezpośredniego użycia w nagłówku HTTP DPoP w żądaniu tokena.

Potwierdzenie jest dodawane jako nagłówek DPoP do żądania odświeżenia tokena:

$ 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"

Gdy używana jest wygasła lub nieprawidłowa liczba jednorazowa albo gdy następuje przejście między różnymi procesami OAuth (np. przejście od początkowej wymiany kodu autoryzacji do żądania odświeżenia tokena), serwer Google wymusza izolację procesu. Oznacza to, że serwer bezwarunkowo odrzuca liczbę jednorazową z wyzwaniem HTTP 400 use_dpop_nonce, aby ustanowić nową przestrzeń nazw liczby jednorazowej dla nowego przepływu pracy.

Oto przykładowa odpowiedź 400, która wymaga ponownej próby i utworzenia nowego dowodu przy użyciu wartości 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."
}

Jeśli się powiedzie, zwracana jest nowa liczba jednorazowa i krótkotrwały token dostępu:

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"
}

Zapisz wartość DPoP-Nonce, aby użyć jej w następnym żądaniu.

Więcej informacji i zaleceń znajdziesz w artykułach Używanie OAuth 2.0 w internetowych aplikacjach serwerowych i Sprawdzone metody.