From f6ee870ab3aeb0f9c9b6f5f7f101954f04021868 Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Fri, 5 Aug 2022 18:42:50 +0000 Subject: [PATCH 01/11] chore: Refrefsh system test creds. --- system_tests/secrets.tar.enc | Bin 10324 -> 10323 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index f86cedc83e95a5349696fde3f72287e3aad5120a..f5652a5a43307c097d5572a2a6fe506ef322c1c8 100644 GIT binary patch literal 10323 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 Date: Fri, 19 Aug 2022 22:32:42 +0000 Subject: [PATCH 02/11] feat: Retry behavior * Introduce `retryable` property to auth library exceptions. This can be used to determine if an exception should be retried. * Introduce `should_retry` parameter to token endpoints. If set to `False` the auth library will not retry failed requests. If set to `True` the auth library will retry failed requests. The default value is `True` to maintain existing behavior. * Expanded list of HTTP Status codes that will be retried. * Modified retry behavior to use exponential backoff. * Increased default retry attempts from 2 to 3. --- google/auth/_exponential_backoff.py | 112 ++++++++++++ google/auth/exceptions.py | 17 +- google/auth/transport/__init__.py | 11 +- google/oauth2/_client.py | 173 +++++++++++++----- google/oauth2/_client_async.py | 105 +++++++---- google/oauth2/_reauth_async.py | 5 +- google/oauth2/reauth.py | 12 +- tests/compute_engine/test_credentials.py | 2 +- tests/oauth2/test__client.py | 200 +++++++++++++++++++-- tests/oauth2/test_reauth.py | 11 +- tests/test__exponential_backoff.py | 41 +++++ tests/test_exceptions.py | 55 ++++++ tests_async/oauth2/test__client_async.py | 214 +++++++++++++++++++++-- tests_async/oauth2/test_reauth_async.py | 16 +- 14 files changed, 860 insertions(+), 114 deletions(-) create mode 100644 google/auth/_exponential_backoff.py create mode 100644 tests/test__exponential_backoff.py create mode 100644 tests/test_exceptions.py diff --git a/google/auth/_exponential_backoff.py b/google/auth/_exponential_backoff.py new file mode 100644 index 000000000..b4451e852 --- /dev/null +++ b/google/auth/_exponential_backoff.py @@ -0,0 +1,112 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import random +import time + +import six + +# The default amount of retry total_attempts +_DEFAULT_RETRY_TOTAL_ATTEMPTS = 3 + +# The default initial backoff period (1.0 second). +_DEFAULT_INITIAL_INTERVAL_SECONDS = 1.0 + +# The default randomization factor (0.1 which results in a random period ranging +# between 10% below and 10% above the retry interval). +_DEFAULT_RANDOMIZATION_FACTOR = 0.1 + +# The default multiplier value (2 which is 100% increase per back off). +_DEFAULT_MULTIPLIER = 2.0 + +"""Exponential Backoff Utility + +This is a private module that implements the exponential back off algorithm. +It can be used as a utility for code that needs to retry on failure, for example +an HTTP request. +""" + + +class ExponentialBackoff(six.Iterator): + """An exponential backoff iterator. This can be used in a for loop to + perform requests with exponential backoff. + + Args: + total_attempts Optional[int]: + The maximum amount of retries that should happen. + The default value is 3 attempts. + initial_wait_seconds Optional[int]: + The amount of time to sleep in the first backoff. This parameter + should be in seconds. + The default value is 1 second. + randomization_factor Optional[float]: + The amount of jitter that should be in each backoff. For example, + a value of 0.1 will introduce a jitter range of 10% to the + current backoff period. + The default value is 0.1. + multiplier Optional[float]: + The backoff multipler. This adjusts how much each backoff will + increase. For example a value of 2.0 leads to a 200% backoff + on each attempt. If the initial_wait is 1.0 it would look like + this sequence [1.0, 2.0, 4.0, 8.0]. + The default value is 2.0. + """ + + def __init__( + self, + total_attempts=_DEFAULT_RETRY_TOTAL_ATTEMPTS, + initial_wait_seconds=_DEFAULT_INITIAL_INTERVAL_SECONDS, + randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR, + multiplier=_DEFAULT_MULTIPLIER, + ): + self._total_attempts = total_attempts + self._initial_wait_seconds = initial_wait_seconds + + # convert secondseconds to seconds for the time.sleep API + self._current_wait_in_seconds = self._initial_wait_seconds + + self._randomization_factor = randomization_factor + self._multiplier = multiplier + self._backoff_count = 0 + + def __iter__(self): + self._backoff_count = 0 + self._current_wait_in_seconds = self._initial_wait_seconds + return self + + def __next__(self): + if self._backoff_count >= self._total_attempts: + raise StopIteration + self._backoff_count += 1 + + jitter_variance = self._current_wait_in_seconds * self._randomization_factor + jitter = random.uniform( + self._current_wait_in_seconds - jitter_variance, + self._current_wait_in_seconds + jitter_variance, + ) + + time.sleep(jitter) + + self._current_wait_in_seconds *= self._multiplier + return self._backoff_count + + @property + def total_attempts(self): + """The total amount of backoff attempts that will be made.""" + return self._total_attempts + + @property + def backoff_count(self): + """The current amount of backoff attempts that have been made.""" + return self._backoff_count diff --git a/google/auth/exceptions.py b/google/auth/exceptions.py index e9e737780..7760c87b8 100644 --- a/google/auth/exceptions.py +++ b/google/auth/exceptions.py @@ -18,6 +18,15 @@ class GoogleAuthError(Exception): """Base class for all google.auth errors.""" + def __init__(self, *args, **kwargs): + super(GoogleAuthError, self).__init__(*args) + retryable = kwargs.get("retryable", False) + self._retryable = retryable + + @property + def retryable(self): + return self._retryable + class TransportError(GoogleAuthError): """Used to indicate an error occurred during an HTTP request.""" @@ -44,6 +53,10 @@ class MutualTLSChannelError(GoogleAuthError): class ClientCertError(GoogleAuthError): """Used to indicate that client certificate is missing or invalid.""" + @property + def retryable(self): + return False + class OAuthError(GoogleAuthError): """Used to indicate an error occurred during an OAuth related HTTP @@ -53,9 +66,9 @@ class OAuthError(GoogleAuthError): class ReauthFailError(RefreshError): """An exception for when reauth failed.""" - def __init__(self, message=None): + def __init__(self, message=None, **kwargs): super(ReauthFailError, self).__init__( - "Reauthentication failed. {0}".format(message) + "Reauthentication failed. {0}".format(message), **kwargs ) diff --git a/google/auth/transport/__init__.py b/google/auth/transport/__init__.py index 374e7b4d7..8b5048e31 100644 --- a/google/auth/transport/__init__.py +++ b/google/auth/transport/__init__.py @@ -29,9 +29,18 @@ import six from six.moves import http_client +DEFAULT_RETRYABLE_STATUS_CODES = ( + http_client.INTERNAL_SERVER_ERROR, + http_client.SERVICE_UNAVAILABLE, + http_client.REQUEST_TIMEOUT, +) +"""Sequence[int]: HTTP status codes indicating a request can be retried. +""" + + DEFAULT_REFRESH_STATUS_CODES = (http_client.UNAUTHORIZED,) """Sequence[int]: Which HTTP status code indicate that credentials should be -refreshed and a request should be retried. +refreshed. """ DEFAULT_MAX_REFRESH_ATTEMPTS = 2 diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 847c5db8a..80752a0af 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -30,9 +30,11 @@ from six.moves import http_client from six.moves import urllib +from google.auth import _exponential_backoff from google.auth import _helpers from google.auth import exceptions from google.auth import jwt +from google.auth import transport _URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" _JSON_CONTENT_TYPE = "application/json" @@ -40,17 +42,22 @@ _REFRESH_GRANT_TYPE = "refresh_token" -def _handle_error_response(response_data): +def _handle_error_response(response_data, retryable_error): """Translates an error response into an exception. Args: response_data (Mapping | str): The decoded response data. + retryable_error Optional[bool]: A boolean indicating if an error is retry able. + Defaults to False. Raises: google.auth.exceptions.RefreshError: The errors contained in response_data. """ + + retryable_error = retryable_error if retryable_error else False + if isinstance(response_data, six.string_types): - raise exceptions.RefreshError(response_data) + raise exceptions.RefreshError(response_data, retryable=retryable_error) try: error_details = "{}: {}".format( response_data["error"], response_data.get("error_description") @@ -59,7 +66,40 @@ def _handle_error_response(response_data): except (KeyError, ValueError): error_details = json.dumps(response_data) - raise exceptions.RefreshError(error_details, response_data) + raise exceptions.RefreshError( + error_details, response_data, retryable=retryable_error + ) + + +def _can_retry(status_code, response_data): + """Checks if a request can be retried by inspecting the status code + and response body of the request. + + Args: + status_code (int): The response status code. + response_data (Mapping | str): The decoded response data. + + Returns: + bool: True if the response is retryable. False otherwise. + """ + try: + error_desc = response_data.get("error_description") or "" + error_code = response_data.get("error") or "" + + # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 + # This is needed because a redirect will not return a 500 status code. + retryable_error_descriptions = {"internal_failure", "server_error"} + + if any(e in retryable_error_descriptions for e in (error_code, error_desc)): + return True + + except AttributeError: + pass + + return ( + status_code in transport.DEFAULT_REFRESH_STATUS_CODES + or status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES + ) def _parse_expiry(response_data): @@ -81,7 +121,13 @@ def _parse_expiry(response_data): def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False, **kwargs + request, + token_uri, + body, + access_token=None, + use_json=False, + should_retry=True, + **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -95,6 +141,7 @@ def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + should_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -104,8 +151,10 @@ def _token_endpoint_request_no_throw( side SSL certificate verification. Returns: - Tuple(bool, Mapping[str, str]): A boolean indicating if the request is - successful, and a mapping for the JSON-decoded response data. + Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating + if the request is successful, a mapping for the JSON-decoded response + data and in the case of an error a boolean indicating if the error + is retryable. """ if use_json: headers = {"Content-Type": _JSON_CONTENT_TYPE} @@ -117,10 +166,7 @@ def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - retry = 0 - # retry to fetch token for maximum of two times if any internal failure - # occurs. - while True: + def _perform_request(): response = request( method="POST", url=token_uri, headers=headers, body=body, **kwargs ) @@ -129,32 +175,53 @@ def _token_endpoint_request_no_throw( if hasattr(response.data, "decode") else response.data ) + response_data = "" + try: + response_data = json.loads(response_body) + except ValueError: + response_data = response_body + return response, response_data + + response, response_data = _perform_request() + + if response.status == http_client.OK: + # response_body should be a JSON + return True, response_data, None + + retryable_error = _can_retry( + status_code=response.status, response_data=response_data + ) + + if not retryable_error or not should_retry: + # For a failed response, response_body could be a string + return False, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + response, response_data = _perform_request() if response.status == http_client.OK: - # response_body should be a JSON - response_data = json.loads(response_body) - break - else: + return True, response_data, None + + retryable_error = _can_retry( + status_code=response.status, response_data=response_data + ) + + if not retryable_error: # For a failed response, response_body could be a string - try: - response_data = json.loads(response_body) - error_desc = response_data.get("error_description") or "" - error_code = response_data.get("error") or "" - if ( - any(e == "internal_failure" for e in (error_code, error_desc)) - and retry < 1 - ): - retry += 1 - continue - except ValueError: - response_data = response_body - return False, response_data - - return True, response_data + return False, response_data, retryable_error + + return False, response_data, retryable_error def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False, **kwargs + request, + token_uri, + body, + access_token=None, + use_json=False, + should_retry=True, + **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -167,6 +234,7 @@ def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + should_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -182,15 +250,22 @@ def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - response_status_ok, response_data = _token_endpoint_request_no_throw( - request, token_uri, body, access_token=access_token, use_json=use_json, **kwargs + + response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( + request, + token_uri, + body, + access_token=access_token, + use_json=use_json, + should_retry=should_retry, + **kwargs ) if not response_status_ok: - _handle_error_response(response_data) + _handle_error_response(response_data, retryable_error) return response_data -def jwt_grant(request, token_uri, assertion): +def jwt_grant(request, token_uri, assertion, should_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -201,6 +276,7 @@ def jwt_grant(request, token_uri, assertion): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -214,12 +290,16 @@ def jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=True + ) six.raise_from(new_exc, caught_exc) expiry = _parse_expiry(response_data) @@ -227,7 +307,7 @@ def jwt_grant(request, token_uri, assertion): return access_token, expiry, response_data -def id_token_jwt_grant(request, token_uri, assertion): +def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -242,6 +322,7 @@ def id_token_jwt_grant(request, token_uri, assertion): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -254,12 +335,16 @@ def id_token_jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No ID token in response.", response_data) + new_exc = exceptions.RefreshError( + "No ID token in response.", response_data, retryable=True + ) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -288,7 +373,9 @@ def _handle_refresh_grant_response(response_data, refresh_token): try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=True + ) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get("refresh_token", refresh_token) @@ -305,6 +392,7 @@ def refresh_grant( client_secret, scopes=None, rapt_token=None, + should_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -324,6 +412,7 @@ def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access @@ -347,5 +436,7 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = _token_endpoint_request(request, token_uri, body) + response_data = _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) return _handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index cf5121137..94c98d1b4 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -30,13 +30,14 @@ from six.moves import http_client from six.moves import urllib +from google.auth import _exponential_backoff from google.auth import exceptions from google.auth import jwt from google.oauth2 import _client as client async def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False + request, token_uri, body, access_token=None, use_json=False, should_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -50,10 +51,13 @@ async def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + should_retry (bool): Enable or disable request retry behavior. Returns: - Tuple(bool, Mapping[str, str]): A boolean indicating if the request is - successful, and a mapping for the JSON-decoded response data. + Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating + if the request is successful, a mapping for the JSON-decoded response + data and in the case of an error a boolean indicating if the error + is retryable. """ if use_json: headers = {"Content-Type": client._JSON_CONTENT_TYPE} @@ -65,11 +69,7 @@ async def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - retry = 0 - # retry to fetch token for maximum of two times if any internal failure - # occurs. - while True: - + async def _perform_request(): response = await request( method="POST", url=token_uri, headers=headers, body=body ) @@ -83,26 +83,46 @@ async def _token_endpoint_request_no_throw( else response_body1 ) - response_data = json.loads(response_body) + try: + response_data = json.loads(response_body) + except ValueError: + response_data = response_body + + return response, response_data + + response, response_data = await _perform_request() + + if response.status == http_client.OK: + return True, response_data, None + + retryable_error = client._can_retry( + status_code=response.status, response_data=response_data + ) + + if not retryable_error or not should_retry: + # For a failed response, response_body could be a string + return False, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + response, response_data = await _perform_request() if response.status == http_client.OK: - break - else: - error_desc = response_data.get("error_description") or "" - error_code = response_data.get("error") or "" - if ( - any(e == "internal_failure" for e in (error_code, error_desc)) - and retry < 1 - ): - retry += 1 - continue - return response.status == http_client.OK, response_data + return True, response_data, None - return response.status == http_client.OK, response_data + retryable_error = client._can_retry( + status_code=response.status, response_data=response_data + ) + + if not retryable_error: + # For a failed response, response_body could be a string + return False, response_data, retryable_error + + return False, response_data, retryable_error async def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False + request, token_uri, body, access_token=None, use_json=False, should_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -115,6 +135,7 @@ async def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. + should_retry (bool): Enable or disable request retry behavior. Returns: Mapping[str, str]: The JSON-decoded response data. @@ -123,15 +144,21 @@ async def _token_endpoint_request( google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ - response_status_ok, response_data = await _token_endpoint_request_no_throw( - request, token_uri, body, access_token=access_token, use_json=use_json + + response_status_ok, response_data, retryable_error = await _token_endpoint_request_no_throw( + request, + token_uri, + body, + access_token=access_token, + use_json=use_json, + should_retry=should_retry, ) if not response_status_ok: - client._handle_error_response(response_data) + client._handle_error_response(response_data, retryable_error) return response_data -async def jwt_grant(request, token_uri, assertion): +async def jwt_grant(request, token_uri, assertion, should_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -142,6 +169,7 @@ async def jwt_grant(request, token_uri, assertion): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -155,12 +183,16 @@ async def jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) try: access_token = response_data["access_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No access token in response.", response_data) + new_exc = exceptions.RefreshError( + "No access token in response.", response_data, retryable=True + ) six.raise_from(new_exc, caught_exc) expiry = client._parse_expiry(response_data) @@ -168,7 +200,7 @@ async def jwt_grant(request, token_uri, assertion): return access_token, expiry, response_data -async def id_token_jwt_grant(request, token_uri, assertion): +async def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -183,6 +215,7 @@ async def id_token_jwt_grant(request, token_uri, assertion): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -195,12 +228,16 @@ async def id_token_jwt_grant(request, token_uri, assertion): """ body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) try: id_token = response_data["id_token"] except KeyError as caught_exc: - new_exc = exceptions.RefreshError("No ID token in response.", response_data) + new_exc = exceptions.RefreshError( + "No ID token in response.", response_data, retryable=True + ) six.raise_from(new_exc, caught_exc) payload = jwt.decode(id_token, verify=False) @@ -217,6 +254,7 @@ async def refresh_grant( client_secret, scopes=None, rapt_token=None, + should_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -236,6 +274,7 @@ async def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. + should_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The @@ -259,5 +298,7 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_data = await _token_endpoint_request(request, token_uri, body) + response_data = await _token_endpoint_request( + request, token_uri, body, should_retry=should_retry + ) return client._handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_reauth_async.py b/google/oauth2/_reauth_async.py index 0276ddd0b..c9770f867 100644 --- a/google/oauth2/_reauth_async.py +++ b/google/oauth2/_reauth_async.py @@ -292,7 +292,7 @@ async def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data = await _client_async._token_endpoint_request_no_throw( + response_status_ok, response_data, retryable_error = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -317,12 +317,13 @@ async def refresh_grant( ( response_status_ok, response_data, + retryable_error, ) = await _client_async._token_endpoint_request_no_throw( request, token_uri, body ) if not response_status_ok: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable_error) refresh_response = _client._handle_refresh_grant_response( response_data, refresh_token ) diff --git a/google/oauth2/reauth.py b/google/oauth2/reauth.py index cbf1d7f09..afd491fd4 100644 --- a/google/oauth2/reauth.py +++ b/google/oauth2/reauth.py @@ -319,7 +319,7 @@ def refresh_grant( if rapt_token: body["rapt"] = rapt_token - response_status_ok, response_data = _client._token_endpoint_request_no_throw( + response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw( request, token_uri, body ) if ( @@ -339,12 +339,14 @@ def refresh_grant( request, client_id, client_secret, refresh_token, token_uri, scopes=scopes ) body["rapt"] = rapt_token - (response_status_ok, response_data) = _client._token_endpoint_request_no_throw( - request, token_uri, body - ) + ( + response_status_ok, + response_data, + retryable_error, + ) = _client._token_endpoint_request_no_throw(request, token_uri, body) if not response_status_ok: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable_error) return _client._handle_refresh_grant_response(response_data, refresh_token) + ( rapt_token, ) diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ff01720c4..ebce176e8 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -609,7 +609,7 @@ def test_refresh_error(self, sign, get, utcnow): request = mock.create_autospec(transport.Request, instance=True) response = mock.Mock() response.data = b'{"error": "http error"}' - response.status = 500 + response.status = 404 # Throw a 404 so the request is not retried. request.side_effect = [response] self.credentials = credentials.IDTokenCredentials( diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index bd4cc5001..c6c22d89e 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -47,12 +47,14 @@ ) -def test__handle_error_response(): +@pytest.mark.parametrize("retryable", [True, False]) +def test__handle_error_response(retryable): response_data = {"error": "help", "error_description": "I'm alive"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, retryable) + assert excinfo.value.retryable == retryable assert excinfo.match(r"help: I\'m alive") @@ -60,8 +62,9 @@ def test__handle_error_response_no_error(): response_data = {"foo": "bar"} with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, False) + assert not excinfo.value.retryable assert excinfo.match(r"{\"foo\": \"bar\"}") @@ -69,11 +72,36 @@ def test__handle_error_response_not_json(): response_data = "this is an error message" with pytest.raises(exceptions.RefreshError) as excinfo: - _client._handle_error_response(response_data) + _client._handle_error_response(response_data, False) + assert not excinfo.value.retryable assert excinfo.match(response_data) +def test__can_retry_retryable(): + retryable_codes = ( + transport.DEFAULT_RETRYABLE_STATUS_CODES + + transport.DEFAULT_REFRESH_STATUS_CODES + ) + for status_code in range(100, 600): + if status_code in retryable_codes: + assert _client._can_retry(status_code, {"error": "invalid_scope"}) + else: + assert not _client._can_retry(status_code, {"error": "invalid_scope"}) + + +@pytest.mark.parametrize( + "response_data", [{"error": "internal_failure"}, {"error": "server_error"}] +) +def test__can_retry_message(response_data): + assert _client._can_retry(http_client.OK, response_data) + + +@pytest.mark.parametrize("response_data", [{"error": "invalid_scope"}]) +def test__can_retry_no_retry_message(response_data): + assert not _client._can_retry(http_client.OK, response_data) + + @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test__parse_expiry(unused_utcnow): result = _client._parse_expiry({"expires_in": 500}) @@ -154,8 +182,8 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) - # request should be called twice due to the retry - assert request.call_count == 2 + # request should be called once and then with 3 retries + assert request.call_count == 4 request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -165,7 +193,55 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) - # request should be called twice due to the retry + # request should be called once and then with 3 retries + assert request.call_count == 4 + + +def test__token_endpoint_request_internal_failure_and_retry_failure_error(): + retryable_error = mock.create_autospec(transport.Response, instance=True) + retryable_error.status = http_client.BAD_REQUEST + retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( + "utf-8" + ) + + unretryable_error = mock.create_autospec(transport.Response, instance=True) + unretryable_error.status = http_client.BAD_REQUEST + unretryable_error.data = json.dumps({"error_description": "invalid_scope"}).encode( + "utf-8" + ) + + request = mock.create_autospec(transport.Request) + + request.side_effect = [retryable_error, retryable_error, unretryable_error] + + with pytest.raises(exceptions.RefreshError): + _client._token_endpoint_request( + request, "http://example.com", {"error_description": "invalid_scope"} + ) + # request should be called three times. Two retryable errors and one + # unretryable error to break the retry loop. + assert request.call_count == 3 + + +def test__token_endpoint_request_internal_failure_and_retry_succeeds(): + retryable_error = mock.create_autospec(transport.Response, instance=True) + retryable_error.status = http_client.BAD_REQUEST + retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( + "utf-8" + ) + + response = mock.create_autospec(transport.Response, instance=True) + response.status = http_client.OK + response.data = json.dumps({"hello": "world"}).encode("utf-8") + + request = mock.create_autospec(transport.Request) + + request.side_effect = [retryable_error, response] + + _ = _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + assert request.call_count == 2 @@ -219,8 +295,9 @@ def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.jwt_grant(request, "http://example.com", "assertion_value") + assert excinfo.value.retryable def test_id_token_jwt_grant(): @@ -255,8 +332,9 @@ def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.id_token_jwt_grant(request, "http://example.com", "assertion_value") + assert excinfo.value.retryable @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @@ -348,7 +426,109 @@ def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) + assert excinfo.value.retryable + + +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): + _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_expiry, should_retry +): + _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), should_retry=should_retry) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_id_token_jwt_grant_retry_default(mock_token_endpoint_request, mock_jwt_decode): + _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_id_token_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_jwt_decode, should_retry +): + _client.id_token_jwt_grant( + mock.Mock(), mock.Mock(), mock.Mock(), should_retry=should_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_expiry): + _client.refresh_grant( + mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +def test_refresh_grant_retry_with_retry( + mock_token_endpoint_request, mock_parse_expiry, should_retry +): + _client.refresh_grant( + mock.Mock(), + mock.Mock(), + mock.Mock(), + mock.Mock(), + mock.Mock(), + should_retry=should_retry, + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@pytest.mark.parametrize("should_retry", [True, False]) +def test__token_endpoint_request_no_throw_with_retry(should_retry): + response_data = {"error": "help", "error_description": "I'm alive"} + body = "dummy body" + + mock_response = mock.create_autospec(transport.Response, instance=True) + mock_response.status = http_client.INTERNAL_SERVER_ERROR + mock_response.data = json.dumps(response_data).encode("utf-8") + + mock_request = mock.create_autospec(transport.Request) + mock_request.return_value = mock_response + + _client._token_endpoint_request_no_throw( + mock_request, + mock.Mock(), + body, + mock.Mock(), + mock.Mock(), + should_retry=should_retry, + ) + + if should_retry: + assert mock_request.call_count == 4 + else: + assert mock_request.call_count == 1 diff --git a/tests/oauth2/test_reauth.py b/tests/oauth2/test_reauth.py index ae64be009..df0636b18 100644 --- a/tests/oauth2/test_reauth.py +++ b/tests/oauth2/test_reauth.py @@ -260,7 +260,7 @@ def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}) + mock_token_request.return_value = (False, {"error": "Bad request"}, False) with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, @@ -273,6 +273,7 @@ def test_refresh_grant_failed(): enable_reauth_refresh=True, ) assert excinfo.match(r"Bad request") + assert not excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -292,8 +293,8 @@ def test_refresh_grant_success(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with mock.patch( "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token" @@ -319,8 +320,8 @@ def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( diff --git a/tests/test__exponential_backoff.py b/tests/test__exponential_backoff.py new file mode 100644 index 000000000..06a54527e --- /dev/null +++ b/tests/test__exponential_backoff.py @@ -0,0 +1,41 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from google.auth import _exponential_backoff + + +@mock.patch("time.sleep", return_value=None) +def test_exponential_backoff(mock_time): + eb = _exponential_backoff.ExponentialBackoff() + curr_wait = eb._current_wait_in_seconds + iteration_count = 0 + + for attempt in eb: + backoff_interval = mock_time.call_args[0][0] + jitter = curr_wait * eb._randomization_factor + + assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) + assert attempt == iteration_count + 1 + assert eb.backoff_count == iteration_count + 1 + assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1) + + curr_wait = eb._current_wait_in_seconds + iteration_count += 1 + + assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 000000000..0991d4275 --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,55 @@ +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest # type: ignore + +from google.auth import exceptions # type:ignore + + +@pytest.fixture( + params=[ + exceptions.GoogleAuthError, + exceptions.TransportError, + exceptions.RefreshError, + exceptions.UserAccessTokenError, + exceptions.DefaultCredentialsError, + exceptions.MutualTLSChannelError, + exceptions.OAuthError, + exceptions.ReauthFailError, + exceptions.ReauthSamlChallengeFailError, + ] +) +def retryable_exception(request): + return request.param + + +@pytest.fixture(params=[exceptions.ClientCertError]) +def non_retryable_exception(request): + return request.param + + +def test_default_retryable_exceptions(retryable_exception): + assert not retryable_exception().retryable + + +@pytest.mark.parametrize("retryable", [True, False]) +def test_retryable_exceptions(retryable_exception, retryable): + retryable_exception = retryable_exception(retryable=retryable) + assert retryable_exception.retryable == retryable + + +@pytest.mark.parametrize("retryable", [True, False]) +def test_non_retryable_exceptions(non_retryable_exception, retryable): + non_retryable_exception = non_retryable_exception(retryable=retryable) + assert not non_retryable_exception.retryable diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py index 91874cdd4..af52ff392 100644 --- a/tests_async/oauth2/test__client_async.py +++ b/tests_async/oauth2/test__client_async.py @@ -29,10 +29,10 @@ from tests.oauth2 import test__client as test_client -def make_request(response_data, status=http_client.OK): +def make_request(response_data, status=http_client.OK, text=False): response = mock.AsyncMock(spec=["transport.Response"]) response.status = status - data = json.dumps(response_data).encode("utf-8") + data = response_data if text else json.dumps(response_data).encode("utf-8") response.data = mock.AsyncMock(spec=["__call__", "read"]) response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) response.content = mock.AsyncMock(spec=["__call__"], return_value=data) @@ -62,6 +62,27 @@ async def test__token_endpoint_request(): assert result == {"test": "response"} +@pytest.mark.asyncio +async def test__token_endpoint_request_text(): + + request = make_request("response", text=True) + + result = await _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + + # Check request call + request.assert_called_with( + method="POST", + url="http://example.com", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + body="test=params".encode("utf-8"), + ) + + # Check result + assert result == "response" + + @pytest.mark.asyncio async def test__token_endpoint_request_json(): @@ -95,8 +116,9 @@ async def test__token_endpoint_request_json(): async def test__token_endpoint_request_error(): request = make_request({}, status=http_client.BAD_REQUEST) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client._token_endpoint_request(request, "http://example.com", {}) + assert not excinfo.value.retryable @pytest.mark.asyncio @@ -105,10 +127,11 @@ async def test__token_endpoint_request_internal_failure_error(): {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) + assert excinfo.value.retryable request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -118,6 +141,61 @@ async def test__token_endpoint_request_internal_failure_error(): await _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) + assert excinfo.value.retryable + + +@pytest.mark.asyncio +async def test__token_endpoint_request_internal_failure_and_retry_failure_error(): + retryable_error = mock.AsyncMock(spec=["transport.Response"]) + retryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") + retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + unretryable_error = mock.AsyncMock(spec=["transport.Response"]) + unretryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "invalid_scope"}).encode("utf-8") + unretryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + unretryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + unretryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + request = mock.AsyncMock(spec=["transport.Request"]) + request.side_effect = [retryable_error, retryable_error, unretryable_error] + + with pytest.raises(exceptions.RefreshError): + await _client._token_endpoint_request( + request, "http://example.com", {"error_description": "invalid_scope"} + ) + # request should be called three times. Two retryable errors and one + # unretryable error to break the retry loop. + assert request.call_count == 3 + + +@pytest.mark.asyncio +async def test__token_endpoint_request_internal_failure_and_retry_succeeds(): + retryable_error = mock.AsyncMock(spec=["transport.Response"]) + retryable_error.status = http_client.BAD_REQUEST + data = json.dumps({"error_description": "internal_failure"}).encode("utf-8") + retryable_error.data = mock.AsyncMock(spec=["__call__", "read"]) + retryable_error.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + retryable_error.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + response = mock.AsyncMock(spec=["transport.Response"]) + response.status = http_client.OK + data = json.dumps({"hello": "world"}).encode("utf-8") + response.data = mock.AsyncMock(spec=["__call__", "read"]) + response.data.read = mock.AsyncMock(spec=["__call__"], return_value=data) + response.content = mock.AsyncMock(spec=["__call__"], return_value=data) + + request = mock.AsyncMock(spec=["transport.Request"]) + request.side_effect = [retryable_error, response] + + _ = await _client._token_endpoint_request( + request, "http://example.com", {"test": "params"} + ) + + assert request.call_count == 2 def verify_request_params(request, params): @@ -128,8 +206,8 @@ def verify_request_params(request, params): assert request_params[key][0] == value -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_jwt_grant(utcnow): request = make_request( {"access_token": "token", "expires_in": 500, "extra": "data"} @@ -161,8 +239,9 @@ async def test_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.jwt_grant(request, "http://example.com", "assertion_value") + assert excinfo.value.retryable @pytest.mark.asyncio @@ -200,14 +279,15 @@ async def test_id_token_jwt_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.id_token_jwt_grant( request, "http://example.com", "assertion_value" ) + assert excinfo.value.retryable -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_refresh_grant(unused_utcnow): request = make_request( { @@ -246,8 +326,8 @@ async def test_refresh_grant(unused_utcnow): assert extra_data["extra"] == "data" -@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) @pytest.mark.asyncio +@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) async def test_refresh_grant_with_scopes(unused_utcnow): request = make_request( { @@ -298,7 +378,121 @@ async def test_refresh_grant_no_access_token(): } ) - with pytest.raises(exceptions.RefreshError): + with pytest.raises(exceptions.RefreshError) as excinfo: await _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) + assert excinfo.value.retryable + + +@pytest.mark.asyncio +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): + _ = await _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_expiry, should_retry +): + _ = await _client.jwt_grant( + mock.AsyncMock(), mock.Mock(), mock.Mock(), should_retry=should_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@pytest.mark.asyncio +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_id_token_jwt_grant_retry_default( + mock_token_endpoint_request, mock_jwt_decode +): + _ = await _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_id_token_jwt_grant_retry_with_retry( + mock_token_endpoint_request, mock_jwt_decode, should_retry +): + _ = await _client.id_token_jwt_grant( + mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), should_retry=should_retry + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@pytest.mark.asyncio +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_refresh_grant_retry_default( + mock_token_endpoint_request, mock_parse_expiry +): + _ = await _client.refresh_grant( + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=True + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("should_retry", [True, False]) +@mock.patch("google.oauth2._client._parse_expiry", return_value=None) +@mock.patch.object(_client, "_token_endpoint_request", autospec=True) +async def test_refresh_grant_retry_with_retry( + mock_token_endpoint_request, mock_parse_expiry, should_retry +): + _ = await _client.refresh_grant( + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + mock.AsyncMock(), + should_retry=should_retry, + ) + mock_token_endpoint_request.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("should_retry", [True, False]) +async def test__token_endpoint_request_no_throw_with_retry(should_retry): + mock_request = make_request( + {"error": "help", "error_description": "I'm alive"}, + http_client.INTERNAL_SERVER_ERROR, + ) + + _ = await _client._token_endpoint_request_no_throw( + mock_request, + mock.AsyncMock(), + "body", + mock.AsyncMock(), + mock.AsyncMock(), + should_retry=should_retry, + ) + + if should_retry: + assert mock_request.call_count == 4 + else: + assert mock_request.call_count == 1 diff --git a/tests_async/oauth2/test_reauth_async.py b/tests_async/oauth2/test_reauth_async.py index 8f51bd3a7..40ca92717 100644 --- a/tests_async/oauth2/test_reauth_async.py +++ b/tests_async/oauth2/test_reauth_async.py @@ -279,7 +279,7 @@ async def test_refresh_grant_failed(): with mock.patch( "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: - mock_token_request.return_value = (False, {"error": "Bad request"}) + mock_token_request.return_value = (False, {"error": "Bad request"}, True) with pytest.raises(exceptions.RefreshError) as excinfo: await _reauth_async.refresh_grant( MOCK_REQUEST, @@ -291,6 +291,7 @@ async def test_refresh_grant_failed(): rapt_token="rapt_token", ) assert excinfo.match(r"Bad request") + assert excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", @@ -311,8 +312,8 @@ async def test_refresh_grant_success(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), + (True, {"access_token": "access_token"}, None), ] with mock.patch( "google.oauth2._reauth_async.get_rapt_token", return_value="new_rapt_token" @@ -339,11 +340,16 @@ async def test_refresh_grant_reauth_refresh_disabled(): "google.oauth2._client_async._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ - (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}), - (True, {"access_token": "access_token"}), + ( + False, + {"error": "invalid_grant", "error_subtype": "rapt_required"}, + False, + ), + (True, {"access_token": "access_token"}, None), ] with pytest.raises(exceptions.RefreshError) as excinfo: assert await _reauth_async.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" ) assert excinfo.match(r"Reauthentication is needed") + assert not excinfo.value.retryable From 92f6c21b08a4593d112320e4a3b3474aea2e304c Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Sat, 27 Aug 2022 00:23:05 +0000 Subject: [PATCH 03/11] Update license for tests/test_exceptions.py --- google/auth/_exponential_backoff.py | 3 +-- google/oauth2/_client.py | 5 ++--- google/oauth2/_client_async.py | 2 -- tests/test_exceptions.py | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/google/auth/_exponential_backoff.py b/google/auth/_exponential_backoff.py index b4451e852..b5801bec9 100644 --- a/google/auth/_exponential_backoff.py +++ b/google/auth/_exponential_backoff.py @@ -17,7 +17,7 @@ import six -# The default amount of retry total_attempts +# The default amount of retry attempts _DEFAULT_RETRY_TOTAL_ATTEMPTS = 3 # The default initial backoff period (1.0 second). @@ -73,7 +73,6 @@ def __init__( self._total_attempts = total_attempts self._initial_wait_seconds = initial_wait_seconds - # convert secondseconds to seconds for the time.sleep API self._current_wait_in_seconds = self._initial_wait_seconds self._randomization_factor = randomization_factor diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 80752a0af..9192299a0 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -83,6 +83,7 @@ def _can_retry(status_code, response_data): bool: True if the response is retryable. False otherwise. """ try: + # For a failed response, response_body could be a string error_desc = response_data.get("error_description") or "" error_code = response_data.get("error") or "" @@ -177,6 +178,7 @@ def _perform_request(): ) response_data = "" try: + # response_body should be a JSON response_data = json.loads(response_body) except ValueError: response_data = response_body @@ -185,7 +187,6 @@ def _perform_request(): response, response_data = _perform_request() if response.status == http_client.OK: - # response_body should be a JSON return True, response_data, None retryable_error = _can_retry( @@ -193,7 +194,6 @@ def _perform_request(): ) if not retryable_error or not should_retry: - # For a failed response, response_body could be a string return False, response_data, retryable_error retries = _exponential_backoff.ExponentialBackoff() @@ -208,7 +208,6 @@ def _perform_request(): ) if not retryable_error: - # For a failed response, response_body could be a string return False, response_data, retryable_error return False, response_data, retryable_error diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index 94c98d1b4..979666024 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -100,7 +100,6 @@ async def _perform_request(): ) if not retryable_error or not should_retry: - # For a failed response, response_body could be a string return False, response_data, retryable_error retries = _exponential_backoff.ExponentialBackoff() @@ -115,7 +114,6 @@ async def _perform_request(): ) if not retryable_error: - # For a failed response, response_body could be a string return False, response_data, retryable_error return False, response_data, retryable_error diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 0991d4275..6f542498f 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,4 @@ -# Copyright 2014 Google Inc. +# Copyright 2022 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From b2ebcbcfc6a1acfa3510598d69ccf26244fb8487 Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Thu, 1 Sep 2022 14:18:35 -0700 Subject: [PATCH 04/11] s/should_retry/can_retry/ --- google/oauth2/_client.py | 30 ++++++++--------- google/oauth2/_client_async.py | 30 ++++++++--------- tests/oauth2/test__client.py | 43 +++++++++++------------- tests_async/oauth2/test__client_async.py | 38 ++++++++++----------- 4 files changed, 68 insertions(+), 73 deletions(-) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 9192299a0..c79ea35da 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -127,7 +127,7 @@ def _token_endpoint_request_no_throw( body, access_token=None, use_json=False, - should_retry=True, + can_retry=True, **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -142,7 +142,7 @@ def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -193,7 +193,7 @@ def _perform_request(): status_code=response.status, response_data=response_data ) - if not retryable_error or not should_retry: + if not retryable_error or not can_retry: return False, response_data, retryable_error retries = _exponential_backoff.ExponentialBackoff() @@ -219,7 +219,7 @@ def _token_endpoint_request( body, access_token=None, use_json=False, - should_retry=True, + can_retry=True, **kwargs ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -233,7 +233,7 @@ def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. kwargs: Additional arguments passed on to the request method. The kwargs will be passed to `requests.request` method, see: https://docs.python-requests.org/en/latest/api/#requests.request. @@ -256,7 +256,7 @@ def _token_endpoint_request( body, access_token=access_token, use_json=use_json, - should_retry=should_retry, + can_retry=can_retry, **kwargs ) if not response_status_ok: @@ -264,7 +264,7 @@ def _token_endpoint_request( return response_data -def jwt_grant(request, token_uri, assertion, should_retry=True): +def jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -275,7 +275,7 @@ def jwt_grant(request, token_uri, assertion, should_retry=True): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -290,7 +290,7 @@ def jwt_grant(request, token_uri, assertion, should_retry=True): body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} response_data = _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) try: @@ -306,7 +306,7 @@ def jwt_grant(request, token_uri, assertion, should_retry=True): return access_token, expiry, response_data -def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): +def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -321,7 +321,7 @@ def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -335,7 +335,7 @@ def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} response_data = _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) try: @@ -391,7 +391,7 @@ def refresh_grant( client_secret, scopes=None, rapt_token=None, - should_retry=True, + can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -411,7 +411,7 @@ def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access @@ -436,6 +436,6 @@ def refresh_grant( body["rapt"] = rapt_token response_data = _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) return _handle_refresh_grant_response(response_data, refresh_token) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index 979666024..8f2fa32c9 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -37,7 +37,7 @@ async def _token_endpoint_request_no_throw( - request, token_uri, body, access_token=None, use_json=False, should_retry=True + request, token_uri, body, access_token=None, use_json=False, can_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. This function doesn't throw on response errors. @@ -51,7 +51,7 @@ async def _token_endpoint_request_no_throw( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating @@ -99,7 +99,7 @@ async def _perform_request(): status_code=response.status, response_data=response_data ) - if not retryable_error or not should_retry: + if not retryable_error or not can_retry: return False, response_data, retryable_error retries = _exponential_backoff.ExponentialBackoff() @@ -120,7 +120,7 @@ async def _perform_request(): async def _token_endpoint_request( - request, token_uri, body, access_token=None, use_json=False, should_retry=True + request, token_uri, body, access_token=None, use_json=False, can_retry=True ): """Makes a request to the OAuth 2.0 authorization server's token endpoint. @@ -133,7 +133,7 @@ async def _token_endpoint_request( access_token (Optional(str)): The access token needed to make the request. use_json (Optional(bool)): Use urlencoded format or json format for the content type. The default value is False. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Mapping[str, str]: The JSON-decoded response data. @@ -149,14 +149,14 @@ async def _token_endpoint_request( body, access_token=access_token, use_json=use_json, - should_retry=should_retry, + can_retry=can_retry, ) if not response_status_ok: client._handle_error_response(response_data, retryable_error) return response_data -async def jwt_grant(request, token_uri, assertion, should_retry=True): +async def jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. @@ -167,7 +167,7 @@ async def jwt_grant(request, token_uri, assertion, should_retry=True): token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, @@ -182,7 +182,7 @@ async def jwt_grant(request, token_uri, assertion, should_retry=True): body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} response_data = await _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) try: @@ -198,7 +198,7 @@ async def jwt_grant(request, token_uri, assertion, should_retry=True): return access_token, expiry, response_data -async def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): +async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but requests an OpenID Connect ID Token instead of an access token. @@ -213,7 +213,7 @@ async def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): URI. assertion (str): JWT token signed by a service account. The token's payload must include a ``target_audience`` claim. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: @@ -227,7 +227,7 @@ async def id_token_jwt_grant(request, token_uri, assertion, should_retry=True): body = {"assertion": assertion, "grant_type": client._JWT_GRANT_TYPE} response_data = await _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) try: @@ -252,7 +252,7 @@ async def refresh_grant( client_secret, scopes=None, rapt_token=None, - should_retry=True, + can_retry=True, ): """Implements the OAuth 2.0 refresh token grant. @@ -272,7 +272,7 @@ async def refresh_grant( token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). rapt_token (Optional(str)): The reauth Proof Token. - should_retry (bool): Enable or disable request retry behavior. + can_retry (bool): Enable or disable request retry behavior. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The @@ -297,6 +297,6 @@ async def refresh_grant( body["rapt"] = rapt_token response_data = await _token_endpoint_request( - request, token_uri, body, should_retry=should_retry + request, token_uri, body, can_retry=can_retry ) return client._handle_refresh_grant_response(response_data, refresh_token) diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index c6c22d89e..23fc49d40 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -438,19 +438,19 @@ def test_refresh_grant_no_access_token(): def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_expiry, should_retry + mock_token_endpoint_request, mock_expiry, can_retry ): - _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), should_retry=should_retry) + _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @@ -459,21 +459,21 @@ def test_jwt_grant_retry_with_retry( def test_id_token_jwt_grant_retry_default(mock_token_endpoint_request, mock_jwt_decode): _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_id_token_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_jwt_decode, should_retry + mock_token_endpoint_request, mock_jwt_decode, can_retry ): _client.id_token_jwt_grant( - mock.Mock(), mock.Mock(), mock.Mock(), should_retry=should_retry + mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @@ -484,15 +484,15 @@ def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_exp mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_refresh_grant_retry_with_retry( - mock_token_endpoint_request, mock_parse_expiry, should_retry + mock_token_endpoint_request, mock_parse_expiry, can_retry ): _client.refresh_grant( mock.Mock(), @@ -500,15 +500,15 @@ def test_refresh_grant_retry_with_retry( mock.Mock(), mock.Mock(), mock.Mock(), - should_retry=should_retry, + can_retry=can_retry, ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) -@pytest.mark.parametrize("should_retry", [True, False]) -def test__token_endpoint_request_no_throw_with_retry(should_retry): +@pytest.mark.parametrize("can_retry", [True, False]) +def test__token_endpoint_request_no_throw_with_retry(can_retry): response_data = {"error": "help", "error_description": "I'm alive"} body = "dummy body" @@ -520,15 +520,10 @@ def test__token_endpoint_request_no_throw_with_retry(should_retry): mock_request.return_value = mock_response _client._token_endpoint_request_no_throw( - mock_request, - mock.Mock(), - body, - mock.Mock(), - mock.Mock(), - should_retry=should_retry, + mock_request, mock.Mock(), body, mock.Mock(), mock.Mock(), can_retry=can_retry ) - if should_retry: + if can_retry: assert mock_request.call_count == 4 else: assert mock_request.call_count == 1 diff --git a/tests_async/oauth2/test__client_async.py b/tests_async/oauth2/test__client_async.py index af52ff392..9a501bf3e 100644 --- a/tests_async/oauth2/test__client_async.py +++ b/tests_async/oauth2/test__client_async.py @@ -391,22 +391,22 @@ async def test_refresh_grant_no_access_token(): async def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry): _ = await _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) @pytest.mark.asyncio -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) async def test_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_expiry, should_retry + mock_token_endpoint_request, mock_expiry, can_retry ): _ = await _client.jwt_grant( - mock.AsyncMock(), mock.Mock(), mock.Mock(), should_retry=should_retry + mock.AsyncMock(), mock.Mock(), mock.Mock(), can_retry=can_retry ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @@ -418,22 +418,22 @@ async def test_id_token_jwt_grant_retry_default( ): _ = await _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) @pytest.mark.asyncio -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) async def test_id_token_jwt_grant_retry_with_retry( - mock_token_endpoint_request, mock_jwt_decode, should_retry + mock_token_endpoint_request, mock_jwt_decode, can_retry ): _ = await _client.id_token_jwt_grant( - mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), should_retry=should_retry + mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), can_retry=can_retry ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @@ -451,16 +451,16 @@ async def test_refresh_grant_retry_default( mock.AsyncMock(), ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=True + mock.ANY, mock.ANY, mock.ANY, can_retry=True ) @pytest.mark.asyncio -@pytest.mark.parametrize("should_retry", [True, False]) +@pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) async def test_refresh_grant_retry_with_retry( - mock_token_endpoint_request, mock_parse_expiry, should_retry + mock_token_endpoint_request, mock_parse_expiry, can_retry ): _ = await _client.refresh_grant( mock.AsyncMock(), @@ -468,16 +468,16 @@ async def test_refresh_grant_retry_with_retry( mock.AsyncMock(), mock.AsyncMock(), mock.AsyncMock(), - should_retry=should_retry, + can_retry=can_retry, ) mock_token_endpoint_request.assert_called_with( - mock.ANY, mock.ANY, mock.ANY, should_retry=should_retry + mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @pytest.mark.asyncio -@pytest.mark.parametrize("should_retry", [True, False]) -async def test__token_endpoint_request_no_throw_with_retry(should_retry): +@pytest.mark.parametrize("can_retry", [True, False]) +async def test__token_endpoint_request_no_throw_with_retry(can_retry): mock_request = make_request( {"error": "help", "error_description": "I'm alive"}, http_client.INTERNAL_SERVER_ERROR, @@ -489,10 +489,10 @@ async def test__token_endpoint_request_no_throw_with_retry(should_retry): "body", mock.AsyncMock(), mock.AsyncMock(), - should_retry=should_retry, + can_retry=can_retry, ) - if should_retry: + if can_retry: assert mock_request.call_count == 4 else: assert mock_request.call_count == 1 From 7863cc6dd266b82495c3bc34f4accf3e700d25c2 Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Tue, 6 Sep 2022 13:45:48 -0700 Subject: [PATCH 05/11] PR feedback. Add temporarily_unavailable as a retryable status_code. --- google/oauth2/_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index c79ea35da..45fb50535 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -89,7 +89,7 @@ def _can_retry(status_code, response_data): # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 # This is needed because a redirect will not return a 500 status code. - retryable_error_descriptions = {"internal_failure", "server_error"} + retryable_error_descriptions = {"internal_failure", "server_error", "temporarily_unavailable"} if any(e in retryable_error_descriptions for e in (error_code, error_desc)): return True From f04a1bbe0f9604a465be229eca7dc36b7077bf03 Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Wed, 7 Sep 2022 10:29:37 -0700 Subject: [PATCH 06/11] PR Feedback * Fixed typo * Removed Retry on `DEFAULT_REFRESH_STATUS_CODES` in the OAUTH2.0 code * Add TOO_MANY_REQUESTS status code --- google/auth/transport/__init__.py | 3 +++ google/oauth2/_client.py | 13 +++++++------ system_tests/secrets.tar.enc | Bin 10323 -> 10324 bytes tests/oauth2/test__client.py | 5 +---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/google/auth/transport/__init__.py b/google/auth/transport/__init__.py index 8b5048e31..8334145a1 100644 --- a/google/auth/transport/__init__.py +++ b/google/auth/transport/__init__.py @@ -29,10 +29,13 @@ import six from six.moves import http_client +TOO_MANY_REQUESTS = 429 # Python 2.7 six is missing this status code. + DEFAULT_RETRYABLE_STATUS_CODES = ( http_client.INTERNAL_SERVER_ERROR, http_client.SERVICE_UNAVAILABLE, http_client.REQUEST_TIMEOUT, + TOO_MANY_REQUESTS, ) """Sequence[int]: HTTP status codes indicating a request can be retried. """ diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 45fb50535..42fcb0706 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -47,7 +47,7 @@ def _handle_error_response(response_data, retryable_error): Args: response_data (Mapping | str): The decoded response data. - retryable_error Optional[bool]: A boolean indicating if an error is retry able. + retryable_error Optional[bool]: A boolean indicating if an error is retryable. Defaults to False. Raises: @@ -89,7 +89,11 @@ def _can_retry(status_code, response_data): # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 # This is needed because a redirect will not return a 500 status code. - retryable_error_descriptions = {"internal_failure", "server_error", "temporarily_unavailable"} + retryable_error_descriptions = { + "internal_failure", + "server_error", + "temporarily_unavailable", + } if any(e in retryable_error_descriptions for e in (error_code, error_desc)): return True @@ -97,10 +101,7 @@ def _can_retry(status_code, response_data): except AttributeError: pass - return ( - status_code in transport.DEFAULT_REFRESH_STATUS_CODES - or status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES - ) + return status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES def _parse_expiry(response_data): diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index f5652a5a43307c097d5572a2a6fe506ef322c1c8..f9b59bf632d5f8c23224628129c5d02f8e681299 100644 GIT binary patch literal 10324 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 mu)mVaKYw}!5l@?UI^|uY(@Ynpq@!GF_2t3LF literal 10323 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 lJh>B>L8vNy;PZbrz=mwr#8V Date: Tue, 13 Sep 2022 12:49:49 -0700 Subject: [PATCH 07/11] chore: Refresh system test creds. --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index f9b59bf632d5f8c23224628129c5d02f8e681299..8681305c41e39958409ba740ef074a398ac21722 100644 GIT binary patch literal 10324 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 literal 10324 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 mu)mVaKYw}!5l@?UI^|uY(@Ynpq@!GF_2t3LF From db8f1ead27ec06c68b352055cc42e41510c1e3bb Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Tue, 13 Sep 2022 12:51:24 -0700 Subject: [PATCH 08/11] PR feedback. --- google/oauth2/_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index 42fcb0706..f07f49e62 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -82,6 +82,9 @@ def _can_retry(status_code, response_data): Returns: bool: True if the response is retryable. False otherwise. """ + if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: + return True + try: # For a failed response, response_body could be a string error_desc = response_data.get("error_description") or "" @@ -101,7 +104,7 @@ def _can_retry(status_code, response_data): except AttributeError: pass - return status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES + return False def _parse_expiry(response_data): From 60904a8eea7322a654dae37855a9d5fa0e9ab2e2 Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Wed, 14 Sep 2022 12:49:25 -0700 Subject: [PATCH 09/11] PR feedback. --- google/oauth2/_client.py | 33 +++++++++++++-------------------- google/oauth2/_client_async.py | 32 ++++++++++++-------------------- system_tests/secrets.tar.enc | Bin 10324 -> 10323 bytes tests/oauth2/test__client.py | 2 +- 4 files changed, 26 insertions(+), 41 deletions(-) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index f07f49e62..c6b1d26d7 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -186,23 +186,6 @@ def _perform_request(): response_data = json.loads(response_body) except ValueError: response_data = response_body - return response, response_data - - response, response_data = _perform_request() - - if response.status == http_client.OK: - return True, response_data, None - - retryable_error = _can_retry( - status_code=response.status, response_data=response_data - ) - - if not retryable_error or not can_retry: - return False, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - response, response_data = _perform_request() if response.status == http_client.OK: return True, response_data, None @@ -211,8 +194,18 @@ def _perform_request(): status_code=response.status, response_data=response_data ) - if not retryable_error: - return False, response_data, retryable_error + return False, response_data, retryable_error + + request_succeeded, response_data, retryable_error = _perform_request() + + if request_succeeded or not retryable_error or not can_retry: + return request_succeeded, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + request_succeeded, response_data, retryable_error = _perform_request() + if request_succeeded or not retryable_error: + return request_succeeded, response_data, retryable_error return False, response_data, retryable_error @@ -301,7 +294,7 @@ def jwt_grant(request, token_uri, assertion, can_retry=True): access_token = response_data["access_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=True + "No access token in response.", response_data, retryable=False ) six.raise_from(new_exc, caught_exc) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index 8f2fa32c9..e113f6e9e 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -88,24 +88,6 @@ async def _perform_request(): except ValueError: response_data = response_body - return response, response_data - - response, response_data = await _perform_request() - - if response.status == http_client.OK: - return True, response_data, None - - retryable_error = client._can_retry( - status_code=response.status, response_data=response_data - ) - - if not retryable_error or not can_retry: - return False, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - response, response_data = await _perform_request() - if response.status == http_client.OK: return True, response_data, None @@ -113,8 +95,18 @@ async def _perform_request(): status_code=response.status, response_data=response_data ) - if not retryable_error: - return False, response_data, retryable_error + return False, response_data, retryable_error + + request_succeeded, response_data, retryable_error = await _perform_request() + + if request_succeeded or not retryable_error or not can_retry: + return request_succeeded, response_data, retryable_error + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: + request_succeeded, response_data, retryable_error = await _perform_request() + if request_succeeded or not retryable_error: + return request_succeeded, response_data, retryable_error return False, response_data, retryable_error diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 8681305c41e39958409ba740ef074a398ac21722..f5652a5a43307c097d5572a2a6fe506ef322c1c8 100644 GIT binary patch literal 10323 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 diff --git a/tests/oauth2/test__client.py b/tests/oauth2/test__client.py index 3228ce25a..863022d0c 100644 --- a/tests/oauth2/test__client.py +++ b/tests/oauth2/test__client.py @@ -294,7 +294,7 @@ def test_jwt_grant_no_access_token(): with pytest.raises(exceptions.RefreshError) as excinfo: _client.jwt_grant(request, "http://example.com", "assertion_value") - assert excinfo.value.retryable + assert not excinfo.value.retryable def test_id_token_jwt_grant(): From 2cb8d68a12db198e773d7ab3a343c66afea2409a Mon Sep 17 00:00:00 2001 From: Carl Lundin Date: Wed, 14 Sep 2022 15:17:55 -0700 Subject: [PATCH 10/11] chore: Refresh system test creds. --- system_tests/secrets.tar.enc | Bin 10323 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index f5652a5a43307c097d5572a2a6fe506ef322c1c8..5cc05d1824663aacce794f338c0a2f0ec733755b 100644 GIT binary patch literal 10324 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 lJh>B>L8vNy;PZbrz=mwr#8V Date: Thu, 22 Sep 2022 14:20:16 -0700 Subject: [PATCH 11/11] PR feedback. --- google/oauth2/_client.py | 4 ++-- google/oauth2/_client_async.py | 4 ++-- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes tests/oauth2/test__client.py | 4 ++-- tests_async/oauth2/test__client_async.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/google/oauth2/_client.py b/google/oauth2/_client.py index c6b1d26d7..7f866d446 100644 --- a/google/oauth2/_client.py +++ b/google/oauth2/_client.py @@ -339,7 +339,7 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): id_token = response_data["id_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError( - "No ID token in response.", response_data, retryable=True + "No ID token in response.", response_data, retryable=False ) six.raise_from(new_exc, caught_exc) @@ -370,7 +370,7 @@ def _handle_refresh_grant_response(response_data, refresh_token): access_token = response_data["access_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=True + "No access token in response.", response_data, retryable=False ) six.raise_from(new_exc, caught_exc) diff --git a/google/oauth2/_client_async.py b/google/oauth2/_client_async.py index e113f6e9e..428084a70 100644 --- a/google/oauth2/_client_async.py +++ b/google/oauth2/_client_async.py @@ -181,7 +181,7 @@ async def jwt_grant(request, token_uri, assertion, can_retry=True): access_token = response_data["access_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError( - "No access token in response.", response_data, retryable=True + "No access token in response.", response_data, retryable=False ) six.raise_from(new_exc, caught_exc) @@ -226,7 +226,7 @@ async def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): id_token = response_data["id_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError( - "No ID token in response.", response_data, retryable=True + "No ID token in response.", response_data, retryable=False ) six.raise_from(new_exc, caught_exc) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 5cc05d1824663aacce794f338c0a2f0ec733755b..735379ef1d88cf0e42dea06bd5ff36e79d99c642 100644 GIT binary patch literal 10324 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 mMYmMtXMF_=EJ