Package oauth2client :: Module client
[hide private]
[frames] | no frames]

Source Code for Module oauth2client.client

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