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 37 38import six 39 40from google.auth import _cloud_sdk 41from google.auth import _helpers 42from google.auth import credentials 43from google.auth import exceptions 44from google.oauth2 import reauth 45 46 47# The Google OAuth 2.0 token endpoint. Used for authorized user credentials. 48_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" 49 50 51class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject): 52 """Credentials using OAuth 2.0 access and refresh tokens. 53 54 The credentials are considered immutable. If you want to modify the 55 quota project, use :meth:`with_quota_project` or :: 56 57 credentials = credentials.with_quota_project('myproject-123) 58 59 Reauth is disabled by default. To enable reauth, set the 60 `enable_reauth_refresh` parameter to True in the constructor. Note that 61 reauth feature is intended for gcloud to use only. 62 If reauth is enabled, `pyu2f` dependency has to be installed in order to use security 63 key reauth feature. Dependency can be installed via `pip install pyu2f` or `pip install 64 google-auth[reauth]`. 65 """ 66 67 def __init__( 68 self, 69 token, 70 refresh_token=None, 71 id_token=None, 72 token_uri=None, 73 client_id=None, 74 client_secret=None, 75 scopes=None, 76 default_scopes=None, 77 quota_project_id=None, 78 expiry=None, 79 rapt_token=None, 80 refresh_handler=None, 81 enable_reauth_refresh=False, 82 ): 83 """ 84 Args: 85 token (Optional(str)): The OAuth 2.0 access token. Can be None 86 if refresh information is provided. 87 refresh_token (str): The OAuth 2.0 refresh token. If specified, 88 credentials can be refreshed. 89 id_token (str): The Open ID Connect ID Token. 90 token_uri (str): The OAuth 2.0 authorization server's token 91 endpoint URI. Must be specified for refresh, can be left as 92 None if the token can not be refreshed. 93 client_id (str): The OAuth 2.0 client ID. Must be specified for 94 refresh, can be left as None if the token can not be refreshed. 95 client_secret(str): The OAuth 2.0 client secret. Must be specified 96 for refresh, can be left as None if the token can not be 97 refreshed. 98 scopes (Sequence[str]): The scopes used to obtain authorization. 99 This parameter is used by :meth:`has_scopes`. OAuth 2.0 100 credentials can not request additional scopes after 101 authorization. The scopes must be derivable from the refresh 102 token if refresh information is provided (e.g. The refresh 103 token scopes are a superset of this or contain a wild card 104 scope like 'https://www.googleapis.com/auth/any-api'). 105 default_scopes (Sequence[str]): Default scopes passed by a 106 Google client library. Use 'scopes' for user-defined scopes. 107 quota_project_id (Optional[str]): The project ID used for quota and billing. 108 This project may be different from the project used to 109 create the credentials. 110 rapt_token (Optional[str]): The reauth Proof Token. 111 refresh_handler (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 112 A callable which takes in the HTTP request callable and the list of 113 OAuth scopes and when called returns an access token string for the 114 requested scopes and its expiry datetime. This is useful when no 115 refresh tokens are provided and tokens are obtained by calling 116 some external process on demand. It is particularly useful for 117 retrieving downscoped tokens from a token broker. 118 enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow 119 should be used. This flag is for gcloud to use only. 120 """ 121 super(Credentials, self).__init__() 122 self.token = token 123 self.expiry = expiry 124 self._refresh_token = refresh_token 125 self._id_token = id_token 126 self._scopes = scopes 127 self._default_scopes = default_scopes 128 self._token_uri = token_uri 129 self._client_id = client_id 130 self._client_secret = client_secret 131 self._quota_project_id = quota_project_id 132 self._rapt_token = rapt_token 133 self.refresh_handler = refresh_handler 134 self._enable_reauth_refresh = enable_reauth_refresh 135 136 def __getstate__(self): 137 """A __getstate__ method must exist for the __setstate__ to be called 138 This is identical to the default implementation. 139 See https://docs.python.org/3.7/library/pickle.html#object.__setstate__ 140 """ 141 state_dict = self.__dict__.copy() 142 # Remove _refresh_handler function as there are limitations pickling and 143 # unpickling certain callables (lambda, functools.partial instances) 144 # because they need to be importable. 145 # Instead, the refresh_handler setter should be used to repopulate this. 146 del state_dict["_refresh_handler"] 147 return state_dict 148 149 def __setstate__(self, d): 150 """Credentials pickled with older versions of the class do not have 151 all the attributes.""" 152 self.token = d.get("token") 153 self.expiry = d.get("expiry") 154 self._refresh_token = d.get("_refresh_token") 155 self._id_token = d.get("_id_token") 156 self._scopes = d.get("_scopes") 157 self._default_scopes = d.get("_default_scopes") 158 self._token_uri = d.get("_token_uri") 159 self._client_id = d.get("_client_id") 160 self._client_secret = d.get("_client_secret") 161 self._quota_project_id = d.get("_quota_project_id") 162 self._rapt_token = d.get("_rapt_token") 163 self._enable_reauth_refresh = d.get("_enable_reauth_refresh") 164 # The refresh_handler setter should be used to repopulate this. 165 self._refresh_handler = None 166 167 @property 168 def refresh_token(self): 169 """Optional[str]: The OAuth 2.0 refresh token.""" 170 return self._refresh_token 171 172 @property 173 def scopes(self): 174 """Optional[str]: The OAuth 2.0 permission scopes.""" 175 return self._scopes 176 177 @property 178 def token_uri(self): 179 """Optional[str]: The OAuth 2.0 authorization server's token endpoint 180 URI.""" 181 return self._token_uri 182 183 @property 184 def id_token(self): 185 """Optional[str]: The Open ID Connect ID Token. 186 187 Depending on the authorization server and the scopes requested, this 188 may be populated when credentials are obtained and updated when 189 :meth:`refresh` is called. This token is a JWT. It can be verified 190 and decoded using :func:`google.oauth2.id_token.verify_oauth2_token`. 191 """ 192 return self._id_token 193 194 @property 195 def client_id(self): 196 """Optional[str]: The OAuth 2.0 client ID.""" 197 return self._client_id 198 199 @property 200 def client_secret(self): 201 """Optional[str]: The OAuth 2.0 client secret.""" 202 return self._client_secret 203 204 @property 205 def requires_scopes(self): 206 """False: OAuth 2.0 credentials have their scopes set when 207 the initial token is requested and can not be changed.""" 208 return False 209 210 @property 211 def rapt_token(self): 212 """Optional[str]: The reauth Proof Token.""" 213 return self._rapt_token 214 215 @property 216 def refresh_handler(self): 217 """Returns the refresh handler if available. 218 219 Returns: 220 Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]: 221 The current refresh handler. 222 """ 223 return self._refresh_handler 224 225 @refresh_handler.setter 226 def refresh_handler(self, value): 227 """Updates the current refresh handler. 228 229 Args: 230 value (Optional[Callable[[google.auth.transport.Request, Sequence[str]], [str, datetime]]]): 231 The updated value of the refresh handler. 232 233 Raises: 234 TypeError: If the value is not a callable or None. 235 """ 236 if not callable(value) and value is not None: 237 raise TypeError("The provided refresh_handler is not a callable or None.") 238 self._refresh_handler = value 239 240 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 241 def with_quota_project(self, quota_project_id): 242 243 return self.__class__( 244 self.token, 245 refresh_token=self.refresh_token, 246 id_token=self.id_token, 247 token_uri=self.token_uri, 248 client_id=self.client_id, 249 client_secret=self.client_secret, 250 scopes=self.scopes, 251 default_scopes=self.default_scopes, 252 quota_project_id=quota_project_id, 253 rapt_token=self.rapt_token, 254 enable_reauth_refresh=self._enable_reauth_refresh, 255 ) 256 257 @_helpers.copy_docstring(credentials.Credentials) 258 def refresh(self, request): 259 scopes = self._scopes if self._scopes is not None else self._default_scopes 260 # Use refresh handler if available and no refresh token is 261 # available. This is useful in general when tokens are obtained by calling 262 # some external process on demand. It is particularly useful for retrieving 263 # downscoped tokens from a token broker. 264 if self._refresh_token is None and self.refresh_handler: 265 token, expiry = self.refresh_handler(request, scopes=scopes) 266 # Validate returned data. 267 if not isinstance(token, six.string_types): 268 raise exceptions.RefreshError( 269 "The refresh_handler returned token is not a string." 270 ) 271 if not isinstance(expiry, datetime): 272 raise exceptions.RefreshError( 273 "The refresh_handler returned expiry is not a datetime object." 274 ) 275 if _helpers.utcnow() >= expiry - _helpers.REFRESH_THRESHOLD: 276 raise exceptions.RefreshError( 277 "The credentials returned by the refresh_handler are " 278 "already expired." 279 ) 280 self.token = token 281 self.expiry = expiry 282 return 283 284 if ( 285 self._refresh_token is None 286 or self._token_uri is None 287 or self._client_id is None 288 or self._client_secret is None 289 ): 290 raise exceptions.RefreshError( 291 "The credentials do not contain the necessary fields need to " 292 "refresh the access token. You must specify refresh_token, " 293 "token_uri, client_id, and client_secret." 294 ) 295 296 ( 297 access_token, 298 refresh_token, 299 expiry, 300 grant_response, 301 rapt_token, 302 ) = reauth.refresh_grant( 303 request, 304 self._token_uri, 305 self._refresh_token, 306 self._client_id, 307 self._client_secret, 308 scopes=scopes, 309 rapt_token=self._rapt_token, 310 enable_reauth_refresh=self._enable_reauth_refresh, 311 ) 312 313 self.token = access_token 314 self.expiry = expiry 315 self._refresh_token = refresh_token 316 self._id_token = grant_response.get("id_token") 317 self._rapt_token = rapt_token 318 319 if scopes and "scope" in grant_response: 320 requested_scopes = frozenset(scopes) 321 granted_scopes = frozenset(grant_response["scope"].split()) 322 scopes_requested_but_not_granted = requested_scopes - granted_scopes 323 if scopes_requested_but_not_granted: 324 raise exceptions.RefreshError( 325 "Not all requested scopes were granted by the " 326 "authorization server, missing scopes {}.".format( 327 ", ".join(scopes_requested_but_not_granted) 328 ) 329 ) 330 331 @classmethod 332 def from_authorized_user_info(cls, info, scopes=None): 333 """Creates a Credentials instance from parsed authorized user info. 334 335 Args: 336 info (Mapping[str, str]): The authorized user info in Google 337 format. 338 scopes (Sequence[str]): Optional list of scopes to include in the 339 credentials. 340 341 Returns: 342 google.oauth2.credentials.Credentials: The constructed 343 credentials. 344 345 Raises: 346 ValueError: If the info is not in the expected format. 347 """ 348 keys_needed = set(("refresh_token", "client_id", "client_secret")) 349 missing = keys_needed.difference(six.iterkeys(info)) 350 351 if missing: 352 raise ValueError( 353 "Authorized user info was not in the expected format, missing " 354 "fields {}.".format(", ".join(missing)) 355 ) 356 357 # access token expiry (datetime obj); auto-expire if not saved 358 expiry = info.get("expiry") 359 if expiry: 360 expiry = datetime.strptime( 361 expiry.rstrip("Z").split(".")[0], "%Y-%m-%dT%H:%M:%S" 362 ) 363 else: 364 expiry = _helpers.utcnow() - _helpers.REFRESH_THRESHOLD 365 366 # process scopes, which needs to be a seq 367 if scopes is None and "scopes" in info: 368 scopes = info.get("scopes") 369 if isinstance(scopes, six.string_types): 370 scopes = scopes.split(" ") 371 372 return cls( 373 token=info.get("token"), 374 refresh_token=info.get("refresh_token"), 375 token_uri=_GOOGLE_OAUTH2_TOKEN_ENDPOINT, # always overrides 376 scopes=scopes, 377 client_id=info.get("client_id"), 378 client_secret=info.get("client_secret"), 379 quota_project_id=info.get("quota_project_id"), # may not exist 380 expiry=expiry, 381 rapt_token=info.get("rapt_token"), # may not exist 382 ) 383 384 @classmethod 385 def from_authorized_user_file(cls, filename, scopes=None): 386 """Creates a Credentials instance from an authorized user json file. 387 388 Args: 389 filename (str): The path to the authorized user json file. 390 scopes (Sequence[str]): Optional list of scopes to include in the 391 credentials. 392 393 Returns: 394 google.oauth2.credentials.Credentials: The constructed 395 credentials. 396 397 Raises: 398 ValueError: If the file is not in the expected format. 399 """ 400 with io.open(filename, "r", encoding="utf-8") as json_file: 401 data = json.load(json_file) 402 return cls.from_authorized_user_info(data, scopes) 403 404 def to_json(self, strip=None): 405 """Utility function that creates a JSON representation of a Credentials 406 object. 407 408 Args: 409 strip (Sequence[str]): Optional list of members to exclude from the 410 generated JSON. 411 412 Returns: 413 str: A JSON representation of this instance. When converted into 414 a dictionary, it can be passed to from_authorized_user_info() 415 to create a new credential instance. 416 """ 417 prep = { 418 "token": self.token, 419 "refresh_token": self.refresh_token, 420 "token_uri": self.token_uri, 421 "client_id": self.client_id, 422 "client_secret": self.client_secret, 423 "scopes": self.scopes, 424 "rapt_token": self.rapt_token, 425 } 426 if self.expiry: # flatten expiry timestamp 427 prep["expiry"] = self.expiry.isoformat() + "Z" 428 429 # Remove empty entries (those which are None) 430 prep = {k: v for k, v in prep.items() if v is not None} 431 432 # Remove entries that explicitely need to be removed 433 if strip is not None: 434 prep = {k: v for k, v in prep.items() if k not in strip} 435 436 return json.dumps(prep) 437 438 439class UserAccessTokenCredentials(credentials.CredentialsWithQuotaProject): 440 """Access token credentials for user account. 441 442 Obtain the access token for a given user account or the current active 443 user account with the ``gcloud auth print-access-token`` command. 444 445 Args: 446 account (Optional[str]): Account to get the access token for. If not 447 specified, the current active account will be used. 448 quota_project_id (Optional[str]): The project ID used for quota 449 and billing. 450 """ 451 452 def __init__(self, account=None, quota_project_id=None): 453 super(UserAccessTokenCredentials, self).__init__() 454 self._account = account 455 self._quota_project_id = quota_project_id 456 457 def with_account(self, account): 458 """Create a new instance with the given account. 459 460 Args: 461 account (str): Account to get the access token for. 462 463 Returns: 464 google.oauth2.credentials.UserAccessTokenCredentials: The created 465 credentials with the given account. 466 """ 467 return self.__class__(account=account, quota_project_id=self._quota_project_id) 468 469 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 470 def with_quota_project(self, quota_project_id): 471 return self.__class__(account=self._account, quota_project_id=quota_project_id) 472 473 def refresh(self, request): 474 """Refreshes the access token. 475 476 Args: 477 request (google.auth.transport.Request): This argument is required 478 by the base class interface but not used in this implementation, 479 so just set it to `None`. 480 481 Raises: 482 google.auth.exceptions.UserAccessTokenError: If the access token 483 refresh failed. 484 """ 485 self.token = _cloud_sdk.get_auth_access_token(self._account) 486 487 @_helpers.copy_docstring(credentials.Credentials) 488 def before_request(self, request, method, url, headers): 489 self.refresh(request) 490 self.apply(headers) 491