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 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
260 request,
261 token_uri,
262 body,
263 access_token=access_token,
264 use_json=use_json,
265 can_retry=can_retry,
266 headers=headers,
267 **kwargs
268 )
269 if not response_status_ok:
270 _handle_error_response(response_data, retryable_error)
271 return response_data
272
273
274def jwt_grant(request, token_uri, assertion, can_retry=True):
275 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
276
277 For more details, see `rfc7523 section 4`_.
278
279 Args:
280 request (google.auth.transport.Request): A callable used to make
281 HTTP requests.
282 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
283 URI.
284 assertion (str): The OAuth 2.0 assertion.
285 can_retry (bool): Enable or disable request retry behavior.
286
287 Returns:
288 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
289 expiration, and additional data returned by the token endpoint.
290
291 Raises:
292 google.auth.exceptions.RefreshError: If the token endpoint returned
293 an error.
294
295 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
296 """
297 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
298
299 response_data = _token_endpoint_request(
300 request,
301 token_uri,
302 body,
303 can_retry=can_retry,
304 headers={
305 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
306 },
307 )
308
309 try:
310 access_token = response_data["access_token"]
311 except KeyError as caught_exc:
312 new_exc = exceptions.RefreshError(
313 "No access token in response.", response_data, retryable=False
314 )
315 raise new_exc from caught_exc
316
317 expiry = _parse_expiry(response_data)
318
319 return access_token, expiry, response_data
320
321
322def call_iam_generate_id_token_endpoint(
323 request,
324 iam_id_token_endpoint,
325 signer_email,
326 audience,
327 access_token,
328 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,
329):
330 """Call iam.generateIdToken endpoint to get ID token.
331
332 Args:
333 request (google.auth.transport.Request): A callable used to make
334 HTTP requests.
335 iam_id_token_endpoint (str): The IAM ID token endpoint to use.
336 signer_email (str): The signer email used to form the IAM
337 generateIdToken endpoint.
338 audience (str): The audience for the ID token.
339 access_token (str): The access token used to call the IAM endpoint.
340
341 Returns:
342 Tuple[str, datetime]: The ID token and expiration.
343 """
344 body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"}
345
346 response_data = _token_endpoint_request(
347 request,
348 iam_id_token_endpoint.replace(
349 credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain
350 ).format(signer_email),
351 body,
352 access_token=access_token,
353 use_json=True,
354 )
355
356 try:
357 id_token = response_data["token"]
358 except KeyError as caught_exc:
359 new_exc = exceptions.RefreshError(
360 "No ID token in response.", response_data, retryable=False
361 )
362 raise new_exc from caught_exc
363
364 payload = jwt.decode(id_token, verify=False)
365 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
366
367 return id_token, expiry
368
369
370def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
371 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
372 requests an OpenID Connect ID Token instead of an access token.
373
374 This is a variant on the standard JWT Profile that is currently unique
375 to Google. This was added for the benefit of authenticating to services
376 that require ID Tokens instead of access tokens or JWT bearer tokens.
377
378 Args:
379 request (google.auth.transport.Request): A callable used to make
380 HTTP requests.
381 token_uri (str): The OAuth 2.0 authorization server's token endpoint
382 URI.
383 assertion (str): JWT token signed by a service account. The token's
384 payload must include a ``target_audience`` claim.
385 can_retry (bool): Enable or disable request retry behavior.
386
387 Returns:
388 Tuple[str, Optional[datetime], Mapping[str, str]]:
389 The (encoded) Open ID Connect ID Token, expiration, and additional
390 data returned by the endpoint.
391
392 Raises:
393 google.auth.exceptions.RefreshError: If the token endpoint returned
394 an error.
395 """
396 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
397
398 response_data = _token_endpoint_request(
399 request,
400 token_uri,
401 body,
402 can_retry=can_retry,
403 headers={
404 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
405 },
406 )
407
408 try:
409 id_token = response_data["id_token"]
410 except KeyError as caught_exc:
411 new_exc = exceptions.RefreshError(
412 "No ID token in response.", response_data, retryable=False
413 )
414 raise new_exc from caught_exc
415
416 payload = jwt.decode(id_token, verify=False)
417 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
418
419 return id_token, expiry, response_data
420
421
422def _handle_refresh_grant_response(response_data, refresh_token):
423 """Extract tokens from refresh grant response.
424
425 Args:
426 response_data (Mapping[str, str]): Refresh grant response data.
427 refresh_token (str): Current refresh token.
428
429 Returns:
430 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
431 refresh token, expiration, and additional data returned by the token
432 endpoint. If response_data doesn't have refresh token, then the current
433 refresh token will be returned.
434
435 Raises:
436 google.auth.exceptions.RefreshError: If the token endpoint returned
437 an error.
438 """
439 try:
440 access_token = response_data["access_token"]
441 except KeyError as caught_exc:
442 new_exc = exceptions.RefreshError(
443 "No access token in response.", response_data, retryable=False
444 )
445 raise new_exc from caught_exc
446
447 refresh_token = response_data.get("refresh_token", refresh_token)
448 expiry = _parse_expiry(response_data)
449
450 return access_token, refresh_token, expiry, response_data
451
452
453def refresh_grant(
454 request,
455 token_uri,
456 refresh_token,
457 client_id,
458 client_secret,
459 scopes=None,
460 rapt_token=None,
461 can_retry=True,
462):
463 """Implements the OAuth 2.0 refresh token grant.
464
465 For more details, see `rfc678 section 6`_.
466
467 Args:
468 request (google.auth.transport.Request): A callable used to make
469 HTTP requests.
470 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
471 URI.
472 refresh_token (str): The refresh token to use to get a new access
473 token.
474 client_id (str): The OAuth 2.0 application's client ID.
475 client_secret (str): The Oauth 2.0 appliaction's client secret.
476 scopes (Optional(Sequence[str])): Scopes to request. If present, all
477 scopes must be authorized for the refresh token. Useful if refresh
478 token has a wild card scope (e.g.
479 'https://www.googleapis.com/auth/any-api').
480 rapt_token (Optional(str)): The reauth Proof Token.
481 can_retry (bool): Enable or disable request retry behavior.
482
483 Returns:
484 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
485 token, new or current refresh token, expiration, and additional data
486 returned by the token endpoint.
487
488 Raises:
489 google.auth.exceptions.RefreshError: If the token endpoint returned
490 an error.
491
492 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
493 """
494 body = {
495 "grant_type": _REFRESH_GRANT_TYPE,
496 "client_id": client_id,
497 "client_secret": client_secret,
498 "refresh_token": refresh_token,
499 }
500 if scopes:
501 body["scope"] = " ".join(scopes)
502 if rapt_token:
503 body["rapt"] = rapt_token
504
505 response_data = _token_endpoint_request(
506 request, token_uri, body, can_retry=can_retry
507 )
508 return _handle_refresh_grant_response(response_data, refresh_token)