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

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

159 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 ( 

33 _exponential_backoff, 

34 _helpers, 

35 credentials, 

36 exceptions, 

37 jwt, 

38 metrics, 

39 transport, 

40) 

41 

42_LOGGER = logging.getLogger(__name__) 

43 

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

45_JSON_CONTENT_TYPE = "application/json" 

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

47_REFRESH_GRANT_TYPE = "refresh_token" 

48_BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT = 3 

49 

50 

51def _handle_error_response(response_data, retryable_error): 

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

53 

54 Args: 

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

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

57 Defaults to False. 

58 

59 Raises: 

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

61 """ 

62 

63 retryable_error = retryable_error if retryable_error else False 

64 

65 if isinstance(response_data, str): 

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

67 try: 

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

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

70 ) 

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

72 except (KeyError, ValueError): 

73 error_details = json.dumps(response_data) 

74 

75 raise exceptions.RefreshError( 

76 error_details, response_data, retryable=retryable_error 

77 ) 

78 

79 

80def _can_retry(status_code, response_data): 

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

82 and response body of the request. 

83 

84 Args: 

85 status_code (int): The response status code. 

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

87 

88 Returns: 

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

90 """ 

91 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: 

92 return True 

93 

94 try: 

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

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

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

98 

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

100 return False 

101 

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

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

104 retryable_error_descriptions = { 

105 "internal_failure", 

106 "server_error", 

107 "temporarily_unavailable", 

108 } 

109 

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

111 return True 

112 

113 except AttributeError: 

114 pass 

115 

116 return False 

117 

118 

119def _parse_expiry(response_data): 

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

121 

122 Args: 

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

124 

125 Returns: 

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

127 specified. 

128 """ 

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

130 

131 if expires_in is not None: 

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

133 # JSON String. 

134 if isinstance(expires_in, str): 

135 expires_in = int(expires_in) 

136 

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

138 else: 

139 return None 

140 

141 

142def _token_endpoint_request_no_throw( 

143 request, 

144 token_uri, 

145 body, 

146 access_token=None, 

147 use_json=False, 

148 can_retry=True, 

149 headers=None, 

150 **kwargs, 

151): 

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

153 This function doesn't throw on response errors. 

154 

155 Args: 

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

157 HTTP requests. 

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

159 URI. 

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

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

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

163 content type. The default value is False. 

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

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

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

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

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

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

170 to set up client side SSL certificate, and use 

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

172 side SSL certificate verification. 

173 

174 Returns: 

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

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

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

178 is retryable. 

179 """ 

180 if use_json: 

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

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

183 else: 

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

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

186 

187 if access_token: 

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

189 

190 if headers: 

191 headers_to_use.update(headers) 

192 

193 response_data = {} 

194 retryable_error = False 

195 

196 retries = _exponential_backoff.ExponentialBackoff() 

197 for _ in retries: 

198 response = request( 

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

200 ) 

201 response_body = ( 

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

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

204 else response.data 

205 ) 

206 

207 try: 

208 # response_body should be a JSON 

209 response_data = json.loads(response_body) 

210 except ValueError: 

211 response_data = response_body 

212 

213 if response.status == http_client.OK: 

214 return True, response_data, None 

215 

216 retryable_error = _can_retry( 

217 status_code=response.status, response_data=response_data 

218 ) 

219 

220 if not can_retry or not retryable_error: 

221 return False, response_data, retryable_error 

222 

223 return False, response_data, retryable_error 

224 

225 

226def _token_endpoint_request( 

227 request, 

228 token_uri, 

229 body, 

230 access_token=None, 

231 use_json=False, 

232 can_retry=True, 

233 headers=None, 

234 **kwargs, 

235): 

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

237 

238 Args: 

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

240 HTTP requests. 

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

242 URI. 

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

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

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

246 content type. The default value is False. 

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

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

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

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

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

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

253 to set up client side SSL certificate, and use 

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

255 side SSL certificate verification. 

256 

257 Returns: 

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

259 

260 Raises: 

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

262 an error. 

263 """ 

264 

265 ( 

266 response_status_ok, 

267 response_data, 

268 retryable_error, 

269 ) = _token_endpoint_request_no_throw( 

270 request, 

271 token_uri, 

272 body, 

273 access_token=access_token, 

274 use_json=use_json, 

275 can_retry=can_retry, 

276 headers=headers, 

277 **kwargs, 

278 ) 

279 if not response_status_ok: 

280 _handle_error_response(response_data, retryable_error) 

281 return response_data 

282 

283 

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

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

286 

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

288 

289 Args: 

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

291 HTTP requests. 

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

293 URI. 

294 assertion (str): The OAuth 2.0 assertion. 

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

296 

297 Returns: 

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

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

300 

301 Raises: 

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

303 an error. 

304 

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

306 """ 

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

308 

309 response_data = _token_endpoint_request( 

310 request, 

311 token_uri, 

312 body, 

313 can_retry=can_retry, 

314 headers={ 

315 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion() 

316 }, 

317 ) 

318 

319 try: 

320 access_token = response_data["access_token"] 

321 except KeyError as caught_exc: 

322 new_exc = exceptions.RefreshError( 

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

324 ) 

325 raise new_exc from caught_exc 

326 

327 expiry = _parse_expiry(response_data) 

328 

329 return access_token, expiry, response_data 

330 

331 

332def call_iam_generate_id_token_endpoint( 

333 request, 

334 iam_id_token_endpoint, 

335 signer_email, 

336 audience, 

337 access_token, 

338 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN, 

339): 

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

341 

342 Args: 

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

344 HTTP requests. 

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

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

347 generateIdToken endpoint. 

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

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

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

351 default is ``googleapis.com``. 

352 

353 Returns: 

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

355 """ 

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

357 

358 response_data = _token_endpoint_request( 

359 request, 

360 iam_id_token_endpoint.replace( 

361 credentials.DEFAULT_UNIVERSE_DOMAIN, universe_domain 

362 ).format(signer_email), 

363 body, 

364 access_token=access_token, 

365 use_json=True, 

366 ) 

367 

368 try: 

369 id_token = response_data["token"] 

370 except KeyError as caught_exc: 

371 new_exc = exceptions.RefreshError( 

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

373 ) 

374 raise new_exc from caught_exc 

375 

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

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

378 

379 return id_token, expiry 

380 

381 

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

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

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

385 

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

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

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

389 

390 Args: 

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

392 HTTP requests. 

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

394 URI. 

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

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

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

398 

399 Returns: 

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

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

402 data returned by the endpoint. 

403 

404 Raises: 

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

406 an error. 

407 """ 

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

409 

410 response_data = _token_endpoint_request( 

411 request, 

412 token_uri, 

413 body, 

414 can_retry=can_retry, 

415 headers={ 

416 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion() 

417 }, 

418 ) 

419 

420 try: 

421 id_token = response_data["id_token"] 

422 except KeyError as caught_exc: 

423 new_exc = exceptions.RefreshError( 

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

425 ) 

426 raise new_exc from caught_exc 

427 

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

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

430 

431 return id_token, expiry, response_data 

432 

433 

434def _handle_refresh_grant_response(response_data, refresh_token): 

435 """Extract tokens from refresh grant response. 

436 

437 Args: 

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

439 refresh_token (str): Current refresh token. 

440 

441 Returns: 

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

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

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

445 refresh token will be returned. 

446 

447 Raises: 

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

449 an error. 

450 """ 

451 try: 

452 access_token = response_data["access_token"] 

453 except KeyError as caught_exc: 

454 new_exc = exceptions.RefreshError( 

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

456 ) 

457 raise new_exc from caught_exc 

458 

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

460 expiry = _parse_expiry(response_data) 

461 

462 return access_token, refresh_token, expiry, response_data 

463 

464 

465def refresh_grant( 

466 request, 

467 token_uri, 

468 refresh_token, 

469 client_id, 

470 client_secret, 

471 scopes=None, 

472 rapt_token=None, 

473 can_retry=True, 

474): 

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

476 

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

478 

479 Args: 

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

481 HTTP requests. 

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

483 URI. 

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

485 token. 

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

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

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

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

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

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

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

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

494 

495 Returns: 

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

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

498 returned by the token endpoint. 

499 

500 Raises: 

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

502 an error. 

503 

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

505 """ 

506 body = { 

507 "grant_type": _REFRESH_GRANT_TYPE, 

508 "client_id": client_id, 

509 "client_secret": client_secret, 

510 "refresh_token": refresh_token, 

511 } 

512 if scopes: 

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

514 if rapt_token: 

515 body["rapt"] = rapt_token 

516 

517 response_data = _token_endpoint_request( 

518 request, token_uri, body, can_retry=can_retry 

519 ) 

520 return _handle_refresh_grant_response(response_data, refresh_token) 

521 

522 

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

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

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

526 parse the response. Service account credentials, workload identity 

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

528 Args: 

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

530 HTTP requests. 

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

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

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

534 Returns: 

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

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

537 "encodedLocations" as a hex string. 

538 e.g: 

539 { 

540 "locations": [ 

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

542 ], 

543 "encodedLocations": "0xA30" 

544 } 

545 """ 

546 

547 response_data = _lookup_regional_access_boundary_request( 

548 request, url, headers=headers, fail_fast=fail_fast 

549 ) 

550 if response_data is None: 

551 # Error was already logged by _lookup_regional_access_boundary_request 

552 return None 

553 

554 if not isinstance(response_data, dict) or "encodedLocations" not in response_data: 

555 _LOGGER.error( 

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

557 response_data, 

558 ) 

559 return None 

560 return response_data 

561 

562 

563def _lookup_regional_access_boundary_request( 

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

565): 

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

567 

568 Args: 

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

570 HTTP requests. 

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

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

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

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

575 

576 Returns: 

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

578 """ 

579 ( 

580 response_status_ok, 

581 response_data, 

582 retryable_error, 

583 ) = _lookup_regional_access_boundary_request_no_throw( 

584 request, url, can_retry=can_retry, headers=headers, fail_fast=fail_fast 

585 ) 

586 if not response_status_ok: 

587 _LOGGER.debug( 

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

589 response_data, 

590 retryable_error, 

591 ) 

592 return None 

593 return response_data 

594 

595 

596def _lookup_regional_access_boundary_request_no_throw( 

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

598): 

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

600 function doesn't throw on response errors. 

601 

602 Args: 

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

604 HTTP requests. 

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

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

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

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

609 

610 Returns: 

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

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

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

614 is retryable. 

615 """ 

616 

617 response_data = {} 

618 retryable_error = False 

619 

620 timeout = _BLOCKING_REGIONAL_ACCESS_BOUNDARY_LOOKUP_TIMEOUT if fail_fast else None 

621 total_attempts = 1 if fail_fast else 6 

622 retries = _exponential_backoff.ExponentialBackoff(total_attempts=total_attempts) 

623 

624 for _ in retries: 

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

626 response_body = ( 

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

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

629 else response.data 

630 ) 

631 

632 try: 

633 # response_body should be a JSON 

634 response_data = json.loads(response_body) 

635 except ValueError: 

636 response_data = response_body 

637 

638 if response.status == http_client.OK: 

639 return True, response_data, None 

640 

641 retryable_error = _can_retry( 

642 status_code=response.status, response_data=response_data 

643 ) 

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

645 if response.status == http_client.BAD_GATEWAY: 

646 retryable_error = True 

647 

648 if not can_retry or not retryable_error: 

649 return False, response_data, retryable_error 

650 

651 return False, response_data, retryable_error