Z tego przewodnika dowiesz się, jak zaimplementować DPoP (Demonstrating Proof-of-Possession) w integracjach OAuth 2.0 z platformą OAuth Google. DPoP (zdefiniowany w RFC 9449) chroni aplikacje przed kradzieżą tokenów i atakami typu replay, kryptograficznie wiążąc tokeny z wygenerowaną przez klienta asymetryczną parą kluczy.
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 z dowodem DPoP oraz dołączenia tego dowodu jako nagłówka HTTP podczas wymiany kodu autoryzacji na token odświeżania, jak pokazano w krokach 5 i 6 na rysunku 1.
Żądanie kodu autoryzacji
Żądanie autoryzacji jest tworzone normalnie. 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 zwracany 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 do punktu końcowego tokena.
Czyste, bezsekretowe aplikacje SPA po stronie klienta nie mogą bezpośrednio używać DPoP ze względu na wymaganie client_secret i ograniczenia CORS dotyczące nagłówka DPoP-Nonce. Aby zabezpieczyć aplikacje SPA, kieruj ruch przez backend dla frontendu (BFF), który działa jako klient poufny, włącza access_type=offline i używa 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 dołącz współrzędne klucza publicznego (x i y) w parametrze jwk. Możliwa jest też para kluczy RSA, ale nie jest zalecana ze względu na wyższe koszty obliczeniowe.
Oto przykład nagłówka JOSE:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "VC91y9ZYdfSWaDv8JaI6gx5ifOw2rn3YdqkAB51Uu6E",
"y": "ikPjOtea4k7fWPVrRYwaA4Ww6iVY3pOOICotHwwGV3o"
}
}
Aby utworzyć ładunek dowodu, wymagane są 4 wartości.
2 deklaracje: htm: POST i htu: https://oauth2.googleapis.com/token to stałe wartości, które nie zmieniają się podczas wysyłania żądania do punktu końcowego tokena Google.
Pozostałe 2 deklaracje: iat i jti muszą być generowane dla każdego żądania. Wartość iat to sygnatura czasowa wydania, która zmienia się w zależności od żądania. Wartość deklaracji identyfikatora JWT (jti) zależy od typu wymiany. Gdy kod autoryzacji
jest wymieniany na tokeny dostępu i odświeżania, wartość jti jest
haszem SHA256 kodu autoryzacji zakodowanym w formacie Base64 i URL, np. jti =
BASE64URL(SHA-256(authorization_code)).
Oto przykład treści ładunku:
{
"jti": "o29CN8LIY0l_N8iy5-ilon1guad9NFQHFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"iat": 1784822025
}
Nagłówek JOSE i treść ładunku są kodowane jako token JWT (RFC7519) do bezpośredniego użycia w nagłówku HTTP DPoP w żądaniu tokena:
$ 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"
Token odświeżania powiązany z DPoP jest zwracany wraz z nagłówkiem HTTP DPoP-Nonce, np.:
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"
}
Wartość nonce wygenerowana przez serwer autoryzacji Google musi być uwzględniona w każdym kolejnym żądaniu tokena. Pamiętaj, że liczba jednorazowa jest używana tylko raz, a brakująca, nieprawidłowa, wygasła lub ponownie użyta liczba jednorazowa jest odrzucana z odpowiedzią HTTP 400. W takim przypadku zwracana jest nowa wartość nonce do użycia w ponownych próbach.
Zmiany w przepływie odświeżania tokena
Zaktualizowanie istniejącego przepływu odświeżania tokena OAuth 2.0 wymaga wygenerowania i wysłania dowodu DPoP jako nagłówka HTTP podczas wymiany tokena odświeżania na nowe tokeny, jak pokazano w krokach 2–5 na rysunku 2.
Tworzenie dowodu DPoP
Metoda tworzenia dowodu na potrzeby odświeżania tokena różni się od metody stosowanej w przypadku kodu autoryzacji. Nagłówek JOSE jest tworzony w taki sam sposób jak opisany wcześniej podczas tworzenia żądania kodu autoryzacji. Treść dowodu jest tworzona podobnie, ale zawiera deklarację nonce, a jti zawiera unikalny losowy ciąg znaków.
Aby utworzyć treść ładunku, należy uwzględnić wartość nagłówka HTTP DPoP-Nonce zwróconą wcześniej w deklaracji nonce i zaktualizować sygnaturę czasową wydania (iat) dla każdego żądania. Identyfikator JWT (jti) to unikalny losowy ciąg znaków generowany na potrzeby każdego żądania za pomocą wbudowanego interfejsu WebCrypto API crypto.getRandomValues(new Uint8Array(24)) i kodowania Base64URL.
Oto przykład treści ładunku zawierającej jti, nonce i iat:
{
"jti": "o29CN8ZIY0l_K8iy5-ilon1gwad9NF6HFOdXTzrBNck",
"htm": "POST",
"htu": "https://oauth2.googleapis.com/token",
"nonce": "AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI",
"iat": 1784822025
}
Nagłówek JOSE i treść ładunku są kodowane jako token JWT (RFC7519) do bezpośredniego użycia w nagłówku HTTP DPoP w żądaniu tokena.
Dowód jest dodawany 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: 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"
Gdy używana jest wygasła, nieprawidłowa lub ponownie użyta wartość nonce albo gdy następuje przejście między różnymi przepływami OAuth (np. przejście od początkowej wymiany kodu autoryzacji do żądania odświeżenia tokena), serwer Google wymusza izolację przepływu. Oznacza to, że serwer bezwarunkowo odrzuca wartość nonce z wyzwaniem HTTP 400 use_dpop_nonce, aby utworzyć nową przestrzeń nazw nonce dla nowego przepływu.
Oto przykład odpowiedzi 400, która wymaga ponowienia próby i utworzenia nowego dowodu z użyciem 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."
}
W przypadku powodzenia zwracana jest nowa wartość nonce 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",
"challenge": "AO4t07IXuovyCbtLEr6VVFZQ_Kb78MMOXTt6-CyZpsJeF62HZ3P_EW55XbWqYcU76Jg"
}
Zapisz wartość DPoP-Nonce do użycia w następnym żądaniu.
Więcej informacji i zaleceń znajdziesz w artykułach Używanie OAuth 2.0 w internetowych aplikacjach serwerowych i Najlepsze praktyki.