1# Copyright 2016 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""OAuth 2.0 client.
16
17This is a client for interacting with an OAuth 2.0 authorization server's
18token endpoint.
19
20For more information about the token endpoint, see
21`Section 3.1 of rfc6749`_
22
23.. _Section 3.1 of rfc6749: https://tools.ietf.org/html/rfc6749#section-3.2
24"""
25
26import datetime
27import http.client as http_client
28import json
29import urllib
30
31from google.auth import _exponential_backoff
32from google.auth import _helpers
33from google.auth import credentials
34from google.auth import exceptions
35from google.auth import jwt
36from google.auth import metrics
37from google.auth import transport
38
39_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
40_JSON_CONTENT_TYPE = "application/json"
41_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
42_REFRESH_GRANT_TYPE = "refresh_token"
43
44
45def _handle_error_response(response_data, retryable_error):
46 """Translates an error response into an exception.
47
48 Args:
49 response_data (Mapping | str): The decoded response data.
50 retryable_error Optional[bool]: A boolean indicating if an error is retryable.
51 Defaults to False.
52
53 Raises:
54 google.auth.exceptions.RefreshError: The errors contained in response_data.
55 """
56
57 retryable_error = retryable_error if retryable_error else False
58
59 if isinstance(response_data, str):
60 raise exceptions.RefreshError(response_data, retryable=retryable_error)
61 try:
62 error_details = "{}: {}".format(
63 response_data["error"], response_data.get("error_description")
64 )
65 # If no details could be extracted, use the response data.
66 except (KeyError, ValueError):
67 error_details = json.dumps(response_data)
68
69 raise exceptions.RefreshError(
70 error_details, response_data, retryable=retryable_error
71 )
72
73
74def _can_retry(status_code, response_data):
75 """Checks if a request can be retried by inspecting the status code
76 and response body of the request.
77
78 Args:
79 status_code (int): The response status code.
80 response_data (Mapping | str): The decoded response data.
81
82 Returns:
83 bool: True if the response is retryable. False otherwise.
84 """
85 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
86 return True
87
88 try:
89 # For a failed response, response_body could be a string
90 error_desc = response_data.get("error_description") or ""
91 error_code = response_data.get("error") or ""
92
93 if not isinstance(error_code, str) or not isinstance(error_desc, str):
94 return False
95
96 # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
97 # This is needed because a redirect will not return a 500 status code.
98 retryable_error_descriptions = {
99 "internal_failure",
100 "server_error",
101 "temporarily_unavailable",
102 }
103
104 if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
105 return True
106
107 except AttributeError:
108 pass
109
110 return False
111
112
113def _parse_expiry(response_data):
114 """Parses the expiry field from a response into a datetime.
115
116 Args:
117 response_data (Mapping): The JSON-parsed response data.
118
119 Returns:
120 Optional[datetime]: The expiration or ``None`` if no expiration was
121 specified.
122 """
123 expires_in = response_data.get("expires_in", None)
124
125 if expires_in is not None:
126 # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
127 # JSON String.
128 if isinstance(expires_in, str):
129 expires_in = int(expires_in)
130
131 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
132 else:
133 return None
134
135
136def _token_endpoint_request_no_throw(
137 request,
138 token_uri,
139 body,
140 access_token=None,
141 use_json=False,
142 can_retry=True,
143 headers=None,
144 **kwargs
145):
146 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
147 This function doesn't throw on response errors.
148
149 Args:
150 request (google.auth.transport.Request): A callable used to make
151 HTTP requests.
152 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
153 URI.
154 body (Mapping[str, str]): The parameters to send in the request body.
155 access_token (Optional(str)): The access token needed to make the request.
156 use_json (Optional(bool)): Use urlencoded format or json format for the
157 content type. The default value is False.
158 can_retry (bool): Enable or disable request retry behavior.
159 headers (Optional[Mapping[str, str]]): The headers for the request.
160 kwargs: Additional arguments passed on to the request method. The
161 kwargs will be passed to `requests.request` method, see:
162 https://docs.python-requests.org/en/latest/api/#requests.request.
163 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
164 to set up client side SSL certificate, and use
165 `verify="ca_bundle_path"` to set up the CA certificates for sever
166 side SSL certificate verification.
167
168 Returns:
169 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
170 if the request is successful, a mapping for the JSON-decoded response
171 data and in the case of an error a boolean indicating if the error
172 is retryable.
173 """
174 if use_json:
175 headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
176 body = json.dumps(body).encode("utf-8")
177 else:
178 headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
179 body = urllib.parse.urlencode(body).encode("utf-8")
180
181 if access_token:
182 headers_to_use["Authorization"] = "Bearer {}".format(access_token)
183
184 if headers:
185 headers_to_use.update(headers)
186
187 response_data = {}
188 retryable_error = False
189
190 retries = _exponential_backoff.ExponentialBackoff()
191 for _ in retries:
192 response = request(
193 method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
194 )
195 response_body = (
196 response.data.decode("utf-8")
197 if hasattr(response.data, "decode")
198 else response.data
199 )
200
201 try:
202 # response_body should be a JSON
203 response_data = json.loads(response_body)
204 except ValueError:
205 response_data = response_body
206
207 if response.status == http_client.OK:
208 return True, response_data, None
209
210 retryable_error = _can_retry(
211 status_code=response.status, response_data=response_data
212 )
213
214 if not can_retry or not retryable_error:
215 return False, response_data, retryable_error
216
217 return False, response_data, retryable_error
218
219
220def _token_endpoint_request(
221 request,
222 token_uri,
223 body,
224 access_token=None,
225 use_json=False,
226 can_retry=True,
227 headers=None,
228 **kwargs
229):
230 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
231
232 Args:
233 request (google.auth.transport.Request): A callable used to make
234 HTTP requests.
235 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
236 URI.
237 body (Mapping[str, str]): The parameters to send in the request body.
238 access_token (Optional(str)): The access token needed to make the request.
239 use_json (Optional(bool)): Use urlencoded format or json format for the
240 content type. The default value is False.
241 can_retry (bool): Enable or disable request retry behavior.
242 headers (Optional[Mapping[str, str]]): The headers for the request.
243 kwargs: Additional arguments passed on to the request method. The
244 kwargs will be passed to `requests.request` method, see:
245 https://docs.python-requests.org/en/latest/api/#requests.request.
246 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
247 to set up client side SSL certificate, and use
248 `verify="ca_bundle_path"` to set up the CA certificates for sever
249 side SSL certificate verification.
250
251 Returns:
252 Mapping[str, str]: The JSON-decoded response data.
253
254 Raises:
255 google.auth.exceptions.RefreshError: If the token endpoint returned
256 an error.
257 """
258
259 (
260 response_status_ok,
261 response_data,
262 retryable_error,
263 ) = _token_endpoint_request_no_throw(
264 request,
265 token_uri,
266 body,
267 access_token=access_token,
268 use_json=use_json,
269 can_retry=can_retry,
270 headers=headers,
271 **kwargs
272 )
273 if not response_status_ok:
274 _handle_error_response(response_data, retryable_error)
275 return response_data
276
277
278def jwt_grant(request, token_uri, assertion, can_retry=True):
279 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
280
281 For more details, see `rfc7523 section 4`_.
282
283 Args:
284 request (google.auth.transport.Request): A callable used to make
285 HTTP requests.
286 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
287 URI.
288 assertion (str): The OAuth 2.0 assertion.
289 can_retry (bool): Enable or disable request retry behavior.
290
291 Returns:
292 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
293 expiration, and additional data returned by the token endpoint.
294
295 Raises:
296 google.auth.exceptions.RefreshError: If the token endpoint returned
297 an error.
298
299 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
300 """
301 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
302
303 response_data = _token_endpoint_request(
304 request,
305 token_uri,
306 body,
307 can_retry=can_retry,
308 headers={
309 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
310 },
311 )
312
313 try:
314 access_token = response_data["access_token"]
315 except KeyError as caught_exc:
316 new_exc = exceptions.RefreshError(
317 "No access token in response.", response_data, retryable=False
318 )
319 raise new_exc from caught_exc
320
321 expiry = _parse_expiry(response_data)
322
323 return access_token, expiry, response_data
324
325
326def call_iam_generate_id_token_endpoint(
327 request,
328 iam_id_token_endpoint,
329 signer_email,
330 audience,
331 access_token,
332 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
333):
334 """Call iam.generateIdToken endpoint to get ID token.
335
336 Args:
337 request (google.auth.transport.Request): A callable used to make
338 HTTP requests.
339 iam_id_token_endpoint (str): The IAM ID token endpoint to use.
340 signer_email (str): The signer email used to form the IAM
341 generateIdToken endpoint.
342 audience (str): The audience for the ID token.
343 access_token (str): The access token used to call the IAM endpoint.
344 universe_domain (str): The universe domain for the request. The
345 default is ``googleapis.com``.
346
347 Returns:
348 Tuple[str, datetime]: The ID token and expiration.
349 """
350 body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
351
352 response_data = _token_endpoint_request(
353 request,
354 iam_id_token_endpoint.replace(
355 credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain
356 ).format(signer_email),
357 body,
358 access_token=access_token,
359 use_json=True,
360 )
361
362 try:
363 id_token = response_data["token"]
364 except KeyError as caught_exc:
365 new_exc = exceptions.RefreshError(
366 "No ID token in response.", response_data, retryable=False
367 )
368 raise new_exc from caught_exc
369
370 payload = jwt.decode(id_token, verify=False)
371 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
372
373 return id_token, expiry
374
375
376def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
377 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
378 requests an OpenID Connect ID Token instead of an access token.
379
380 This is a variant on the standard JWT Profile that is currently unique
381 to Google. This was added for the benefit of authenticating to services
382 that require ID Tokens instead of access tokens or JWT bearer tokens.
383
384 Args:
385 request (google.auth.transport.Request): A callable used to make
386 HTTP requests.
387 token_uri (str): The OAuth 2.0 authorization server's token endpoint
388 URI.
389 assertion (str): JWT token signed by a service account. The token's
390 payload must include a ``target_audience`` claim.
391 can_retry (bool): Enable or disable request retry behavior.
392
393 Returns:
394 Tuple[str, Optional[datetime], Mapping[str, str]]:
395 The (encoded) Open ID Connect ID Token, expiration, and additional
396 data returned by the endpoint.
397
398 Raises:
399 google.auth.exceptions.RefreshError: If the token endpoint returned
400 an error.
401 """
402 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
403
404 response_data = _token_endpoint_request(
405 request,
406 token_uri,
407 body,
408 can_retry=can_retry,
409 headers={
410 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
411 },
412 )
413
414 try:
415 id_token = response_data["id_token"]
416 except KeyError as caught_exc:
417 new_exc = exceptions.RefreshError(
418 "No ID token in response.", response_data, retryable=False
419 )
420 raise new_exc from caught_exc
421
422 payload = jwt.decode(id_token, verify=False)
423 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
424
425 return id_token, expiry, response_data
426
427
428def _handle_refresh_grant_response(response_data, refresh_token):
429 """Extract tokens from refresh grant response.
430
431 Args:
432 response_data (Mapping[str, str]): Refresh grant response data.
433 refresh_token (str): Current refresh token.
434
435 Returns:
436 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
437 refresh token, expiration, and additional data returned by the token
438 endpoint. If response_data doesn't have refresh token, then the current
439 refresh token will be returned.
440
441 Raises:
442 google.auth.exceptions.RefreshError: If the token endpoint returned
443 an error.
444 """
445 try:
446 access_token = response_data["access_token"]
447 except KeyError as caught_exc:
448 new_exc = exceptions.RefreshError(
449 "No access token in response.", response_data, retryable=False
450 )
451 raise new_exc from caught_exc
452
453 refresh_token = response_data.get("refresh_token", refresh_token)
454 expiry = _parse_expiry(response_data)
455
456 return access_token, refresh_token, expiry, response_data
457
458
459def refresh_grant(
460 request,
461 token_uri,
462 refresh_token,
463 client_id,
464 client_secret,
465 scopes=None,
466 rapt_token=None,
467 can_retry=True,
468):
469 """Implements the OAuth 2.0 refresh token grant.
470
471 For more details, see `rfc678 section 6`_.
472
473 Args:
474 request (google.auth.transport.Request): A callable used to make
475 HTTP requests.
476 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
477 URI.
478 refresh_token (str): The refresh token to use to get a new access
479 token.
480 client_id (str): The OAuth 2.0 application's client ID.
481 client_secret (str): The Oauth 2.0 appliaction's client secret.
482 scopes (Optional(Sequence[str])): Scopes to request. If present, all
483 scopes must be authorized for the refresh token. Useful if refresh
484 token has a wild card scope (e.g.
485 'https://www.googleapis.com/auth/any-api').
486 rapt_token (Optional(str)): The reauth Proof Token.
487 can_retry (bool): Enable or disable request retry behavior.
488
489 Returns:
490 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
491 token, new or current refresh token, expiration, and additional data
492 returned by the token endpoint.
493
494 Raises:
495 google.auth.exceptions.RefreshError: If the token endpoint returned
496 an error.
497
498 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
499 """
500 body = {
501 "grant_type": _REFRESH_GRANT_TYPE,
502 "client_id": client_id,
503 "client_secret": client_secret,
504 "refresh_token": refresh_token,
505 }
506 if scopes:
507 body["scope"] = " ".join(scopes)
508 if rapt_token:
509 body["rapt"] = rapt_token
510
511 response_data = _token_endpoint_request(
512 request, token_uri, body, can_retry=can_retry
513 )
514 return _handle_refresh_grant_response(response_data, refresh_token)
515
516
517def _lookup_trust_boundary(request, url, headers=None):
518 """Implements the global lookup of a credential trust boundary.
519 For the lookup, we send a request to the global lookup endpoint and then
520 parse the response. Service account credentials, workload identity
521 pools and workforce pools implementation may have trust boundaries configured.
522 Args:
523 request (google.auth.transport.Request): A callable used to make
524 HTTP requests.
525 url (str): The trust boundary lookup url.
526 headers (Optional[Mapping[str, str]]): The headers for the request.
527 Returns:
528 Mapping[str,list|str]: A dictionary containing
529 "locations" as a list of allowed locations as strings and
530 "encodedLocations" as a hex string.
531 e.g:
532 {
533 "locations": [
534 "us-central1", "us-east1", "europe-west1", "asia-east1"
535 ],
536 "encodedLocations": "0xA30"
537 }
538 If the credential is not set up with explicit trust boundaries, a trust boundary
539 of "all" will be returned as a default response.
540 {
541 "locations": [],
542 "encodedLocations": "0x0"
543 }
544 Raises:
545 exceptions.RefreshError: If the response status code is not 200.
546 exceptions.MalformedError: If the response is not in a valid format.
547 """
548
549 response_data = _lookup_trust_boundary_request(request, url, headers=headers)
550 # In case of no-op response, the "locations" list may or may not be present as an empty list.
551 if "encodedLocations" not in response_data:
552 raise exceptions.MalformedError(
553 "Invalid trust boundary info: {}".format(response_data)
554 )
555 return response_data
556
557
558def _lookup_trust_boundary_request(request, url, can_retry=True, headers=None):
559 """Makes a request to the trust boundary lookup endpoint.
560
561 Args:
562 request (google.auth.transport.Request): A callable used to make
563 HTTP requests.
564 url (str): The trust boundary lookup url.
565 can_retry (bool): Enable or disable request retry behavior. Defaults to true.
566 headers (Optional[Mapping[str, str]]): The headers for the request.
567
568 Returns:
569 Mapping[str, str]: The JSON-decoded response data.
570
571 Raises:
572 google.auth.exceptions.RefreshError: If the token endpoint returned
573 an error.
574 """
575 (
576 response_status_ok,
577 response_data,
578 retryable_error,
579 ) = _lookup_trust_boundary_request_no_throw(request, url, can_retry, headers)
580 if not response_status_ok:
581 _handle_error_response(response_data, retryable_error)
582 return response_data
583
584
585def _lookup_trust_boundary_request_no_throw(request, url, can_retry=True, headers=None):
586 """Makes a request to the trust boundary lookup endpoint. This
587 function doesn't throw on response errors.
588
589 Args:
590 request (google.auth.transport.Request): A callable used to make
591 HTTP requests.
592 url (str): The trust boundary lookup url.
593 can_retry (bool): Enable or disable request retry behavior. Defaults to true.
594 headers (Optional[Mapping[str, str]]): The headers for the request.
595
596 Returns:
597 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
598 if the request is successful, a mapping for the JSON-decoded response
599 data and in the case of an error a boolean indicating if the error
600 is retryable.
601 """
602
603 response_data = {}
604 retryable_error = False
605
606 retries = _exponential_backoff.ExponentialBackoff()
607 for _ in retries:
608 response = request(method="GET", url=url, headers=headers)
609 response_body = (
610 response.data.decode("utf-8")
611 if hasattr(response.data, "decode")
612 else response.data
613 )
614
615 try:
616 # response_body should be a JSON
617 response_data = json.loads(response_body)
618 except ValueError:
619 response_data = response_body
620
621 if response.status == http_client.OK:
622 return True, response_data, None
623
624 retryable_error = _can_retry(
625 status_code=response.status, response_data=response_data
626 )
627
628 if not can_retry or not retryable_error:
629 return False, response_data, retryable_error
630
631 return False, response_data, retryable_error