_client.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. # Copyright 2016 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """OAuth 2.0 client.
  15. This is a client for interacting with an OAuth 2.0 authorization server's
  16. token endpoint.
  17. For more information about the token endpoint, see
  18. `Section 3.1 of rfc6749`_
  19. .. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
  20. """
  21. import datetime
  22. import http.client as http_client
  23. import json
  24. import urllib
  25. from google.auth import _exponential_backoff
  26. from google.auth import _helpers
  27. from google.auth import exceptions
  28. from google.auth import jwt
  29. from google.auth import metrics
  30. from google.auth import transport
  31. _URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
  32. _JSON_CONTENT_TYPE = "application/json"
  33. _JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
  34. _REFRESH_GRANT_TYPE = "refresh_token"
  35. _IAM_IDTOKEN_ENDPOINT = (
  36. "https://iamcredentials.googleapis.com/v1/"
  37. + "projects/-/serviceAccounts/{}:generateIdToken"
  38. )
  39. def _handle_error_response(response_data, retryable_error):
  40. """Translates an error response into an exception.
  41. Args:
  42. response_data (Mapping | str): The decoded response data.
  43. retryable_error Optional[bool]: A boolean indicating if an error is retryable.
  44. Defaults to False.
  45. Raises:
  46. google.auth.exceptions.RefreshError: The errors contained in response_data.
  47. """
  48. retryable_error = retryable_error if retryable_error else False
  49. if isinstance(response_data, str):
  50. raise exceptions.RefreshError(response_data, retryable=retryable_error)
  51. try:
  52. error_details = "{}: {}".format(
  53. response_data["error"], response_data.get("error_description")
  54. )
  55. # If no details could be extracted, use the response data.
  56. except (KeyError, ValueError):
  57. error_details = json.dumps(response_data)
  58. raise exceptions.RefreshError(
  59. error_details, response_data, retryable=retryable_error
  60. )
  61. def _can_retry(status_code, response_data):
  62. """Checks if a request can be retried by inspecting the status code
  63. and response body of the request.
  64. Args:
  65. status_code (int): The response status code.
  66. response_data (Mapping | str): The decoded response data.
  67. Returns:
  68. bool: True if the response is retryable. False otherwise.
  69. """
  70. if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
  71. return True
  72. try:
  73. # For a failed response, response_body could be a string
  74. error_desc = response_data.get("error_description") or ""
  75. error_code = response_data.get("error") or ""
  76. if not isinstance(error_code, str) or not isinstance(error_desc, str):
  77. return False
  78. # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
  79. # This is needed because a redirect will not return a 500 status code.
  80. retryable_error_descriptions = {
  81. "internal_failure",
  82. "server_error",
  83. "temporarily_unavailable",
  84. }
  85. if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
  86. return True
  87. except AttributeError:
  88. pass
  89. return False
  90. def _parse_expiry(response_data):
  91. """Parses the expiry field from a response into a datetime.
  92. Args:
  93. response_data (Mapping): The JSON-parsed response data.
  94. Returns:
  95. Optional[datetime]: The expiration or ``None`` if no expiration was
  96. specified.
  97. """
  98. expires_in = response_data.get("expires_in", None)
  99. if expires_in is not None:
  100. # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
  101. # JSON String.
  102. if isinstance(expires_in, str):
  103. expires_in = int(expires_in)
  104. return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
  105. else:
  106. return None
  107. def _token_endpoint_request_no_throw(
  108. request,
  109. token_uri,
  110. body,
  111. access_token=None,
  112. use_json=False,
  113. can_retry=True,
  114. headers=None,
  115. **kwargs
  116. ):
  117. """Makes a request to the OAuth 2.0 authorization server's token endpoint.
  118. This function doesn't throw on response errors.
  119. Args:
  120. request (google.auth.transport.Request): A callable used to make
  121. HTTP requests.
  122. token_uri (str): The OAuth 2.0 authorizations server's token endpoint
  123. URI.
  124. body (Mapping[str, str]): The parameters to send in the request body.
  125. access_token (Optional(str)): The access token needed to make the request.
  126. use_json (Optional(bool)): Use urlencoded format or json format for the
  127. content type. The default value is False.
  128. can_retry (bool): Enable or disable request retry behavior.
  129. headers (Optional[Mapping[str, str]]): The headers for the request.
  130. kwargs: Additional arguments passed on to the request method. The
  131. kwargs will be passed to `requests.request` method, see:
  132. https://docs.python-requests.org/en/latest/api/#requests.request.
  133. For example, you can use `cert=("cert_pem_path", "key_pem_path")`
  134. to set up client side SSL certificate, and use
  135. `verify="ca_bundle_path"` to set up the CA certificates for sever
  136. side SSL certificate verification.
  137. Returns:
  138. Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
  139. if the request is successful, a mapping for the JSON-decoded response
  140. data and in the case of an error a boolean indicating if the error
  141. is retryable.
  142. """
  143. if use_json:
  144. headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
  145. body = json.dumps(body).encode("utf-8")
  146. else:
  147. headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
  148. body = urllib.parse.urlencode(body).encode("utf-8")
  149. if access_token:
  150. headers_to_use["Authorization"] = "Bearer {}".format(access_token)
  151. if headers:
  152. headers_to_use.update(headers)
  153. def _perform_request():
  154. response = request(
  155. method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
  156. )
  157. response_body = (
  158. response.data.decode("utf-8")
  159. if hasattr(response.data, "decode")
  160. else response.data
  161. )
  162. response_data = ""
  163. try:
  164. # response_body should be a JSON
  165. response_data = json.loads(response_body)
  166. except ValueError:
  167. response_data = response_body
  168. if response.status == http_client.OK:
  169. return True, response_data, None
  170. retryable_error = _can_retry(
  171. status_code=response.status, response_data=response_data
  172. )
  173. return False, response_data, retryable_error
  174. request_succeeded, response_data, retryable_error = _perform_request()
  175. if request_succeeded or not retryable_error or not can_retry:
  176. return request_succeeded, response_data, retryable_error
  177. retries = _exponential_backoff.ExponentialBackoff()
  178. for _ in retries:
  179. request_succeeded, response_data, retryable_error = _perform_request()
  180. if request_succeeded or not retryable_error:
  181. return request_succeeded, response_data, retryable_error
  182. return False, response_data, retryable_error
  183. def _token_endpoint_request(
  184. request,
  185. token_uri,
  186. body,
  187. access_token=None,
  188. use_json=False,
  189. can_retry=True,
  190. headers=None,
  191. **kwargs
  192. ):
  193. """Makes a request to the OAuth 2.0 authorization server's token endpoint.
  194. Args:
  195. request (google.auth.transport.Request): A callable used to make
  196. HTTP requests.
  197. token_uri (str): The OAuth 2.0 authorizations server's token endpoint
  198. URI.
  199. body (Mapping[str, str]): The parameters to send in the request body.
  200. access_token (Optional(str)): The access token needed to make the request.
  201. use_json (Optional(bool)): Use urlencoded format or json format for the
  202. content type. The default value is False.
  203. can_retry (bool): Enable or disable request retry behavior.
  204. headers (Optional[Mapping[str, str]]): The headers for the request.
  205. kwargs: Additional arguments passed on to the request method. The
  206. kwargs will be passed to `requests.request` method, see:
  207. https://docs.python-requests.org/en/latest/api/#requests.request.
  208. For example, you can use `cert=("cert_pem_path", "key_pem_path")`
  209. to set up client side SSL certificate, and use
  210. `verify="ca_bundle_path"` to set up the CA certificates for sever
  211. side SSL certificate verification.
  212. Returns:
  213. Mapping[str, str]: The JSON-decoded response data.
  214. Raises:
  215. google.auth.exceptions.RefreshError: If the token endpoint returned
  216. an error.
  217. """
  218. response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
  219. request,
  220. token_uri,
  221. body,
  222. access_token=access_token,
  223. use_json=use_json,
  224. can_retry=can_retry,
  225. headers=headers,
  226. **kwargs
  227. )
  228. if not response_status_ok:
  229. _handle_error_response(response_data, retryable_error)
  230. return response_data
  231. def jwt_grant(request, token_uri, assertion, can_retry=True):
  232. """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
  233. For more details, see `rfc7523 section 4`_.
  234. Args:
  235. request (google.auth.transport.Request): A callable used to make
  236. HTTP requests.
  237. token_uri (str): The OAuth 2.0 authorizations server's token endpoint
  238. URI.
  239. assertion (str): The OAuth 2.0 assertion.
  240. can_retry (bool): Enable or disable request retry behavior.
  241. Returns:
  242. Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
  243. expiration, and additional data returned by the token endpoint.
  244. Raises:
  245. google.auth.exceptions.RefreshError: If the token endpoint returned
  246. an error.
  247. .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
  248. """
  249. body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
  250. response_data = _token_endpoint_request(
  251. request,
  252. token_uri,
  253. body,
  254. can_retry=can_retry,
  255. headers={
  256. metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
  257. },
  258. )
  259. try:
  260. access_token = response_data["access_token"]
  261. except KeyError as caught_exc:
  262. new_exc = exceptions.RefreshError(
  263. "No access token in response.", response_data, retryable=False
  264. )
  265. raise new_exc from caught_exc
  266. expiry = _parse_expiry(response_data)
  267. return access_token, expiry, response_data
  268. def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):
  269. """Call iam.generateIdToken endpoint to get ID token.
  270. Args:
  271. request (google.auth.transport.Request): A callable used to make
  272. HTTP requests.
  273. signer_email (str): The signer email used to form the IAM
  274. generateIdToken endpoint.
  275. audience (str): The audience for the ID token.
  276. access_token (str): The access token used to call the IAM endpoint.
  277. Returns:
  278. Tuple[str, datetime]: The ID token and expiration.
  279. """
  280. body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
  281. response_data = _token_endpoint_request(
  282. request,
  283. _IAM_IDTOKEN_ENDPOINT.format(signer_email),
  284. body,
  285. access_token=access_token,
  286. use_json=True,
  287. )
  288. try:
  289. id_token = response_data["token"]
  290. except KeyError as caught_exc:
  291. new_exc = exceptions.RefreshError(
  292. "No ID token in response.", response_data, retryable=False
  293. )
  294. raise new_exc from caught_exc
  295. payload = jwt.decode(id_token, verify=False)
  296. expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
  297. return id_token, expiry
  298. def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
  299. """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
  300. requests an OpenID Connect ID Token instead of an access token.
  301. This is a variant on the standard JWT Profile that is currently unique
  302. to Google. This was added for the benefit of authenticating to services
  303. that require ID Tokens instead of access tokens or JWT bearer tokens.
  304. Args:
  305. request (google.auth.transport.Request): A callable used to make
  306. HTTP requests.
  307. token_uri (str): The OAuth 2.0 authorization server's token endpoint
  308. URI.
  309. assertion (str): JWT token signed by a service account. The token's
  310. payload must include a ``target_audience`` claim.
  311. can_retry (bool): Enable or disable request retry behavior.
  312. Returns:
  313. Tuple[str, Optional[datetime], Mapping[str, str]]:
  314. The (encoded) Open ID Connect ID Token, expiration, and additional
  315. data returned by the endpoint.
  316. Raises:
  317. google.auth.exceptions.RefreshError: If the token endpoint returned
  318. an error.
  319. """
  320. body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
  321. response_data = _token_endpoint_request(
  322. request,
  323. token_uri,
  324. body,
  325. can_retry=can_retry,
  326. headers={
  327. metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
  328. },
  329. )
  330. try:
  331. id_token = response_data["id_token"]
  332. except KeyError as caught_exc:
  333. new_exc = exceptions.RefreshError(
  334. "No ID token in response.", response_data, retryable=False
  335. )
  336. raise new_exc from caught_exc
  337. payload = jwt.decode(id_token, verify=False)
  338. expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
  339. return id_token, expiry, response_data
  340. def _handle_refresh_grant_response(response_data, refresh_token):
  341. """Extract tokens from refresh grant response.
  342. Args:
  343. response_data (Mapping[str, str]): Refresh grant response data.
  344. refresh_token (str): Current refresh token.
  345. Returns:
  346. Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
  347. refresh token, expiration, and additional data returned by the token
  348. endpoint. If response_data doesn't have refresh token, then the current
  349. refresh token will be returned.
  350. Raises:
  351. google.auth.exceptions.RefreshError: If the token endpoint returned
  352. an error.
  353. """
  354. try:
  355. access_token = response_data["access_token"]
  356. except KeyError as caught_exc:
  357. new_exc = exceptions.RefreshError(
  358. "No access token in response.", response_data, retryable=False
  359. )
  360. raise new_exc from caught_exc
  361. refresh_token = response_data.get("refresh_token", refresh_token)
  362. expiry = _parse_expiry(response_data)
  363. return access_token, refresh_token, expiry, response_data
  364. def refresh_grant(
  365. request,
  366. token_uri,
  367. refresh_token,
  368. client_id,
  369. client_secret,
  370. scopes=None,
  371. rapt_token=None,
  372. can_retry=True,
  373. ):
  374. """Implements the OAuth 2.0 refresh token grant.
  375. For more details, see `rfc678 section 6`_.
  376. Args:
  377. request (google.auth.transport.Request): A callable used to make
  378. HTTP requests.
  379. token_uri (str): The OAuth 2.0 authorizations server's token endpoint
  380. URI.
  381. refresh_token (str): The refresh token to use to get a new access
  382. token.
  383. client_id (str): The OAuth 2.0 application's client ID.
  384. client_secret (str): The Oauth 2.0 appliaction's client secret.
  385. scopes (Optional(Sequence[str])): Scopes to request. If present, all
  386. scopes must be authorized for the refresh token. Useful if refresh
  387. token has a wild card scope (e.g.
  388. 'https://www.googleapis.com/auth/any-api').
  389. rapt_token (Optional(str)): The reauth Proof Token.
  390. can_retry (bool): Enable or disable request retry behavior.
  391. Returns:
  392. Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
  393. token, new or current refresh token, expiration, and additional data
  394. returned by the token endpoint.
  395. Raises:
  396. google.auth.exceptions.RefreshError: If the token endpoint returned
  397. an error.
  398. .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
  399. """
  400. body = {
  401. "grant_type": _REFRESH_GRANT_TYPE,
  402. "client_id": client_id,
  403. "client_secret": client_secret,
  404. "refresh_token": refresh_token,
  405. }
  406. if scopes:
  407. body["scope"] = " ".join(scopes)
  408. if rapt_token:
  409. body["rapt"] = rapt_token
  410. response_data = _token_endpoint_request(
  411. request, token_uri, body, can_retry=can_retry
  412. )
  413. return _handle_refresh_grant_response(response_data, refresh_token)