codekingpro/portable-devtools
114k
1# Copyright 2016 Google LLC2#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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# 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 (resource19owner).20 21Specifically, this is intended to use access tokens acquired using the22`Authorization Code grant`_ and can refresh those tokens using a23optional `refresh token`_.24 25Obtaining the initial access and refresh token is outside of the scope of this26module. Consult `rfc6749 section 4.1`_ for complete details on the27Authorization Code grant flow.28 29.. _Authorization Code grant: https://tools.ietf.org/html/rfc6749#section-1.3.130.. _refresh token: https://tools.ietf.org/html/rfc6749#section-631.. _rfc6749 section 4.1: https://tools.ietf.org/html/rfc6749#section-4.132"""33 34from datetime import datetime35import io36import json37import logging38import warnings39 40from google.auth import _cloud_sdk41from google.auth import _helpers42from google.auth import credentials43from google.auth import exceptions44from google.auth import metrics45from google.oauth2 import reauth46 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 54class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):55 """Credentials using OAuth 2.0 access and refresh tokens.56 57 The credentials are considered immutable except the tokens and the token58 expiry, which are updated after refresh. If you want to modify the quota59 project, use :meth:`with_quota_project` or ::60 61 credentials = credentials.with_quota_project('myproject-123')62 63 Reauth is disabled by default. To enable reauth, set the64 `enable_reauth_refresh` parameter to True in the constructor. Note that65 reauth feature is intended for gcloud to use only.66 If reauth is enabled, `pyu2f` dependency has to be installed in order to use security67 key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install68 google-auth[reauth]`.69 """70 71 def __init__(72 self,73 token,74 refresh_token=None,75 id_token=None,76 token_uri=None,77 client_id=None,78 client_secret=None,79 scopes=None,80 default_scopes=None,81 quota_project_id=None,82 expiry=None,83 rapt_token=None,84 refresh_handler=None,85 enable_reauth_refresh=False,86 granted_scopes=None,87 trust_boundary=None,88 universe_domain=credentials.DEFAULT_UNIVERSE_DOMAIN,89 account=None,90 ):91 """92 Args:93 token (Optional(str)): The OAuth 2.0 access token. Can be None94 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 token99 endpoint URI. Must be specified for refresh, can be left as100 None if the token can not be refreshed.101 client_id (str): The OAuth 2.0 client ID. Must be specified for102 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 specified104 for refresh, can be left as None if the token can not be105 refreshed.106 scopes (Sequence[str]): The scopes used to obtain authorization.107 This parameter is used by :meth:`has_scopes`. OAuth 2.0108 credentials can not request additional scopes after109 authorization. The scopes must be derivable from the refresh110 token if refresh information is provided (e.g. The refresh111 token scopes are a superset of this or contain a wild card112 scope like 'https://www.googleapis.com/auth/any-api').113 default_scopes (Sequence[str]): Default scopes passed by a114 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 to117 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 of121 OAuth scopes and when called returns an access token string for the122 requested scopes and its expiry datetime. This is useful when no123 refresh tokens are provided and tokens are obtained by calling124 some external process on demand. It is particularly useful for125 retrieving downscoped tokens from a token broker.126 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow127 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 granted130 and requested scopes were same.131 trust_boundary (str): String representation of trust boundary meta.132 universe_domain (Optional[str]): The universe domain. The default133 universe domain is googleapis.com.134 account (Optional[str]): The account associated with the credential.135 """136 super(Credentials, self).__init__()137 self.token = token138 self.expiry = expiry139 self._refresh_token = refresh_token140 self._id_token = id_token141 self._scopes = scopes142 self._default_scopes = default_scopes143 self._granted_scopes = granted_scopes144 self._token_uri = token_uri145 self._client_id = client_id146 self._client_secret = client_secret147 self._quota_project_id = quota_project_id148 self._rapt_token = rapt_token149 self.refresh_handler = refresh_handler150 self._enable_reauth_refresh = enable_reauth_refresh151 self._trust_boundary = trust_boundary152 self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN153 self._account = account or ""154 155 def __getstate__(self):156 """A __getstate__ method must exist for the __setstate__ to be called157 This is identical to the default implementation.158 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__159 """160 state_dict = self.__dict__.copy()161 # Remove _refresh_handler function as there are limitations pickling and162 # unpickling certain callables (lambda, functools.partial instances)163 # because they need to be importable.164 # Instead, the refresh_handler setter should be used to repopulate this.165 if "_refresh_handler" in state_dict:166 del state_dict["_refresh_handler"]167 168 if "_refresh_worker" in state_dict:169 del state_dict["_refresh_worker"]170 return state_dict171 172 def __setstate__(self, d):173 """Credentials pickled with older versions of the class do not have174 all the attributes."""175 self.token = d.get("token")176 self.expiry = d.get("expiry")177 self._refresh_token = d.get("_refresh_token")178 self._id_token = d.get("_id_token")179 self._scopes = d.get("_scopes")180 self._default_scopes = d.get("_default_scopes")181 self._granted_scopes = d.get("_granted_scopes")182 self._token_uri = d.get("_token_uri")183 self._client_id = d.get("_client_id")184 self._client_secret = d.get("_client_secret")185 self._quota_project_id = d.get("_quota_project_id")186 self._rapt_token = d.get("_rapt_token")187 self._enable_reauth_refresh = d.get("_enable_reauth_refresh")188 self._trust_boundary = d.get("_trust_boundary")189 self._universe_domain = (190 d.get("_universe_domain") or credentials.DEFAULT_UNIVERSE_DOMAIN191 )192 # The refresh_handler setter should be used to repopulate this.193 self._refresh_handler = None194 self._refresh_worker = None195 self._use_non_blocking_refresh = d.get("_use_non_blocking_refresh", False)196 self._account = d.get("_account", "")197 198 @property199 def refresh_token(self):200 """Optional[str]: The OAuth 2.0 refresh token."""201 return self._refresh_token202 203 @property204 def scopes(self):205 """Optional[str]: The OAuth 2.0 permission scopes."""206 return self._scopes207 208 @property209 def granted_scopes(self):210 """Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were granted by the user."""211 return self._granted_scopes212 213 @property214 def token_uri(self):215 """Optional[str]: The OAuth 2.0 authorization server's token endpoint216 URI."""217 return self._token_uri218 219 @property220 def id_token(self):221 """Optional[str]: The Open ID Connect ID Token.222 223 Depending on the authorization server and the scopes requested, this224 may be populated when credentials are obtained and updated when225 :meth:`refresh` is called. This token is a JWT. It can be verified226 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`.227 """228 return self._id_token229 230 @property231 def client_id(self):232 """Optional[str]: The OAuth 2.0 client ID."""233 return self._client_id234 235 @property236 def client_secret(self):237 """Optional[str]: The OAuth 2.0 client secret."""238 return self._client_secret239 240 @property241 def requires_scopes(self):242 """False: OAuth 2.0 credentials have their scopes set when243 the initial token is requested and can not be changed."""244 return False245 246 @property247 def rapt_token(self):248 """Optional[str]: The reauth Proof Token."""249 return self._rapt_token250 251 @property252 def refresh_handler(self):253 """Returns the refresh handler if available.254 255 Returns:256 Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]:257 The current refresh handler.258 """259 return self._refresh_handler260 261 @refresh_handler.setter262 def refresh_handler(self, value):263 """Updates the current refresh handler.264 265 Args:266 value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]):267 The updated value of the refresh handler.268 269 Raises:270 TypeError: If the value is not a callable or None.271 """272 if not callable(value) and value is not None:273 raise TypeError("The provided refresh_handler is not a callable or None.")274 self._refresh_handler = value275 276 @property277 def account(self):278 """str: The user account associated with the credential. If the account is unknown an empty string is returned."""279 return self._account280 281 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)282 def with_quota_project(self, quota_project_id):283 284 return self.__class__(285 self.token,286 refresh_token=self.refresh_token,287 id_token=self.id_token,288 token_uri=self.token_uri,289 client_id=self.client_id,290 client_secret=self.client_secret,291 scopes=self.scopes,292 default_scopes=self.default_scopes,293 granted_scopes=self.granted_scopes,294 quota_project_id=quota_project_id,295 rapt_token=self.rapt_token,296 enable_reauth_refresh=self._enable_reauth_refresh,297 trust_boundary=self._trust_boundary,298 universe_domain=self._universe_domain,299 account=self._account,300 )301 302 @_helpers.copy_docstring(credentials.CredentialsWithTokenUri)303 def with_token_uri(self, token_uri):304 305 return self.__class__(306 self.token,307 refresh_token=self.refresh_token,308 id_token=self.id_token,309 token_uri=token_uri,310 client_id=self.client_id,311 client_secret=self.client_secret,312 scopes=self.scopes,313 default_scopes=self.default_scopes,314 granted_scopes=self.granted_scopes,315 quota_project_id=self.quota_project_id,316 rapt_token=self.rapt_token,317 enable_reauth_refresh=self._enable_reauth_refresh,318 trust_boundary=self._trust_boundary,319 universe_domain=self._universe_domain,320 account=self._account,321 )322 323 def with_account(self, account):324 """Returns a copy of these credentials with a modified account.325 326 Args:327 account (str): The account to set328 329 Returns:330 google.oauth2.credentials.Credentials: A new credentials instance.331 """332 333 return self.__class__(334 self.token,335 refresh_token=self.refresh_token,336 id_token=self.id_token,337 token_uri=self._token_uri,338 client_id=self.client_id,339 client_secret=self.client_secret,340 scopes=self.scopes,341 default_scopes=self.default_scopes,342 granted_scopes=self.granted_scopes,343 quota_project_id=self.quota_project_id,344 rapt_token=self.rapt_token,345 enable_reauth_refresh=self._enable_reauth_refresh,346 trust_boundary=self._trust_boundary,347 universe_domain=self._universe_domain,348 account=account,349 )350 351 @_helpers.copy_docstring(credentials.CredentialsWithUniverseDomain)352 def with_universe_domain(self, universe_domain):353 354 return self.__class__(355 self.token,356 refresh_token=self.refresh_token,357 id_token=self.id_token,358 token_uri=self._token_uri,359 client_id=self.client_id,360 client_secret=self.client_secret,361 scopes=self.scopes,362 default_scopes=self.default_scopes,363 granted_scopes=self.granted_scopes,364 quota_project_id=self.quota_project_id,365 rapt_token=self.rapt_token,366 enable_reauth_refresh=self._enable_reauth_refresh,367 trust_boundary=self._trust_boundary,368 universe_domain=universe_domain,369 account=self._account,370 )371 372 def _metric_header_for_usage(self):373 return metrics.CRED_TYPE_USER374 375 @_helpers.copy_docstring(credentials.Credentials)376 def refresh(self, request):377 if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:378 raise exceptions.RefreshError(379 "User credential refresh is only supported in the default "380 "googleapis.com universe domain, but the current universe "381 "domain is {}. If you created the credential with an access "382 "token, it's likely that the provided token is expired now, "383 "please update your code with a valid token.".format(384 self._universe_domain385 )386 )387 388 scopes = self._scopes if self._scopes is not None else self._default_scopes389 # Use refresh handler if available and no refresh token is390 # available. This is useful in general when tokens are obtained by calling391 # some external process on demand. It is particularly useful for retrieving392 # downscoped tokens from a token broker.393 if self._refresh_token is None and self.refresh_handler:394 token, expiry = self.refresh_handler(request, scopes=scopes)395 # Validate returned data.396 if not isinstance(token, str):397 raise exceptions.RefreshError(398 "The refresh_handler returned token is not a string."399 )400 if not isinstance(expiry, datetime):401 raise exceptions.RefreshError(402 "The refresh_handler returned expiry is not a datetime object."403 )404 if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD:405 raise exceptions.RefreshError(406 "The credentials returned by the refresh_handler are "407 "already expired."408 )409 self.token = token410 self.expiry = expiry411 return412 413 if (414 self._refresh_token is None415 or self._token_uri is None416 or self._client_id is None417 or self._client_secret is None418 ):419 raise exceptions.RefreshError(420 "The credentials do not contain the necessary fields need to "421 "refresh the access token. You must specify refresh_token, "422 "token_uri, client_id, and client_secret."423 )424 425 (426 access_token,427 refresh_token,428 expiry,429 grant_response,430 rapt_token,431 ) = reauth.refresh_grant(432 request,433 self._token_uri,434 self._refresh_token,435 self._client_id,436 self._client_secret,437 scopes=scopes,438 rapt_token=self._rapt_token,439 enable_reauth_refresh=self._enable_reauth_refresh,440 )441 442 self.token = access_token443 self.expiry = expiry444 self._refresh_token = refresh_token445 self._id_token = grant_response.get("id_token")446 self._rapt_token = rapt_token447 448 if scopes and "scope" in grant_response:449 requested_scopes = frozenset(scopes)450 self._granted_scopes = grant_response["scope"].split()451 granted_scopes = frozenset(self._granted_scopes)452 scopes_requested_but_not_granted = requested_scopes - granted_scopes453 if scopes_requested_but_not_granted:454 # User might be presented with unbundled scopes at the time of455 # consent. So it is a valid scenario to not have all the requested456 # scopes as part of granted scopes but log a warning in case the457 # developer wants to debug the scenario.458 _LOGGER.warning(459 "Not all requested scopes were granted by the "460 "authorization server, missing scopes {}.".format(461 ", ".join(scopes_requested_but_not_granted)462 )463 )464 465 @classmethod466 def from_authorized_user_info(cls, info, scopes=None):467 """Creates a Credentials instance from parsed authorized user info.468 469 Args:470 info (Mapping[str, str]): The authorized user info in Google471 format.472 scopes (Sequence[str]): Optional list of scopes to include in the473 credentials.474 475 Returns:476 google.oauth2.credentials.Credentials: The constructed477 credentials.478 479 Raises:480 ValueError: If the info is not in the expected format.481 """482 keys_needed = set(("refresh_token", "client_id", "client_secret"))483 missing = keys_needed.difference(info.keys())484 485 if missing:486 raise ValueError(487 "Authorized user info was not in the expected format, missing "488 "fields {}.".format(", ".join(missing))489 )490 491 # access token expiry (datetime obj); auto-expire if not saved492 expiry = info.get("expiry")493 if expiry:494 expiry = datetime.strptime(495 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S"496 )497 else:498 expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD499 500 # process scopes, which needs to be a seq501 if scopes is None and "scopes" in info:502 scopes = info.get("scopes")503 if isinstance(scopes, str):504 scopes = scopes.split(" ")505 506 return cls(507 token=info.get("token"),508 refresh_token=info.get("refresh_token"),509 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides510 scopes=scopes,511 client_id=info.get("client_id"),512 client_secret=info.get("client_secret"),513 quota_project_id=info.get("quota_project_id"), # may not exist514 expiry=expiry,515 rapt_token=info.get("rapt_token"), # may not exist516 trust_boundary=info.get("trust_boundary"), # may not exist517 universe_domain=info.get("universe_domain"), # may not exist518 account=info.get("account", ""), # may not exist519 )520 521 @classmethod522 def from_authorized_user_file(cls, filename, scopes=None):523 """Creates a Credentials instance from an authorized user json file.524 525 Args:526 filename (str): The path to the authorized user json file.527 scopes (Sequence[str]): Optional list of scopes to include in the528 credentials.529 530 Returns:531 google.oauth2.credentials.Credentials: The constructed532 credentials.533 534 Raises:535 ValueError: If the file is not in the expected format.536 """537 with io.open(filename, "r", encoding="utf-8") as json_file:538 data = json.load(json_file)539 return cls.from_authorized_user_info(data, scopes)540 541 def to_json(self, strip=None):542 """Utility function that creates a JSON representation of a Credentials543 object.544 545 Args:546 strip (Sequence[str]): Optional list of members to exclude from the547 generated JSON.548 549 Returns:550 str: A JSON representation of this instance. When converted into551 a dictionary, it can be passed to from_authorized_user_info()552 to create a new credential instance.553 """554 prep = {555 "token": self.token,556 "refresh_token": self.refresh_token,557 "token_uri": self.token_uri,558 "client_id": self.client_id,559 "client_secret": self.client_secret,560 "scopes": self.scopes,561 "rapt_token": self.rapt_token,562 "universe_domain": self._universe_domain,563 "account": self._account,564 }565 if self.expiry: # flatten expiry timestamp566 prep["expiry"] = self.expiry.isoformat() + "Z"567 568 # Remove empty entries (those which are None)569 prep = {k: v for k, v in prep.items() if v is not None}570 571 # Remove entries that explicitely need to be removed572 if strip is not None:573 prep = {k: v for k, v in prep.items() if k not in strip}574 575 return json.dumps(prep)576 577 578class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject):579 """Access token credentials for user account.580 581 Obtain the access token for a given user account or the current active582 user account with the ``gcloud auth print-access-token`` command.583 584 Args:585 account (Optional[str]): Account to get the access token for. If not586 specified, the current active account will be used.587 quota_project_id (Optional[str]): The project ID used for quota588 and billing.589 """590 591 def __init__(self, account=None, quota_project_id=None):592 warnings.warn(593 "UserAccessTokenCredentials is deprecated, please use "594 "google.oauth2.credentials.Credentials instead. To use "595 "that credential type, simply run "596 "`gcloud auth application-default login` and let the "597 "client libraries pick up the application default credentials."598 )599 super(UserAccessTokenCredentials, self).__init__()600 self._account = account601 self._quota_project_id = quota_project_id602 603 def with_account(self, account):604 """Create a new instance with the given account.605 606 Args:607 account (str): Account to get the access token for.608 609 Returns:610 google.oauth2.credentials.UserAccessTokenCredentials: The created611 credentials with the given account.612 """613 return self.__class__(account=account, quota_project_id=self._quota_project_id)614 615 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)616 def with_quota_project(self, quota_project_id):617 return self.__class__(account=self._account, quota_project_id=quota_project_id)618 619 def refresh(self, request):620 """Refreshes the access token.621 622 Args:623 request (google.auth.transport.Request): This argument is required624 by the base class interface but not used in this implementation,625 so just set it to `None`.626 627 Raises:628 google.auth.exceptions.UserAccessTokenError: If the access token629 refresh failed.630 """631 self.token = _cloud_sdk.get_auth_access_token(self._account)632 633 @_helpers.copy_docstring(credentials.Credentials)634 def before_request(self, request, method, url, headers):635 self.refresh(request)636 self.apply(headers)637 