codekingpro/portable-devtools
114k
1# Copyright 2016 Google LLC2#3# Licensed under the Apache License, Version 2.0 (the "License");4# you may not use this file except in compliance with the License.5# You may obtain a copy of the License at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# distributed under the License is distributed on an "AS IS" BASIS,11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12# See the License for the specific language governing permissions and13# limitations under the License.14 15"""OAuth 2.0 client.16 17This is a client for interacting with an OAuth 2.0 authorization server's18token endpoint.19 20For more information about the token endpoint, see21`Section 3.1 of rfc6749`_22 23.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.224"""25 26import datetime27import http.client as http_client28import json29import urllib30 31from google.auth import _exponential_backoff32from google.auth import _helpers33from google.auth import exceptions34from google.auth import jwt35from google.auth import metrics36from google.auth import transport37 38_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"39_JSON_CONTENT_TYPE = "application/json"40_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"41_REFRESH_GRANT_TYPE = "refresh_token"42_IAM_IDTOKEN_ENDPOINT = (43 "https://iamcredentials.googleapis.com/v1/"44 + "projects/-/serviceAccounts/{}:generateIdToken"45)46 47 48def _handle_error_response(response_data, retryable_error):49 """Translates an error response into an exception.50 51 Args:52 response_data (Mapping | str): The decoded response data.53 retryable_error Optional[bool]: A boolean indicating if an error is retryable.54 Defaults to False.55 56 Raises:57 google.auth.exceptions.RefreshError: The errors contained in response_data.58 """59 60 retryable_error = retryable_error if retryable_error else False61 62 if isinstance(response_data, str):63 raise exceptions.RefreshError(response_data, retryable=retryable_error)64 try:65 error_details = "{}: {}".format(66 response_data["error"], response_data.get("error_description")67 )68 # If no details could be extracted, use the response data.69 except (KeyError, ValueError):70 error_details = json.dumps(response_data)71 72 raise exceptions.RefreshError(73 error_details, response_data, retryable=retryable_error74 )75 76 77def _can_retry(status_code, response_data):78 """Checks if a request can be retried by inspecting the status code79 and response body of the request.80 81 Args:82 status_code (int): The response status code.83 response_data (Mapping | str): The decoded response data.84 85 Returns:86 bool: True if the response is retryable. False otherwise.87 """88 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:89 return True90 91 try:92 # For a failed response, response_body could be a string93 error_desc = response_data.get("error_description") or ""94 error_code = response_data.get("error") or ""95 96 if not isinstance(error_code, str) or not isinstance(error_desc, str):97 return False98 99 # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1100 # This is needed because a redirect will not return a 500 status code.101 retryable_error_descriptions = {102 "internal_failure",103 "server_error",104 "temporarily_unavailable",105 }106 107 if any(e in retryable_error_descriptions for e in (error_code, error_desc)):108 return True109 110 except AttributeError:111 pass112 113 return False114 115 116def _parse_expiry(response_data):117 """Parses the expiry field from a response into a datetime.118 119 Args:120 response_data (Mapping): The JSON-parsed response data.121 122 Returns:123 Optional[datetime]: The expiration or ``None`` if no expiration was124 specified.125 """126 expires_in = response_data.get("expires_in", None)127 128 if expires_in is not None:129 # Some services do not respect the OAUTH2.0 RFC and send expires_in as a130 # JSON String.131 if isinstance(expires_in, str):132 expires_in = int(expires_in)133 134 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)135 else:136 return None137 138 139def _token_endpoint_request_no_throw(140 request,141 token_uri,142 body,143 access_token=None,144 use_json=False,145 can_retry=True,146 headers=None,147 **kwargs148):149 """Makes a request to the OAuth 2.0 authorization server's token endpoint.150 This function doesn't throw on response errors.151 152 Args:153 request (google.auth.transport.Request): A callable used to make154 HTTP requests.155 token_uri (str): The OAuth 2.0 authorizations server's token endpoint156 URI.157 body (Mapping[str, str]): The parameters to send in the request body.158 access_token (Optional(str)): The access token needed to make the request.159 use_json (Optional(bool)): Use urlencoded format or json format for the160 content type. The default value is False.161 can_retry (bool): Enable or disable request retry behavior.162 headers (Optional[Mapping[str, str]]): The headers for the request.163 kwargs: Additional arguments passed on to the request method. The164 kwargs will be passed to `requests.request` method, see:165 https://docs.python-requests.org/en/latest/api/#requests.request.166 For example, you can use `cert=("cert_pem_path", "key_pem_path")`167 to set up client side SSL certificate, and use168 `verify="ca_bundle_path"` to set up the CA certificates for sever169 side SSL certificate verification.170 171 Returns:172 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating173 if the request is successful, a mapping for the JSON-decoded response174 data and in the case of an error a boolean indicating if the error175 is retryable.176 """177 if use_json:178 headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}179 body = json.dumps(body).encode("utf-8")180 else:181 headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}182 body = urllib.parse.urlencode(body).encode("utf-8")183 184 if access_token:185 headers_to_use["Authorization"] = "Bearer {}".format(access_token)186 187 if headers:188 headers_to_use.update(headers)189 190 def _perform_request():191 response = request(192 method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs193 )194 response_body = (195 response.data.decode("utf-8")196 if hasattr(response.data, "decode")197 else response.data198 )199 response_data = ""200 try:201 # response_body should be a JSON202 response_data = json.loads(response_body)203 except ValueError:204 response_data = response_body205 206 if response.status == http_client.OK:207 return True, response_data, None208 209 retryable_error = _can_retry(210 status_code=response.status, response_data=response_data211 )212 213 return False, response_data, retryable_error214 215 request_succeeded, response_data, retryable_error = _perform_request()216 217 if request_succeeded or not retryable_error or not can_retry:218 return request_succeeded, response_data, retryable_error219 220 retries = _exponential_backoff.ExponentialBackoff()221 for _ in retries:222 request_succeeded, response_data, retryable_error = _perform_request()223 if request_succeeded or not retryable_error:224 return request_succeeded, response_data, retryable_error225 226 return False, response_data, retryable_error227 228 229def _token_endpoint_request(230 request,231 token_uri,232 body,233 access_token=None,234 use_json=False,235 can_retry=True,236 headers=None,237 **kwargs238):239 """Makes a request to the OAuth 2.0 authorization server's token endpoint.240 241 Args:242 request (google.auth.transport.Request): A callable used to make243 HTTP requests.244 token_uri (str): The OAuth 2.0 authorizations server's token endpoint245 URI.246 body (Mapping[str, str]): The parameters to send in the request body.247 access_token (Optional(str)): The access token needed to make the request.248 use_json (Optional(bool)): Use urlencoded format or json format for the249 content type. The default value is False.250 can_retry (bool): Enable or disable request retry behavior.251 headers (Optional[Mapping[str, str]]): The headers for the request.252 kwargs: Additional arguments passed on to the request method. The253 kwargs will be passed to `requests.request` method, see:254 https://docs.python-requests.org/en/latest/api/#requests.request.255 For example, you can use `cert=("cert_pem_path", "key_pem_path")`256 to set up client side SSL certificate, and use257 `verify="ca_bundle_path"` to set up the CA certificates for sever258 side SSL certificate verification.259 260 Returns:261 Mapping[str, str]: The JSON-decoded response data.262 263 Raises:264 google.auth.exceptions.RefreshError: If the token endpoint returned265 an error.266 """267 268 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(269 request,270 token_uri,271 body,272 access_token=access_token,273 use_json=use_json,274 can_retry=can_retry,275 headers=headers,276 **kwargs277 )278 if not response_status_ok:279 _handle_error_response(response_data, retryable_error)280 return response_data281 282 283def jwt_grant(request, token_uri, assertion, can_retry=True):284 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.285 286 For more details, see `rfc7523 section 4`_.287 288 Args:289 request (google.auth.transport.Request): A callable used to make290 HTTP requests.291 token_uri (str): The OAuth 2.0 authorizations server's token endpoint292 URI.293 assertion (str): The OAuth 2.0 assertion.294 can_retry (bool): Enable or disable request retry behavior.295 296 Returns:297 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,298 expiration, and additional data returned by the token endpoint.299 300 Raises:301 google.auth.exceptions.RefreshError: If the token endpoint returned302 an error.303 304 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4305 """306 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}307 308 response_data = _token_endpoint_request(309 request,310 token_uri,311 body,312 can_retry=can_retry,313 headers={314 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()315 },316 )317 318 try:319 access_token = response_data["access_token"]320 except KeyError as caught_exc:321 new_exc = exceptions.RefreshError(322 "No access token in response.", response_data, retryable=False323 )324 raise new_exc from caught_exc325 326 expiry = _parse_expiry(response_data)327 328 return access_token, expiry, response_data329 330 331def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):332 """Call iam.generateIdToken endpoint to get ID token.333 334 Args:335 request (google.auth.transport.Request): A callable used to make336 HTTP requests.337 signer_email (str): The signer email used to form the IAM338 generateIdToken endpoint.339 audience (str): The audience for the ID token.340 access_token (str): The access token used to call the IAM endpoint.341 342 Returns:343 Tuple[str, datetime]: The ID token and expiration.344 """345 body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}346 347 response_data = _token_endpoint_request(348 request,349 _IAM_IDTOKEN_ENDPOINT.format(signer_email),350 body,351 access_token=access_token,352 use_json=True,353 )354 355 try:356 id_token = response_data["token"]357 except KeyError as caught_exc:358 new_exc = exceptions.RefreshError(359 "No ID token in response.", response_data, retryable=False360 )361 raise new_exc from caught_exc362 363 payload = jwt.decode(id_token, verify=False)364 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])365 366 return id_token, expiry367 368 369def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):370 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but371 requests an OpenID Connect ID Token instead of an access token.372 373 This is a variant on the standard JWT Profile that is currently unique374 to Google. This was added for the benefit of authenticating to services375 that require ID Tokens instead of access tokens or JWT bearer tokens.376 377 Args:378 request (google.auth.transport.Request): A callable used to make379 HTTP requests.380 token_uri (str): The OAuth 2.0 authorization server's token endpoint381 URI.382 assertion (str): JWT token signed by a service account. The token's383 payload must include a ``target_audience`` claim.384 can_retry (bool): Enable or disable request retry behavior.385 386 Returns:387 Tuple[str, Optional[datetime], Mapping[str, str]]:388 The (encoded) Open ID Connect ID Token, expiration, and additional389 data returned by the endpoint.390 391 Raises:392 google.auth.exceptions.RefreshError: If the token endpoint returned393 an error.394 """395 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}396 397 response_data = _token_endpoint_request(398 request,399 token_uri,400 body,401 can_retry=can_retry,402 headers={403 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()404 },405 )406 407 try:408 id_token = response_data["id_token"]409 except KeyError as caught_exc:410 new_exc = exceptions.RefreshError(411 "No ID token in response.", response_data, retryable=False412 )413 raise new_exc from caught_exc414 415 payload = jwt.decode(id_token, verify=False)416 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])417 418 return id_token, expiry, response_data419 420 421def _handle_refresh_grant_response(response_data, refresh_token):422 """Extract tokens from refresh grant response.423 424 Args:425 response_data (Mapping[str, str]): Refresh grant response data.426 refresh_token (str): Current refresh token.427 428 Returns:429 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,430 refresh token, expiration, and additional data returned by the token431 endpoint. If response_data doesn't have refresh token, then the current432 refresh token will be returned.433 434 Raises:435 google.auth.exceptions.RefreshError: If the token endpoint returned436 an error.437 """438 try:439 access_token = response_data["access_token"]440 except KeyError as caught_exc:441 new_exc = exceptions.RefreshError(442 "No access token in response.", response_data, retryable=False443 )444 raise new_exc from caught_exc445 446 refresh_token = response_data.get("refresh_token", refresh_token)447 expiry = _parse_expiry(response_data)448 449 return access_token, refresh_token, expiry, response_data450 451 452def refresh_grant(453 request,454 token_uri,455 refresh_token,456 client_id,457 client_secret,458 scopes=None,459 rapt_token=None,460 can_retry=True,461):462 """Implements the OAuth 2.0 refresh token grant.463 464 For more details, see `rfc678 section 6`_.465 466 Args:467 request (google.auth.transport.Request): A callable used to make468 HTTP requests.469 token_uri (str): The OAuth 2.0 authorizations server's token endpoint470 URI.471 refresh_token (str): The refresh token to use to get a new access472 token.473 client_id (str): The OAuth 2.0 application's client ID.474 client_secret (str): The Oauth 2.0 appliaction's client secret.475 scopes (Optional(Sequence[str])): Scopes to request. If present, all476 scopes must be authorized for the refresh token. Useful if refresh477 token has a wild card scope (e.g.478 'https://www.googleapis.com/auth/any-api').479 rapt_token (Optional(str)): The reauth Proof Token.480 can_retry (bool): Enable or disable request retry behavior.481 482 Returns:483 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access484 token, new or current refresh token, expiration, and additional data485 returned by the token endpoint.486 487 Raises:488 google.auth.exceptions.RefreshError: If the token endpoint returned489 an error.490 491 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6492 """493 body = {494 "grant_type": _REFRESH_GRANT_TYPE,495 "client_id": client_id,496 "client_secret": client_secret,497 "refresh_token": refresh_token,498 }499 if scopes:500 body["scope"] = " ".join(scopes)501 if rapt_token:502 body["rapt"] = rapt_token503 504 response_data = _token_endpoint_request(505 request, token_uri, body, can_retry=can_retry506 )507 return _handle_refresh_grant_response(response_data, refresh_token)508 