1# Copyright 2014 Google Inc. All rights reserved. 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"""An OAuth 2.0 client. 16 17Tools for interacting with OAuth 2.0 protected resources. 18""" 19 20import collections 21import copy 22import datetime 23import json 24import logging 25import os 26import shutil 27import socket 28import sys 29import tempfile 30 31import six 32from six.moves import http_client 33from six.moves import urllib 34 35import oauth2client 36from oauth2client import _helpers 37from oauth2client import clientsecrets 38from oauth2client import transport 39from oauth2client import util 40 41 42__author__ = 'jcgregorio@google.com (Joe Gregorio)' 43 44HAS_OPENSSL = False 45HAS_CRYPTO = False 46try: 47 from oauth2client import crypt 48 HAS_CRYPTO = True 49 HAS_OPENSSL = crypt.OpenSSLVerifier is not None 50except ImportError: # pragma: NO COVER 51 pass 52 53 54logger = logging.getLogger(__name__) 55 56# Expiry is stored in RFC3339 UTC format 57EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 58 59# Which certs to use to validate id_tokens received. 60ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' 61# This symbol previously had a typo in the name; we keep the old name 62# around for now, but will remove it in the future. 63ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_CERTS 64 65# Constant to use for the out of band OAuth 2.0 flow. 66OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' 67 68# The value representing user credentials. 69AUTHORIZED_USER = 'authorized_user' 70 71# The value representing service account credentials. 72SERVICE_ACCOUNT = 'service_account' 73 74# The environment variable pointing the file with local 75# Application Default Credentials. 76GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' 77# The ~/.config subdirectory containing gcloud credentials. Intended 78# to be swapped out in tests. 79_CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' 80# The environment variable name which can replace ~/.config if set. 81_CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG' 82 83# The error message we show users when we can't find the Application 84# Default Credentials. 85ADC_HELP_MSG = ( 86 'The Application Default Credentials are not available. They are ' 87 'available if running in Google Compute Engine. Otherwise, the ' 88 'environment variable ' + 89 GOOGLE_APPLICATION_CREDENTIALS + 90 ' must be defined pointing to a file defining the credentials. See ' 91 'https://developers.google.com/accounts/docs/' 92 'application-default-credentials for more information.') 93 94_WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' 95 96# The access token along with the seconds in which it expires. 97AccessTokenInfo = collections.namedtuple( 98 'AccessTokenInfo', ['access_token', 'expires_in']) 99 100DEFAULT_ENV_NAME = 'UNKNOWN' 101 102# If set to True _get_environment avoid GCE check (_detect_gce_environment) 103NO_GCE_CHECK = os.environ.setdefault('NO_GCE_CHECK', 'False') 104 105# Timeout in seconds to wait for the GCE metadata server when detecting the 106# GCE environment. 107try: 108 GCE_METADATA_TIMEOUT = int( 109 os.environ.setdefault('GCE_METADATA_TIMEOUT', '3')) 110except ValueError: # pragma: NO COVER 111 GCE_METADATA_TIMEOUT = 3 112 113_SERVER_SOFTWARE = 'SERVER_SOFTWARE' 114_GCE_METADATA_HOST = '169.254.169.254' 115_METADATA_FLAVOR_HEADER = 'Metadata-Flavor' 116_DESIRED_METADATA_FLAVOR = 'Google' 117 118# Expose utcnow() at module level to allow for 119# easier testing (by replacing with a stub). 120_UTCNOW = datetime.datetime.utcnow 121 122# NOTE: These names were previously defined in this module but have been 123# moved into `oauth2client.transport`, 124clean_headers = transport.clean_headers 125MemoryCache = transport.MemoryCache 126REFRESH_STATUS_CODES = transport.REFRESH_STATUS_CODES 127 128 129class SETTINGS(object): 130 """Settings namespace for globally defined values.""" 131 env_name = None 132 133 134class Error(Exception): 135 """Base error for this module.""" 136 137 138class FlowExchangeError(Error): 139 """Error trying to exchange an authorization grant for an access token.""" 140 141 142class AccessTokenRefreshError(Error): 143 """Error trying to refresh an expired access token.""" 144 145 146class HttpAccessTokenRefreshError(AccessTokenRefreshError): 147 """Error (with HTTP status) trying to refresh an expired access token.""" 148 def __init__(self, *args, **kwargs): 149 super(HttpAccessTokenRefreshError, self).__init__(*args) 150 self.status = kwargs.get('status') 151 152 153class TokenRevokeError(Error): 154 """Error trying to revoke a token.""" 155 156 157class UnknownClientSecretsFlowError(Error): 158 """The client secrets file called for an unknown type of OAuth 2.0 flow.""" 159 160 161class AccessTokenCredentialsError(Error): 162 """Having only the access_token means no refresh is possible.""" 163 164 165class VerifyJwtTokenError(Error): 166 """Could not retrieve certificates for validation.""" 167 168 169class NonAsciiHeaderError(Error): 170 """Header names and values must be ASCII strings.""" 171 172 173class ApplicationDefaultCredentialsError(Error): 174 """Error retrieving the Application Default Credentials.""" 175 176 177class OAuth2DeviceCodeError(Error): 178 """Error trying to retrieve a device code.""" 179 180 181class CryptoUnavailableError(Error, NotImplementedError): 182 """Raised when a crypto library is required, but none is available.""" 183 184 185def _parse_expiry(expiry): 186 if expiry and isinstance(expiry, datetime.datetime): 187 return expiry.strftime(EXPIRY_FORMAT) 188 else: 189 return None 190 191 192class Credentials(object): 193 """Base class for all Credentials objects. 194 195 Subclasses must define an authorize() method that applies the credentials 196 to an HTTP transport. 197 198 Subclasses must also specify a classmethod named 'from_json' that takes a 199 JSON string as input and returns an instantiated Credentials object. 200 """ 201 202 NON_SERIALIZED_MEMBERS = frozenset(['store']) 203 204 def authorize(self, http): 205 """Take an httplib2.Http instance (or equivalent) and authorizes it. 206 207 Authorizes it for the set of credentials, usually by replacing 208 http.request() with a method that adds in the appropriate headers and 209 then delegates to the original Http.request() method. 210 211 Args: 212 http: httplib2.Http, an http object to be used to make the refresh 213 request. 214 """ 215 raise NotImplementedError 216 217 def refresh(self, http): 218 """Forces a refresh of the access_token. 219 220 Args: 221 http: httplib2.Http, an http object to be used to make the refresh 222 request. 223 """ 224 raise NotImplementedError 225 226 def revoke(self, http): 227 """Revokes a refresh_token and makes the credentials void. 228 229 Args: 230 http: httplib2.Http, an http object to be used to make the revoke 231 request. 232 """ 233 raise NotImplementedError 234 235 def apply(self, headers): 236 """Add the authorization to the headers. 237 238 Args: 239 headers: dict, the headers to add the Authorization header to. 240 """ 241 raise NotImplementedError 242 243 def _to_json(self, strip, to_serialize=None): 244 """Utility function that creates JSON repr. of a Credentials object. 245 246 Args: 247 strip: array, An array of names of members to exclude from the 248 JSON. 249 to_serialize: dict, (Optional) The properties for this object 250 that will be serialized. This allows callers to 251 modify before serializing. 252 253 Returns: 254 string, a JSON representation of this instance, suitable to pass to 255 from_json(). 256 """ 257 curr_type = self.__class__ 258 if to_serialize is None: 259 to_serialize = copy.copy(self.__dict__) 260 else: 261 # Assumes it is a str->str dictionary, so we don't deep copy. 262 to_serialize = copy.copy(to_serialize) 263 for member in strip: 264 if member in to_serialize: 265 del to_serialize[member] 266 to_serialize['token_expiry'] = _parse_expiry( 267 to_serialize.get('token_expiry')) 268 # Add in information we will need later to reconstitute this instance. 269 to_serialize['_class'] = curr_type.__name__ 270 to_serialize['_module'] = curr_type.__module__ 271 for key, val in to_serialize.items(): 272 if isinstance(val, bytes): 273 to_serialize[key] = val.decode('utf-8') 274 if isinstance(val, set): 275 to_serialize[key] = list(val) 276 return json.dumps(to_serialize) 277 278 def to_json(self): 279 """Creating a JSON representation of an instance of Credentials. 280 281 Returns: 282 string, a JSON representation of this instance, suitable to pass to 283 from_json(). 284 """ 285 return self._to_json(self.NON_SERIALIZED_MEMBERS) 286 287 @classmethod 288 def new_from_json(cls, json_data): 289 """Utility class method to instantiate a Credentials subclass from JSON. 290 291 Expects the JSON string to have been produced by to_json(). 292 293 Args: 294 json_data: string or bytes, JSON from to_json(). 295 296 Returns: 297 An instance of the subclass of Credentials that was serialized with 298 to_json(). 299 """ 300 json_data_as_unicode = _helpers._from_bytes(json_data) 301 data = json.loads(json_data_as_unicode) 302 # Find and call the right classmethod from_json() to restore 303 # the object. 304 module_name = data['_module'] 305 try: 306 module_obj = __import__(module_name) 307 except ImportError: 308 # In case there's an object from the old package structure, 309 # update it 310 module_name = module_name.replace('.googleapiclient', '') 311 module_obj = __import__(module_name) 312 313 module_obj = __import__(module_name, 314 fromlist=module_name.split('.')[:-1]) 315 kls = getattr(module_obj, data['_class']) 316 return kls.from_json(json_data_as_unicode) 317 318 @classmethod 319 def from_json(cls, unused_data): 320 """Instantiate a Credentials object from a JSON description of it. 321 322 The JSON should have been produced by calling .to_json() on the object. 323 324 Args: 325 unused_data: dict, A deserialized JSON object. 326 327 Returns: 328 An instance of a Credentials subclass. 329 """ 330 return Credentials() 331 332 333class Flow(object): 334 """Base class for all Flow objects.""" 335 pass 336 337 338class Storage(object): 339 """Base class for all Storage objects. 340 341 Store and retrieve a single credential. This class supports locking 342 such that multiple processes and threads can operate on a single 343 store. 344 """ 345 def __init__(self, lock=None): 346 """Create a Storage instance. 347 348 Args: 349 lock: An optional threading.Lock-like object. Must implement at 350 least acquire() and release(). Does not need to be 351 re-entrant. 352 """ 353 self._lock = lock 354 355 def acquire_lock(self): 356 """Acquires any lock necessary to access this Storage. 357 358 This lock is not reentrant. 359 """ 360 if self._lock is not None: 361 self._lock.acquire() 362 363 def release_lock(self): 364 """Release the Storage lock. 365 366 Trying to release a lock that isn't held will result in a 367 RuntimeError in the case of a threading.Lock or multiprocessing.Lock. 368 """ 369 if self._lock is not None: 370 self._lock.release() 371 372 def locked_get(self): 373 """Retrieve credential. 374 375 The Storage lock must be held when this is called. 376 377 Returns: 378 oauth2client.client.Credentials 379 """ 380 raise NotImplementedError 381 382 def locked_put(self, credentials): 383 """Write a credential. 384 385 The Storage lock must be held when this is called. 386 387 Args: 388 credentials: Credentials, the credentials to store. 389 """ 390 raise NotImplementedError 391 392 def locked_delete(self): 393 """Delete a credential. 394 395 The Storage lock must be held when this is called. 396 """ 397 raise NotImplementedError 398 399 def get(self): 400 """Retrieve credential. 401 402 The Storage lock must *not* be held when this is called. 403 404 Returns: 405 oauth2client.client.Credentials 406 """ 407 self.acquire_lock() 408 try: 409 return self.locked_get() 410 finally: 411 self.release_lock() 412 413 def put(self, credentials): 414 """Write a credential. 415 416 The Storage lock must be held when this is called. 417 418 Args: 419 credentials: Credentials, the credentials to store. 420 """ 421 self.acquire_lock() 422 try: 423 self.locked_put(credentials) 424 finally: 425 self.release_lock() 426 427 def delete(self): 428 """Delete credential. 429 430 Frees any resources associated with storing the credential. 431 The Storage lock must *not* be held when this is called. 432 433 Returns: 434 None 435 """ 436 self.acquire_lock() 437 try: 438 return self.locked_delete() 439 finally: 440 self.release_lock() 441 442 443def _update_query_params(uri, params): 444 """Updates a URI with new query parameters. 445 446 Args: 447 uri: string, A valid URI, with potential existing query parameters. 448 params: dict, A dictionary of query parameters. 449 450 Returns: 451 The same URI but with the new query parameters added. 452 """ 453 parts = urllib.parse.urlparse(uri) 454 query_params = dict(urllib.parse.parse_qsl(parts.query)) 455 query_params.update(params) 456 new_parts = parts._replace(query=urllib.parse.urlencode(query_params)) 457 return urllib.parse.urlunparse(new_parts) 458 459 460class OAuth2Credentials(Credentials): 461 """Credentials object for OAuth 2.0. 462 463 Credentials can be applied to an httplib2.Http object using the authorize() 464 method, which then adds the OAuth 2.0 access token to each request. 465 466 OAuth2Credentials objects may be safely pickled and unpickled. 467 """ 468 469 @util.positional(8) 470 def __init__(self, access_token, client_id, client_secret, refresh_token, 471 token_expiry, token_uri, user_agent, revoke_uri=None, 472 id_token=None, token_response=None, scopes=None, 473 token_info_uri=None): 474 """Create an instance of OAuth2Credentials. 475 476 This constructor is not usually called by the user, instead 477 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. 478 479 Args: 480 access_token: string, access token. 481 client_id: string, client identifier. 482 client_secret: string, client secret. 483 refresh_token: string, refresh token. 484 token_expiry: datetime, when the access_token expires. 485 token_uri: string, URI of token endpoint. 486 user_agent: string, The HTTP User-Agent to provide for this 487 application. 488 revoke_uri: string, URI for revoke endpoint. Defaults to None; a 489 token can't be revoked if this is None. 490 id_token: object, The identity of the resource owner. 491 token_response: dict, the decoded response to the token request. 492 None if a token hasn't been requested yet. Stored 493 because some providers (e.g. wordpress.com) include 494 extra fields that clients may want. 495 scopes: list, authorized scopes for these credentials. 496 token_info_uri: string, the URI for the token info endpoint. Defaults 497 to None; scopes can not be refreshed if this is None. 498 499 Notes: 500 store: callable, A callable that when passed a Credential 501 will store the credential back to where it came from. 502 This is needed to store the latest access_token if it 503 has expired and been refreshed. 504 """ 505 self.access_token = access_token 506 self.client_id = client_id 507 self.client_secret = client_secret 508 self.refresh_token = refresh_token 509 self.store = None 510 self.token_expiry = token_expiry 511 self.token_uri = token_uri 512 self.user_agent = user_agent 513 self.revoke_uri = revoke_uri 514 self.id_token = id_token 515 self.token_response = token_response 516 self.scopes = set(util.string_to_scopes(scopes or [])) 517 self.token_info_uri = token_info_uri 518 519 # True if the credentials have been revoked or expired and can't be 520 # refreshed. 521 self.invalid = False 522 523 def authorize(self, http): 524 """Authorize an httplib2.Http instance with these credentials. 525 526 The modified http.request method will add authentication headers to 527 each request and will refresh access_tokens when a 401 is received on a 528 request. In addition the http.request method has a credentials 529 property, http.request.credentials, which is the Credentials object 530 that authorized it. 531 532 Args: 533 http: An instance of ``httplib2.Http`` or something that acts 534 like it. 535 536 Returns: 537 A modified instance of http that was passed in. 538 539 Example:: 540 541 h = httplib2.Http() 542 h = credentials.authorize(h) 543 544 You can't create a new OAuth subclass of httplib2.Authentication 545 because it never gets passed the absolute URI, which is needed for 546 signing. So instead we have to overload 'request' with a closure 547 that adds in the Authorization header and then calls the original 548 version of 'request()'. 549 """ 550 transport.wrap_http_for_auth(self, http) 551 return http 552 553 def refresh(self, http): 554 """Forces a refresh of the access_token. 555 556 Args: 557 http: httplib2.Http, an http object to be used to make the refresh 558 request. 559 """ 560 self._refresh(http.request) 561 562 def revoke(self, http): 563 """Revokes a refresh_token and makes the credentials void. 564 565 Args: 566 http: httplib2.Http, an http object to be used to make the revoke 567 request. 568 """ 569 self._revoke(http.request) 570 571 def apply(self, headers): 572 """Add the authorization to the headers. 573 574 Args: 575 headers: dict, the headers to add the Authorization header to. 576 """ 577 headers['Authorization'] = 'Bearer ' + self.access_token 578 579 def has_scopes(self, scopes): 580 """Verify that the credentials are authorized for the given scopes. 581 582 Returns True if the credentials authorized scopes contain all of the 583 scopes given. 584 585 Args: 586 scopes: list or string, the scopes to check. 587 588 Notes: 589 There are cases where the credentials are unaware of which scopes 590 are authorized. Notably, credentials obtained and stored before 591 this code was added will not have scopes, AccessTokenCredentials do 592 not have scopes. In both cases, you can use refresh_scopes() to 593 obtain the canonical set of scopes. 594 """ 595 scopes = util.string_to_scopes(scopes) 596 return set(scopes).issubset(self.scopes) 597 598 def retrieve_scopes(self, http): 599 """Retrieves the canonical list of scopes for this access token. 600 601 Gets the scopes from the OAuth2 provider. 602 603 Args: 604 http: httplib2.Http, an http object to be used to make the refresh 605 request. 606 607 Returns: 608 A set of strings containing the canonical list of scopes. 609 """ 610 self._retrieve_scopes(http.request) 611 return self.scopes 612 613 @classmethod 614 def from_json(cls, json_data): 615 """Instantiate a Credentials object from a JSON description of it. 616 617 The JSON should have been produced by calling .to_json() on the object. 618 619 Args: 620 json_data: string or bytes, JSON to deserialize. 621 622 Returns: 623 An instance of a Credentials subclass. 624 """ 625 data = json.loads(_helpers._from_bytes(json_data)) 626 if (data.get('token_expiry') and 627 not isinstance(data['token_expiry'], datetime.datetime)): 628 try: 629 data['token_expiry'] = datetime.datetime.strptime( 630 data['token_expiry'], EXPIRY_FORMAT) 631 except ValueError: 632 data['token_expiry'] = None 633 retval = cls( 634 data['access_token'], 635 data['client_id'], 636 data['client_secret'], 637 data['refresh_token'], 638 data['token_expiry'], 639 data['token_uri'], 640 data['user_agent'], 641 revoke_uri=data.get('revoke_uri', None), 642 id_token=data.get('id_token', None), 643 token_response=data.get('token_response', None), 644 scopes=data.get('scopes', None), 645 token_info_uri=data.get('token_info_uri', None)) 646 retval.invalid = data['invalid'] 647 return retval 648 649 @property 650 def access_token_expired(self): 651 """True if the credential is expired or invalid. 652 653 If the token_expiry isn't set, we assume the token doesn't expire. 654 """ 655 if self.invalid: 656 return True 657 658 if not self.token_expiry: 659 return False 660 661 now = _UTCNOW() 662 if now >= self.token_expiry: 663 logger.info('access_token is expired. Now: %s, token_expiry: %s', 664 now, self.token_expiry) 665 return True 666 return False 667 668 def get_access_token(self, http=None): 669 """Return the access token and its expiration information. 670 671 If the token does not exist, get one. 672 If the token expired, refresh it. 673 """ 674 if not self.access_token or self.access_token_expired: 675 if not http: 676 http = transport.get_http_object() 677 self.refresh(http) 678 return AccessTokenInfo(access_token=self.access_token, 679 expires_in=self._expires_in()) 680 681 def set_store(self, store): 682 """Set the Storage for the credential. 683 684 Args: 685 store: Storage, an implementation of Storage object. 686 This is needed to store the latest access_token if it 687 has expired and been refreshed. This implementation uses 688 locking to check for updates before updating the 689 access_token. 690 """ 691 self.store = store 692 693 def _expires_in(self): 694 """Return the number of seconds until this token expires. 695 696 If token_expiry is in the past, this method will return 0, meaning the 697 token has already expired. 698 699 If token_expiry is None, this method will return None. Note that 700 returning 0 in such a case would not be fair: the token may still be 701 valid; we just don't know anything about it. 702 """ 703 if self.token_expiry: 704 now = _UTCNOW() 705 if self.token_expiry > now: 706 time_delta = self.token_expiry - now 707 # TODO(orestica): return time_delta.total_seconds() 708 # once dropping support for Python 2.6 709 return time_delta.days * 86400 + time_delta.seconds 710 else: 711 return 0 712 713 def _updateFromCredential(self, other): 714 """Update this Credential from another instance.""" 715 self.__dict__.update(other.__getstate__()) 716 717 def __getstate__(self): 718 """Trim the state down to something that can be pickled.""" 719 d = copy.copy(self.__dict__) 720 del d['store'] 721 return d 722 723 def __setstate__(self, state): 724 """Reconstitute the state of the object from being pickled.""" 725 self.__dict__.update(state) 726 self.store = None 727 728 def _generate_refresh_request_body(self): 729 """Generate the body that will be used in the refresh request.""" 730 body = urllib.parse.urlencode({ 731 'grant_type': 'refresh_token', 732 'client_id': self.client_id, 733 'client_secret': self.client_secret, 734 'refresh_token': self.refresh_token, 735 }) 736 return body 737 738 def _generate_refresh_request_headers(self): 739 """Generate the headers that will be used in the refresh request.""" 740 headers = { 741 'content-type': 'application/x-www-form-urlencoded', 742 } 743 744 if self.user_agent is not None: 745 headers['user-agent'] = self.user_agent 746 747 return headers 748 749 def _refresh(self, http_request): 750 """Refreshes the access_token. 751 752 This method first checks by reading the Storage object if available. 753 If a refresh is still needed, it holds the Storage lock until the 754 refresh is completed. 755 756 Args: 757 http_request: callable, a callable that matches the method 758 signature of httplib2.Http.request, used to make the 759 refresh request. 760 761 Raises: 762 HttpAccessTokenRefreshError: When the refresh fails. 763 """ 764 if not self.store: 765 self._do_refresh_request(http_request) 766 else: 767 self.store.acquire_lock() 768 try: 769 new_cred = self.store.locked_get() 770 771 if (new_cred and not new_cred.invalid and 772 new_cred.access_token != self.access_token and 773 not new_cred.access_token_expired): 774 logger.info('Updated access_token read from Storage') 775 self._updateFromCredential(new_cred) 776 else: 777 self._do_refresh_request(http_request) 778 finally: 779 self.store.release_lock() 780 781 def _do_refresh_request(self, http_request): 782 """Refresh the access_token using the refresh_token. 783 784 Args: 785 http_request: callable, a callable that matches the method 786 signature of httplib2.Http.request, used to make the 787 refresh request. 788 789 Raises: 790 HttpAccessTokenRefreshError: When the refresh fails. 791 """ 792 body = self._generate_refresh_request_body() 793 headers = self._generate_refresh_request_headers() 794 795 logger.info('Refreshing access_token') 796 resp, content = http_request( 797 self.token_uri, method='POST', body=body, headers=headers) 798 content = _helpers._from_bytes(content) 799 if resp.status == http_client.OK: 800 d = json.loads(content) 801 self.token_response = d 802 self.access_token = d['access_token'] 803 self.refresh_token = d.get('refresh_token', self.refresh_token) 804 if 'expires_in' in d: 805 delta = datetime.timedelta(seconds=int(d['expires_in'])) 806 self.token_expiry = delta + _UTCNOW() 807 else: 808 self.token_expiry = None 809 if 'id_token' in d: 810 self.id_token = _extract_id_token(d['id_token']) 811 else: 812 self.id_token = None 813 # On temporary refresh errors, the user does not actually have to 814 # re-authorize, so we unflag here. 815 self.invalid = False 816 if self.store: 817 self.store.locked_put(self) 818 else: 819 # An {'error':...} response body means the token is expired or 820 # revoked, so we flag the credentials as such. 821 logger.info('Failed to retrieve access token: %s', content) 822 error_msg = 'Invalid response {0}.'.format(resp['status']) 823 try: 824 d = json.loads(content) 825 if 'error' in d: 826 error_msg = d['error'] 827 if 'error_description' in d: 828 error_msg += ': ' + d['error_description'] 829 self.invalid = True 830 if self.store is not None: 831 self.store.locked_put(self) 832 except (TypeError, ValueError): 833 pass 834 raise HttpAccessTokenRefreshError(error_msg, status=resp.status) 835 836 def _revoke(self, http_request): 837 """Revokes this credential and deletes the stored copy (if it exists). 838 839 Args: 840 http_request: callable, a callable that matches the method 841 signature of httplib2.Http.request, used to make the 842 revoke request. 843 """ 844 self._do_revoke(http_request, self.refresh_token or self.access_token) 845 846 def _do_revoke(self, http_request, token): 847 """Revokes this credential and deletes the stored copy (if it exists). 848 849 Args: 850 http_request: callable, a callable that matches the method 851 signature of httplib2.Http.request, used to make the 852 refresh request. 853 token: A string used as the token to be revoked. Can be either an 854 access_token or refresh_token. 855 856 Raises: 857 TokenRevokeError: If the revoke request does not return with a 858 200 OK. 859 """ 860 logger.info('Revoking token') 861 query_params = {'token': token} 862 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) 863 resp, content = http_request(token_revoke_uri) 864 if resp.status == http_client.OK: 865 self.invalid = True 866 else: 867 error_msg = 'Invalid response {0}.'.format(resp.status) 868 try: 869 d = json.loads(_helpers._from_bytes(content)) 870 if 'error' in d: 871 error_msg = d['error'] 872 except (TypeError, ValueError): 873 pass 874 raise TokenRevokeError(error_msg) 875 876 if self.store: 877 self.store.delete() 878 879 def _retrieve_scopes(self, http_request): 880 """Retrieves the list of authorized scopes from the OAuth2 provider. 881 882 Args: 883 http_request: callable, a callable that matches the method 884 signature of httplib2.Http.request, used to make the 885 revoke request. 886 """ 887 self._do_retrieve_scopes(http_request, self.access_token) 888 889 def _do_retrieve_scopes(self, http_request, token): 890 """Retrieves the list of authorized scopes from the OAuth2 provider. 891 892 Args: 893 http_request: callable, a callable that matches the method 894 signature of httplib2.Http.request, used to make the 895 refresh request. 896 token: A string used as the token to identify the credentials to 897 the provider. 898 899 Raises: 900 Error: When refresh fails, indicating the the access token is 901 invalid. 902 """ 903 logger.info('Refreshing scopes') 904 query_params = {'access_token': token, 'fields': 'scope'} 905 token_info_uri = _update_query_params(self.token_info_uri, 906 query_params) 907 resp, content = http_request(token_info_uri) 908 content = _helpers._from_bytes(content) 909 if resp.status == http_client.OK: 910 d = json.loads(content) 911 self.scopes = set(util.string_to_scopes(d.get('scope', ''))) 912 else: 913 error_msg = 'Invalid response {0}.'.format(resp.status) 914 try: 915 d = json.loads(content) 916 if 'error_description' in d: 917 error_msg = d['error_description'] 918 except (TypeError, ValueError): 919 pass 920 raise Error(error_msg) 921 922 923class AccessTokenCredentials(OAuth2Credentials): 924 """Credentials object for OAuth 2.0. 925 926 Credentials can be applied to an httplib2.Http object using the 927 authorize() method, which then signs each request from that object 928 with the OAuth 2.0 access token. This set of credentials is for the 929 use case where you have acquired an OAuth 2.0 access_token from 930 another place such as a JavaScript client or another web 931 application, and wish to use it from Python. Because only the 932 access_token is present it can not be refreshed and will in time 933 expire. 934 935 AccessTokenCredentials objects may be safely pickled and unpickled. 936 937 Usage:: 938 939 credentials = AccessTokenCredentials('<an access token>', 940 'my-user-agent/1.0') 941 http = httplib2.Http() 942 http = credentials.authorize(http) 943 944 Raises: 945 AccessTokenCredentialsExpired: raised when the access_token expires or 946 is revoked. 947 """ 948 949 def __init__(self, access_token, user_agent, revoke_uri=None): 950 """Create an instance of OAuth2Credentials 951 952 This is one of the few types if Credentials that you should contrust, 953 Credentials objects are usually instantiated by a Flow. 954 955 Args: 956 access_token: string, access token. 957 user_agent: string, The HTTP User-Agent to provide for this 958 application. 959 revoke_uri: string, URI for revoke endpoint. Defaults to None; a 960 token can't be revoked if this is None. 961 """ 962 super(AccessTokenCredentials, self).__init__( 963 access_token, 964 None, 965 None, 966 None, 967 None, 968 None, 969 user_agent, 970 revoke_uri=revoke_uri) 971 972 @classmethod 973 def from_json(cls, json_data): 974 data = json.loads(_helpers._from_bytes(json_data)) 975 retval = AccessTokenCredentials( 976 data['access_token'], 977 data['user_agent']) 978 return retval 979 980 def _refresh(self, http_request): 981 raise AccessTokenCredentialsError( 982 'The access_token is expired or invalid and can\'t be refreshed.') 983 984 def _revoke(self, http_request): 985 """Revokes the access_token and deletes the store if available. 986 987 Args: 988 http_request: callable, a callable that matches the method 989 signature of httplib2.Http.request, used to make the 990 revoke request. 991 """ 992 self._do_revoke(http_request, self.access_token) 993 994 995def _detect_gce_environment(): 996 """Determine if the current environment is Compute Engine. 997 998 Returns: 999 Boolean indicating whether or not the current environment is Google 1000 Compute Engine. 1001 """ 1002 # NOTE: The explicit ``timeout`` is a workaround. The underlying 1003 # issue is that resolving an unknown host on some networks will take 1004 # 20-30 seconds; making this timeout short fixes the issue, but 1005 # could lead to false negatives in the event that we are on GCE, but 1006 # the metadata resolution was particularly slow. The latter case is 1007 # "unlikely". 1008 connection = six.moves.http_client.HTTPConnection( 1009 _GCE_METADATA_HOST, timeout=GCE_METADATA_TIMEOUT) 1010 1011 try: 1012 headers = {_METADATA_FLAVOR_HEADER: _DESIRED_METADATA_FLAVOR} 1013 connection.request('GET', '/', headers=headers) 1014 response = connection.getresponse() 1015 if response.status == http_client.OK: 1016 return (response.getheader(_METADATA_FLAVOR_HEADER) == 1017 _DESIRED_METADATA_FLAVOR) 1018 except socket.error: # socket.timeout or socket.error(64, 'Host is down') 1019 logger.info('Timeout attempting to reach GCE metadata service.') 1020 return False 1021 finally: 1022 connection.close() 1023 1024 1025def _in_gae_environment(): 1026 """Detects if the code is running in the App Engine environment. 1027 1028 Returns: 1029 True if running in the GAE environment, False otherwise. 1030 """ 1031 if SETTINGS.env_name is not None: 1032 return SETTINGS.env_name in ('GAE_PRODUCTION', 'GAE_LOCAL') 1033 1034 try: 1035 import google.appengine # noqa: unused import 1036 except ImportError: 1037 pass 1038 else: 1039 server_software = os.environ.get(_SERVER_SOFTWARE, '') 1040 if server_software.startswith('Google App Engine/'): 1041 SETTINGS.env_name = 'GAE_PRODUCTION' 1042 return True 1043 elif server_software.startswith('Development/'): 1044 SETTINGS.env_name = 'GAE_LOCAL' 1045 return True 1046 1047 return False 1048 1049 1050def _in_gce_environment(): 1051 """Detect if the code is running in the Compute Engine environment. 1052 1053 Returns: 1054 True if running in the GCE environment, False otherwise. 1055 """ 1056 if SETTINGS.env_name is not None: 1057 return SETTINGS.env_name == 'GCE_PRODUCTION' 1058 1059 if NO_GCE_CHECK != 'True' and _detect_gce_environment(): 1060 SETTINGS.env_name = 'GCE_PRODUCTION' 1061 return True 1062 return False 1063 1064 1065class GoogleCredentials(OAuth2Credentials): 1066 """Application Default Credentials for use in calling Google APIs. 1067 1068 The Application Default Credentials are being constructed as a function of 1069 the environment where the code is being run. 1070 More details can be found on this page: 1071 https://developers.google.com/accounts/docs/application-default-credentials 1072 1073 Here is an example of how to use the Application Default Credentials for a 1074 service that requires authentication:: 1075 1076 from googleapiclient.discovery import build 1077 from oauth2client.client import GoogleCredentials 1078 1079 credentials = GoogleCredentials.get_application_default() 1080 service = build('compute', 'v1', credentials=credentials) 1081 1082 PROJECT = 'bamboo-machine-422' 1083 ZONE = 'us-central1-a' 1084 request = service.instances().list(project=PROJECT, zone=ZONE) 1085 response = request.execute() 1086 1087 print(response) 1088 """ 1089 1090 NON_SERIALIZED_MEMBERS = ( 1091 frozenset(['_private_key']) | 1092 OAuth2Credentials.NON_SERIALIZED_MEMBERS) 1093 """Members that aren't serialized when object is converted to JSON.""" 1094 1095 def __init__(self, access_token, client_id, client_secret, refresh_token, 1096 token_expiry, token_uri, user_agent, 1097 revoke_uri=oauth2client.GOOGLE_REVOKE_URI): 1098 """Create an instance of GoogleCredentials. 1099 1100 This constructor is not usually called by the user, instead 1101 GoogleCredentials objects are instantiated by 1102 GoogleCredentials.from_stream() or 1103 GoogleCredentials.get_application_default(). 1104 1105 Args: 1106 access_token: string, access token. 1107 client_id: string, client identifier. 1108 client_secret: string, client secret. 1109 refresh_token: string, refresh token. 1110 token_expiry: datetime, when the access_token expires. 1111 token_uri: string, URI of token endpoint. 1112 user_agent: string, The HTTP User-Agent to provide for this 1113 application. 1114 revoke_uri: string, URI for revoke endpoint. Defaults to 1115 oauth2client.GOOGLE_REVOKE_URI; a token can't be 1116 revoked if this is None. 1117 """ 1118 super(GoogleCredentials, self).__init__( 1119 access_token, client_id, client_secret, refresh_token, 1120 token_expiry, token_uri, user_agent, revoke_uri=revoke_uri) 1121 1122 def create_scoped_required(self): 1123 """Whether this Credentials object is scopeless. 1124 1125 create_scoped(scopes) method needs to be called in order to create 1126 a Credentials object for API calls. 1127 """ 1128 return False 1129 1130 def create_scoped(self, scopes): 1131 """Create a Credentials object for the given scopes. 1132 1133 The Credentials type is preserved. 1134 """ 1135 return self 1136 1137 @classmethod 1138 def from_json(cls, json_data): 1139 # TODO(issue 388): eliminate the circularity that is the reason for 1140 # this non-top-level import. 1141 from oauth2client import service_account 1142 data = json.loads(_helpers._from_bytes(json_data)) 1143 1144 # We handle service_account.ServiceAccountCredentials since it is a 1145 # possible return type of GoogleCredentials.get_application_default() 1146 if (data['_module'] == 'oauth2client.service_account' and 1147 data['_class'] == 'ServiceAccountCredentials'): 1148 return service_account.ServiceAccountCredentials.from_json(data) 1149 elif (data['_module'] == 'oauth2client.service_account' and 1150 data['_class'] == '_JWTAccessCredentials'): 1151 return service_account._JWTAccessCredentials.from_json(data) 1152 1153 token_expiry = _parse_expiry(data.get('token_expiry')) 1154 google_credentials = cls( 1155 data['access_token'], 1156 data['client_id'], 1157 data['client_secret'], 1158 data['refresh_token'], 1159 token_expiry, 1160 data['token_uri'], 1161 data['user_agent'], 1162 revoke_uri=data.get('revoke_uri', None)) 1163 google_credentials.invalid = data['invalid'] 1164 return google_credentials 1165 1166 @property 1167 def serialization_data(self): 1168 """Get the fields and values identifying the current credentials.""" 1169 return { 1170 'type': 'authorized_user', 1171 'client_id': self.client_id, 1172 'client_secret': self.client_secret, 1173 'refresh_token': self.refresh_token 1174 } 1175 1176 @staticmethod 1177 def _implicit_credentials_from_gae(): 1178 """Attempts to get implicit credentials in Google App Engine env. 1179 1180 If the current environment is not detected as App Engine, returns None, 1181 indicating no Google App Engine credentials can be detected from the 1182 current environment. 1183 1184 Returns: 1185 None, if not in GAE, else an appengine.AppAssertionCredentials 1186 object. 1187 """ 1188 if not _in_gae_environment(): 1189 return None 1190 1191 return _get_application_default_credential_GAE() 1192 1193 @staticmethod 1194 def _implicit_credentials_from_gce(): 1195 """Attempts to get implicit credentials in Google Compute Engine env. 1196 1197 If the current environment is not detected as Compute Engine, returns 1198 None, indicating no Google Compute Engine credentials can be detected 1199 from the current environment. 1200 1201 Returns: 1202 None, if not in GCE, else a gce.AppAssertionCredentials object. 1203 """ 1204 if not _in_gce_environment(): 1205 return None 1206 1207 return _get_application_default_credential_GCE() 1208 1209 @staticmethod 1210 def _implicit_credentials_from_files(): 1211 """Attempts to get implicit credentials from local credential files. 1212 1213 First checks if the environment variable GOOGLE_APPLICATION_CREDENTIALS 1214 is set with a filename and then falls back to a configuration file (the 1215 "well known" file) associated with the 'gcloud' command line tool. 1216 1217 Returns: 1218 Credentials object associated with the 1219 GOOGLE_APPLICATION_CREDENTIALS file or the "well known" file if 1220 either exist. If neither file is define, returns None, indicating 1221 no credentials from a file can detected from the current 1222 environment. 1223 """ 1224 credentials_filename = _get_environment_variable_file() 1225 if not credentials_filename: 1226 credentials_filename = _get_well_known_file() 1227 if os.path.isfile(credentials_filename): 1228 extra_help = (' (produced automatically when running' 1229 ' "gcloud auth login" command)') 1230 else: 1231 credentials_filename = None 1232 else: 1233 extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + 1234 ' environment variable)') 1235 1236 if not credentials_filename: 1237 return 1238 1239 # If we can read the credentials from a file, we don't need to know 1240 # what environment we are in. 1241 SETTINGS.env_name = DEFAULT_ENV_NAME 1242 1243 try: 1244 return _get_application_default_credential_from_file( 1245 credentials_filename) 1246 except (ApplicationDefaultCredentialsError, ValueError) as error: 1247 _raise_exception_for_reading_json(credentials_filename, 1248 extra_help, error) 1249 1250 @classmethod 1251 def _get_implicit_credentials(cls): 1252 """Gets credentials implicitly from the environment. 1253 1254 Checks environment in order of precedence: 1255 - Environment variable GOOGLE_APPLICATION_CREDENTIALS pointing to 1256 a file with stored credentials information. 1257 - Stored "well known" file associated with `gcloud` command line tool. 1258 - Google App Engine (production and testing) 1259 - Google Compute Engine production environment. 1260 1261 Raises: 1262 ApplicationDefaultCredentialsError: raised when the credentials 1263 fail to be retrieved. 1264 """ 1265 # Environ checks (in order). 1266 environ_checkers = [ 1267 cls._implicit_credentials_from_files, 1268 cls._implicit_credentials_from_gae, 1269 cls._implicit_credentials_from_gce, 1270 ] 1271 1272 for checker in environ_checkers: 1273 credentials = checker() 1274 if credentials is not None: 1275 return credentials 1276 1277 # If no credentials, fail. 1278 raise ApplicationDefaultCredentialsError(ADC_HELP_MSG) 1279 1280 @staticmethod 1281 def get_application_default(): 1282 """Get the Application Default Credentials for the current environment. 1283 1284 Raises: 1285 ApplicationDefaultCredentialsError: raised when the credentials 1286 fail to be retrieved. 1287 """ 1288 return GoogleCredentials._get_implicit_credentials() 1289 1290 @staticmethod 1291 def from_stream(credential_filename): 1292 """Create a Credentials object by reading information from a file. 1293 1294 It returns an object of type GoogleCredentials. 1295 1296 Args: 1297 credential_filename: the path to the file from where the 1298 credentials are to be read 1299 1300 Raises: 1301 ApplicationDefaultCredentialsError: raised when the credentials 1302 fail to be retrieved. 1303 """ 1304 if credential_filename and os.path.isfile(credential_filename): 1305 try: 1306 return _get_application_default_credential_from_file( 1307 credential_filename) 1308 except (ApplicationDefaultCredentialsError, ValueError) as error: 1309 extra_help = (' (provided as parameter to the ' 1310 'from_stream() method)') 1311 _raise_exception_for_reading_json(credential_filename, 1312 extra_help, 1313 error) 1314 else: 1315 raise ApplicationDefaultCredentialsError( 1316 'The parameter passed to the from_stream() ' 1317 'method should point to a file.') 1318 1319 1320def _save_private_file(filename, json_contents): 1321 """Saves a file with read-write permissions on for the owner. 1322 1323 Args: 1324 filename: String. Absolute path to file. 1325 json_contents: JSON serializable object to be saved. 1326 """ 1327 temp_filename = tempfile.mktemp() 1328 file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) 1329 with os.fdopen(file_desc, 'w') as file_handle: 1330 json.dump(json_contents, file_handle, sort_keys=True, 1331 indent=2, separators=(',', ': ')) 1332 shutil.move(temp_filename, filename) 1333 1334 1335def save_to_well_known_file(credentials, well_known_file=None): 1336 """Save the provided GoogleCredentials to the well known file. 1337 1338 Args: 1339 credentials: the credentials to be saved to the well known file; 1340 it should be an instance of GoogleCredentials 1341 well_known_file: the name of the file where the credentials are to be 1342 saved; this parameter is supposed to be used for 1343 testing only 1344 """ 1345 # TODO(orestica): move this method to tools.py 1346 # once the argparse import gets fixed (it is not present in Python 2.6) 1347 1348 if well_known_file is None: 1349 well_known_file = _get_well_known_file() 1350 1351 config_dir = os.path.dirname(well_known_file) 1352 if not os.path.isdir(config_dir): 1353 raise OSError( 1354 'Config directory does not exist: {0}'.format(config_dir)) 1355 1356 credentials_data = credentials.serialization_data 1357 _save_private_file(well_known_file, credentials_data) 1358 1359 1360def _get_environment_variable_file(): 1361 application_default_credential_filename = ( 1362 os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, None)) 1363 1364 if application_default_credential_filename: 1365 if os.path.isfile(application_default_credential_filename): 1366 return application_default_credential_filename 1367 else: 1368 raise ApplicationDefaultCredentialsError( 1369 'File ' + application_default_credential_filename + 1370 ' (pointed by ' + 1371 GOOGLE_APPLICATION_CREDENTIALS + 1372 ' environment variable) does not exist!') 1373 1374 1375def _get_well_known_file(): 1376 """Get the well known file produced by command 'gcloud auth login'.""" 1377 # TODO(orestica): Revisit this method once gcloud provides a better way 1378 # of pinpointing the exact location of the file. 1379 default_config_dir = os.getenv(_CLOUDSDK_CONFIG_ENV_VAR) 1380 if default_config_dir is None: 1381 if os.name == 'nt': 1382 try: 1383 default_config_dir = os.path.join(os.environ['APPDATA'], 1384 _CLOUDSDK_CONFIG_DIRECTORY) 1385 except KeyError: 1386 # This should never happen unless someone is really 1387 # messing with things. 1388 drive = os.environ.get('SystemDrive', 'C:') 1389 default_config_dir = os.path.join(drive, '\\', 1390 _CLOUDSDK_CONFIG_DIRECTORY) 1391 else: 1392 default_config_dir = os.path.join(os.path.expanduser('~'), 1393 '.config', 1394 _CLOUDSDK_CONFIG_DIRECTORY) 1395 1396 return os.path.join(default_config_dir, _WELL_KNOWN_CREDENTIALS_FILE) 1397 1398 1399def _get_application_default_credential_from_file(filename): 1400 """Build the Application Default Credentials from file.""" 1401 # read the credentials from the file 1402 with open(filename) as file_obj: 1403 client_credentials = json.load(file_obj) 1404 1405 credentials_type = client_credentials.get('type') 1406 if credentials_type == AUTHORIZED_USER: 1407 required_fields = set(['client_id', 'client_secret', 'refresh_token']) 1408 elif credentials_type == SERVICE_ACCOUNT: 1409 required_fields = set(['client_id', 'client_email', 'private_key_id', 1410 'private_key']) 1411 else: 1412 raise ApplicationDefaultCredentialsError( 1413 "'type' field should be defined (and have one of the '" + 1414 AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") 1415 1416 missing_fields = required_fields.difference(client_credentials.keys()) 1417 1418 if missing_fields: 1419 _raise_exception_for_missing_fields(missing_fields) 1420 1421 if client_credentials['type'] == AUTHORIZED_USER: 1422 return GoogleCredentials( 1423 access_token=None, 1424 client_id=client_credentials['client_id'], 1425 client_secret=client_credentials['client_secret'], 1426 refresh_token=client_credentials['refresh_token'], 1427 token_expiry=None, 1428 token_uri=oauth2client.GOOGLE_TOKEN_URI, 1429 user_agent='Python client library') 1430 else: # client_credentials['type'] == SERVICE_ACCOUNT 1431 from oauth2client import service_account 1432 return service_account._JWTAccessCredentials.from_json_keyfile_dict( 1433 client_credentials) 1434 1435 1436def _raise_exception_for_missing_fields(missing_fields): 1437 raise ApplicationDefaultCredentialsError( 1438 'The following field(s) must be defined: ' + ', '.join(missing_fields)) 1439 1440 1441def _raise_exception_for_reading_json(credential_file, 1442 extra_help, 1443 error): 1444 raise ApplicationDefaultCredentialsError( 1445 'An error was encountered while reading json file: ' + 1446 credential_file + extra_help + ': ' + str(error)) 1447 1448 1449def _get_application_default_credential_GAE(): 1450 from oauth2client.contrib.appengine import AppAssertionCredentials 1451 1452 return AppAssertionCredentials([]) 1453 1454 1455def _get_application_default_credential_GCE(): 1456 from oauth2client.contrib.gce import AppAssertionCredentials 1457 1458 return AppAssertionCredentials() 1459 1460 1461class AssertionCredentials(GoogleCredentials): 1462 """Abstract Credentials object used for OAuth 2.0 assertion grants. 1463 1464 This credential does not require a flow to instantiate because it 1465 represents a two legged flow, and therefore has all of the required 1466 information to generate and refresh its own access tokens. It must 1467 be subclassed to generate the appropriate assertion string. 1468 1469 AssertionCredentials objects may be safely pickled and unpickled. 1470 """ 1471 1472 @util.positional(2) 1473 def __init__(self, assertion_type, user_agent=None, 1474 token_uri=oauth2client.GOOGLE_TOKEN_URI, 1475 revoke_uri=oauth2client.GOOGLE_REVOKE_URI, 1476 **unused_kwargs): 1477 """Constructor for AssertionFlowCredentials. 1478 1479 Args: 1480 assertion_type: string, assertion type that will be declared to the 1481 auth server 1482 user_agent: string, The HTTP User-Agent to provide for this 1483 application. 1484 token_uri: string, URI for token endpoint. For convenience defaults 1485 to Google's endpoints but any OAuth 2.0 provider can be 1486 used. 1487 revoke_uri: string, URI for revoke endpoint. 1488 """ 1489 super(AssertionCredentials, self).__init__( 1490 None, 1491 None, 1492 None, 1493 None, 1494 None, 1495 token_uri, 1496 user_agent, 1497 revoke_uri=revoke_uri) 1498 self.assertion_type = assertion_type 1499 1500 def _generate_refresh_request_body(self): 1501 assertion = self._generate_assertion() 1502 1503 body = urllib.parse.urlencode({ 1504 'assertion': assertion, 1505 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 1506 }) 1507 1508 return body 1509 1510 def _generate_assertion(self): 1511 """Generate assertion string to be used in the access token request.""" 1512 raise NotImplementedError 1513 1514 def _revoke(self, http_request): 1515 """Revokes the access_token and deletes the store if available. 1516 1517 Args: 1518 http_request: callable, a callable that matches the method 1519 signature of httplib2.Http.request, used to make the 1520 revoke request. 1521 """ 1522 self._do_revoke(http_request, self.access_token) 1523 1524 def sign_blob(self, blob): 1525 """Cryptographically sign a blob (of bytes). 1526 1527 Args: 1528 blob: bytes, Message to be signed. 1529 1530 Returns: 1531 tuple, A pair of the private key ID used to sign the blob and 1532 the signed contents. 1533 """ 1534 raise NotImplementedError('This method is abstract.') 1535 1536 1537def _require_crypto_or_die(): 1538 """Ensure we have a crypto library, or throw CryptoUnavailableError. 1539 1540 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL 1541 to be available in order to function, but these are optional 1542 dependencies. 1543 """ 1544 if not HAS_CRYPTO: 1545 raise CryptoUnavailableError('No crypto library available') 1546 1547 1548@util.positional(2) 1549def verify_id_token(id_token, audience, http=None, 1550 cert_uri=ID_TOKEN_VERIFICATION_CERTS): 1551 """Verifies a signed JWT id_token. 1552 1553 This function requires PyOpenSSL and because of that it does not work on 1554 App Engine. 1555 1556 Args: 1557 id_token: string, A Signed JWT. 1558 audience: string, The audience 'aud' that the token should be for. 1559 http: httplib2.Http, instance to use to make the HTTP request. Callers 1560 should supply an instance that has caching enabled. 1561 cert_uri: string, URI of the certificates in JSON format to 1562 verify the JWT against. 1563 1564 Returns: 1565 The deserialized JSON in the JWT. 1566 1567 Raises: 1568 oauth2client.crypt.AppIdentityError: if the JWT fails to verify. 1569 CryptoUnavailableError: if no crypto library is available. 1570 """ 1571 _require_crypto_or_die() 1572 if http is None: 1573 http = transport.get_cached_http() 1574 1575 resp, content = http.request(cert_uri) 1576 if resp.status == http_client.OK: 1577 certs = json.loads(_helpers._from_bytes(content)) 1578 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) 1579 else: 1580 raise VerifyJwtTokenError('Status code: {0}'.format(resp.status)) 1581 1582 1583def _extract_id_token(id_token): 1584 """Extract the JSON payload from a JWT. 1585 1586 Does the extraction w/o checking the signature. 1587 1588 Args: 1589 id_token: string or bytestring, OAuth 2.0 id_token. 1590 1591 Returns: 1592 object, The deserialized JSON payload. 1593 """ 1594 if type(id_token) == bytes: 1595 segments = id_token.split(b'.') 1596 else: 1597 segments = id_token.split(u'.') 1598 1599 if len(segments) != 3: 1600 raise VerifyJwtTokenError( 1601 'Wrong number of segments in token: {0}'.format(id_token)) 1602 1603 return json.loads( 1604 _helpers._from_bytes(_helpers._urlsafe_b64decode(segments[1]))) 1605 1606 1607def _parse_exchange_token_response(content): 1608 """Parses response of an exchange token request. 1609 1610 Most providers return JSON but some (e.g. Facebook) return a 1611 url-encoded string. 1612 1613 Args: 1614 content: The body of a response 1615 1616 Returns: 1617 Content as a dictionary object. Note that the dict could be empty, 1618 i.e. {}. That basically indicates a failure. 1619 """ 1620 resp = {} 1621 content = _helpers._from_bytes(content) 1622 try: 1623 resp = json.loads(content) 1624 except Exception: 1625 # different JSON libs raise different exceptions, 1626 # so we just do a catch-all here 1627 resp = dict(urllib.parse.parse_qsl(content)) 1628 1629 # some providers respond with 'expires', others with 'expires_in' 1630 if resp and 'expires' in resp: 1631 resp['expires_in'] = resp.pop('expires') 1632 1633 return resp 1634 1635 1636@util.positional(4) 1637def credentials_from_code(client_id, client_secret, scope, code, 1638 redirect_uri='postmessage', http=None, 1639 user_agent=None, 1640 token_uri=oauth2client.GOOGLE_TOKEN_URI, 1641 auth_uri=oauth2client.GOOGLE_AUTH_URI, 1642 revoke_uri=oauth2client.GOOGLE_REVOKE_URI, 1643 device_uri=oauth2client.GOOGLE_DEVICE_URI, 1644 token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI): 1645 """Exchanges an authorization code for an OAuth2Credentials object. 1646 1647 Args: 1648 client_id: string, client identifier. 1649 client_secret: string, client secret. 1650 scope: string or iterable of strings, scope(s) to request. 1651 code: string, An authorization code, most likely passed down from 1652 the client 1653 redirect_uri: string, this is generally set to 'postmessage' to match 1654 the redirect_uri that the client specified 1655 http: httplib2.Http, optional http instance to use to do the fetch 1656 token_uri: string, URI for token endpoint. For convenience defaults 1657 to Google's endpoints but any OAuth 2.0 provider can be 1658 used. 1659 auth_uri: string, URI for authorization endpoint. For convenience 1660 defaults to Google's endpoints but any OAuth 2.0 provider 1661 can be used. 1662 revoke_uri: string, URI for revoke endpoint. For convenience 1663 defaults to Google's endpoints but any OAuth 2.0 provider 1664 can be used. 1665 device_uri: string, URI for device authorization endpoint. For 1666 convenience defaults to Google's endpoints but any OAuth 1667 2.0 provider can be used. 1668 1669 Returns: 1670 An OAuth2Credentials object. 1671 1672 Raises: 1673 FlowExchangeError if the authorization code cannot be exchanged for an 1674 access token 1675 """ 1676 flow = OAuth2WebServerFlow(client_id, client_secret, scope, 1677 redirect_uri=redirect_uri, 1678 user_agent=user_agent, auth_uri=auth_uri, 1679 token_uri=token_uri, revoke_uri=revoke_uri, 1680 device_uri=device_uri, 1681 token_info_uri=token_info_uri) 1682 1683 credentials = flow.step2_exchange(code, http=http) 1684 return credentials 1685 1686 1687@util.positional(3) 1688def credentials_from_clientsecrets_and_code(filename, scope, code, 1689 message=None, 1690 redirect_uri='postmessage', 1691 http=None, 1692 cache=None, 1693 device_uri=None): 1694 """Returns OAuth2Credentials from a clientsecrets file and an auth code. 1695 1696 Will create the right kind of Flow based on the contents of the 1697 clientsecrets file or will raise InvalidClientSecretsError for unknown 1698 types of Flows. 1699 1700 Args: 1701 filename: string, File name of clientsecrets. 1702 scope: string or iterable of strings, scope(s) to request. 1703 code: string, An authorization code, most likely passed down from 1704 the client 1705 message: string, A friendly string to display to the user if the 1706 clientsecrets file is missing or invalid. If message is 1707 provided then sys.exit will be called in the case of an error. 1708 If message in not provided then 1709 clientsecrets.InvalidClientSecretsError will be raised. 1710 redirect_uri: string, this is generally set to 'postmessage' to match 1711 the redirect_uri that the client specified 1712 http: httplib2.Http, optional http instance to use to do the fetch 1713 cache: An optional cache service client that implements get() and set() 1714 methods. See clientsecrets.loadfile() for details. 1715 device_uri: string, OAuth 2.0 device authorization endpoint 1716 1717 Returns: 1718 An OAuth2Credentials object. 1719 1720 Raises: 1721 FlowExchangeError: if the authorization code cannot be exchanged for an 1722 access token 1723 UnknownClientSecretsFlowError: if the file describes an unknown kind 1724 of Flow. 1725 clientsecrets.InvalidClientSecretsError: if the clientsecrets file is 1726 invalid. 1727 """ 1728 flow = flow_from_clientsecrets(filename, scope, message=message, 1729 cache=cache, redirect_uri=redirect_uri, 1730 device_uri=device_uri) 1731 credentials = flow.step2_exchange(code, http=http) 1732 return credentials 1733 1734 1735class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( 1736 'device_code', 'user_code', 'interval', 'verification_url', 1737 'user_code_expiry'))): 1738 """Intermediate information the OAuth2 for devices flow.""" 1739 1740 @classmethod 1741 def FromResponse(cls, response): 1742 """Create a DeviceFlowInfo from a server response. 1743 1744 The response should be a dict containing entries as described here: 1745 1746 http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 1747 """ 1748 # device_code, user_code, and verification_url are required. 1749 kwargs = { 1750 'device_code': response['device_code'], 1751 'user_code': response['user_code'], 1752 } 1753 # The response may list the verification address as either 1754 # verification_url or verification_uri, so we check for both. 1755 verification_url = response.get( 1756 'verification_url', response.get('verification_uri')) 1757 if verification_url is None: 1758 raise OAuth2DeviceCodeError( 1759 'No verification_url provided in server response') 1760 kwargs['verification_url'] = verification_url 1761 # expires_in and interval are optional. 1762 kwargs.update({ 1763 'interval': response.get('interval'), 1764 'user_code_expiry': None, 1765 }) 1766 if 'expires_in' in response: 1767 kwargs['user_code_expiry'] = ( 1768 _UTCNOW() + 1769 datetime.timedelta(seconds=int(response['expires_in']))) 1770 return cls(**kwargs) 1771 1772 1773def _oauth2_web_server_flow_params(kwargs): 1774 """Configures redirect URI parameters for OAuth2WebServerFlow.""" 1775 params = { 1776 'access_type': 'offline', 1777 'response_type': 'code', 1778 } 1779 1780 params.update(kwargs) 1781 1782 # Check for the presence of the deprecated approval_prompt param and 1783 # warn appropriately. 1784 approval_prompt = params.get('approval_prompt') 1785 if approval_prompt is not None: 1786 logger.warning( 1787 'The approval_prompt parameter for OAuth2WebServerFlow is ' 1788 'deprecated. Please use the prompt parameter instead.') 1789 1790 if approval_prompt == 'force': 1791 logger.warning( 1792 'approval_prompt="force" has been adjusted to ' 1793 'prompt="consent"') 1794 params['prompt'] = 'consent' 1795 del params['approval_prompt'] 1796 1797 return params 1798 1799 1800class OAuth2WebServerFlow(Flow): 1801 """Does the Web Server Flow for OAuth 2.0. 1802 1803 OAuth2WebServerFlow objects may be safely pickled and unpickled. 1804 """ 1805 1806 @util.positional(4) 1807 def __init__(self, client_id, 1808 client_secret=None, 1809 scope=None, 1810 redirect_uri=None, 1811 user_agent=None, 1812 auth_uri=oauth2client.GOOGLE_AUTH_URI, 1813 token_uri=oauth2client.GOOGLE_TOKEN_URI, 1814 revoke_uri=oauth2client.GOOGLE_REVOKE_URI, 1815 login_hint=None, 1816 device_uri=oauth2client.GOOGLE_DEVICE_URI, 1817 token_info_uri=oauth2client.GOOGLE_TOKEN_INFO_URI, 1818 authorization_header=None, 1819 **kwargs): 1820 """Constructor for OAuth2WebServerFlow. 1821 1822 The kwargs argument is used to set extra query parameters on the 1823 auth_uri. For example, the access_type and prompt 1824 query parameters can be set via kwargs. 1825 1826 Args: 1827 client_id: string, client identifier. 1828 client_secret: string client secret. 1829 scope: string or iterable of strings, scope(s) of the credentials 1830 being requested. 1831 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' 1832 for a non-web-based application, or a URI that 1833 handles the callback from the authorization server. 1834 user_agent: string, HTTP User-Agent to provide for this 1835 application. 1836 auth_uri: string, URI for authorization endpoint. For convenience 1837 defaults to Google's endpoints but any OAuth 2.0 provider 1838 can be used. 1839 token_uri: string, URI for token endpoint. For convenience 1840 defaults to Google's endpoints but any OAuth 2.0 1841 provider can be used. 1842 revoke_uri: string, URI for revoke endpoint. For convenience 1843 defaults to Google's endpoints but any OAuth 2.0 1844 provider can be used. 1845 login_hint: string, Either an email address or domain. Passing this 1846 hint will either pre-fill the email box on the sign-in 1847 form or select the proper multi-login session, thereby 1848 simplifying the login flow. 1849 device_uri: string, URI for device authorization endpoint. For 1850 convenience defaults to Google's endpoints but any 1851 OAuth 2.0 provider can be used. 1852 authorization_header: string, For use with OAuth 2.0 providers that 1853 require a client to authenticate using a 1854 header value instead of passing client_secret 1855 in the POST body. 1856 **kwargs: dict, The keyword arguments are all optional and required 1857 parameters for the OAuth calls. 1858 """ 1859 # scope is a required argument, but to preserve backwards-compatibility 1860 # we don't want to rearrange the positional arguments 1861 if scope is None: 1862 raise TypeError("The value of scope must not be None") 1863 self.client_id = client_id 1864 self.client_secret = client_secret 1865 self.scope = util.scopes_to_string(scope) 1866 self.redirect_uri = redirect_uri 1867 self.login_hint = login_hint 1868 self.user_agent = user_agent 1869 self.auth_uri = auth_uri 1870 self.token_uri = token_uri 1871 self.revoke_uri = revoke_uri 1872 self.device_uri = device_uri 1873 self.token_info_uri = token_info_uri 1874 self.authorization_header = authorization_header 1875 self.params = _oauth2_web_server_flow_params(kwargs) 1876 1877 @util.positional(1) 1878 def step1_get_authorize_url(self, redirect_uri=None, state=None): 1879 """Returns a URI to redirect to the provider. 1880 1881 Args: 1882 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' 1883 for a non-web-based application, or a URI that 1884 handles the callback from the authorization server. 1885 This parameter is deprecated, please move to passing 1886 the redirect_uri in via the constructor. 1887 state: string, Opaque state string which is passed through the 1888 OAuth2 flow and returned to the client as a query parameter 1889 in the callback. 1890 1891 Returns: 1892 A URI as a string to redirect the user to begin the authorization 1893 flow. 1894 """ 1895 if redirect_uri is not None: 1896 logger.warning(( 1897 'The redirect_uri parameter for ' 1898 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. ' 1899 'Please move to passing the redirect_uri in via the ' 1900 'constructor.')) 1901 self.redirect_uri = redirect_uri 1902 1903 if self.redirect_uri is None: 1904 raise ValueError('The value of redirect_uri must not be None.') 1905 1906 query_params = { 1907 'client_id': self.client_id, 1908 'redirect_uri': self.redirect_uri, 1909 'scope': self.scope, 1910 } 1911 if state is not None: 1912 query_params['state'] = state 1913 if self.login_hint is not None: 1914 query_params['login_hint'] = self.login_hint 1915 query_params.update(self.params) 1916 return _update_query_params(self.auth_uri, query_params) 1917 1918 @util.positional(1) 1919 def step1_get_device_and_user_codes(self, http=None): 1920 """Returns a user code and the verification URL where to enter it 1921 1922 Returns: 1923 A user code as a string for the user to authorize the application 1924 An URL as a string where the user has to enter the code 1925 """ 1926 if self.device_uri is None: 1927 raise ValueError('The value of device_uri must not be None.') 1928 1929 body = urllib.parse.urlencode({ 1930 'client_id': self.client_id, 1931 'scope': self.scope, 1932 }) 1933 headers = { 1934 'content-type': 'application/x-www-form-urlencoded', 1935 } 1936 1937 if self.user_agent is not None: 1938 headers['user-agent'] = self.user_agent 1939 1940 if http is None: 1941 http = transport.get_http_object() 1942 1943 resp, content = http.request(self.device_uri, method='POST', body=body, 1944 headers=headers) 1945 content = _helpers._from_bytes(content) 1946 if resp.status == http_client.OK: 1947 try: 1948 flow_info = json.loads(content) 1949 except ValueError as exc: 1950 raise OAuth2DeviceCodeError( 1951 'Could not parse server response as JSON: "{0}", ' 1952 'error: "{1}"'.format(content, exc)) 1953 return DeviceFlowInfo.FromResponse(flow_info) 1954 else: 1955 error_msg = 'Invalid response {0}.'.format(resp.status) 1956 try: 1957 error_dict = json.loads(content) 1958 if 'error' in error_dict: 1959 error_msg += ' Error: {0}'.format(error_dict['error']) 1960 except ValueError: 1961 # Couldn't decode a JSON response, stick with the 1962 # default message. 1963 pass 1964 raise OAuth2DeviceCodeError(error_msg) 1965 1966 @util.positional(2) 1967 def step2_exchange(self, code=None, http=None, device_flow_info=None): 1968 """Exchanges a code for OAuth2Credentials. 1969 1970 Args: 1971 code: string, a dict-like object, or None. For a non-device 1972 flow, this is either the response code as a string, or a 1973 dictionary of query parameters to the redirect_uri. For a 1974 device flow, this should be None. 1975 http: httplib2.Http, optional http instance to use when fetching 1976 credentials. 1977 device_flow_info: DeviceFlowInfo, return value from step1 in the 1978 case of a device flow. 1979 1980 Returns: 1981 An OAuth2Credentials object that can be used to authorize requests. 1982 1983 Raises: 1984 FlowExchangeError: if a problem occurred exchanging the code for a 1985 refresh_token. 1986 ValueError: if code and device_flow_info are both provided or both 1987 missing. 1988 """ 1989 if code is None and device_flow_info is None: 1990 raise ValueError('No code or device_flow_info provided.') 1991 if code is not None and device_flow_info is not None: 1992 raise ValueError('Cannot provide both code and device_flow_info.') 1993 1994 if code is None: 1995 code = device_flow_info.device_code 1996 elif not isinstance(code, (six.string_types, six.binary_type)): 1997 if 'code' not in code: 1998 raise FlowExchangeError(code.get( 1999 'error', 'No code was supplied in the query parameters.')) 2000 code = code['code'] 2001 2002 post_data = { 2003 'client_id': self.client_id, 2004 'code': code, 2005 'scope': self.scope, 2006 } 2007 if self.client_secret is not None: 2008 post_data['client_secret'] = self.client_secret 2009 if device_flow_info is not None: 2010 post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' 2011 else: 2012 post_data['grant_type'] = 'authorization_code' 2013 post_data['redirect_uri'] = self.redirect_uri 2014 body = urllib.parse.urlencode(post_data) 2015 headers = { 2016 'content-type': 'application/x-www-form-urlencoded', 2017 } 2018 if self.authorization_header is not None: 2019 headers['Authorization'] = self.authorization_header 2020 if self.user_agent is not None: 2021 headers['user-agent'] = self.user_agent 2022 2023 if http is None: 2024 http = transport.get_http_object() 2025 2026 resp, content = http.request(self.token_uri, method='POST', body=body, 2027 headers=headers) 2028 d = _parse_exchange_token_response(content) 2029 if resp.status == http_client.OK and 'access_token' in d: 2030 access_token = d['access_token'] 2031 refresh_token = d.get('refresh_token', None) 2032 if not refresh_token: 2033 logger.info( 2034 'Received token response with no refresh_token. Consider ' 2035 "reauthenticating with prompt='consent'.") 2036 token_expiry = None 2037 if 'expires_in' in d: 2038 delta = datetime.timedelta(seconds=int(d['expires_in'])) 2039 token_expiry = delta + _UTCNOW() 2040 2041 extracted_id_token = None 2042 if 'id_token' in d: 2043 extracted_id_token = _extract_id_token(d['id_token']) 2044 2045 logger.info('Successfully retrieved access token') 2046 return OAuth2Credentials( 2047 access_token, self.client_id, self.client_secret, 2048 refresh_token, token_expiry, self.token_uri, self.user_agent, 2049 revoke_uri=self.revoke_uri, id_token=extracted_id_token, 2050 token_response=d, scopes=self.scope, 2051 token_info_uri=self.token_info_uri) 2052 else: 2053 logger.info('Failed to retrieve access token: %s', content) 2054 if 'error' in d: 2055 # you never know what those providers got to say 2056 error_msg = (str(d['error']) + 2057 str(d.get('error_description', ''))) 2058 else: 2059 error_msg = 'Invalid response: {0}.'.format(str(resp.status)) 2060 raise FlowExchangeError(error_msg) 2061 2062 2063@util.positional(2) 2064def flow_from_clientsecrets(filename, scope, redirect_uri=None, 2065 message=None, cache=None, login_hint=None, 2066 device_uri=None): 2067 """Create a Flow from a clientsecrets file. 2068 2069 Will create the right kind of Flow based on the contents of the 2070 clientsecrets file or will raise InvalidClientSecretsError for unknown 2071 types of Flows. 2072 2073 Args: 2074 filename: string, File name of client secrets. 2075 scope: string or iterable of strings, scope(s) to request. 2076 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 2077 a non-web-based application, or a URI that handles the 2078 callback from the authorization server. 2079 message: string, A friendly string to display to the user if the 2080 clientsecrets file is missing or invalid. If message is 2081 provided then sys.exit will be called in the case of an error. 2082 If message in not provided then 2083 clientsecrets.InvalidClientSecretsError will be raised. 2084 cache: An optional cache service client that implements get() and set() 2085 methods. See clientsecrets.loadfile() for details. 2086 login_hint: string, Either an email address or domain. Passing this 2087 hint will either pre-fill the email box on the sign-in form 2088 or select the proper multi-login session, thereby 2089 simplifying the login flow. 2090 device_uri: string, URI for device authorization endpoint. For 2091 convenience defaults to Google's endpoints but any 2092 OAuth 2.0 provider can be used. 2093 2094 Returns: 2095 A Flow object. 2096 2097 Raises: 2098 UnknownClientSecretsFlowError: if the file describes an unknown kind of 2099 Flow. 2100 clientsecrets.InvalidClientSecretsError: if the clientsecrets file is 2101 invalid. 2102 """ 2103 try: 2104 client_type, client_info = clientsecrets.loadfile(filename, 2105 cache=cache) 2106 if client_type in (clientsecrets.TYPE_WEB, 2107 clientsecrets.TYPE_INSTALLED): 2108 constructor_kwargs = { 2109 'redirect_uri': redirect_uri, 2110 'auth_uri': client_info['auth_uri'], 2111 'token_uri': client_info['token_uri'], 2112 'login_hint': login_hint, 2113 } 2114 revoke_uri = client_info.get('revoke_uri') 2115 if revoke_uri is not None: 2116 constructor_kwargs['revoke_uri'] = revoke_uri 2117 if device_uri is not None: 2118 constructor_kwargs['device_uri'] = device_uri 2119 return OAuth2WebServerFlow( 2120 client_info['client_id'], client_info['client_secret'], 2121 scope, **constructor_kwargs) 2122 2123 except clientsecrets.InvalidClientSecretsError as e: 2124 if message is not None: 2125 if e.args: 2126 message = ('The client secrets were invalid: ' 2127 '\n{0}\n{1}'.format(e, message)) 2128 sys.exit(message) 2129 else: 2130 raise 2131 else: 2132 raise UnknownClientSecretsFlowError( 2133 'This OAuth 2.0 flow is unsupported: {0!r}'.format(client_type)) 2134