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