Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

130 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 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)