Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/oauth2/_client.py: 20%
131 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
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.
15"""OAuth 2.0 client.
17This is a client for interacting with an OAuth 2.0 authorization server's
18token endpoint.
20For more information about the token endpoint, see
21`Section 3.1 of rfc6749`_
23.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
24"""
26import datetime
27import http.client as http_client
28import json
29import urllib
31from google.auth import _exponential_backoff
32from google.auth import _helpers
33from google.auth import exceptions
34from google.auth import jwt
35from google.auth import metrics
36from google.auth import transport
38_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
39_JSON_CONTENT_TYPE = "application/json"
40_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
41_REFRESH_GRANT_TYPE = "refresh_token"
42_IAM_IDTOKEN_ENDPOINT = (
43 "https://iamcredentials.googleapis.com/v1/"
44 + "projects/-/serviceAccounts/{}:generateIdToken"
45)
48def _handle_error_response(response_data, retryable_error):
49 """Translates an error response into an exception.
51 Args:
52 response_data (Mapping | str): The decoded response data.
53 retryable_error Optional[bool]: A boolean indicating if an error is retryable.
54 Defaults to False.
56 Raises:
57 google.auth.exceptions.RefreshError: The errors contained in response_data.
58 """
60 retryable_error = retryable_error if retryable_error else False
62 if isinstance(response_data, str):
63 raise exceptions.RefreshError(response_data, retryable=retryable_error)
64 try:
65 error_details = "{}: {}".format(
66 response_data["error"], response_data.get("error_description")
67 )
68 # If no details could be extracted, use the response data.
69 except (KeyError, ValueError):
70 error_details = json.dumps(response_data)
72 raise exceptions.RefreshError(
73 error_details, response_data, retryable=retryable_error
74 )
77def _can_retry(status_code, response_data):
78 """Checks if a request can be retried by inspecting the status code
79 and response body of the request.
81 Args:
82 status_code (int): The response status code.
83 response_data (Mapping | str): The decoded response data.
85 Returns:
86 bool: True if the response is retryable. False otherwise.
87 """
88 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
89 return True
91 try:
92 # For a failed response, response_body could be a string
93 error_desc = response_data.get("error_description") or ""
94 error_code = response_data.get("error") or ""
96 if not isinstance(error_code, str) or not isinstance(error_desc, str):
97 return False
99 # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
100 # This is needed because a redirect will not return a 500 status code.
101 retryable_error_descriptions = {
102 "internal_failure",
103 "server_error",
104 "temporarily_unavailable",
105 }
107 if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
108 return True
110 except AttributeError:
111 pass
113 return False
116def _parse_expiry(response_data):
117 """Parses the expiry field from a response into a datetime.
119 Args:
120 response_data (Mapping): The JSON-parsed response data.
122 Returns:
123 Optional[datetime]: The expiration or ``None`` if no expiration was
124 specified.
125 """
126 expires_in = response_data.get("expires_in", None)
128 if expires_in is not None:
129 # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
130 # JSON String.
131 if isinstance(expires_in, str):
132 expires_in = int(expires_in)
134 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
135 else:
136 return None
139def _token_endpoint_request_no_throw(
140 request,
141 token_uri,
142 body,
143 access_token=None,
144 use_json=False,
145 can_retry=True,
146 headers=None,
147 **kwargs
148):
149 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
150 This function doesn't throw on response errors.
152 Args:
153 request (google.auth.transport.Request): A callable used to make
154 HTTP requests.
155 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
156 URI.
157 body (Mapping[str, str]): The parameters to send in the request body.
158 access_token (Optional(str)): The access token needed to make the request.
159 use_json (Optional(bool)): Use urlencoded format or json format for the
160 content type. The default value is False.
161 can_retry (bool): Enable or disable request retry behavior.
162 headers (Optional[Mapping[str, str]]): The headers for the request.
163 kwargs: Additional arguments passed on to the request method. The
164 kwargs will be passed to `requests.request` method, see:
165 https://docs.python-requests.org/en/latest/api/#requests.request.
166 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
167 to set up client side SSL certificate, and use
168 `verify="ca_bundle_path"` to set up the CA certificates for sever
169 side SSL certificate verification.
171 Returns:
172 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
173 if the request is successful, a mapping for the JSON-decoded response
174 data and in the case of an error a boolean indicating if the error
175 is retryable.
176 """
177 if use_json:
178 headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
179 body = json.dumps(body).encode("utf-8")
180 else:
181 headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
182 body = urllib.parse.urlencode(body).encode("utf-8")
184 if access_token:
185 headers_to_use["Authorization"] = "Bearer {}".format(access_token)
187 if headers:
188 headers_to_use.update(headers)
190 def _perform_request():
191 response = request(
192 method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
193 )
194 response_body = (
195 response.data.decode("utf-8")
196 if hasattr(response.data, "decode")
197 else response.data
198 )
199 response_data = ""
200 try:
201 # response_body should be a JSON
202 response_data = json.loads(response_body)
203 except ValueError:
204 response_data = response_body
206 if response.status == http_client.OK:
207 return True, response_data, None
209 retryable_error = _can_retry(
210 status_code=response.status, response_data=response_data
211 )
213 return False, response_data, retryable_error
215 request_succeeded, response_data, retryable_error = _perform_request()
217 if request_succeeded or not retryable_error or not can_retry:
218 return request_succeeded, response_data, retryable_error
220 retries = _exponential_backoff.ExponentialBackoff()
221 for _ in retries:
222 request_succeeded, response_data, retryable_error = _perform_request()
223 if request_succeeded or not retryable_error:
224 return request_succeeded, response_data, retryable_error
226 return False, response_data, retryable_error
229def _token_endpoint_request(
230 request,
231 token_uri,
232 body,
233 access_token=None,
234 use_json=False,
235 can_retry=True,
236 headers=None,
237 **kwargs
238):
239 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
241 Args:
242 request (google.auth.transport.Request): A callable used to make
243 HTTP requests.
244 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
245 URI.
246 body (Mapping[str, str]): The parameters to send in the request body.
247 access_token (Optional(str)): The access token needed to make the request.
248 use_json (Optional(bool)): Use urlencoded format or json format for the
249 content type. The default value is False.
250 can_retry (bool): Enable or disable request retry behavior.
251 headers (Optional[Mapping[str, str]]): The headers for the request.
252 kwargs: Additional arguments passed on to the request method. The
253 kwargs will be passed to `requests.request` method, see:
254 https://docs.python-requests.org/en/latest/api/#requests.request.
255 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
256 to set up client side SSL certificate, and use
257 `verify="ca_bundle_path"` to set up the CA certificates for sever
258 side SSL certificate verification.
260 Returns:
261 Mapping[str, str]: The JSON-decoded response data.
263 Raises:
264 google.auth.exceptions.RefreshError: If the token endpoint returned
265 an error.
266 """
268 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
269 request,
270 token_uri,
271 body,
272 access_token=access_token,
273 use_json=use_json,
274 can_retry=can_retry,
275 headers=headers,
276 **kwargs
277 )
278 if not response_status_ok:
279 _handle_error_response(response_data, retryable_error)
280 return response_data
283def jwt_grant(request, token_uri, assertion, can_retry=True):
284 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
286 For more details, see `rfc7523 section 4`_.
288 Args:
289 request (google.auth.transport.Request): A callable used to make
290 HTTP requests.
291 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
292 URI.
293 assertion (str): The OAuth 2.0 assertion.
294 can_retry (bool): Enable or disable request retry behavior.
296 Returns:
297 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
298 expiration, and additional data returned by the token endpoint.
300 Raises:
301 google.auth.exceptions.RefreshError: If the token endpoint returned
302 an error.
304 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
305 """
306 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
308 response_data = _token_endpoint_request(
309 request,
310 token_uri,
311 body,
312 can_retry=can_retry,
313 headers={
314 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
315 },
316 )
318 try:
319 access_token = response_data["access_token"]
320 except KeyError as caught_exc:
321 new_exc = exceptions.RefreshError(
322 "No access token in response.", response_data, retryable=False
323 )
324 raise new_exc from caught_exc
326 expiry = _parse_expiry(response_data)
328 return access_token, expiry, response_data
331def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):
332 """Call iam.generateIdToken endpoint to get ID token.
334 Args:
335 request (google.auth.transport.Request): A callable used to make
336 HTTP requests.
337 signer_email (str): The signer email used to form the IAM
338 generateIdToken endpoint.
339 audience (str): The audience for the ID token.
340 access_token (str): The access token used to call the IAM endpoint.
342 Returns:
343 Tuple[str, datetime]: The ID token and expiration.
344 """
345 body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
347 response_data = _token_endpoint_request(
348 request,
349 _IAM_IDTOKEN_ENDPOINT.format(signer_email),
350 body,
351 access_token=access_token,
352 use_json=True,
353 )
355 try:
356 id_token = response_data["token"]
357 except KeyError as caught_exc:
358 new_exc = exceptions.RefreshError(
359 "No ID token in response.", response_data, retryable=False
360 )
361 raise new_exc from caught_exc
363 payload = jwt.decode(id_token, verify=False)
364 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
366 return id_token, expiry
369def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
370 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
371 requests an OpenID Connect ID Token instead of an access token.
373 This is a variant on the standard JWT Profile that is currently unique
374 to Google. This was added for the benefit of authenticating to services
375 that require ID Tokens instead of access tokens or JWT bearer tokens.
377 Args:
378 request (google.auth.transport.Request): A callable used to make
379 HTTP requests.
380 token_uri (str): The OAuth 2.0 authorization server's token endpoint
381 URI.
382 assertion (str): JWT token signed by a service account. The token's
383 payload must include a ``target_audience`` claim.
384 can_retry (bool): Enable or disable request retry behavior.
386 Returns:
387 Tuple[str, Optional[datetime], Mapping[str, str]]:
388 The (encoded) Open ID Connect ID Token, expiration, and additional
389 data returned by the endpoint.
391 Raises:
392 google.auth.exceptions.RefreshError: If the token endpoint returned
393 an error.
394 """
395 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
397 response_data = _token_endpoint_request(
398 request,
399 token_uri,
400 body,
401 can_retry=can_retry,
402 headers={
403 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
404 },
405 )
407 try:
408 id_token = response_data["id_token"]
409 except KeyError as caught_exc:
410 new_exc = exceptions.RefreshError(
411 "No ID token in response.", response_data, retryable=False
412 )
413 raise new_exc from caught_exc
415 payload = jwt.decode(id_token, verify=False)
416 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
418 return id_token, expiry, response_data
421def _handle_refresh_grant_response(response_data, refresh_token):
422 """Extract tokens from refresh grant response.
424 Args:
425 response_data (Mapping[str, str]): Refresh grant response data.
426 refresh_token (str): Current refresh token.
428 Returns:
429 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
430 refresh token, expiration, and additional data returned by the token
431 endpoint. If response_data doesn't have refresh token, then the current
432 refresh token will be returned.
434 Raises:
435 google.auth.exceptions.RefreshError: If the token endpoint returned
436 an error.
437 """
438 try:
439 access_token = response_data["access_token"]
440 except KeyError as caught_exc:
441 new_exc = exceptions.RefreshError(
442 "No access token in response.", response_data, retryable=False
443 )
444 raise new_exc from caught_exc
446 refresh_token = response_data.get("refresh_token", refresh_token)
447 expiry = _parse_expiry(response_data)
449 return access_token, refresh_token, expiry, response_data
452def refresh_grant(
453 request,
454 token_uri,
455 refresh_token,
456 client_id,
457 client_secret,
458 scopes=None,
459 rapt_token=None,
460 can_retry=True,
461):
462 """Implements the OAuth 2.0 refresh token grant.
464 For more details, see `rfc678 section 6`_.
466 Args:
467 request (google.auth.transport.Request): A callable used to make
468 HTTP requests.
469 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
470 URI.
471 refresh_token (str): The refresh token to use to get a new access
472 token.
473 client_id (str): The OAuth 2.0 application's client ID.
474 client_secret (str): The Oauth 2.0 appliaction's client secret.
475 scopes (Optional(Sequence[str])): Scopes to request. If present, all
476 scopes must be authorized for the refresh token. Useful if refresh
477 token has a wild card scope (e.g.
478 'https://www.googleapis.com/auth/any-api').
479 rapt_token (Optional(str)): The reauth Proof Token.
480 can_retry (bool): Enable or disable request retry behavior.
482 Returns:
483 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
484 token, new or current refresh token, expiration, and additional data
485 returned by the token endpoint.
487 Raises:
488 google.auth.exceptions.RefreshError: If the token endpoint returned
489 an error.
491 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
492 """
493 body = {
494 "grant_type": _REFRESH_GRANT_TYPE,
495 "client_id": client_id,
496 "client_secret": client_secret,
497 "refresh_token": refresh_token,
498 }
499 if scopes:
500 body["scope"] = " ".join(scopes)
501 if rapt_token:
502 body["rapt"] = rapt_token
504 response_data = _token_endpoint_request(
505 request, token_uri, body, can_retry=can_retry
506 )
507 return _handle_refresh_grant_response(response_data, refresh_token)