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

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

214 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 Credentials. 

16 

17This module provides credentials based on OAuth 2.0 access and refresh tokens. 

18These credentials usually access resources on behalf of a user (resource 

19owner). 

20 

21Specifically, this is intended to use access tokens acquired using the 

22`Authorization Code grant`_ and can refresh those tokens using a 

23optional `refresh token`_. 

24 

25Obtaining the initial access and refresh token is outside of the scope of this 

26module. Consult `rfc6749 section 4.1`_ for complete details on the 

27Authorization Code grant flow. 

28 

29.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.1 

30.. _refresh token: https://tools.ietf.org/html/rfc6749#section-6 

31.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.1 

32""" 

33 

34from datetime import datetime 

35import io 

36import json 

37import logging 

38import warnings 

39 

40from google.auth import _cloud_sdk 

41from google.auth import _helpers 

42from google.auth import credentials 

43from google.auth import exceptions 

44from google.auth import metrics 

45from google.oauth2 import reauth 

46 

47_LOGGER = logging.getLogger(__name__) 

48 

49 

50# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. 

51_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" 

52 

53# The Google OAuth 2.0 token info endpoint. Used for getting token info JSON from access tokens. 

54_GOOGLE_OAUTH2_TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo" 

55 

56 

57class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject): 

58 """Credentials using OAuth 2.0 access and refresh tokens. 

59 

60 The credentials are considered immutable except the tokens and the token 

61 expiry, which are updated after refresh. If you want to modify the quota 

62 project, use :meth:`with_quota_project` or :: 

63 

64 credentials = credentials.with_quota_project('myproject-123') 

65 

66 Reauth is disabled by default. To enable reauth, set the 

67 `enable_reauth_refresh` parameter to True in the constructor. Note that 

68 reauth feature is intended for gcloud to use only. 

69 If reauth is enabled, `pyu2f` dependency has to be installed in order to use security 

70 key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install 

71 google-auth[reauth]`. 

72 """ 

73 

74 def __init__( 

75 self, 

76 token, 

77 refresh_token=None, 

78 id_token=None, 

79 token_uri=None, 

80 client_id=None, 

81 client_secret=None, 

82 scopes=None, 

83 default_scopes=None, 

84 quota_project_id=None, 

85 expiry=None, 

86 rapt_token=None, 

87 refresh_handler=None, 

88 enable_reauth_refresh=False, 

89 granted_scopes=None, 

90 trust_boundary=None, 

91 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN, 

92 account=None, 

93 ): 

94 """ 

95 Args: 

96 token (Optional(str)): The OAuth 2.0 access token. Can be None 

97 if refresh information is provided. 

98 refresh_token (str): The OAuth 2.0 refresh token. If specified, 

99 credentials can be refreshed. 

100 id_token (str): The Open ID Connect ID Token. 

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

102 endpoint URI. Must be specified for refresh, can be left as 

103 None if the token can not be refreshed. 

104 client_id (str): The OAuth 2.0 client ID. Must be specified for 

105 refresh, can be left as None if the token can not be refreshed. 

106 client_secret(str): The OAuth 2.0 client secret. Must be specified 

107 for refresh, can be left as None if the token can not be 

108 refreshed. 

109 scopes (Sequence[str]): The scopes used to obtain authorization. 

110 This parameter is used by :meth:`has_scopes`. OAuth 2.0 

111 credentials can not request additional scopes after 

112 authorization. The scopes must be derivable from the refresh 

113 token if refresh information is provided (e.g. The refresh 

114 token scopes are a superset of this or contain a wild card 

115 scope like 'https://www.googleapis.com/auth/any-api'). 

116 default_scopes (Sequence[str]): Default scopes passed by a 

117 Google client library. Use 'scopes' for user-defined scopes. 

118 quota_project_id (Optional[str]): The project ID used for quota and billing. 

119 This project may be different from the project used to 

120 create the credentials. 

121 rapt_token (Optional[str]): The reauth Proof Token. 

122 refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 

123 A callable which takes in the HTTP request callable and the list of 

124 OAuth scopes and when called returns an access token string for the 

125 requested scopes and its expiry datetime. This is useful when no 

126 refresh tokens are provided and tokens are obtained by calling 

127 some external process on demand. It is particularly useful for 

128 retrieving downscoped tokens from a token broker. 

129 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow 

130 should be used. This flag is for gcloud to use only. 

131 granted_scopes (Optional[Sequence[str]]): The scopes that were consented/granted by the user. 

132 This could be different from the requested scopes and it could be empty if granted 

133 and requested scopes were same. 

134 trust_boundary (str): String representation of trust boundary meta. 

135 universe_domain (Optional[str]): The universe domain. The default 

136 universe domain is googleapis.com. 

137 account (Optional[str]): The account associated with the credential. 

138 """ 

139 super(Credentials, self).__init__() 

140 self.token = token 

141 self.expiry = expiry 

142 self._refresh_token = refresh_token 

143 self._id_token = id_token 

144 if scopes is not None and isinstance(scopes, set): 

145 self._scopes = list(scopes) 

146 else: 

147 self._scopes = scopes 

148 self._default_scopes = default_scopes 

149 self._granted_scopes = granted_scopes 

150 self._token_uri = token_uri 

151 self._client_id = client_id 

152 self._client_secret = client_secret 

153 self._quota_project_id = quota_project_id 

154 self._rapt_token = rapt_token 

155 self.refresh_handler = refresh_handler 

156 self._enable_reauth_refresh = enable_reauth_refresh 

157 self._trust_boundary = trust_boundary 

158 self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN 

159 self._account = account or "" 

160 self._cred_file_path = None 

161 

162 def __getstate__(self): 

163 """A __getstate__ method must exist for the __setstate__ to be called 

164 This is identical to the default implementation. 

165 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__ 

166 """ 

167 state_dict = self.__dict__.copy() 

168 # Remove _refresh_handler function as there are limitations pickling and 

169 # unpickling certain callables (lambda, functools.partial instances) 

170 # because they need to be importable. 

171 # Instead, the refresh_handler setter should be used to repopulate this. 

172 if "_refresh_handler" in state_dict: 

173 del state_dict["_refresh_handler"] 

174 

175 if "_refresh_worker" in state_dict: 

176 del state_dict["_refresh_worker"] 

177 return state_dict 

178 

179 def __setstate__(self, d): 

180 """Credentials pickled with older versions of the class do not have 

181 all the attributes.""" 

182 self.token = d.get("token") 

183 self.expiry = d.get("expiry") 

184 self._refresh_token = d.get("_refresh_token") 

185 self._id_token = d.get("_id_token") 

186 self._scopes = d.get("_scopes") 

187 self._default_scopes = d.get("_default_scopes") 

188 self._granted_scopes = d.get("_granted_scopes") 

189 self._token_uri = d.get("_token_uri") 

190 self._client_id = d.get("_client_id") 

191 self._client_secret = d.get("_client_secret") 

192 self._quota_project_id = d.get("_quota_project_id") 

193 self._rapt_token = d.get("_rapt_token") 

194 self._enable_reauth_refresh = d.get("_enable_reauth_refresh") 

195 self._trust_boundary = d.get("_trust_boundary") 

196 self._universe_domain = ( 

197 d.get("_universe_domain") or credentials.DEFAULT_UNIVERSE_DOMAIN 

198 ) 

199 self._cred_file_path = d.get("_cred_file_path") 

200 # The refresh_handler setter should be used to repopulate this. 

201 self._refresh_handler = None 

202 self._refresh_worker = None 

203 self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False) 

204 self._account = d.get("_account", "") 

205 

206 @property 

207 def refresh_token(self): 

208 """Optional[str]: The OAuth 2.0 refresh token.""" 

209 return self._refresh_token 

210 

211 @property 

212 def scopes(self): 

213 """Optional[Sequence[str]]: The OAuth 2.0 permission scopes.""" 

214 return self._scopes 

215 

216 @property 

217 def granted_scopes(self): 

218 """Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user.""" 

219 return self._granted_scopes 

220 

221 @property 

222 def token_uri(self): 

223 """Optional[str]: The OAuth 2.0 authorization server's token endpoint 

224 URI.""" 

225 return self._token_uri 

226 

227 @property 

228 def id_token(self): 

229 """Optional[str]: The Open ID Connect ID Token. 

230 

231 Depending on the authorization server and the scopes requested, this 

232 may be populated when credentials are obtained and updated when 

233 :meth:`refresh` is called. This token is a JWT. It can be verified 

234 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`. 

235 """ 

236 return self._id_token 

237 

238 @property 

239 def client_id(self): 

240 """Optional[str]: The OAuth 2.0 client ID.""" 

241 return self._client_id 

242 

243 @property 

244 def client_secret(self): 

245 """Optional[str]: The OAuth 2.0 client secret.""" 

246 return self._client_secret 

247 

248 @property 

249 def requires_scopes(self): 

250 """False: OAuth 2.0 credentials have their scopes set when 

251 the initial token is requested and can not be changed.""" 

252 return False 

253 

254 @property 

255 def rapt_token(self): 

256 """Optional[str]: The reauth Proof Token.""" 

257 return self._rapt_token 

258 

259 @property 

260 def refresh_handler(self): 

261 """Returns the refresh handler if available. 

262 

263 Returns: 

264 Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]: 

265 The current refresh handler. 

266 """ 

267 return self._refresh_handler 

268 

269 @refresh_handler.setter 

270 def refresh_handler(self, value): 

271 """Updates the current refresh handler. 

272 

273 Args: 

274 value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 

275 The updated value of the refresh handler. 

276 

277 Raises: 

278 TypeError: If the value is not a callable or None. 

279 """ 

280 if not callable(value) and value is not None: 

281 raise TypeError("The provided refresh_handler is not a callable or None.") 

282 self._refresh_handler = value 

283 

284 @property 

285 def account(self): 

286 """str: The user account associated with the credential. If the account is unknown an empty string is returned.""" 

287 return self._account 

288 

289 def _make_copy(self): 

290 cred = self.__class__( 

291 self.token, 

292 refresh_token=self.refresh_token, 

293 id_token=self.id_token, 

294 token_uri=self.token_uri, 

295 client_id=self.client_id, 

296 client_secret=self.client_secret, 

297 scopes=self.scopes, 

298 default_scopes=self.default_scopes, 

299 granted_scopes=self.granted_scopes, 

300 quota_project_id=self.quota_project_id, 

301 rapt_token=self.rapt_token, 

302 enable_reauth_refresh=self._enable_reauth_refresh, 

303 trust_boundary=self._trust_boundary, 

304 universe_domain=self._universe_domain, 

305 account=self._account, 

306 ) 

307 cred._cred_file_path = self._cred_file_path 

308 return cred 

309 

310 @_helpers.copy_docstring(credentials.Credentials) 

311 def get_cred_info(self): 

312 if self._cred_file_path: 

313 cred_info = { 

314 "credential_source": self._cred_file_path, 

315 "credential_type": "user credentials", 

316 } 

317 if self.account: 

318 cred_info["principal"] = self.account 

319 return cred_info 

320 return None 

321 

322 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

323 def with_quota_project(self, quota_project_id): 

324 cred = self._make_copy() 

325 cred._quota_project_id = quota_project_id 

326 return cred 

327 

328 @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) 

329 def with_token_uri(self, token_uri): 

330 cred = self._make_copy() 

331 cred._token_uri = token_uri 

332 return cred 

333 

334 def with_account(self, account): 

335 """Returns a copy of these credentials with a modified account. 

336 

337 Args: 

338 account (str): The account to set 

339 

340 Returns: 

341 google.oauth2.credentials.Credentials: A new credentials instance. 

342 """ 

343 cred = self._make_copy() 

344 cred._account = account 

345 return cred 

346 

347 @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain) 

348 def with_universe_domain(self, universe_domain): 

349 cred = self._make_copy() 

350 cred._universe_domain = universe_domain 

351 return cred 

352 

353 def _metric_header_for_usage(self): 

354 return metrics.CRED_TYPE_USER 

355 

356 @_helpers.copy_docstring(credentials.Credentials) 

357 def refresh(self, request): 

358 if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN: 

359 raise exceptions.RefreshError( 

360 "User credential refresh is only supported in the default " 

361 "googleapis.com universe domain, but the current universe " 

362 "domain is {}. If you created the credential with an access " 

363 "token, it's likely that the provided token is expired now, " 

364 "please update your code with a valid token.".format( 

365 self._universe_domain 

366 ) 

367 ) 

368 

369 scopes = self._scopes if self._scopes is not None else self._default_scopes 

370 # Use refresh handler if available and no refresh token is 

371 # available. This is useful in general when tokens are obtained by calling 

372 # some external process on demand. It is particularly useful for retrieving 

373 # downscoped tokens from a token broker. 

374 if self._refresh_token is None and self.refresh_handler: 

375 token, expiry = self.refresh_handler(request, scopes=scopes) 

376 # Validate returned data. 

377 if not isinstance(token, str): 

378 raise exceptions.RefreshError( 

379 "The refresh_handler returned token is not a string." 

380 ) 

381 if not isinstance(expiry, datetime): 

382 raise exceptions.RefreshError( 

383 "The refresh_handler returned expiry is not a datetime object." 

384 ) 

385 if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD: 

386 raise exceptions.RefreshError( 

387 "The credentials returned by the refresh_handler are " 

388 "already expired." 

389 ) 

390 self.token = token 

391 self.expiry = expiry 

392 return 

393 

394 if ( 

395 self._refresh_token is None 

396 or self._token_uri is None 

397 or self._client_id is None 

398 or self._client_secret is None 

399 ): 

400 raise exceptions.RefreshError( 

401 "The credentials do not contain the necessary fields need to " 

402 "refresh the access token. You must specify refresh_token, " 

403 "token_uri, client_id, and client_secret." 

404 ) 

405 

406 ( 

407 access_token, 

408 refresh_token, 

409 expiry, 

410 grant_response, 

411 rapt_token, 

412 ) = reauth.refresh_grant( 

413 request, 

414 self._token_uri, 

415 self._refresh_token, 

416 self._client_id, 

417 self._client_secret, 

418 scopes=scopes, 

419 rapt_token=self._rapt_token, 

420 enable_reauth_refresh=self._enable_reauth_refresh, 

421 ) 

422 

423 self.token = access_token 

424 self.expiry = expiry 

425 self._refresh_token = refresh_token 

426 self._id_token = grant_response.get("id_token") 

427 self._rapt_token = rapt_token 

428 

429 if scopes and "scope" in grant_response: 

430 requested_scopes = frozenset(scopes) 

431 self._granted_scopes = grant_response["scope"].split() 

432 granted_scopes = frozenset(self._granted_scopes) 

433 scopes_requested_but_not_granted = requested_scopes - granted_scopes 

434 if scopes_requested_but_not_granted: 

435 # User might be presented with unbundled scopes at the time of 

436 # consent. So it is a valid scenario to not have all the requested 

437 # scopes as part of granted scopes but log a warning in case the 

438 # developer wants to debug the scenario. 

439 _LOGGER.warning( 

440 "Not all requested scopes were granted by the " 

441 "authorization server, missing scopes {}.".format( 

442 ", ".join(scopes_requested_but_not_granted) 

443 ) 

444 ) 

445 

446 @classmethod 

447 def from_authorized_user_info(cls, info, scopes=None): 

448 """Creates a Credentials instance from parsed authorized user info. 

449 

450 Args: 

451 info (Mapping[str, str]): The authorized user info in Google 

452 format. 

453 scopes (Sequence[str]): Optional list of scopes to include in the 

454 credentials. 

455 

456 Returns: 

457 google.oauth2.credentials.Credentials: The constructed 

458 credentials. 

459 

460 Raises: 

461 ValueError: If the info is not in the expected format. 

462 """ 

463 keys_needed = set(("refresh_token", "client_id", "client_secret")) 

464 missing = keys_needed.difference(info.keys()) 

465 

466 if missing: 

467 raise ValueError( 

468 "Authorized user info was not in the expected format, missing " 

469 "fields {}.".format(", ".join(missing)) 

470 ) 

471 

472 # access token expiry (datetime obj); auto-expire if not saved 

473 expiry = info.get("expiry") 

474 if expiry: 

475 expiry = datetime.strptime( 

476 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S" 

477 ) 

478 else: 

479 expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD 

480 

481 # process scopes, which needs to be a seq 

482 if scopes is None and "scopes" in info: 

483 scopes = info.get("scopes") 

484 if isinstance(scopes, str): 

485 scopes = scopes.split(" ") 

486 

487 return cls( 

488 token=info.get("token"), 

489 refresh_token=info.get("refresh_token"), 

490 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides 

491 scopes=scopes, 

492 client_id=info.get("client_id"), 

493 client_secret=info.get("client_secret"), 

494 quota_project_id=info.get("quota_project_id"), # may not exist 

495 expiry=expiry, 

496 rapt_token=info.get("rapt_token"), # may not exist 

497 trust_boundary=info.get("trust_boundary"), # may not exist 

498 universe_domain=info.get("universe_domain"), # may not exist 

499 account=info.get("account", ""), # may not exist 

500 ) 

501 

502 @classmethod 

503 def from_authorized_user_file(cls, filename, scopes=None): 

504 """Creates a Credentials instance from an authorized user json file. 

505 

506 Args: 

507 filename (str): The path to the authorized user json file. 

508 scopes (Sequence[str]): Optional list of scopes to include in the 

509 credentials. 

510 

511 Returns: 

512 google.oauth2.credentials.Credentials: The constructed 

513 credentials. 

514 

515 Raises: 

516 ValueError: If the file is not in the expected format. 

517 """ 

518 with io.open(filename, "r", encoding="utf-8") as json_file: 

519 data = json.load(json_file) 

520 return cls.from_authorized_user_info(data, scopes) 

521 

522 def to_json(self, strip=None): 

523 """Utility function that creates a JSON representation of a Credentials 

524 object. 

525 

526 Args: 

527 strip (Sequence[str]): Optional list of members to exclude from the 

528 generated JSON. 

529 

530 Returns: 

531 str: A JSON representation of this instance. When converted into 

532 a dictionary, it can be passed to from_authorized_user_info() 

533 to create a new credential instance. 

534 """ 

535 prep = { 

536 "token": self.token, 

537 "refresh_token": self.refresh_token, 

538 "token_uri": self.token_uri, 

539 "client_id": self.client_id, 

540 "client_secret": self.client_secret, 

541 "scopes": self.scopes, 

542 "rapt_token": self.rapt_token, 

543 "universe_domain": self._universe_domain, 

544 "account": self._account, 

545 } 

546 if self.expiry: # flatten expiry timestamp 

547 prep["expiry"] = self.expiry.isoformat() + "Z" 

548 

549 # Remove empty entries (those which are None) 

550 prep = {k: v for k, v in prep.items() if v is not None} 

551 

552 # Remove entries that explicitely need to be removed 

553 if strip is not None: 

554 prep = {k: v for k, v in prep.items() if k not in strip} 

555 

556 return json.dumps(prep) 

557 

558 

559class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject): 

560 """Access token credentials for user account. 

561 

562 Obtain the access token for a given user account or the current active 

563 user account with the ``gcloud auth print-access-token`` command. 

564 

565 Args: 

566 account (Optional[str]): Account to get the access token for. If not 

567 specified, the current active account will be used. 

568 quota_project_id (Optional[str]): The project ID used for quota 

569 and billing. 

570 """ 

571 

572 def __init__(self, account=None, quota_project_id=None): 

573 warnings.warn( 

574 "UserAccessTokenCredentials is deprecated, please use " 

575 "google.oauth2.credentials.Credentials instead. To use " 

576 "that credential type, simply run " 

577 "`gcloud auth application-default login` and let the " 

578 "client libraries pick up the application default credentials." 

579 ) 

580 super(UserAccessTokenCredentials, self).__init__() 

581 self._account = account 

582 self._quota_project_id = quota_project_id 

583 

584 def with_account(self, account): 

585 """Create a new instance with the given account. 

586 

587 Args: 

588 account (str): Account to get the access token for. 

589 

590 Returns: 

591 google.oauth2.credentials.UserAccessTokenCredentials: The created 

592 credentials with the given account. 

593 """ 

594 return self.__class__(account=account, quota_project_id=self._quota_project_id) 

595 

596 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

597 def with_quota_project(self, quota_project_id): 

598 return self.__class__(account=self._account, quota_project_id=quota_project_id) 

599 

600 def refresh(self, request): 

601 """Refreshes the access token. 

602 

603 Args: 

604 request (google.auth.transport.Request): This argument is required 

605 by the base class interface but not used in this implementation, 

606 so just set it to `None`. 

607 

608 Raises: 

609 google.auth.exceptions.UserAccessTokenError: If the access token 

610 refresh failed. 

611 """ 

612 self.token = _cloud_sdk.get_auth_access_token(self._account) 

613 

614 @_helpers.copy_docstring(credentials.Credentials) 

615 def before_request(self, request, method, url, headers): 

616 self.refresh(request) 

617 self.apply(headers)