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

131 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:40 +0000

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_IAM_IDTOKEN_ENDPOINT = ( 

43 "https://iamcredentials.googleapis.com/v1/" 

44 + "projects/-/serviceAccounts/{}:generateIdToken" 

45) 

46 

47 

48def _handle_error_response(response_data, retryable_error): 

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

50 

51 Args: 

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

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

54 Defaults to False. 

55 

56 Raises: 

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

58 """ 

59 

60 retryable_error = retryable_error if retryable_error else False 

61 

62 if isinstance(response_data, str): 

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

64 try: 

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

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

67 ) 

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

69 except (KeyError, ValueError): 

70 error_details = json.dumps(response_data) 

71 

72 raise exceptions.RefreshError( 

73 error_details, response_data, retryable=retryable_error 

74 ) 

75 

76 

77def _can_retry(status_code, response_data): 

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

79 and response body of the request. 

80 

81 Args: 

82 status_code (int): The response status code. 

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

84 

85 Returns: 

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

87 """ 

88 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: 

89 return True 

90 

91 try: 

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

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

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

95 

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

97 return False 

98 

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

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

101 retryable_error_descriptions = { 

102 "internal_failure", 

103 "server_error", 

104 "temporarily_unavailable", 

105 } 

106 

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

108 return True 

109 

110 except AttributeError: 

111 pass 

112 

113 return False 

114 

115 

116def _parse_expiry(response_data): 

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

118 

119 Args: 

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

121 

122 Returns: 

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

124 specified. 

125 """ 

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

127 

128 if expires_in is not None: 

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

130 # JSON String. 

131 if isinstance(expires_in, str): 

132 expires_in = int(expires_in) 

133 

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

135 else: 

136 return None 

137 

138 

139def _token_endpoint_request_no_throw( 

140 request, 

141 token_uri, 

142 body, 

143 access_token=None, 

144 use_json=False, 

145 can_retry=True, 

146 headers=None, 

147 **kwargs 

148): 

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

150 This function doesn't throw on response errors. 

151 

152 Args: 

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

154 HTTP requests. 

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

156 URI. 

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

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

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

160 content type. The default value is False. 

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

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

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

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

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

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

167 to set up client side SSL certificate, and use 

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

169 side SSL certificate verification. 

170 

171 Returns: 

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

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

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

175 is retryable. 

176 """ 

177 if use_json: 

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

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

180 else: 

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

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

183 

184 if access_token: 

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

186 

187 if headers: 

188 headers_to_use.update(headers) 

189 

190 def _perform_request(): 

191 response = request( 

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

193 ) 

194 response_body = ( 

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

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

197 else response.data 

198 ) 

199 response_data = "" 

200 try: 

201 # response_body should be a JSON 

202 response_data = json.loads(response_body) 

203 except ValueError: 

204 response_data = response_body 

205 

206 if response.status == http_client.OK: 

207 return True, response_data, None 

208 

209 retryable_error = _can_retry( 

210 status_code=response.status, response_data=response_data 

211 ) 

212 

213 return False, response_data, retryable_error 

214 

215 request_succeeded, response_data, retryable_error = _perform_request() 

216 

217 if request_succeeded or not retryable_error or not can_retry: 

218 return request_succeeded, response_data, retryable_error 

219 

220 retries = _exponential_backoff.ExponentialBackoff() 

221 for _ in retries: 

222 request_succeeded, response_data, retryable_error = _perform_request() 

223 if request_succeeded or not retryable_error: 

224 return request_succeeded, response_data, retryable_error 

225 

226 return False, response_data, retryable_error 

227 

228 

229def _token_endpoint_request( 

230 request, 

231 token_uri, 

232 body, 

233 access_token=None, 

234 use_json=False, 

235 can_retry=True, 

236 headers=None, 

237 **kwargs 

238): 

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

240 

241 Args: 

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

243 HTTP requests. 

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

245 URI. 

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

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

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

249 content type. The default value is False. 

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

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

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

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

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

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

256 to set up client side SSL certificate, and use 

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

258 side SSL certificate verification. 

259 

260 Returns: 

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

262 

263 Raises: 

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

265 an error. 

266 """ 

267 

268 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( 

269 request, 

270 token_uri, 

271 body, 

272 access_token=access_token, 

273 use_json=use_json, 

274 can_retry=can_retry, 

275 headers=headers, 

276 **kwargs 

277 ) 

278 if not response_status_ok: 

279 _handle_error_response(response_data, retryable_error) 

280 return response_data 

281 

282 

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

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

285 

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

287 

288 Args: 

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

290 HTTP requests. 

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

292 URI. 

293 assertion (str): The OAuth 2.0 assertion. 

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

295 

296 Returns: 

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

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

299 

300 Raises: 

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

302 an error. 

303 

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

305 """ 

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

307 

308 response_data = _token_endpoint_request( 

309 request, 

310 token_uri, 

311 body, 

312 can_retry=can_retry, 

313 headers={ 

314 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion() 

315 }, 

316 ) 

317 

318 try: 

319 access_token = response_data["access_token"] 

320 except KeyError as caught_exc: 

321 new_exc = exceptions.RefreshError( 

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

323 ) 

324 raise new_exc from caught_exc 

325 

326 expiry = _parse_expiry(response_data) 

327 

328 return access_token, expiry, response_data 

329 

330 

331def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token): 

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

333 

334 Args: 

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

336 HTTP requests. 

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

338 generateIdToken endpoint. 

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

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

341 

342 Returns: 

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

344 """ 

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

346 

347 response_data = _token_endpoint_request( 

348 request, 

349 _IAM_IDTOKEN_ENDPOINT.format(signer_email), 

350 body, 

351 access_token=access_token, 

352 use_json=True, 

353 ) 

354 

355 try: 

356 id_token = response_data["token"] 

357 except KeyError as caught_exc: 

358 new_exc = exceptions.RefreshError( 

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

360 ) 

361 raise new_exc from caught_exc 

362 

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

364 expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 

365 

366 return id_token, expiry 

367 

368 

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

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

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

372 

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

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

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

376 

377 Args: 

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

379 HTTP requests. 

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

381 URI. 

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

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

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

385 

386 Returns: 

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

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

389 data returned by the endpoint. 

390 

391 Raises: 

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

393 an error. 

394 """ 

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

396 

397 response_data = _token_endpoint_request( 

398 request, 

399 token_uri, 

400 body, 

401 can_retry=can_retry, 

402 headers={ 

403 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion() 

404 }, 

405 ) 

406 

407 try: 

408 id_token = response_data["id_token"] 

409 except KeyError as caught_exc: 

410 new_exc = exceptions.RefreshError( 

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

412 ) 

413 raise new_exc from caught_exc 

414 

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

416 expiry = datetime.datetime.utcfromtimestamp(payload["exp"]) 

417 

418 return id_token, expiry, response_data 

419 

420 

421def _handle_refresh_grant_response(response_data, refresh_token): 

422 """Extract tokens from refresh grant response. 

423 

424 Args: 

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

426 refresh_token (str): Current refresh token. 

427 

428 Returns: 

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

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

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

432 refresh token will be returned. 

433 

434 Raises: 

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

436 an error. 

437 """ 

438 try: 

439 access_token = response_data["access_token"] 

440 except KeyError as caught_exc: 

441 new_exc = exceptions.RefreshError( 

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

443 ) 

444 raise new_exc from caught_exc 

445 

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

447 expiry = _parse_expiry(response_data) 

448 

449 return access_token, refresh_token, expiry, response_data 

450 

451 

452def refresh_grant( 

453 request, 

454 token_uri, 

455 refresh_token, 

456 client_id, 

457 client_secret, 

458 scopes=None, 

459 rapt_token=None, 

460 can_retry=True, 

461): 

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

463 

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

465 

466 Args: 

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

468 HTTP requests. 

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

470 URI. 

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

472 token. 

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

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

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

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

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

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

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

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

481 

482 Returns: 

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

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

485 returned by the token endpoint. 

486 

487 Raises: 

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

489 an error. 

490 

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

492 """ 

493 body = { 

494 "grant_type": _REFRESH_GRANT_TYPE, 

495 "client_id": client_id, 

496 "client_secret": client_secret, 

497 "refresh_token": refresh_token, 

498 } 

499 if scopes: 

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

501 if rapt_token: 

502 body["rapt"] = rapt_token 

503 

504 response_data = _token_endpoint_request( 

505 request, token_uri, body, can_retry=can_retry 

506 ) 

507 return _handle_refresh_grant_response(response_data, refresh_token)