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

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

211 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 self._scopes = scopes 

145 self._default_scopes = default_scopes 

146 self._granted_scopes = granted_scopes 

147 self._token_uri = token_uri 

148 self._client_id = client_id 

149 self._client_secret = client_secret 

150 self._quota_project_id = quota_project_id 

151 self._rapt_token = rapt_token 

152 self.refresh_handler = refresh_handler 

153 self._enable_reauth_refresh = enable_reauth_refresh 

154 self._trust_boundary = trust_boundary 

155 self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN 

156 self._account = account or "" 

157 self._cred_file_path = None 

158 

159 def __getstate__(self): 

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

161 This is identical to the default implementation. 

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

163 """ 

164 state_dict = self.__dict__.copy() 

165 # Remove _refresh_handler function as there are limitations pickling and 

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

167 # because they need to be importable. 

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

169 if "_refresh_handler" in state_dict: 

170 del state_dict["_refresh_handler"] 

171 

172 if "_refresh_worker" in state_dict: 

173 del state_dict["_refresh_worker"] 

174 return state_dict 

175 

176 def __setstate__(self, d): 

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

178 all the attributes.""" 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

193 self._universe_domain = ( 

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

195 ) 

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

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

198 self._refresh_handler = None 

199 self._refresh_worker = None 

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

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

202 

203 @property 

204 def refresh_token(self): 

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

206 return self._refresh_token 

207 

208 @property 

209 def scopes(self): 

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

211 return self._scopes 

212 

213 @property 

214 def granted_scopes(self): 

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

216 return self._granted_scopes 

217 

218 @property 

219 def token_uri(self): 

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

221 URI.""" 

222 return self._token_uri 

223 

224 @property 

225 def id_token(self): 

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

227 

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

229 may be populated when credentials are obtained and updated when 

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

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

232 """ 

233 return self._id_token 

234 

235 @property 

236 def client_id(self): 

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

238 return self._client_id 

239 

240 @property 

241 def client_secret(self): 

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

243 return self._client_secret 

244 

245 @property 

246 def requires_scopes(self): 

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

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

249 return False 

250 

251 @property 

252 def rapt_token(self): 

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

254 return self._rapt_token 

255 

256 @property 

257 def refresh_handler(self): 

258 """Returns the refresh handler if available. 

259 

260 Returns: 

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

262 The current refresh handler. 

263 """ 

264 return self._refresh_handler 

265 

266 @refresh_handler.setter 

267 def refresh_handler(self, value): 

268 """Updates the current refresh handler. 

269 

270 Args: 

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

272 The updated value of the refresh handler. 

273 

274 Raises: 

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

276 """ 

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

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

279 self._refresh_handler = value 

280 

281 @property 

282 def account(self): 

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

284 return self._account 

285 

286 def _make_copy(self): 

287 cred = self.__class__( 

288 self.token, 

289 refresh_token=self.refresh_token, 

290 id_token=self.id_token, 

291 token_uri=self.token_uri, 

292 client_id=self.client_id, 

293 client_secret=self.client_secret, 

294 scopes=self.scopes, 

295 default_scopes=self.default_scopes, 

296 granted_scopes=self.granted_scopes, 

297 quota_project_id=self.quota_project_id, 

298 rapt_token=self.rapt_token, 

299 enable_reauth_refresh=self._enable_reauth_refresh, 

300 trust_boundary=self._trust_boundary, 

301 universe_domain=self._universe_domain, 

302 account=self._account, 

303 ) 

304 cred._cred_file_path = self._cred_file_path 

305 return cred 

306 

307 @_helpers.copy_docstring(credentials.Credentials) 

308 def get_cred_info(self): 

309 if self._cred_file_path: 

310 cred_info = { 

311 "credential_source": self._cred_file_path, 

312 "credential_type": "user credentials", 

313 } 

314 if self.account: 

315 cred_info["principal"] = self.account 

316 return cred_info 

317 return None 

318 

319 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

320 def with_quota_project(self, quota_project_id): 

321 cred = self._make_copy() 

322 cred._quota_project_id = quota_project_id 

323 return cred 

324 

325 @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) 

326 def with_token_uri(self, token_uri): 

327 cred = self._make_copy() 

328 cred._token_uri = token_uri 

329 return cred 

330 

331 def with_account(self, account): 

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

333 

334 Args: 

335 account (str): The account to set 

336 

337 Returns: 

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

339 """ 

340 cred = self._make_copy() 

341 cred._account = account 

342 return cred 

343 

344 @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain) 

345 def with_universe_domain(self, universe_domain): 

346 cred = self._make_copy() 

347 cred._universe_domain = universe_domain 

348 return cred 

349 

350 def _metric_header_for_usage(self): 

351 return metrics.CRED_TYPE_USER 

352 

353 @_helpers.copy_docstring(credentials.Credentials) 

354 def refresh(self, request): 

355 if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN: 

356 raise exceptions.RefreshError( 

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

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

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

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

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

362 self._universe_domain 

363 ) 

364 ) 

365 

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

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

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

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

370 # downscoped tokens from a token broker. 

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

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

373 # Validate returned data. 

374 if not isinstance(token, str): 

375 raise exceptions.RefreshError( 

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

377 ) 

378 if not isinstance(expiry, datetime): 

379 raise exceptions.RefreshError( 

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

381 ) 

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

383 raise exceptions.RefreshError( 

384 "The credentials returned by the refresh_handler are " 

385 "already expired." 

386 ) 

387 self.token = token 

388 self.expiry = expiry 

389 return 

390 

391 if ( 

392 self._refresh_token is None 

393 or self._token_uri is None 

394 or self._client_id is None 

395 or self._client_secret is None 

396 ): 

397 raise exceptions.RefreshError( 

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

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

400 "token_uri, client_id, and client_secret." 

401 ) 

402 

403 ( 

404 access_token, 

405 refresh_token, 

406 expiry, 

407 grant_response, 

408 rapt_token, 

409 ) = reauth.refresh_grant( 

410 request, 

411 self._token_uri, 

412 self._refresh_token, 

413 self._client_id, 

414 self._client_secret, 

415 scopes=scopes, 

416 rapt_token=self._rapt_token, 

417 enable_reauth_refresh=self._enable_reauth_refresh, 

418 ) 

419 

420 self.token = access_token 

421 self.expiry = expiry 

422 self._refresh_token = refresh_token 

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

424 self._rapt_token = rapt_token 

425 

426 if scopes and "scope" in grant_response: 

427 requested_scopes = frozenset(scopes) 

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

429 granted_scopes = frozenset(self._granted_scopes) 

430 scopes_requested_but_not_granted = requested_scopes - granted_scopes 

431 if scopes_requested_but_not_granted: 

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

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

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

435 # developer wants to debug the scenario. 

436 _LOGGER.warning( 

437 "Not all requested scopes were granted by the " 

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

439 ", ".join(scopes_requested_but_not_granted) 

440 ) 

441 ) 

442 

443 @classmethod 

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

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

446 

447 Args: 

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

449 format. 

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

451 credentials. 

452 

453 Returns: 

454 google.oauth2.credentials.Credentials: The constructed 

455 credentials. 

456 

457 Raises: 

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

459 """ 

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

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

462 

463 if missing: 

464 raise ValueError( 

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

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

467 ) 

468 

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

470 expiry = info.get("expiry") 

471 if expiry: 

472 expiry = datetime.strptime( 

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

474 ) 

475 else: 

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

477 

478 # process scopes, which needs to be a seq 

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

480 scopes = info.get("scopes") 

481 if isinstance(scopes, str): 

482 scopes = scopes.split(" ") 

483 

484 return cls( 

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

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

487 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides 

488 scopes=scopes, 

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

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

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

492 expiry=expiry, 

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

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

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

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

497 ) 

498 

499 @classmethod 

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

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

502 

503 Args: 

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

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

506 credentials. 

507 

508 Returns: 

509 google.oauth2.credentials.Credentials: The constructed 

510 credentials. 

511 

512 Raises: 

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

514 """ 

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

516 data = json.load(json_file) 

517 return cls.from_authorized_user_info(data, scopes) 

518 

519 def to_json(self, strip=None): 

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

521 object. 

522 

523 Args: 

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

525 generated JSON. 

526 

527 Returns: 

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

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

530 to create a new credential instance. 

531 """ 

532 prep = { 

533 "token": self.token, 

534 "refresh_token": self.refresh_token, 

535 "token_uri": self.token_uri, 

536 "client_id": self.client_id, 

537 "client_secret": self.client_secret, 

538 "scopes": self.scopes, 

539 "rapt_token": self.rapt_token, 

540 "universe_domain": self._universe_domain, 

541 "account": self._account, 

542 } 

543 if self.expiry: # flatten expiry timestamp 

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

545 

546 # Remove empty entries (those which are None) 

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

548 

549 # Remove entries that explicitely need to be removed 

550 if strip is not None: 

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

552 

553 return json.dumps(prep) 

554 

555 

556class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject): 

557 """Access token credentials for user account. 

558 

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

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

561 

562 Args: 

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

564 specified, the current active account will be used. 

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

566 and billing. 

567 """ 

568 

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

570 warnings.warn( 

571 "UserAccessTokenCredentials is deprecated, please use " 

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

573 "that credential type, simply run " 

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

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

576 ) 

577 super(UserAccessTokenCredentials, self).__init__() 

578 self._account = account 

579 self._quota_project_id = quota_project_id 

580 

581 def with_account(self, account): 

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

583 

584 Args: 

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

586 

587 Returns: 

588 google.oauth2.credentials.UserAccessTokenCredentials: The created 

589 credentials with the given account. 

590 """ 

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

592 

593 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

594 def with_quota_project(self, quota_project_id): 

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

596 

597 def refresh(self, request): 

598 """Refreshes the access token. 

599 

600 Args: 

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

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

603 so just set it to `None`. 

604 

605 Raises: 

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

607 refresh failed. 

608 """ 

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

610 

611 @_helpers.copy_docstring(credentials.Credentials) 

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

613 self.refresh(request) 

614 self.apply(headers)