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