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 exceptions
34from google.auth import jwt
35from google.auth import metrics
36from google.auth import transport
37
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
43
44def _handle_error_response(response_data, retryable_error):
45 """Translates an error response into an exception.
46
47 Args:
48 response_data (Mapping | str): The decoded response data.
49 retryable_error Optional[bool]: A boolean indicating if an error is retryable.
50 Defaults to False.
51
52 Raises:
53 google.auth.exceptions.RefreshError: The errors contained in response_data.
54 """
55
56 retryable_error = retryable_error if retryable_error else False
57
58 if isinstance(response_data, str):
59 raise exceptions.RefreshError(response_data, retryable=retryable_error)
60 try:
61 error_details = "{}: {}".format(
62 response_data["error"], response_data.get("error_description")
63 )
64 # If no details could be extracted, use the response data.
65 except (KeyError, ValueError):
66 error_details = json.dumps(response_data)
67
68 raise exceptions.RefreshError(
69 error_details, response_data, retryable=retryable_error
70 )
71
72
73def _can_retry(status_code, response_data):
74 """Checks if a request can be retried by inspecting the status code
75 and response body of the request.
76
77 Args:
78 status_code (int): The response status code.
79 response_data (Mapping | str): The decoded response data.
80
81 Returns:
82 bool: True if the response is retryable. False otherwise.
83 """
84 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES:
85 return True
86
87 try:
88 # For a failed response, response_body could be a string
89 error_desc = response_data.get("error_description") or ""
90 error_code = response_data.get("error") or ""
91
92 if not isinstance(error_code, str) or not isinstance(error_desc, str):
93 return False
94
95 # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1
96 # This is needed because a redirect will not return a 500 status code.
97 retryable_error_descriptions = {
98 "internal_failure",
99 "server_error",
100 "temporarily_unavailable",
101 }
102
103 if any(e in retryable_error_descriptions for e in (error_code, error_desc)):
104 return True
105
106 except AttributeError:
107 pass
108
109 return False
110
111
112def _parse_expiry(response_data):
113 """Parses the expiry field from a response into a datetime.
114
115 Args:
116 response_data (Mapping): The JSON-parsed response data.
117
118 Returns:
119 Optional[datetime]: The expiration or ``None`` if no expiration was
120 specified.
121 """
122 expires_in = response_data.get("expires_in", None)
123
124 if expires_in is not None:
125 # Some services do not respect the OAUTH2.0 RFC and send expires_in as a
126 # JSON String.
127 if isinstance(expires_in, str):
128 expires_in = int(expires_in)
129
130 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in)
131 else:
132 return None
133
134
135def _token_endpoint_request_no_throw(
136 request,
137 token_uri,
138 body,
139 access_token=None,
140 use_json=False,
141 can_retry=True,
142 headers=None,
143 **kwargs
144):
145 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
146 This function doesn't throw on response errors.
147
148 Args:
149 request (google.auth.transport.Request): A callable used to make
150 HTTP requests.
151 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
152 URI.
153 body (Mapping[str, str]): The parameters to send in the request body.
154 access_token (Optional(str)): The access token needed to make the request.
155 use_json (Optional(bool)): Use urlencoded format or json format for the
156 content type. The default value is False.
157 can_retry (bool): Enable or disable request retry behavior.
158 headers (Optional[Mapping[str, str]]): The headers for the request.
159 kwargs: Additional arguments passed on to the request method. The
160 kwargs will be passed to `requests.request` method, see:
161 https://docs.python-requests.org/en/latest/api/#requests.request.
162 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
163 to set up client side SSL certificate, and use
164 `verify="ca_bundle_path"` to set up the CA certificates for sever
165 side SSL certificate verification.
166
167 Returns:
168 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating
169 if the request is successful, a mapping for the JSON-decoded response
170 data and in the case of an error a boolean indicating if the error
171 is retryable.
172 """
173 if use_json:
174 headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
175 body = json.dumps(body).encode("utf-8")
176 else:
177 headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
178 body = urllib.parse.urlencode(body).encode("utf-8")
179
180 if access_token:
181 headers_to_use["Authorization"] = "Bearer {}".format(access_token)
182
183 if headers:
184 headers_to_use.update(headers)
185
186 def _perform_request():
187 response = request(
188 method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
189 )
190 response_body = (
191 response.data.decode("utf-8")
192 if hasattr(response.data, "decode")
193 else response.data
194 )
195 response_data = ""
196 try:
197 # response_body should be a JSON
198 response_data = json.loads(response_body)
199 except ValueError:
200 response_data = response_body
201
202 if response.status == http_client.OK:
203 return True, response_data, None
204
205 retryable_error = _can_retry(
206 status_code=response.status, response_data=response_data
207 )
208
209 return False, response_data, retryable_error
210
211 request_succeeded, response_data, retryable_error = _perform_request()
212
213 if request_succeeded or not retryable_error or not can_retry:
214 return request_succeeded, response_data, retryable_error
215
216 retries = _exponential_backoff.ExponentialBackoff()
217 for _ in retries:
218 request_succeeded, response_data, retryable_error = _perform_request()
219 if request_succeeded or not retryable_error:
220 return request_succeeded, response_data, retryable_error
221
222 return False, response_data, retryable_error
223
224
225def _token_endpoint_request(
226 request,
227 token_uri,
228 body,
229 access_token=None,
230 use_json=False,
231 can_retry=True,
232 headers=None,
233 **kwargs
234):
235 """Makes a request to the OAuth 2.0 authorization server's token endpoint.
236
237 Args:
238 request (google.auth.transport.Request): A callable used to make
239 HTTP requests.
240 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
241 URI.
242 body (Mapping[str, str]): The parameters to send in the request body.
243 access_token (Optional(str)): The access token needed to make the request.
244 use_json (Optional(bool)): Use urlencoded format or json format for the
245 content type. The default value is False.
246 can_retry (bool): Enable or disable request retry behavior.
247 headers (Optional[Mapping[str, str]]): The headers for the request.
248 kwargs: Additional arguments passed on to the request method. The
249 kwargs will be passed to `requests.request` method, see:
250 https://docs.python-requests.org/en/latest/api/#requests.request.
251 For example, you can use `cert=("cert_pem_path", "key_pem_path")`
252 to set up client side SSL certificate, and use
253 `verify="ca_bundle_path"` to set up the CA certificates for sever
254 side SSL certificate verification.
255
256 Returns:
257 Mapping[str, str]: The JSON-decoded response data.
258
259 Raises:
260 google.auth.exceptions.RefreshError: If the token endpoint returned
261 an error.
262 """
263
264 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw(
265 request,
266 token_uri,
267 body,
268 access_token=access_token,
269 use_json=use_json,
270 can_retry=can_retry,
271 headers=headers,
272 **kwargs
273 )
274 if not response_status_ok:
275 _handle_error_response(response_data, retryable_error)
276 return response_data
277
278
279def jwt_grant(request, token_uri, assertion, can_retry=True):
280 """Implements the JWT Profile for OAuth 2.0 Authorization Grants.
281
282 For more details, see `rfc7523 section 4`_.
283
284 Args:
285 request (google.auth.transport.Request): A callable used to make
286 HTTP requests.
287 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
288 URI.
289 assertion (str): The OAuth 2.0 assertion.
290 can_retry (bool): Enable or disable request retry behavior.
291
292 Returns:
293 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token,
294 expiration, and additional data returned by the token endpoint.
295
296 Raises:
297 google.auth.exceptions.RefreshError: If the token endpoint returned
298 an error.
299
300 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4
301 """
302 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
303
304 response_data = _token_endpoint_request(
305 request,
306 token_uri,
307 body,
308 can_retry=can_retry,
309 headers={
310 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
311 },
312 )
313
314 try:
315 access_token = response_data["access_token"]
316 except KeyError as caught_exc:
317 new_exc = exceptions.RefreshError(
318 "No access token in response.", response_data, retryable=False
319 )
320 raise new_exc from caught_exc
321
322 expiry = _parse_expiry(response_data)
323
324 return access_token, expiry, response_data
325
326
327def call_iam_generate_id_token_endpoint(
328 request, iam_id_token_endpoint, signer_email, audience, access_token
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.format(signer_email),
349 body,
350 access_token=access_token,
351 use_json=True,
352 )
353
354 try:
355 id_token = response_data["token"]
356 except KeyError as caught_exc:
357 new_exc = exceptions.RefreshError(
358 "No ID token in response.", response_data, retryable=False
359 )
360 raise new_exc from caught_exc
361
362 payload = jwt.decode(id_token, verify=False)
363 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
364
365 return id_token, expiry
366
367
368def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
369 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but
370 requests an OpenID Connect ID Token instead of an access token.
371
372 This is a variant on the standard JWT Profile that is currently unique
373 to Google. This was added for the benefit of authenticating to services
374 that require ID Tokens instead of access tokens or JWT bearer tokens.
375
376 Args:
377 request (google.auth.transport.Request): A callable used to make
378 HTTP requests.
379 token_uri (str): The OAuth 2.0 authorization server's token endpoint
380 URI.
381 assertion (str): JWT token signed by a service account. The token's
382 payload must include a ``target_audience`` claim.
383 can_retry (bool): Enable or disable request retry behavior.
384
385 Returns:
386 Tuple[str, Optional[datetime], Mapping[str, str]]:
387 The (encoded) Open ID Connect ID Token, expiration, and additional
388 data returned by the endpoint.
389
390 Raises:
391 google.auth.exceptions.RefreshError: If the token endpoint returned
392 an error.
393 """
394 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
395
396 response_data = _token_endpoint_request(
397 request,
398 token_uri,
399 body,
400 can_retry=can_retry,
401 headers={
402 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
403 },
404 )
405
406 try:
407 id_token = response_data["id_token"]
408 except KeyError as caught_exc:
409 new_exc = exceptions.RefreshError(
410 "No ID token in response.", response_data, retryable=False
411 )
412 raise new_exc from caught_exc
413
414 payload = jwt.decode(id_token, verify=False)
415 expiry = datetime.datetime.utcfromtimestamp(payload["exp"])
416
417 return id_token, expiry, response_data
418
419
420def _handle_refresh_grant_response(response_data, refresh_token):
421 """Extract tokens from refresh grant response.
422
423 Args:
424 response_data (Mapping[str, str]): Refresh grant response data.
425 refresh_token (str): Current refresh token.
426
427 Returns:
428 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token,
429 refresh token, expiration, and additional data returned by the token
430 endpoint. If response_data doesn't have refresh token, then the current
431 refresh token will be returned.
432
433 Raises:
434 google.auth.exceptions.RefreshError: If the token endpoint returned
435 an error.
436 """
437 try:
438 access_token = response_data["access_token"]
439 except KeyError as caught_exc:
440 new_exc = exceptions.RefreshError(
441 "No access token in response.", response_data, retryable=False
442 )
443 raise new_exc from caught_exc
444
445 refresh_token = response_data.get("refresh_token", refresh_token)
446 expiry = _parse_expiry(response_data)
447
448 return access_token, refresh_token, expiry, response_data
449
450
451def refresh_grant(
452 request,
453 token_uri,
454 refresh_token,
455 client_id,
456 client_secret,
457 scopes=None,
458 rapt_token=None,
459 can_retry=True,
460):
461 """Implements the OAuth 2.0 refresh token grant.
462
463 For more details, see `rfc678 section 6`_.
464
465 Args:
466 request (google.auth.transport.Request): A callable used to make
467 HTTP requests.
468 token_uri (str): The OAuth 2.0 authorizations server's token endpoint
469 URI.
470 refresh_token (str): The refresh token to use to get a new access
471 token.
472 client_id (str): The OAuth 2.0 application's client ID.
473 client_secret (str): The Oauth 2.0 appliaction's client secret.
474 scopes (Optional(Sequence[str])): Scopes to request. If present, all
475 scopes must be authorized for the refresh token. Useful if refresh
476 token has a wild card scope (e.g.
477 'https://www.googleapis.com/auth/any-api').
478 rapt_token (Optional(str)): The reauth Proof Token.
479 can_retry (bool): Enable or disable request retry behavior.
480
481 Returns:
482 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access
483 token, new or current refresh token, expiration, and additional data
484 returned by the token endpoint.
485
486 Raises:
487 google.auth.exceptions.RefreshError: If the token endpoint returned
488 an error.
489
490 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6
491 """
492 body = {
493 "grant_type": _REFRESH_GRANT_TYPE,
494 "client_id": client_id,
495 "client_secret": client_secret,
496 "refresh_token": refresh_token,
497 }
498 if scopes:
499 body["scope"] = " ".join(scopes)
500 if rapt_token:
501 body["rapt"] = rapt_token
502
503 response_data = _token_endpoint_request(
504 request, token_uri, body, can_retry=can_retry
505 )
506 return _handle_refresh_grant_response(response_data, refresh_token)