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