def _Refresh(self, request): if (self._refresh_token is None or self._token_uri is None or self._client_id is None or self._client_secret is None): raise google_auth_exceptions.RefreshError( 'The credentials do not contain the necessary fields need to ' 'refresh the access token. You must specify refresh_token, ' 'token_uri, client_id, and client_secret.') rapt_token = getattr(self, '_rapt_token', None) access_token, refresh_token, expiry, grant_response = _RefreshGrant( request, self._token_uri, self._refresh_token, self._client_id, self._client_secret, self._scopes, rapt_token) self.token = access_token self.expiry = expiry self._refresh_token = refresh_token self._id_token = grant_response.get('id_token') # id_token in oauth2client creds is decoded and it uses id_tokenb64 to # store the encoded copy. id_token in google-auth creds is encoded. # Here, we add id_tokenb64 to google-auth creds for consistency. self.id_tokenb64 = grant_response.get('id_token') if self._scopes and 'scope' in grant_response: requested_scopes = frozenset(self._scopes) granted_scopes = frozenset(grant_response['scope'].split()) scopes_requested_but_not_granted = requested_scopes - granted_scopes if scopes_requested_but_not_granted: raise google_auth_exceptions.RefreshError( 'Not all requested scopes were granted by the ' 'authorization server, missing scopes {}.'.format( ', '.join(scopes_requested_but_not_granted)))
def _get_metadata_role_name(self, request): """Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server. This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security credentials needed to sign requests to AWS APIs. Args: request (google.auth.transport.Request): A callable used to make HTTP requests. Returns: str: The AWS role name. Raises: google.auth.exceptions.RefreshError: If an error occurs while retrieving the AWS role name. """ if self._security_credentials_url is None: raise exceptions.RefreshError( "Unable to determine the AWS metadata server security credentials endpoint" ) response = request(url=self._security_credentials_url, method="GET") # support both string and bytes type response.data response_body = (response.data.decode("utf-8") if hasattr( response.data, "decode") else response.data) if response.status != http.client.OK: raise exceptions.RefreshError("Unable to retrieve AWS role name", response_body) return response_body
def refresh(self, request): if (self._refresh_token is None or self._token_uri is None or self._client_id is None or self._client_secret is None): raise exceptions.RefreshError( "The credentials do not contain the necessary fields need to " "refresh the access token. You must specify refresh_token, " "token_uri, client_id, and client_secret.") access_token, refresh_token, expiry, grant_response = _client.refresh_grant( request, self._token_uri, self._refresh_token, self._client_id, self._client_secret, self._scopes, ) self.token = access_token self.expiry = expiry self._refresh_token = refresh_token self._id_token = grant_response.get("id_token") if self._scopes and "scopes" in grant_response: requested_scopes = frozenset(self._scopes) granted_scopes = frozenset(grant_response["scopes"].split()) scopes_requested_but_not_granted = requested_scopes - granted_scopes if scopes_requested_but_not_granted: raise exceptions.RefreshError( "Not all requested scopes were granted by the " "authorization server, missing scopes {}.".format( ", ".join(scopes_requested_but_not_granted)))
def refresh(self, request): if (self._refresh_token is None or self._token_uri is None or self._client_id is None or self._client_secret is None): raise exceptions.RefreshError( 'The credentials do not contain the necessary fields need to ' 'refresh the access token. You must specify refresh_token, ' 'token_uri, client_id, and client_secret.') access_token, refresh_token, expiry, grant_response = ( _client.refresh_grant( request, self._token_uri, self._refresh_token, self._client_id, self._client_secret, self._scopes)) self.token = access_token self.expiry = expiry self._refresh_token = refresh_token self._id_token = grant_response.get('id_token') if self._scopes and 'scopes' in grant_response: requested_scopes = frozenset(self._scopes) granted_scopes = frozenset(grant_response['scopes'].split()) scopes_requested_but_not_granted = ( requested_scopes - granted_scopes) if scopes_requested_but_not_granted: raise exceptions.RefreshError( 'Not all requested scopes were granted by the ' 'authorization server, missing scopes {}.'.format( ', '.join(scopes_requested_but_not_granted)))
def _make_iam_token_request(request, principal, headers, body, iam_endpoint_override=None): """Makes a request to the Google Cloud IAM service for an access token. Args: request (Request): The Request object to use. principal (str): The principal to request an access token for. headers (Mapping[str, str]): Map of headers to transmit. body (Mapping[str, str]): JSON Payload body for the iamcredentials API call. iam_endpoint_override (Optiona[str]): The full IAM endpoint override with the target_principal embedded. This is useful when supporting impersonation with regional endpoints. Raises: google.auth.exceptions.TransportError: Raised if there is an underlying HTTP connection error google.auth.exceptions.RefreshError: Raised if the impersonated credentials are not available. Common reasons are `iamcredentials.googleapis.com` is not enabled or the `Service Account Token Creator` is not assigned """ iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal) body = json.dumps(body).encode("utf-8") response = request(url=iam_endpoint, method="POST", headers=headers, body=body) # support both string and bytes type response.data response_body = (response.data.decode("utf-8") if hasattr( response.data, "decode") else response.data) if response.status != http_client.OK: exceptions.RefreshError(_REFRESH_ERROR, response_body) try: token_response = json.loads(response_body) token = token_response["accessToken"] expiry = datetime.strptime(token_response["expireTime"], "%Y-%m-%dT%H:%M:%SZ") return token, expiry except (KeyError, ValueError) as caught_exc: new_exc = exceptions.RefreshError( "{}: No access token or invalid expiration in response.".format( _REFRESH_ERROR), response_body, ) six.raise_from(new_exc, caught_exc)
def _call_metadata_identity_endpoint(self, request): """Request ID token from metadata identity endpoint. Args: request (google.auth.transport.Request): The object used to make HTTP requests. Raises: google.auth.exceptions.RefreshError: If the Compute Engine metadata service can't be reached or if the instance has no credentials. ValueError: If extracting expiry from the obtained ID token fails. """ try: id_token = _metadata.get( request, "instance/service-accounts/default/identity?audience={}&format=full".format( self._target_audience ), ) except exceptions.TransportError as caught_exc: new_exc = exceptions.RefreshError(caught_exc) six.raise_from(new_exc, caught_exc) _, payload, _, _ = jwt._unverified_decode(id_token) return id_token, payload["exp"]
def testRefreshWithException(self): self.StartObjectPatch( store, 'Refresh', side_effect=google_auth_exceptions.RefreshError()) with self.assertRaises(auth_exceptions.AuthenticationError): self.Run('auth print-access-token')
def test_gather_logs_token_error(self, log_mock): """GSuiteReportsApp - Gather Logs, Google API Token Error""" with patch.object(self._app, '_activities_service') as service_mock: error = exceptions.RefreshError('bad') service_mock.list.return_value.execute.side_effect = error assert_false(self._app._gather_logs()) log_mock.assert_called_with('[%s] Failed to execute activities listing', self._app)
def testRaiseTokenRefreshError(self): self.mock_refresh_grant.side_effect = google_auth_exceptions.RefreshError( 'API error') with self.AssertRaisesExceptionMatches( c_store.TokenRefreshError, '$ gcloud auth application-default login'): self.Run('auth application-default print-access-token')
def testExceptionHandling(self): http_client = HttpClient() orig_request = self.StartObjectPatch(http_client, 'request', return_value={ 'status': httplib.OK, }) fake_creds = mock.Mock() self.StartObjectPatch(store, 'LoadIfEnabled', return_value=fake_creds) RequestWrapper().WrapCredentials(http_client) http_client.request('uri', 'method') orig_request.side_effect = TypeError with self.assertRaises(TypeError): http_client.request('uri', 'method') orig_request.side_effect = client.AccessTokenRefreshError with self.assertRaises(store.TokenRefreshError): http_client.request('uri', 'method') orig_request.side_effect = google_auth_exceptions.RefreshError with self.assertRaises(store.TokenRefreshError): http_client.request('uri', 'method') orig_request.side_effect = google_auth_exceptions.RefreshError( 'access_denied: Account restricted') with self.assertRaises(store.TokenRefreshDeniedByCAAError): http_client.request('uri', 'method')
def _handle_refresh_grant_response(response_data, refresh_token): """Extract tokens from refresh grant response. Args: response_data (Mapping[str, str]): Refresh grant response data. refresh_token (str): Current refresh token. Returns: Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token, refresh token, expiration, and additional data returned by the token endpoint. If response_data doesn't have refresh token, then the current refresh token will be returned. Raises: google.auth.exceptions.RefreshError: If the token endpoint returned an error. """ try: access_token = response_data["access_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError("No access token in response.", response_data) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get("refresh_token", refresh_token) expiry = _parse_expiry(response_data) return access_token, refresh_token, expiry, response_data
def _get_file_data(self, filename): if not os.path.exists(filename): raise exceptions.RefreshError( "File '{}' was not found.".format(filename)) with io.open(filename, "r", encoding="utf-8") as file_obj: return file_obj.read(), filename
def jwt_grant(request, token_uri, assertion): """Implements the JWT Profile for OAuth 2.0 Authorization Grants. For more details, see `rfc7523 section 4`_. Args: request (google.auth.transport.Request): A callable used to make HTTP requests. token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. assertion (str): The OAuth 2.0 assertion. Returns: Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, expiration, and additional data returned by the token endpoint. Raises: google.auth.exceptions.RefreshError: If the token endpoint returned an error. .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4 """ body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} response_data = _token_endpoint_request(request, token_uri, body) try: access_token = response_data["access_token"] except KeyError as caught_exc: new_exc = exceptions.RefreshError("No access token in response.", response_data) six.raise_from(new_exc, caught_exc) expiry = _parse_expiry(response_data) return access_token, expiry, response_data
def _RefreshGrant(request, token_uri, refresh_token, client_id, client_secret, scopes=None, rapt_token=None): """Prepares the request to send to auth server to refresh tokens.""" body = [ ('grant_type', google_auth_client._REFRESH_GRANT_TYPE), # pylint: disable=protected-access ('client_id', client_id), ('client_secret', client_secret), ('refresh_token', refresh_token), ] if scopes: body.append(('scope', ' '.join(scopes))) if rapt_token: body.append(('rapt', rapt_token)) response_data = _TokenEndpointRequestWithRetry(request, token_uri, body) try: access_token = response_data['access_token'] except KeyError as caught_exc: new_exc = google_auth_exceptions.RefreshError( 'No access token in response.', response_data) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get('refresh_token', refresh_token) expiry = google_auth_client._parse_expiry(response_data) # pylint: disable=protected-access return access_token, refresh_token, expiry, response_data
def _call_metadata_identity_endpoint(self, request): """Request ID token from metadata identity endpoint. Args: request (google.auth.transport.Request): The object used to make HTTP requests. Returns: Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token. Raises: google.auth.exceptions.RefreshError: If the Compute Engine metadata service can't be reached or if the instance has no credentials. ValueError: If extracting expiry from the obtained ID token fails. """ try: path = "instance/service-accounts/default/identity" params = {"audience": self._target_audience, "format": "full"} id_token = _metadata.get(request, path, params=params) except exceptions.TransportError as caught_exc: new_exc = exceptions.RefreshError(caught_exc) six.raise_from(new_exc, caught_exc) _, payload, _, _ = jwt._unverified_decode(id_token) return id_token, datetime.datetime.fromtimestamp(payload["exp"])
def _update_token(self, request): """Updates credentials with a new access_token representing the downscoped credentials. Args: request (google.auth.transport.requests.Request): Request object to use for refreshing credentials. """ # Refresh our source credentials. self._source_credentials.refresh(request) request = google.auth.transport.requests.Request() ac = AnonymousCredentials() authed_session = AuthorizedSession(credentials=ac) body = { "grant_type": 'urn:ietf:params:oauth:grant-type:token-exchange', "subject_token_type": 'urn:ietf:params:oauth:token-type:access_token', "requested_token_type": 'urn:ietf:params:oauth:token-type:access_token', "subject_token": self._source_credentials.token, "options": json.dumps(self._downscoped_options) } resp = authed_session.post(_STS_ENDPOINT, data=body) if resp.status_code != http_client.OK: raise exceptions.RefreshError(_REFRESH_ERROR) data = resp.json() self.token = data['access_token'] if 'expires_in' in data: self.expiry = datetime.now() + \ timedelta(seconds=int(data['expires_in'])) else: authed_session = AuthorizedSession(credentials=ac) payload = {'access_token': self._source_credentials.token} token_response = authed_session.get(_TOKEN_INFO_ENDPOINT, params=payload) if token_response.status_code != http_client.OK: raise exceptions.RefreshError(_TOKEN_INFO_ERROR) tokeninfo_data = token_response.json() self.expiry = datetime.now() + \ timedelta(seconds=int(tokeninfo_data['expires_in']))
def _make_iam_token_request(request, principal, headers, body): """Makes a request to the Google Cloud IAM service for an access token. Args: request (Request): The Request object to use. principal (str): The principal to request an access token for. headers (Mapping[str, str]): Map of headers to transmit. body (Mapping[str, str]): JSON Payload body for the iamcredentials API call. Raises: TransportError: Raised if there is an underlying HTTP connection Error DefaultCredentialsError: Raised if the impersonated credentials are not available. Common reasons are `iamcredentials.googleapis.com` is not enabled or the `Service Account Token Creator` is not assigned """ iam_endpoint = _IAM_ENDPOINT.format(principal) body = json.dumps(body) response = request( url=iam_endpoint, method='POST', headers=headers, body=body) response_body = response.data.decode('utf-8') if response.status != http_client.OK: exceptions.RefreshError(_REFRESH_ERROR, response_body) try: token_response = json.loads(response.data.decode('utf-8')) token = token_response['accessToken'] expiry = datetime.strptime( token_response['expireTime'], '%Y-%m-%dT%H:%M:%SZ') return token, expiry except (KeyError, ValueError) as caught_exc: new_exc = exceptions.RefreshError( '{}: No access token or invalid expiration in response.'.format( _REFRESH_ERROR), response_body) six.raise_from(new_exc, caught_exc)
def refresh(self, request): if self._raise_error: raise exceptions.RefreshError( "Failed to refresh access token in source credentials.") now = _helpers.utcnow() self._counter += 1 self.token = "ACCESS_TOKEN_{}".format(self._counter) self.expiry = now + datetime.timedelta(seconds=self._expires_in)
def refresh_grant(request, token_uri, refresh_token, client_id, client_secret, scopes=None): """Implements the OAuth 2.0 refresh token grant. For more details, see `rfc678 section 6`_. Args: request (google.auth.transport.Request): A callable used to make HTTP requests. token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. refresh_token (str): The refresh token to use to get a new access token. client_id (str): The OAuth 2.0 application's client ID. client_secret (str): The Oauth 2.0 appliaction's client secret. scopes (Optional(Sequence[str])): Scopes to request. If present, all scopes must be authorized for the refresh token. Useful if refresh token has a wild card scope (e.g. 'https://www.googleapis.com/auth/any-api'). Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The access token, new refresh token, expiration, and additional data returned by the token endpoint. Raises: google.auth.exceptions.RefreshError: If the token endpoint returned an error. .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 """ body = { 'grant_type': _REFRESH_GRANT_TYPE, 'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, } if scopes: body['scope'] = ' '.join(scopes) response_data = _token_endpoint_request(request, token_uri, body) try: access_token = response_data['access_token'] except KeyError as caught_exc: new_exc = exceptions.RefreshError('No access token in response.', response_data) six.raise_from(new_exc, caught_exc) refresh_token = response_data.get('refresh_token', refresh_token) expiry = _parse_expiry(response_data) return access_token, refresh_token, expiry, response_data
def testTokenRefreshDeniedByCAAError(self): refresh_mock = self.StartObjectPatch(credentials.Credentials, 'before_request') refresh_mock.side_effect = google_auth_exceptions.RefreshError( 'access_denied: Account restricted') http_client = creds_requests.GetSession() with self.assertRaisesRegex( store.TokenRefreshDeniedByCAAError, 'Access was blocked due to an organization policy'): http_client.request('GET', 'http://foo.com')
def _get_region(self, request, url): """Retrieves the current AWS region from either the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS metadata server. Args: request (google.auth.transport.Request): A callable used to make HTTP requests. url (str): The AWS metadata server region URL. Returns: str: The current AWS region. Raises: google.auth.exceptions.RefreshError: If an error occurs while retrieving the AWS region. """ # The AWS metadata server is not available in some AWS environments # such as AWS lambda. Instead, it is available via environment # variable. env_aws_region = os.environ.get(environment_vars.AWS_REGION) if env_aws_region is not None: return env_aws_region env_aws_region = os.environ.get(environment_vars.AWS_DEFAULT_REGION) if env_aws_region is not None: return env_aws_region if not self._region_url: raise exceptions.RefreshError("Unable to determine AWS region") response = request(url=self._region_url, method="GET") # Support both string and bytes type response.data. response_body = (response.data.decode("utf-8") if hasattr( response.data, "decode") else response.data) if response.status != 200: raise exceptions.RefreshError("Unable to retrieve AWS region", response_body) # This endpoint will return the region in format: us-east-2b. # Only the us-east-2 part should be used. return response_body[:-1]
def _parse_token_data(self, token_content, format_type="text", subject_token_field_name=None): content, filename = token_content if format_type == "text": token = content else: try: # Parse file content as JSON. response_data = json.loads(content) # Get the subject_token. token = response_data[subject_token_field_name] except (KeyError, ValueError): raise exceptions.RefreshError( "Unable to parse subject_token from JSON file '{}' using key '{}'" .format(filename, subject_token_field_name)) if not token: raise exceptions.RefreshError( "Missing subject_token in the credential_source file") return token
def testBatchTokenRefreshDeniedByCAAErrorGoogleAuth(self): refresh_mock = self.StartObjectPatch(credentials.Credentials, 'before_request') refresh_mock.side_effect = google_auth_exceptions.RefreshError( 'access_denied: Account restricted') http_client = http.Http(use_google_auth=True) batch_http_request = batch.BatchHttpRequest( 'https://www.googleapis.com/batch/compute') with self.assertRaisesRegex( store.TokenRefreshDeniedByCAAError, 'Access was blocked due to an organization policy'): batch_http_request.Execute(http_client)
def _get_url_data(self, request, url, headers): response = request(url=url, method="GET", headers=headers) # support both string and bytes type response.data response_body = (response.data.decode("utf-8") if hasattr( response.data, "decode") else response.data) if response.status != 200: raise exceptions.RefreshError( "Unable to retrieve Identity Pool subject token", response_body) return response_body, url
def _initialize_impersonated_credentials(self): """Generates an impersonated credentials. For more details, see `projects.serviceAccounts.generateAccessToken`_. .. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken Returns: impersonated_credentials.Credential: The impersonated credentials object. Raises: google.auth.exceptions.RefreshError: If the generateAccessToken endpoint returned an error. """ # Return copy of instance with no service account impersonation. source_credentials = self.__class__( audience=self._audience, subject_token_type=self._subject_token_type, token_url=self._token_url, credential_source=self._credential_source, service_account_impersonation_url=None, client_id=self._client_id, client_secret=self._client_secret, quota_project_id=self._quota_project_id, scopes=self._scopes, default_scopes=self._default_scopes, ) # Determine target_principal. start_index = self._service_account_impersonation_url.rfind("/") end_index = self._service_account_impersonation_url.find( ":generateAccessToken") if start_index != -1 and end_index != -1 and start_index < end_index: start_index = start_index + 1 target_principal = self._service_account_impersonation_url[ start_index:end_index] else: raise exceptions.RefreshError( "Unable to determine target principal from service account impersonation URL." ) scopes = self._scopes if self._scopes is not None else self._default_scopes # Initialize and return impersonated credentials. return impersonated_credentials.Credentials( source_credentials=source_credentials, target_principal=target_principal, target_scopes=scopes, quota_project_id=self._quota_project_id, iam_endpoint_override=self._service_account_impersonation_url, )
def refresh(self, request): """Raises an exception, these credentials can not be directly refreshed. Args: request (Any): Unused. Raises: google.auth.RefreshError """ # pylint: disable=unused-argument # (pylint doesn't correctly recognize overridden methods.) raise exceptions.RefreshError( 'OnDemandCredentials can not be directly refreshed.')
def _initialize_impersonated_credentials(self): """Generates an impersonated credentials. For more details, see `projects.serviceAccounts.generateAccessToken`_. .. _projects.serviceAccounts.generateAccessToken: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken Returns: impersonated_credentials.Credential: The impersonated credentials object. Raises: google.auth.exceptions.RefreshError: If the generateAccessToken endpoint returned an error. """ # Return copy of instance with no service account impersonation. d = dict( audience=self._audience, subject_token_type=self._subject_token_type, token_url=self._token_url, credential_source=self._credential_source, service_account_impersonation_url=None, client_id=self._client_id, client_secret=self._client_secret, quota_project_id=self._quota_project_id, scopes=self._scopes, default_scopes=self._default_scopes, workforce_pool_user_project=self._workforce_pool_user_project, ) if not self.is_workforce_pool: d.pop("workforce_pool_user_project") source_credentials = self.__class__(**d) # Determine target_principal. target_principal = self.service_account_email if not target_principal: raise exceptions.RefreshError( "Unable to determine target principal from service account impersonation URL." ) scopes = self._scopes if self._scopes is not None else self._default_scopes # Initialize and return impersonated credentials. return impersonated_credentials.Credentials( source_credentials=source_credentials, target_principal=target_principal, target_scopes=scopes, quota_project_id=self._quota_project_id, iam_endpoint_override=self._service_account_impersonation_url, )
def refresh(self, request): if (self._refresh_token is None or self._token_uri is None or self._client_id is None or self._client_secret is None): raise exceptions.RefreshError( 'The credentials do not contain the necessary fields need to ' 'refresh the access token. You must specify refresh_token, ' 'token_uri, client_id, and client_secret.') access_token, refresh_token, expiry, grant_response = ( _client.refresh_grant(request, self._token_uri, self._refresh_token, self._client_id, self._client_secret)) self.token = access_token self.expiry = expiry self._refresh_token = refresh_token self._id_token = grant_response.get('id_token')
def _handle_error_response(response_data): """Translates an error response into an exception. Args: response_data (Mapping): The decoded response data. Raises: google.auth.exceptions.RefreshError: The errors contained in response_data. """ try: error_details = "{}: {}".format(response_data["error"], response_data.get("error_description")) # If no details could be extracted, use the response data. except (KeyError, ValueError): error_details = json.dumps(response_data) raise exceptions.RefreshError(error_details, response_data)
async def refresh_grant(session: ClientSession, token_uri, refresh_token, client_id, client_secret): """Implements the OAuth 2.0 refresh token grant. For more details, see `rfc678 section 6`_. Args: request (google.auth.transport.Request): A callable used to make HTTP requests. token_uri (str): The OAuth 2.0 authorizations server's token endpoint URI. refresh_token (str): The refresh token to use to get a new access token. client_id (str): The OAuth 2.0 application's client ID. client_secret (str): The Oauth 2.0 appliaction's client secret. Returns: Tuple[str, Optional[str], Optional[datetime], Mapping[str, str]]: The access token, new refresh token, expiration, and additional data returned by the token endpoint. Raises: google.auth.exceptions.RefreshError: If the token endpoint returned an error. .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 """ body = { 'grant_type': _REFRESH_GRANT_TYPE, 'client_id': client_id, 'client_secret': client_secret, 'refresh_token': refresh_token, } response_data = await _token_endpoint_request(session, token_uri, body) try: access_token = response_data['access_token'] except KeyError: raise exceptions.RefreshError('No access token in response.', response_data) refresh_token = response_data.get('refresh_token', refresh_token) expiry = _parse_expiry(response_data) return access_token, refresh_token, expiry, response_data