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

176 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +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 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_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com" 

53 

54 

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

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

57 

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

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

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

61 

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

63 

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

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

66 reauth feature is intended for gcloud to use only. 

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

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

69 google-auth[reauth]`. 

70 """ 

71 

72 def __init__( 

73 self, 

74 token, 

75 refresh_token=None, 

76 id_token=None, 

77 token_uri=None, 

78 client_id=None, 

79 client_secret=None, 

80 scopes=None, 

81 default_scopes=None, 

82 quota_project_id=None, 

83 expiry=None, 

84 rapt_token=None, 

85 refresh_handler=None, 

86 enable_reauth_refresh=False, 

87 granted_scopes=None, 

88 trust_boundary=None, 

89 universe_domain=_DEFAULT_UNIVERSE_DOMAIN, 

90 ): 

91 """ 

92 Args: 

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

94 if refresh information is provided. 

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

96 credentials can be refreshed. 

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

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

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

100 None if the token can not be refreshed. 

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

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

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

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

105 refreshed. 

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

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

108 credentials can not request additional scopes after 

109 authorization. The scopes must be derivable from the refresh 

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

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

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

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

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

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

116 This project may be different from the project used to 

117 create the credentials. 

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

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

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

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

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

123 refresh tokens are provided and tokens are obtained by calling 

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

125 retrieving downscoped tokens from a token broker. 

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

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

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

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

130 and requested scopes were same. 

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

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

133 universe domain is googleapis.com. 

134 """ 

135 super(Credentials, self).__init__() 

136 self.token = token 

137 self.expiry = expiry 

138 self._refresh_token = refresh_token 

139 self._id_token = id_token 

140 self._scopes = scopes 

141 self._default_scopes = default_scopes 

142 self._granted_scopes = granted_scopes 

143 self._token_uri = token_uri 

144 self._client_id = client_id 

145 self._client_secret = client_secret 

146 self._quota_project_id = quota_project_id 

147 self._rapt_token = rapt_token 

148 self.refresh_handler = refresh_handler 

149 self._enable_reauth_refresh = enable_reauth_refresh 

150 self._trust_boundary = trust_boundary 

151 self._universe_domain = universe_domain or _DEFAULT_UNIVERSE_DOMAIN 

152 

153 def __getstate__(self): 

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

155 This is identical to the default implementation. 

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

157 """ 

158 state_dict = self.__dict__.copy() 

159 # Remove _refresh_handler function as there are limitations pickling and 

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

161 # because they need to be importable. 

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

163 del state_dict["_refresh_handler"] 

164 return state_dict 

165 

166 def __setstate__(self, d): 

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

168 all the attributes.""" 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

185 self._refresh_handler = None 

186 

187 @property 

188 def refresh_token(self): 

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

190 return self._refresh_token 

191 

192 @property 

193 def scopes(self): 

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

195 return self._scopes 

196 

197 @property 

198 def granted_scopes(self): 

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

200 return self._granted_scopes 

201 

202 @property 

203 def token_uri(self): 

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

205 URI.""" 

206 return self._token_uri 

207 

208 @property 

209 def id_token(self): 

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

211 

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

213 may be populated when credentials are obtained and updated when 

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

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

216 """ 

217 return self._id_token 

218 

219 @property 

220 def client_id(self): 

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

222 return self._client_id 

223 

224 @property 

225 def client_secret(self): 

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

227 return self._client_secret 

228 

229 @property 

230 def requires_scopes(self): 

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

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

233 return False 

234 

235 @property 

236 def rapt_token(self): 

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

238 return self._rapt_token 

239 

240 @property 

241 def refresh_handler(self): 

242 """Returns the refresh handler if available. 

243 

244 Returns: 

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

246 The current refresh handler. 

247 """ 

248 return self._refresh_handler 

249 

250 @refresh_handler.setter 

251 def refresh_handler(self, value): 

252 """Updates the current refresh handler. 

253 

254 Args: 

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

256 The updated value of the refresh handler. 

257 

258 Raises: 

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

260 """ 

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

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

263 self._refresh_handler = value 

264 

265 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

266 def with_quota_project(self, quota_project_id): 

267 

268 return self.__class__( 

269 self.token, 

270 refresh_token=self.refresh_token, 

271 id_token=self.id_token, 

272 token_uri=self.token_uri, 

273 client_id=self.client_id, 

274 client_secret=self.client_secret, 

275 scopes=self.scopes, 

276 default_scopes=self.default_scopes, 

277 granted_scopes=self.granted_scopes, 

278 quota_project_id=quota_project_id, 

279 rapt_token=self.rapt_token, 

280 enable_reauth_refresh=self._enable_reauth_refresh, 

281 trust_boundary=self._trust_boundary, 

282 universe_domain=self._universe_domain, 

283 ) 

284 

285 @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) 

286 def with_token_uri(self, token_uri): 

287 

288 return self.__class__( 

289 self.token, 

290 refresh_token=self.refresh_token, 

291 id_token=self.id_token, 

292 token_uri=token_uri, 

293 client_id=self.client_id, 

294 client_secret=self.client_secret, 

295 scopes=self.scopes, 

296 default_scopes=self.default_scopes, 

297 granted_scopes=self.granted_scopes, 

298 quota_project_id=self.quota_project_id, 

299 rapt_token=self.rapt_token, 

300 enable_reauth_refresh=self._enable_reauth_refresh, 

301 trust_boundary=self._trust_boundary, 

302 universe_domain=self._universe_domain, 

303 ) 

304 

305 def with_universe_domain(self, universe_domain): 

306 """Create a copy of the credential with the given universe domain. 

307 

308 Args: 

309 universe_domain (str): The universe domain value. 

310 

311 Returns: 

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

313 """ 

314 

315 return self.__class__( 

316 self.token, 

317 refresh_token=self.refresh_token, 

318 id_token=self.id_token, 

319 token_uri=self._token_uri, 

320 client_id=self.client_id, 

321 client_secret=self.client_secret, 

322 scopes=self.scopes, 

323 default_scopes=self.default_scopes, 

324 granted_scopes=self.granted_scopes, 

325 quota_project_id=self.quota_project_id, 

326 rapt_token=self.rapt_token, 

327 enable_reauth_refresh=self._enable_reauth_refresh, 

328 trust_boundary=self._trust_boundary, 

329 universe_domain=universe_domain, 

330 ) 

331 

332 def _metric_header_for_usage(self): 

333 return metrics.CRED_TYPE_USER 

334 

335 @_helpers.copy_docstring(credentials.Credentials) 

336 def refresh(self, request): 

337 if self._universe_domain != _DEFAULT_UNIVERSE_DOMAIN: 

338 raise exceptions.RefreshError( 

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

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

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

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

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

344 self._universe_domain 

345 ) 

346 ) 

347 

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

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

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

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

352 # downscoped tokens from a token broker. 

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

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

355 # Validate returned data. 

356 if not isinstance(token, str): 

357 raise exceptions.RefreshError( 

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

359 ) 

360 if not isinstance(expiry, datetime): 

361 raise exceptions.RefreshError( 

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

363 ) 

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

365 raise exceptions.RefreshError( 

366 "The credentials returned by the refresh_handler are " 

367 "already expired." 

368 ) 

369 self.token = token 

370 self.expiry = expiry 

371 return 

372 

373 if ( 

374 self._refresh_token is None 

375 or self._token_uri is None 

376 or self._client_id is None 

377 or self._client_secret is None 

378 ): 

379 raise exceptions.RefreshError( 

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

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

382 "token_uri, client_id, and client_secret." 

383 ) 

384 

385 ( 

386 access_token, 

387 refresh_token, 

388 expiry, 

389 grant_response, 

390 rapt_token, 

391 ) = reauth.refresh_grant( 

392 request, 

393 self._token_uri, 

394 self._refresh_token, 

395 self._client_id, 

396 self._client_secret, 

397 scopes=scopes, 

398 rapt_token=self._rapt_token, 

399 enable_reauth_refresh=self._enable_reauth_refresh, 

400 ) 

401 

402 self.token = access_token 

403 self.expiry = expiry 

404 self._refresh_token = refresh_token 

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

406 self._rapt_token = rapt_token 

407 

408 if scopes and "scope" in grant_response: 

409 requested_scopes = frozenset(scopes) 

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

411 granted_scopes = frozenset(self._granted_scopes) 

412 scopes_requested_but_not_granted = requested_scopes - granted_scopes 

413 if scopes_requested_but_not_granted: 

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

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

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

417 # developer wants to debug the scenario. 

418 _LOGGER.warning( 

419 "Not all requested scopes were granted by the " 

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

421 ", ".join(scopes_requested_but_not_granted) 

422 ) 

423 ) 

424 

425 @classmethod 

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

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

428 

429 Args: 

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

431 format. 

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

433 credentials. 

434 

435 Returns: 

436 google.oauth2.credentials.Credentials: The constructed 

437 credentials. 

438 

439 Raises: 

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

441 """ 

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

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

444 

445 if missing: 

446 raise ValueError( 

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

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

449 ) 

450 

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

452 expiry = info.get("expiry") 

453 if expiry: 

454 expiry = datetime.strptime( 

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

456 ) 

457 else: 

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

459 

460 # process scopes, which needs to be a seq 

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

462 scopes = info.get("scopes") 

463 if isinstance(scopes, str): 

464 scopes = scopes.split(" ") 

465 

466 return cls( 

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

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

469 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides 

470 scopes=scopes, 

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

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

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

474 expiry=expiry, 

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

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

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

478 ) 

479 

480 @classmethod 

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

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

483 

484 Args: 

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

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

487 credentials. 

488 

489 Returns: 

490 google.oauth2.credentials.Credentials: The constructed 

491 credentials. 

492 

493 Raises: 

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

495 """ 

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

497 data = json.load(json_file) 

498 return cls.from_authorized_user_info(data, scopes) 

499 

500 def to_json(self, strip=None): 

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

502 object. 

503 

504 Args: 

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

506 generated JSON. 

507 

508 Returns: 

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

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

511 to create a new credential instance. 

512 """ 

513 prep = { 

514 "token": self.token, 

515 "refresh_token": self.refresh_token, 

516 "token_uri": self.token_uri, 

517 "client_id": self.client_id, 

518 "client_secret": self.client_secret, 

519 "scopes": self.scopes, 

520 "rapt_token": self.rapt_token, 

521 "universe_domain": self._universe_domain, 

522 } 

523 if self.expiry: # flatten expiry timestamp 

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

525 

526 # Remove empty entries (those which are None) 

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

528 

529 # Remove entries that explicitely need to be removed 

530 if strip is not None: 

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

532 

533 return json.dumps(prep) 

534 

535 

536class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject): 

537 """Access token credentials for user account. 

538 

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

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

541 

542 Args: 

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

544 specified, the current active account will be used. 

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

546 and billing. 

547 """ 

548 

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

550 warnings.warn( 

551 "UserAccessTokenCredentials is deprecated, please use " 

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

553 "that credential type, simply run " 

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

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

556 ) 

557 super(UserAccessTokenCredentials, self).__init__() 

558 self._account = account 

559 self._quota_project_id = quota_project_id 

560 

561 def with_account(self, account): 

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

563 

564 Args: 

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

566 

567 Returns: 

568 google.oauth2.credentials.UserAccessTokenCredentials: The created 

569 credentials with the given account. 

570 """ 

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

572 

573 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 

574 def with_quota_project(self, quota_project_id): 

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

576 

577 def refresh(self, request): 

578 """Refreshes the access token. 

579 

580 Args: 

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

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

583 so just set it to `None`. 

584 

585 Raises: 

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

587 refresh failed. 

588 """ 

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

590 

591 @_helpers.copy_docstring(credentials.Credentials) 

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

593 self.refresh(request) 

594 self.apply(headers)