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

132 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-06 06:03 +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 json 

28 

29import six 

30from six.moves import http_client 

31from six.moves import urllib 

32 

33from google.auth import _exponential_backoff 

34from google.auth import _helpers 

35from google.auth import exceptions 

36from google.auth import jwt 

37from google.auth import metrics 

38from google.auth import transport 

39 

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

41_JSON_CONTENT_TYPE = "application/json" 

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

43_REFRESH_GRANT_TYPE = "refresh_token" 

44_IAM_IDTOKEN_ENDPOINT = ( 

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

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

47) 

48 

49 

50def _handle_error_response(response_data, retryable_error): 

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

52 

53 Args: 

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

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

56 Defaults to False. 

57 

58 Raises: 

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

60 """ 

61 

62 retryable_error = retryable_error if retryable_error else False 

63 

64 if isinstance(response_data, six.string_types): 

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

66 try: 

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

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

69 ) 

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

71 except (KeyError, ValueError): 

72 error_details = json.dumps(response_data) 

73 

74 raise exceptions.RefreshError( 

75 error_details, response_data, retryable=retryable_error 

76 ) 

77 

78 

79def _can_retry(status_code, response_data): 

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

81 and response body of the request. 

82 

83 Args: 

84 status_code (int): The response status code. 

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

86 

87 Returns: 

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

89 """ 

90 if status_code in transport.DEFAULT_RETRYABLE_STATUS_CODES: 

91 return True 

92 

93 try: 

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

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

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

97 

98 if not isinstance(error_code, six.string_types) or not isinstance( 

99 error_desc, six.string_types 

100 ): 

101 return False 

102 

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

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

105 retryable_error_descriptions = { 

106 "internal_failure", 

107 "server_error", 

108 "temporarily_unavailable", 

109 } 

110 

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

112 return True 

113 

114 except AttributeError: 

115 pass 

116 

117 return False 

118 

119 

120def _parse_expiry(response_data): 

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

122 

123 Args: 

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

125 

126 Returns: 

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

128 specified. 

129 """ 

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

131 

132 if expires_in is not None: 

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

134 # JSON String. 

135 if isinstance(expires_in, str): 

136 expires_in = int(expires_in) 

137 

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

139 else: 

140 return None 

141 

142 

143def _token_endpoint_request_no_throw( 

144 request, 

145 token_uri, 

146 body, 

147 access_token=None, 

148 use_json=False, 

149 can_retry=True, 

150 headers=None, 

151 **kwargs 

152): 

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

154 This function doesn't throw on response errors. 

155 

156 Args: 

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

158 HTTP requests. 

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

160 URI. 

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

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

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

164 content type. The default value is False. 

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

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

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

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

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

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

171 to set up client side SSL certificate, and use 

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

173 side SSL certificate verification. 

174 

175 Returns: 

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

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

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

179 is retryable. 

180 """ 

181 if use_json: 

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

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

184 else: 

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

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

187 

188 if access_token: 

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

190 

191 if headers: 

192 headers_to_use.update(headers) 

193 

194 def _perform_request(): 

195 response = request( 

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

197 ) 

198 response_body = ( 

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

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

201 else response.data 

202 ) 

203 response_data = "" 

204 try: 

205 # response_body should be a JSON 

206 response_data = json.loads(response_body) 

207 except ValueError: 

208 response_data = response_body 

209 

210 if response.status == http_client.OK: 

211 return True, response_data, None 

212 

213 retryable_error = _can_retry( 

214 status_code=response.status, response_data=response_data 

215 ) 

216 

217 return False, response_data, retryable_error 

218 

219 request_succeeded, response_data, retryable_error = _perform_request() 

220 

221 if request_succeeded or not retryable_error or not can_retry: 

222 return request_succeeded, response_data, retryable_error 

223 

224 retries = _exponential_backoff.ExponentialBackoff() 

225 for _ in retries: 

226 request_succeeded, response_data, retryable_error = _perform_request() 

227 if request_succeeded or not retryable_error: 

228 return request_succeeded, response_data, retryable_error 

229 

230 return False, response_data, retryable_error 

231 

232 

233def _token_endpoint_request( 

234 request, 

235 token_uri, 

236 body, 

237 access_token=None, 

238 use_json=False, 

239 can_retry=True, 

240 headers=None, 

241 **kwargs 

242): 

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

244 

245 Args: 

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

247 HTTP requests. 

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

249 URI. 

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

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

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

253 content type. The default value is False. 

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

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

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

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

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

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

260 to set up client side SSL certificate, and use 

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

262 side SSL certificate verification. 

263 

264 Returns: 

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

266 

267 Raises: 

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

269 an error. 

270 """ 

271 

272 response_status_ok, response_data, retryable_error = _token_endpoint_request_no_throw( 

273 request, 

274 token_uri, 

275 body, 

276 access_token=access_token, 

277 use_json=use_json, 

278 can_retry=can_retry, 

279 headers=headers, 

280 **kwargs 

281 ) 

282 if not response_status_ok: 

283 _handle_error_response(response_data, retryable_error) 

284 return response_data 

285 

286 

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

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

289 

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

291 

292 Args: 

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

294 HTTP requests. 

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

296 URI. 

297 assertion (str): The OAuth 2.0 assertion. 

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

299 

300 Returns: 

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

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

303 

304 Raises: 

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

306 an error. 

307 

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

309 """ 

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

311 

312 response_data = _token_endpoint_request( 

313 request, 

314 token_uri, 

315 body, 

316 can_retry=can_retry, 

317 headers={ 

318 metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion() 

319 }, 

320 ) 

321 

322 try: 

323 access_token = response_data["access_token"] 

324 except KeyError as caught_exc: 

325 new_exc = exceptions.RefreshError( 

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

327 ) 

328 six.raise_from(new_exc, caught_exc) 

329 

330 expiry = _parse_expiry(response_data) 

331 

332 return access_token, expiry, response_data 

333 

334 

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

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

337 

338 Args: 

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

340 HTTP requests. 

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

342 generateIdToken endpoint. 

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

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

345 

346 Returns: 

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

348 """ 

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

350 

351 response_data = _token_endpoint_request( 

352 request, 

353 _IAM_IDTOKEN_ENDPOINT.format(signer_email), 

354 body, 

355 access_token=access_token, 

356 use_json=True, 

357 ) 

358 

359 try: 

360 id_token = response_data["token"] 

361 except KeyError as caught_exc: 

362 new_exc = exceptions.RefreshError( 

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

364 ) 

365 six.raise_from(new_exc, caught_exc) 

366 

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

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

369 

370 return id_token, expiry 

371 

372 

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

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

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

376 

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

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

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

380 

381 Args: 

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

383 HTTP requests. 

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

385 URI. 

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

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

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

389 

390 Returns: 

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

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

393 data returned by the endpoint. 

394 

395 Raises: 

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

397 an error. 

398 """ 

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

400 

401 response_data = _token_endpoint_request( 

402 request, 

403 token_uri, 

404 body, 

405 can_retry=can_retry, 

406 headers={ 

407 metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion() 

408 }, 

409 ) 

410 

411 try: 

412 id_token = response_data["id_token"] 

413 except KeyError as caught_exc: 

414 new_exc = exceptions.RefreshError( 

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

416 ) 

417 six.raise_from(new_exc, caught_exc) 

418 

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

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

421 

422 return id_token, expiry, response_data 

423 

424 

425def _handle_refresh_grant_response(response_data, refresh_token): 

426 """Extract tokens from refresh grant response. 

427 

428 Args: 

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

430 refresh_token (str): Current refresh token. 

431 

432 Returns: 

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

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

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

436 refresh token will be returned. 

437 

438 Raises: 

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

440 an error. 

441 """ 

442 try: 

443 access_token = response_data["access_token"] 

444 except KeyError as caught_exc: 

445 new_exc = exceptions.RefreshError( 

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

447 ) 

448 six.raise_from(new_exc, caught_exc) 

449 

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

451 expiry = _parse_expiry(response_data) 

452 

453 return access_token, refresh_token, expiry, response_data 

454 

455 

456def refresh_grant( 

457 request, 

458 token_uri, 

459 refresh_token, 

460 client_id, 

461 client_secret, 

462 scopes=None, 

463 rapt_token=None, 

464 can_retry=True, 

465): 

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

467 

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

469 

470 Args: 

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

472 HTTP requests. 

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

474 URI. 

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

476 token. 

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

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

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

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

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

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

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

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

485 

486 Returns: 

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

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

489 returned by the token endpoint. 

490 

491 Raises: 

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

493 an error. 

494 

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

496 """ 

497 body = { 

498 "grant_type": _REFRESH_GRANT_TYPE, 

499 "client_id": client_id, 

500 "client_secret": client_secret, 

501 "refresh_token": refresh_token, 

502 } 

503 if scopes: 

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

505 if rapt_token: 

506 body["rapt"] = rapt_token 

507 

508 response_data = _token_endpoint_request( 

509 request, token_uri, body, can_retry=can_retry 

510 ) 

511 return _handle_refresh_grant_response(response_data, refresh_token)