Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/oauth2/_client.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

165 statements  

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 logging 

30import urllib 

31 

32from google.auth import _exponential_backoff 

33from google.auth import _helpers 

34from google.auth import credentials 

35from google.auth import exceptions 

36from google.auth import jwt 

37from google.auth import metrics 

38from google.auth import transport 

39 

40_LOGGER = logging.getLogger(__name__) 

41 

42_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded" 

43_JSON_CONTENT_TYPE = "application/json" 

44_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer" 

45_REFRESH_GRANT_TYPE = "refresh_token" 

46_BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT = 3 

47 

48 

49def _handle_error_response(response_data, retryable_error): 

50 """Translates an error response into an exception. 

51 

52 Args: 

53 response_data (Mapping | str): The decoded response data. 

54 retryable_error Optional[bool]: A boolean indicating if an error is retryable. 

55 Defaults to False. 

56 

57 Raises: 

58 google.auth.exceptions.RefreshError: The errors contained in response_data. 

59 """ 

60 

61 retryable_error = retryable_error if retryable_error else False 

62 

63 if isinstance(response_data, str): 

64 raise exceptions.RefreshError(response_data, retryable=retryable_error) 

65 try: 

66 error_details = "{}: {}".format( 

67 response_data["error"], response_data.get("error_description") 

68 ) 

69 # If no details could be extracted, use the response data. 

70 except (KeyError, ValueError): 

71 error_details = json.dumps(response_data) 

72 

73 raise exceptions.RefreshError( 

74 error_details, response_data, retryable=retryable_error 

75 ) 

76 

77 

78def _can_retry(status_code, response_data): 

79 """Checks if a request can be retried by inspecting the status code 

80 and response body of the request. 

81 

82 Args: 

83 status_code (int): The response status code. 

84 response_data (Mapping | str): The decoded response data. 

85 

86 Returns: 

87 bool: True if the response is retryable. False otherwise. 

88 """ 

89 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: 

90 return True 

91 

92 try: 

93 # For a failed response, response_body could be a string 

94 error_desc = response_data.get("error_description") or "" 

95 error_code = response_data.get("error") or "" 

96 

97 if not isinstance(error_code, str) or not isinstance(error_desc, str): 

98 return False 

99 

100 # Per Oauth 2.0 RFC https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2.1 

101 # This is needed because a redirect will not return a 500 status code. 

102 retryable_error_descriptions = { 

103 "internal_failure", 

104 "server_error", 

105 "temporarily_unavailable", 

106 } 

107 

108 if any(e in retryable_error_descriptions for e in (error_code, error_desc)): 

109 return True 

110 

111 except AttributeError: 

112 pass 

113 

114 return False 

115 

116 

117def _parse_expiry(response_data): 

118 """Parses the expiry field from a response into a datetime. 

119 

120 Args: 

121 response_data (Mapping): The JSON-parsed response data. 

122 

123 Returns: 

124 Optional[datetime]: The expiration or ``None`` if no expiration was 

125 specified. 

126 """ 

127 expires_in = response_data.get("expires_in", None) 

128 

129 if expires_in is not None: 

130 # Some services do not respect the OAUTH2.0 RFC and send expires_in as a 

131 # JSON String. 

132 if isinstance(expires_in, str): 

133 expires_in = int(expires_in) 

134 

135 return _helpers.utcnow() + datetime.timedelta(seconds=expires_in) 

136 else: 

137 return None 

138 

139 

140def _token_endpoint_request_no_throw( 

141 request, 

142 token_uri, 

143 body, 

144 access_token=None, 

145 use_json=False, 

146 can_retry=True, 

147 headers=None, 

148 **kwargs 

149): 

150 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 

151 This function doesn't throw on response errors. 

152 

153 Args: 

154 request (google.auth.transport.Request): A callable used to make 

155 HTTP requests. 

156 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 

157 URI. 

158 body (Mapping[str, str]): The parameters to send in the request body. 

159 access_token (Optional(str)): The access token needed to make the request. 

160 use_json (Optional(bool)): Use urlencoded format or json format for the 

161 content type. The default value is False. 

162 can_retry (bool): Enable or disable request retry behavior. 

163 headers (Optional[Mapping[str, str]]): The headers for the request. 

164 kwargs: Additional arguments passed on to the request method. The 

165 kwargs will be passed to `requests.request` method, see: 

166 https://docs.python-requests.org/en/latest/api/#requests.request. 

167 For example, you can use `cert=("cert_pem_path", "key_pem_path")` 

168 to set up client side SSL certificate, and use 

169 `verify="ca_bundle_path"` to set up the CA certificates for sever 

170 side SSL certificate verification. 

171 

172 Returns: 

173 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating 

174 if the request is successful, a mapping for the JSON-decoded response 

175 data and in the case of an error a boolean indicating if the error 

176 is retryable. 

177 """ 

178 if use_json: 

179 headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE} 

180 body = json.dumps(body).encode("utf-8") 

181 else: 

182 headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE} 

183 body = urllib.parse.urlencode(body).encode("utf-8") 

184 

185 if access_token: 

186 headers_to_use["Authorization"] = "Bearer {}".format(access_token) 

187 

188 if headers: 

189 headers_to_use.update(headers) 

190 

191 response_data = {} 

192 retryable_error = False 

193 

194 retries = _exponential_backoff.ExponentialBackoff() 

195 for _ in retries: 

196 response = request( 

197 method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs 

198 ) 

199 response_body = ( 

200 response.data.decode("utf-8") 

201 if hasattr(response.data, "decode") 

202 else response.data 

203 ) 

204 

205 try: 

206 # response_body should be a JSON 

207 response_data = json.loads(response_body) 

208 except ValueError: 

209 response_data = response_body 

210 

211 if response.status == http_client.OK: 

212 return True, response_data, None 

213 

214 retryable_error = _can_retry( 

215 status_code=response.status, response_data=response_data 

216 ) 

217 

218 if not can_retry or not retryable_error: 

219 return False, response_data, retryable_error 

220 

221 return False, response_data, retryable_error 

222 

223 

224def _token_endpoint_request( 

225 request, 

226 token_uri, 

227 body, 

228 access_token=None, 

229 use_json=False, 

230 can_retry=True, 

231 headers=None, 

232 **kwargs 

233): 

234 """Makes a request to the OAuth 2.0 authorization server's token endpoint. 

235 

236 Args: 

237 request (google.auth.transport.Request): A callable used to make 

238 HTTP requests. 

239 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 

240 URI. 

241 body (Mapping[str, str]): The parameters to send in the request body. 

242 access_token (Optional(str)): The access token needed to make the request. 

243 use_json (Optional(bool)): Use urlencoded format or json format for the 

244 content type. The default value is False. 

245 can_retry (bool): Enable or disable request retry behavior. 

246 headers (Optional[Mapping[str, str]]): The headers for the request. 

247 kwargs: Additional arguments passed on to the request method. The 

248 kwargs will be passed to `requests.request` method, see: 

249 https://docs.python-requests.org/en/latest/api/#requests.request. 

250 For example, you can use `cert=("cert_pem_path", "key_pem_path")` 

251 to set up client side SSL certificate, and use 

252 `verify="ca_bundle_path"` to set up the CA certificates for sever 

253 side SSL certificate verification. 

254 

255 Returns: 

256 Mapping[str, str]: The JSON-decoded response data. 

257 

258 Raises: 

259 google.auth.exceptions.RefreshError: If the token endpoint returned 

260 an error. 

261 """ 

262 

263 ( 

264 response_status_ok, 

265 response_data, 

266 retryable_error, 

267 ) = _token_endpoint_request_no_throw( 

268 request, 

269 token_uri, 

270 body, 

271 access_token=access_token, 

272 use_json=use_json, 

273 can_retry=can_retry, 

274 headers=headers, 

275 **kwargs 

276 ) 

277 if not response_status_ok: 

278 _handle_error_response(response_data, retryable_error) 

279 return response_data 

280 

281 

282def jwt_grant(request, token_uri, assertion, can_retry=True): 

283 """Implements the JWT Profile for OAuth 2.0 Authorization Grants. 

284 

285 For more details, see `rfc7523 section 4`_. 

286 

287 Args: 

288 request (google.auth.transport.Request): A callable used to make 

289 HTTP requests. 

290 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 

291 URI. 

292 assertion (str): The OAuth 2.0 assertion. 

293 can_retry (bool): Enable or disable request retry behavior. 

294 

295 Returns: 

296 Tuple[str, Optional[datetime], Mapping[str, str]]: The access token, 

297 expiration, and additional data returned by the token endpoint. 

298 

299 Raises: 

300 google.auth.exceptions.RefreshError: If the token endpoint returned 

301 an error. 

302 

303 .. _rfc7523 section 4: https://tools.ietf.org/html/rfc7523#section-4 

304 """ 

305 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 

306 

307 response_data = _token_endpoint_request( 

308 request, 

309 token_uri, 

310 body, 

311 can_retry=can_retry, 

312 headers={ 

313 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion() 

314 }, 

315 ) 

316 

317 try: 

318 access_token = response_data["access_token"] 

319 except KeyError as caught_exc: 

320 new_exc = exceptions.RefreshError( 

321 "No access token in response.", response_data, retryable=False 

322 ) 

323 raise new_exc from caught_exc 

324 

325 expiry = _parse_expiry(response_data) 

326 

327 return access_token, expiry, response_data 

328 

329 

330def call_iam_generate_id_token_endpoint( 

331 request, 

332 iam_id_token_endpoint, 

333 signer_email, 

334 audience, 

335 access_token, 

336 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN, 

337): 

338 """Call iam.generateIdToken endpoint to get ID token. 

339 

340 Args: 

341 request (google.auth.transport.Request): A callable used to make 

342 HTTP requests. 

343 iam_id_token_endpoint (str): The IAM ID token endpoint to use. 

344 signer_email (str): The signer email used to form the IAM 

345 generateIdToken endpoint. 

346 audience (str): The audience for the ID token. 

347 access_token (str): The access token used to call the IAM endpoint. 

348 universe_domain (str): The universe domain for the request. The 

349 default is ``googleapis.com``. 

350 

351 Returns: 

352 Tuple[str, datetime]: The ID token and expiration. 

353 """ 

354 body = {"audience": audience, "includeEmail": "true", "useEmailAzp": "true"} 

355 

356 response_data = _token_endpoint_request( 

357 request, 

358 iam_id_token_endpoint.replace( 

359 credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain 

360 ).format(signer_email), 

361 body, 

362 access_token=access_token, 

363 use_json=True, 

364 ) 

365 

366 try: 

367 id_token = response_data["token"] 

368 except KeyError as caught_exc: 

369 new_exc = exceptions.RefreshError( 

370 "No ID token in response.", response_data, retryable=False 

371 ) 

372 raise new_exc from caught_exc 

373 

374 payload = jwt.decode(id_token, verify=False) 

375 expiry = _helpers.utcfromtimestamp(payload["exp"]) 

376 

377 return id_token, expiry 

378 

379 

380def id_token_jwt_grant(request, token_uri, assertion, can_retry=True): 

381 """Implements the JWT Profile for OAuth 2.0 Authorization Grants, but 

382 requests an OpenID Connect ID Token instead of an access token. 

383 

384 This is a variant on the standard JWT Profile that is currently unique 

385 to Google. This was added for the benefit of authenticating to services 

386 that require ID Tokens instead of access tokens or JWT bearer tokens. 

387 

388 Args: 

389 request (google.auth.transport.Request): A callable used to make 

390 HTTP requests. 

391 token_uri (str): The OAuth 2.0 authorization server's token endpoint 

392 URI. 

393 assertion (str): JWT token signed by a service account. The token's 

394 payload must include a ``target_audience`` claim. 

395 can_retry (bool): Enable or disable request retry behavior. 

396 

397 Returns: 

398 Tuple[str, Optional[datetime], Mapping[str, str]]: 

399 The (encoded) Open ID Connect ID Token, expiration, and additional 

400 data returned by the endpoint. 

401 

402 Raises: 

403 google.auth.exceptions.RefreshError: If the token endpoint returned 

404 an error. 

405 """ 

406 body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE} 

407 

408 response_data = _token_endpoint_request( 

409 request, 

410 token_uri, 

411 body, 

412 can_retry=can_retry, 

413 headers={ 

414 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion() 

415 }, 

416 ) 

417 

418 try: 

419 id_token = response_data["id_token"] 

420 except KeyError as caught_exc: 

421 new_exc = exceptions.RefreshError( 

422 "No ID token in response.", response_data, retryable=False 

423 ) 

424 raise new_exc from caught_exc 

425 

426 payload = jwt.decode(id_token, verify=False) 

427 expiry = _helpers.utcfromtimestamp(payload["exp"]) 

428 

429 return id_token, expiry, response_data 

430 

431 

432def _handle_refresh_grant_response(response_data, refresh_token): 

433 """Extract tokens from refresh grant response. 

434 

435 Args: 

436 response_data (Mapping[str, str]): Refresh grant response data. 

437 refresh_token (str): Current refresh token. 

438 

439 Returns: 

440 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access token, 

441 refresh token, expiration, and additional data returned by the token 

442 endpoint. If response_data doesn't have refresh token, then the current 

443 refresh token will be returned. 

444 

445 Raises: 

446 google.auth.exceptions.RefreshError: If the token endpoint returned 

447 an error. 

448 """ 

449 try: 

450 access_token = response_data["access_token"] 

451 except KeyError as caught_exc: 

452 new_exc = exceptions.RefreshError( 

453 "No access token in response.", response_data, retryable=False 

454 ) 

455 raise new_exc from caught_exc 

456 

457 refresh_token = response_data.get("refresh_token", refresh_token) 

458 expiry = _parse_expiry(response_data) 

459 

460 return access_token, refresh_token, expiry, response_data 

461 

462 

463def refresh_grant( 

464 request, 

465 token_uri, 

466 refresh_token, 

467 client_id, 

468 client_secret, 

469 scopes=None, 

470 rapt_token=None, 

471 can_retry=True, 

472): 

473 """Implements the OAuth 2.0 refresh token grant. 

474 

475 For more details, see `rfc678 section 6`_. 

476 

477 Args: 

478 request (google.auth.transport.Request): A callable used to make 

479 HTTP requests. 

480 token_uri (str): The OAuth 2.0 authorizations server's token endpoint 

481 URI. 

482 refresh_token (str): The refresh token to use to get a new access 

483 token. 

484 client_id (str): The OAuth 2.0 application's client ID. 

485 client_secret (str): The Oauth 2.0 appliaction's client secret. 

486 scopes (Optional(Sequence[str])): Scopes to request. If present, all 

487 scopes must be authorized for the refresh token. Useful if refresh 

488 token has a wild card scope (e.g. 

489 'https://www.googleapis.com/auth/any-api'). 

490 rapt_token (Optional(str)): The reauth Proof Token. 

491 can_retry (bool): Enable or disable request retry behavior. 

492 

493 Returns: 

494 Tuple[str, str, Optional[datetime], Mapping[str, str]]: The access 

495 token, new or current refresh token, expiration, and additional data 

496 returned by the token endpoint. 

497 

498 Raises: 

499 google.auth.exceptions.RefreshError: If the token endpoint returned 

500 an error. 

501 

502 .. _rfc6748 section 6: https://tools.ietf.org/html/rfc6749#section-6 

503 """ 

504 body = { 

505 "grant_type": _REFRESH_GRANT_TYPE, 

506 "client_id": client_id, 

507 "client_secret": client_secret, 

508 "refresh_token": refresh_token, 

509 } 

510 if scopes: 

511 body["scope"] = " ".join(scopes) 

512 if rapt_token: 

513 body["rapt"] = rapt_token 

514 

515 response_data = _token_endpoint_request( 

516 request, token_uri, body, can_retry=can_retry 

517 ) 

518 return _handle_refresh_grant_response(response_data, refresh_token) 

519 

520 

521def _lookup_regional_access_boundary(request, url, headers=None, fail_fast=False): 

522 """Implements the global lookup of a credential Regional Access Boundary. 

523 For the lookup, we send a request to the global lookup endpoint and then 

524 parse the response. Service account credentials, workload identity 

525 pools and workforce pools implementation may have Regional Access Boundaries configured. 

526 Args: 

527 request (google.auth.transport.Request): A callable used to make 

528 HTTP requests. 

529 url (str): The Regional Access Boundary lookup url. 

530 headers (Optional[Mapping[str, str]]): The headers for the request. 

531 fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries). 

532 Returns: 

533 Optional[Mapping[str,list|str]]: A dictionary containing 

534 "locations" as a list of allowed locations as strings and 

535 "encodedLocations" as a hex string. 

536 e.g: 

537 { 

538 "locations": [ 

539 "us-central1", "us-east1", "europe-west1", "asia-east1" 

540 ], 

541 "encodedLocations": "0xA30" 

542 } 

543 """ 

544 

545 response_data = _lookup_regional_access_boundary_request( 

546 request, url, headers=headers, fail_fast=fail_fast 

547 ) 

548 if response_data is None: 

549 # Error was already logged by _lookup_regional_access_boundary_request 

550 return None 

551 

552 if "encodedLocations" not in response_data: 

553 _LOGGER.error( 

554 "Regional Access Boundary response malformed: missing 'encodedLocations' key in %s", 

555 response_data, 

556 ) 

557 return None 

558 return response_data 

559 

560 

561def _lookup_regional_access_boundary_request( 

562 request, url, can_retry=True, headers=None, fail_fast=False 

563): 

564 """Makes a request to the Regional Access Boundary lookup endpoint. 

565 

566 Args: 

567 request (google.auth.transport.Request): A callable used to make 

568 HTTP requests. 

569 url (str): The Regional Access Boundary lookup url. 

570 can_retry (bool): Enable or disable request retry behavior. Defaults to true. 

571 headers (Optional[Mapping[str, str]]): The headers for the request. 

572 fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries). 

573 

574 Returns: 

575 Optional[Mapping[str, str]]: The JSON-decoded response data on success, or None on failure. 

576 """ 

577 ( 

578 response_status_ok, 

579 response_data, 

580 retryable_error, 

581 ) = _lookup_regional_access_boundary_request_no_throw( 

582 request, url, can_retry=can_retry, headers=headers, fail_fast=fail_fast 

583 ) 

584 if not response_status_ok: 

585 _LOGGER.warning( 

586 "Regional Access Boundary HTTP request failed after retries: response_data=%s, retryable_error=%s", 

587 response_data, 

588 retryable_error, 

589 ) 

590 return None 

591 return response_data 

592 

593 

594def _lookup_regional_access_boundary_request_no_throw( 

595 request, url, can_retry=True, headers=None, fail_fast=False 

596): 

597 """Makes a request to the Regional Access Boundary lookup endpoint. This 

598 function doesn't throw on response errors. 

599 

600 Args: 

601 request (google.auth.transport.Request): A callable used to make 

602 HTTP requests. 

603 url (str): The Regional Access Boundary lookup url. 

604 can_retry (bool): Enable or disable request retry behavior. Defaults to true. 

605 headers (Optional[Mapping[str, str]]): The headers for the request. 

606 fail_fast (bool): Whether the lookup should fail fast (uses a short timeout and no retries). 

607 

608 Returns: 

609 Tuple(bool, Mapping[str, str], Optional[bool]): A boolean indicating 

610 if the request is successful, a mapping for the JSON-decoded response 

611 data and in the case of an error a boolean indicating if the error 

612 is retryable. 

613 """ 

614 

615 response_data = {} 

616 retryable_error = False 

617 

618 timeout = _BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT if fail_fast else None 

619 total_attempts = 1 if fail_fast else 6 

620 retries = _exponential_backoff.ExponentialBackoff(total_attempts=total_attempts) 

621 

622 for _ in retries: 

623 response = request(method="GET", url=url, headers=headers, timeout=timeout) 

624 response_body = ( 

625 response.data.decode("utf-8") 

626 if hasattr(response.data, "decode") 

627 else response.data 

628 ) 

629 

630 try: 

631 # response_body should be a JSON 

632 response_data = json.loads(response_body) 

633 except ValueError: 

634 response_data = response_body 

635 

636 if response.status == http_client.OK: 

637 return True, response_data, None 

638 

639 retryable_error = _can_retry( 

640 status_code=response.status, response_data=response_data 

641 ) 

642 # Add 502 (Bad Gateway) as a retryable error for RAB lookups. 

643 if response.status == http_client.BAD_GATEWAY: 

644 retryable_error = True 

645 

646 if not can_retry or not retryable_error: 

647 return False, response_data, retryable_error 

648 

649 return False, response_data, retryable_error