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

Source Code for Module oauth2client.client

   1  # Copyright (C) 2010 Google Inc. 
   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 clientsecrets 
  24  import copy 
  25  import datetime 
  26  import httplib2 
  27  import logging 
  28  import os 
  29  import sys 
  30  import time 
  31  import urllib 
  32  import urlparse 
  33   
  34  from oauth2client import util 
  35  from oauth2client.anyjson import simplejson 
  36   
  37  HAS_CRYPTO = False 
  38  try: 
  39    from oauth2client import crypt 
  40    HAS_CRYPTO = True 
  41  except ImportError: 
  42    pass 
  43   
  44  try: 
  45    from urlparse import parse_qsl 
  46  except ImportError: 
  47    from cgi import parse_qsl 
  48   
  49  logger = logging.getLogger(__name__) 
  50   
  51  # Expiry is stored in RFC3339 UTC format 
  52  EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 
  53   
  54  # Which certs to use to validate id_tokens received. 
  55  ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' 
  56   
  57  # Constant to use for the out of band OAuth 2.0 flow. 
  58  OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob' 
  59   
  60  # Google Data client libraries may need to set this to [401, 403]. 
  61  REFRESH_STATUS_CODES = [401] 
62 63 64 -class Error(Exception):
65 """Base error for this module.""" 66 pass
67
68 69 -class FlowExchangeError(Error):
70 """Error trying to exchange an authorization grant for an access token.""" 71 pass
72
73 74 -class AccessTokenRefreshError(Error):
75 """Error trying to refresh an expired access token.""" 76 pass
77
78 -class UnknownClientSecretsFlowError(Error):
79 """The client secrets file called for an unknown type of OAuth 2.0 flow. """ 80 pass
81
82 83 -class AccessTokenCredentialsError(Error):
84 """Having only the access_token means no refresh is possible.""" 85 pass
86
87 88 -class VerifyJwtTokenError(Error):
89 """Could on retrieve certificates for validation.""" 90 pass
91
92 93 -class NonAsciiHeaderError(Error):
94 """Header names and values must be ASCII strings.""" 95 pass
96
97 98 -def _abstract():
99 raise NotImplementedError('You need to override this function')
100
101 102 -class MemoryCache(object):
103 """httplib2 Cache implementation which only caches locally.""" 104
105 - def __init__(self):
106 self.cache = {}
107
108 - def get(self, key):
109 return self.cache.get(key)
110
111 - def set(self, key, value):
112 self.cache[key] = value
113
114 - def delete(self, key):
115 self.cache.pop(key, None)
116
117 118 -class Credentials(object):
119 """Base class for all Credentials objects. 120 121 Subclasses must define an authorize() method that applies the credentials to 122 an HTTP transport. 123 124 Subclasses must also specify a classmethod named 'from_json' that takes a JSON 125 string as input and returns an instaniated Credentials object. 126 """ 127 128 NON_SERIALIZED_MEMBERS = ['store'] 129
130 - def authorize(self, http):
131 """Take an httplib2.Http instance (or equivalent) and 132 authorizes it for the set of credentials, usually by 133 replacing http.request() with a method that adds in 134 the appropriate headers and then delegates to the original 135 Http.request() method. 136 """ 137 _abstract()
138
139 - def refresh(self, http):
140 """Forces a refresh of the access_token. 141 142 Args: 143 http: httplib2.Http, an http object to be used to make the refresh 144 request. 145 """ 146 _abstract()
147
148 - def apply(self, headers):
149 """Add the authorization to the headers. 150 151 Args: 152 headers: dict, the headers to add the Authorization header to. 153 """ 154 _abstract()
155
156 - def _to_json(self, strip):
157 """Utility function for creating a JSON representation of an instance of Credentials. 158 159 Args: 160 strip: array, An array of names of members to not include in the JSON. 161 162 Returns: 163 string, a JSON representation of this instance, suitable to pass to 164 from_json(). 165 """ 166 t = type(self) 167 d = copy.copy(self.__dict__) 168 for member in strip: 169 if member in d: 170 del d[member] 171 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): 172 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) 173 # Add in information we will need later to reconsistitue this instance. 174 d['_class'] = t.__name__ 175 d['_module'] = t.__module__ 176 return simplejson.dumps(d)
177
178 - def to_json(self):
179 """Creating a JSON representation of an instance of Credentials. 180 181 Returns: 182 string, a JSON representation of this instance, suitable to pass to 183 from_json(). 184 """ 185 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
186 187 @classmethod
188 - def new_from_json(cls, s):
189 """Utility class method to instantiate a Credentials subclass from a JSON 190 representation produced by to_json(). 191 192 Args: 193 s: string, JSON from to_json(). 194 195 Returns: 196 An instance of the subclass of Credentials that was serialized with 197 to_json(). 198 """ 199 data = simplejson.loads(s) 200 # Find and call the right classmethod from_json() to restore the object. 201 module = data['_module'] 202 try: 203 m = __import__(module) 204 except ImportError: 205 # In case there's an object from the old package structure, update it 206 module = module.replace('.apiclient', '') 207 m = __import__(module) 208 209 m = __import__(module, fromlist=module.split('.')[:-1]) 210 kls = getattr(m, data['_class']) 211 from_json = getattr(kls, 'from_json') 212 return from_json(s)
213 214 @classmethod
215 - def from_json(cls, s):
216 """Instantiate a Credentials object from a JSON description of it. 217 218 The JSON should have been produced by calling .to_json() on the object. 219 220 Args: 221 data: dict, A deserialized JSON object. 222 223 Returns: 224 An instance of a Credentials subclass. 225 """ 226 return Credentials()
227
228 229 -class Flow(object):
230 """Base class for all Flow objects.""" 231 pass
232
233 234 -class Storage(object):
235 """Base class for all Storage objects. 236 237 Store and retrieve a single credential. This class supports locking 238 such that multiple processes and threads can operate on a single 239 store. 240 """ 241
242 - def acquire_lock(self):
243 """Acquires any lock necessary to access this Storage. 244 245 This lock is not reentrant. 246 """ 247 pass
248
249 - def release_lock(self):
250 """Release the Storage lock. 251 252 Trying to release a lock that isn't held will result in a 253 RuntimeError. 254 """ 255 pass
256
257 - def locked_get(self):
258 """Retrieve credential. 259 260 The Storage lock must be held when this is called. 261 262 Returns: 263 oauth2client.client.Credentials 264 """ 265 _abstract()
266
267 - def locked_put(self, credentials):
268 """Write a credential. 269 270 The Storage lock must be held when this is called. 271 272 Args: 273 credentials: Credentials, the credentials to store. 274 """ 275 _abstract()
276
277 - def locked_delete(self):
278 """Delete a credential. 279 280 The Storage lock must be held when this is called. 281 """ 282 _abstract()
283
284 - def get(self):
285 """Retrieve credential. 286 287 The Storage lock must *not* be held when this is called. 288 289 Returns: 290 oauth2client.client.Credentials 291 """ 292 self.acquire_lock() 293 try: 294 return self.locked_get() 295 finally: 296 self.release_lock()
297
298 - def put(self, credentials):
299 """Write a credential. 300 301 The Storage lock must be held when this is called. 302 303 Args: 304 credentials: Credentials, the credentials to store. 305 """ 306 self.acquire_lock() 307 try: 308 self.locked_put(credentials) 309 finally: 310 self.release_lock()
311
312 - def delete(self):
313 """Delete credential. 314 315 Frees any resources associated with storing the credential. 316 The Storage lock must *not* be held when this is called. 317 318 Returns: 319 None 320 """ 321 self.acquire_lock() 322 try: 323 return self.locked_delete() 324 finally: 325 self.release_lock()
326
327 328 -def clean_headers(headers):
329 """Forces header keys and values to be strings, i.e not unicode. 330 331 The httplib module just concats the header keys and values in a way that may 332 make the message header a unicode string, which, if it then tries to 333 contatenate to a binary request body may result in a unicode decode error. 334 335 Args: 336 headers: dict, A dictionary of headers. 337 338 Returns: 339 The same dictionary but with all the keys converted to strings. 340 """ 341 clean = {} 342 try: 343 for k, v in headers.iteritems(): 344 clean[str(k)] = str(v) 345 except UnicodeEncodeError: 346 raise NonAsciiHeaderError(k + ': ' + v) 347 return clean
348
349 350 -class OAuth2Credentials(Credentials):
351 """Credentials object for OAuth 2.0. 352 353 Credentials can be applied to an httplib2.Http object using the authorize() 354 method, which then adds the OAuth 2.0 access token to each request. 355 356 OAuth2Credentials objects may be safely pickled and unpickled. 357 """ 358 359 @util.positional(8)
360 - def __init__(self, access_token, client_id, client_secret, refresh_token, 361 token_expiry, token_uri, user_agent, id_token=None):
362 """Create an instance of OAuth2Credentials. 363 364 This constructor is not usually called by the user, instead 365 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. 366 367 Args: 368 access_token: string, access token. 369 client_id: string, client identifier. 370 client_secret: string, client secret. 371 refresh_token: string, refresh token. 372 token_expiry: datetime, when the access_token expires. 373 token_uri: string, URI of token endpoint. 374 user_agent: string, The HTTP User-Agent to provide for this application. 375 id_token: object, The identity of the resource owner. 376 377 Notes: 378 store: callable, A callable that when passed a Credential 379 will store the credential back to where it came from. 380 This is needed to store the latest access_token if it 381 has expired and been refreshed. 382 """ 383 self.access_token = access_token 384 self.client_id = client_id 385 self.client_secret = client_secret 386 self.refresh_token = refresh_token 387 self.store = None 388 self.token_expiry = token_expiry 389 self.token_uri = token_uri 390 self.user_agent = user_agent 391 self.id_token = id_token 392 393 # True if the credentials have been revoked or expired and can't be 394 # refreshed. 395 self.invalid = False
396
397 - def authorize(self, http):
398 """Authorize an httplib2.Http instance with these credentials. 399 400 The modified http.request method will add authentication headers to each 401 request and will refresh access_tokens when a 401 is received on a 402 request. In addition the http.request method has a credentials property, 403 http.request.credentials, which is the Credentials object that authorized 404 it. 405 406 Args: 407 http: An instance of httplib2.Http 408 or something that acts like it. 409 410 Returns: 411 A modified instance of http that was passed in. 412 413 Example: 414 415 h = httplib2.Http() 416 h = credentials.authorize(h) 417 418 You can't create a new OAuth subclass of httplib2.Authenication 419 because it never gets passed the absolute URI, which is needed for 420 signing. So instead we have to overload 'request' with a closure 421 that adds in the Authorization header and then calls the original 422 version of 'request()'. 423 """ 424 request_orig = http.request 425 426 # The closure that will replace 'httplib2.Http.request'. 427 @util.positional(1) 428 def new_request(uri, method='GET', body=None, headers=None, 429 redirections=httplib2.DEFAULT_MAX_REDIRECTS, 430 connection_type=None): 431 if not self.access_token: 432 logger.info('Attempting refresh to obtain initial access_token') 433 self._refresh(request_orig) 434 435 # Modify the request headers to add the appropriate 436 # Authorization header. 437 if headers is None: 438 headers = {} 439 self.apply(headers) 440 441 if self.user_agent is not None: 442 if 'user-agent' in headers: 443 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] 444 else: 445 headers['user-agent'] = self.user_agent 446 447 resp, content = request_orig(uri, method, body, clean_headers(headers), 448 redirections, connection_type) 449 450 if resp.status in REFRESH_STATUS_CODES: 451 logger.info('Refreshing due to a %s' % str(resp.status)) 452 self._refresh(request_orig) 453 self.apply(headers) 454 return request_orig(uri, method, body, clean_headers(headers), 455 redirections, connection_type) 456 else: 457 return (resp, content)
458 459 # Replace the request method with our own closure. 460 http.request = new_request 461 462 # Set credentials as a property of the request method. 463 setattr(http.request, 'credentials', self) 464 465 return http
466
467 - def refresh(self, http):
468 """Forces a refresh of the access_token. 469 470 Args: 471 http: httplib2.Http, an http object to be used to make the refresh 472 request. 473 """ 474 self._refresh(http.request)
475
476 - def apply(self, headers):
477 """Add the authorization to the headers. 478 479 Args: 480 headers: dict, the headers to add the Authorization header to. 481 """ 482 headers['Authorization'] = 'Bearer ' + self.access_token
483
484 - def to_json(self):
485 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
486 487 @classmethod
488 - def from_json(cls, s):
489 """Instantiate a Credentials object from a JSON description of it. The JSON 490 should have been produced by calling .to_json() on the object. 491 492 Args: 493 data: dict, A deserialized JSON object. 494 495 Returns: 496 An instance of a Credentials subclass. 497 """ 498 data = simplejson.loads(s) 499 if 'token_expiry' in data and not isinstance(data['token_expiry'], 500 datetime.datetime): 501 try: 502 data['token_expiry'] = datetime.datetime.strptime( 503 data['token_expiry'], EXPIRY_FORMAT) 504 except: 505 data['token_expiry'] = None 506 retval = OAuth2Credentials( 507 data['access_token'], 508 data['client_id'], 509 data['client_secret'], 510 data['refresh_token'], 511 data['token_expiry'], 512 data['token_uri'], 513 data['user_agent'], 514 id_token=data.get('id_token', None)) 515 retval.invalid = data['invalid'] 516 return retval
517 518 @property
519 - def access_token_expired(self):
520 """True if the credential is expired or invalid. 521 522 If the token_expiry isn't set, we assume the token doesn't expire. 523 """ 524 if self.invalid: 525 return True 526 527 if not self.token_expiry: 528 return False 529 530 now = datetime.datetime.utcnow() 531 if now >= self.token_expiry: 532 logger.info('access_token is expired. Now: %s, token_expiry: %s', 533 now, self.token_expiry) 534 return True 535 return False
536
537 - def set_store(self, store):
538 """Set the Storage for the credential. 539 540 Args: 541 store: Storage, an implementation of Stroage object. 542 This is needed to store the latest access_token if it 543 has expired and been refreshed. This implementation uses 544 locking to check for updates before updating the 545 access_token. 546 """ 547 self.store = store
548
549 - def _updateFromCredential(self, other):
550 """Update this Credential from another instance.""" 551 self.__dict__.update(other.__getstate__())
552
553 - def __getstate__(self):
554 """Trim the state down to something that can be pickled.""" 555 d = copy.copy(self.__dict__) 556 del d['store'] 557 return d
558
559 - def __setstate__(self, state):
560 """Reconstitute the state of the object from being pickled.""" 561 self.__dict__.update(state) 562 self.store = None
563
564 - def _generate_refresh_request_body(self):
565 """Generate the body that will be used in the refresh request.""" 566 body = urllib.urlencode({ 567 'grant_type': 'refresh_token', 568 'client_id': self.client_id, 569 'client_secret': self.client_secret, 570 'refresh_token': self.refresh_token, 571 }) 572 return body
573
574 - def _generate_refresh_request_headers(self):
575 """Generate the headers that will be used in the refresh request.""" 576 headers = { 577 'content-type': 'application/x-www-form-urlencoded', 578 } 579 580 if self.user_agent is not None: 581 headers['user-agent'] = self.user_agent 582 583 return headers
584
585 - def _refresh(self, http_request):
586 """Refreshes the access_token. 587 588 This method first checks by reading the Storage object if available. 589 If a refresh is still needed, it holds the Storage lock until the 590 refresh is completed. 591 592 Args: 593 http_request: callable, a callable that matches the method signature of 594 httplib2.Http.request, used to make the refresh request. 595 596 Raises: 597 AccessTokenRefreshError: When the refresh fails. 598 """ 599 if not self.store: 600 self._do_refresh_request(http_request) 601 else: 602 self.store.acquire_lock() 603 try: 604 new_cred = self.store.locked_get() 605 if (new_cred and not new_cred.invalid and 606 new_cred.access_token != self.access_token): 607 logger.info('Updated access_token read from Storage') 608 self._updateFromCredential(new_cred) 609 else: 610 self._do_refresh_request(http_request) 611 finally: 612 self.store.release_lock()
613
614 - def _do_refresh_request(self, http_request):
615 """Refresh the access_token using the refresh_token. 616 617 Args: 618 http_request: callable, a callable that matches the method signature of 619 httplib2.Http.request, used to make the refresh request. 620 621 Raises: 622 AccessTokenRefreshError: When the refresh fails. 623 """ 624 body = self._generate_refresh_request_body() 625 headers = self._generate_refresh_request_headers() 626 627 logger.info('Refreshing access_token') 628 resp, content = http_request( 629 self.token_uri, method='POST', body=body, headers=headers) 630 if resp.status == 200: 631 # TODO(jcgregorio) Raise an error if loads fails? 632 d = simplejson.loads(content) 633 self.access_token = d['access_token'] 634 self.refresh_token = d.get('refresh_token', self.refresh_token) 635 if 'expires_in' in d: 636 self.token_expiry = datetime.timedelta( 637 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() 638 else: 639 self.token_expiry = None 640 if self.store: 641 self.store.locked_put(self) 642 else: 643 # An {'error':...} response body means the token is expired or revoked, 644 # so we flag the credentials as such. 645 logger.info('Failed to retrieve access token: %s' % content) 646 error_msg = 'Invalid response %s.' % resp['status'] 647 try: 648 d = simplejson.loads(content) 649 if 'error' in d: 650 error_msg = d['error'] 651 self.invalid = True 652 if self.store: 653 self.store.locked_put(self) 654 except StandardError: 655 pass 656 raise AccessTokenRefreshError(error_msg)
657
658 659 -class AccessTokenCredentials(OAuth2Credentials):
660 """Credentials object for OAuth 2.0. 661 662 Credentials can be applied to an httplib2.Http object using the 663 authorize() method, which then signs each request from that object 664 with the OAuth 2.0 access token. This set of credentials is for the 665 use case where you have acquired an OAuth 2.0 access_token from 666 another place such as a JavaScript client or another web 667 application, and wish to use it from Python. Because only the 668 access_token is present it can not be refreshed and will in time 669 expire. 670 671 AccessTokenCredentials objects may be safely pickled and unpickled. 672 673 Usage: 674 credentials = AccessTokenCredentials('<an access token>', 675 'my-user-agent/1.0') 676 http = httplib2.Http() 677 http = credentials.authorize(http) 678 679 Exceptions: 680 AccessTokenCredentialsExpired: raised when the access_token expires or is 681 revoked. 682 """ 683
684 - def __init__(self, access_token, user_agent):
685 """Create an instance of OAuth2Credentials 686 687 This is one of the few types if Credentials that you should contrust, 688 Credentials objects are usually instantiated by a Flow. 689 690 Args: 691 access_token: string, access token. 692 user_agent: string, The HTTP User-Agent to provide for this application. 693 694 Notes: 695 store: callable, a callable that when passed a Credential 696 will store the credential back to where it came from. 697 """ 698 super(AccessTokenCredentials, self).__init__( 699 access_token, 700 None, 701 None, 702 None, 703 None, 704 None, 705 user_agent)
706 707 708 @classmethod
709 - def from_json(cls, s):
710 data = simplejson.loads(s) 711 retval = AccessTokenCredentials( 712 data['access_token'], 713 data['user_agent']) 714 return retval
715
716 - def _refresh(self, http_request):
717 raise AccessTokenCredentialsError( 718 "The access_token is expired or invalid and can't be refreshed.")
719
720 721 -class AssertionCredentials(OAuth2Credentials):
722 """Abstract Credentials object used for OAuth 2.0 assertion grants. 723 724 This credential does not require a flow to instantiate because it 725 represents a two legged flow, and therefore has all of the required 726 information to generate and refresh its own access tokens. It must 727 be subclassed to generate the appropriate assertion string. 728 729 AssertionCredentials objects may be safely pickled and unpickled. 730 """ 731 732 @util.positional(2)
733 - def __init__(self, assertion_type, user_agent=None, 734 token_uri='https://accounts.google.com/o/oauth2/token', 735 **unused_kwargs):
736 """Constructor for AssertionFlowCredentials. 737 738 Args: 739 assertion_type: string, assertion type that will be declared to the auth 740 server 741 user_agent: string, The HTTP User-Agent to provide for this application. 742 token_uri: string, URI for token endpoint. For convenience 743 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 744 """ 745 super(AssertionCredentials, self).__init__( 746 None, 747 None, 748 None, 749 None, 750 None, 751 token_uri, 752 user_agent) 753 self.assertion_type = assertion_type
754
756 assertion = self._generate_assertion() 757 758 body = urllib.urlencode({ 759 'assertion_type': self.assertion_type, 760 'assertion': assertion, 761 'grant_type': 'assertion', 762 }) 763 764 return body
765
766 - def _generate_assertion(self):
767 """Generate the assertion string that will be used in the access token 768 request. 769 """ 770 _abstract()
771 772 if HAS_CRYPTO:
773 # PyOpenSSL and PyCrypto are not prerequisites for oauth2client, so if it is 774 # missing then don't create the SignedJwtAssertionCredentials or the 775 # verify_id_token() method. 776 777 - class SignedJwtAssertionCredentials(AssertionCredentials):
778 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. 779 780 This credential does not require a flow to instantiate because it represents 781 a two legged flow, and therefore has all of the required information to 782 generate and refresh its own access tokens. 783 784 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 2.6 or 785 later. For App Engine you may also consider using AppAssertionCredentials. 786 """ 787 788 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 789 790 @util.positional(4)
791 - def __init__(self, 792 service_account_name, 793 private_key, 794 scope, 795 private_key_password='notasecret', 796 user_agent=None, 797 token_uri='https://accounts.google.com/o/oauth2/token', 798 **kwargs):
799 """Constructor for SignedJwtAssertionCredentials. 800 801 Args: 802 service_account_name: string, id for account, usually an email address. 803 private_key: string, private key in PKCS12 or PEM format. 804 scope: string or iterable of strings, scope(s) of the credentials being 805 requested. 806 private_key_password: string, password for private_key, unused if 807 private_key is in PEM format. 808 user_agent: string, HTTP User-Agent to provide for this application. 809 token_uri: string, URI for token endpoint. For convenience 810 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 811 kwargs: kwargs, Additional parameters to add to the JWT token, for 812 example prn=joe@xample.org.""" 813 814 super(SignedJwtAssertionCredentials, self).__init__( 815 'http://oauth.net/grant_type/jwt/1.0/bearer', 816 user_agent=user_agent, 817 token_uri=token_uri, 818 ) 819 820 self.scope = util.scopes_to_string(scope) 821 822 # Keep base64 encoded so it can be stored in JSON. 823 self.private_key = base64.b64encode(private_key) 824 825 self.private_key_password = private_key_password 826 self.service_account_name = service_account_name 827 self.kwargs = kwargs
828 829 @classmethod
830 - def from_json(cls, s):
831 data = simplejson.loads(s) 832 retval = SignedJwtAssertionCredentials( 833 data['service_account_name'], 834 base64.b64decode(data['private_key']), 835 data['scope'], 836 private_key_password=data['private_key_password'], 837 user_agent=data['user_agent'], 838 token_uri=data['token_uri'], 839 **data['kwargs'] 840 ) 841 retval.invalid = data['invalid'] 842 retval.access_token = data['access_token'] 843 return retval
844
845 - def _generate_assertion(self):
846 """Generate the assertion that will be used in the request.""" 847 now = long(time.time()) 848 payload = { 849 'aud': self.token_uri, 850 'scope': self.scope, 851 'iat': now, 852 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, 853 'iss': self.service_account_name 854 } 855 payload.update(self.kwargs) 856 logger.debug(str(payload)) 857 858 private_key = base64.b64decode(self.private_key) 859 return crypt.make_signed_jwt(crypt.Signer.from_string( 860 private_key, self.private_key_password), payload)
861 862 # Only used in verify_id_token(), which is always calling to the same URI 863 # for the certs. 864 _cached_http = httplib2.Http(MemoryCache()) 865 866 @util.positional(2)
867 - def verify_id_token(id_token, audience, http=None, 868 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
869 """Verifies a signed JWT id_token. 870 871 This function requires PyOpenSSL and because of that it does not work on 872 App Engine. 873 874 Args: 875 id_token: string, A Signed JWT. 876 audience: string, The audience 'aud' that the token should be for. 877 http: httplib2.Http, instance to use to make the HTTP request. Callers 878 should supply an instance that has caching enabled. 879 cert_uri: string, URI of the certificates in JSON format to 880 verify the JWT against. 881 882 Returns: 883 The deserialized JSON in the JWT. 884 885 Raises: 886 oauth2client.crypt.AppIdentityError if the JWT fails to verify. 887 """ 888 if http is None: 889 http = _cached_http 890 891 resp, content = http.request(cert_uri) 892 893 if resp.status == 200: 894 certs = simplejson.loads(content) 895 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) 896 else: 897 raise VerifyJwtTokenError('Status code: %d' % resp.status)
898
899 900 -def _urlsafe_b64decode(b64string):
901 # Guard against unicode strings, which base64 can't handle. 902 b64string = b64string.encode('ascii') 903 padded = b64string + '=' * (4 - len(b64string) % 4) 904 return base64.urlsafe_b64decode(padded)
905
906 907 -def _extract_id_token(id_token):
908 """Extract the JSON payload from a JWT. 909 910 Does the extraction w/o checking the signature. 911 912 Args: 913 id_token: string, OAuth 2.0 id_token. 914 915 Returns: 916 object, The deserialized JSON payload. 917 """ 918 segments = id_token.split('.') 919 920 if (len(segments) != 3): 921 raise VerifyJwtTokenError( 922 'Wrong number of segments in token: %s' % id_token) 923 924 return simplejson.loads(_urlsafe_b64decode(segments[1]))
925
926 927 -def _parse_exchange_token_response(content):
928 """Parses response of an exchange token request. 929 930 Most providers return JSON but some (e.g. Facebook) return a 931 url-encoded string. 932 933 Args: 934 content: The body of a response 935 936 Returns: 937 Content as a dictionary object. Note that the dict could be empty, 938 i.e. {}. That basically indicates a failure. 939 """ 940 resp = {} 941 try: 942 resp = simplejson.loads(content) 943 except StandardError: 944 # different JSON libs raise different exceptions, 945 # so we just do a catch-all here 946 resp = dict(parse_qsl(content)) 947 948 # some providers respond with 'expires', others with 'expires_in' 949 if resp and 'expires' in resp: 950 resp['expires_in'] = resp.pop('expires') 951 952 return resp
953
954 955 @util.positional(4) 956 -def credentials_from_code(client_id, client_secret, scope, code, 957 redirect_uri='postmessage', http=None, user_agent=None, 958 token_uri='https://accounts.google.com/o/oauth2/token'):
959 """Exchanges an authorization code for an OAuth2Credentials object. 960 961 Args: 962 client_id: string, client identifier. 963 client_secret: string, client secret. 964 scope: string or iterable of strings, scope(s) to request. 965 code: string, An authroization code, most likely passed down from 966 the client 967 redirect_uri: string, this is generally set to 'postmessage' to match the 968 redirect_uri that the client specified 969 http: httplib2.Http, optional http instance to use to do the fetch 970 token_uri: string, URI for token endpoint. For convenience 971 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 972 Returns: 973 An OAuth2Credentials object. 974 975 Raises: 976 FlowExchangeError if the authorization code cannot be exchanged for an 977 access token 978 """ 979 flow = OAuth2WebServerFlow(client_id, client_secret, scope, 980 redirect_uri=redirect_uri, user_agent=user_agent, 981 auth_uri='https://accounts.google.com/o/oauth2/auth', 982 token_uri=token_uri) 983 984 credentials = flow.step2_exchange(code, http=http) 985 return credentials
986
987 988 @util.positional(3) 989 -def credentials_from_clientsecrets_and_code(filename, scope, code, 990 message = None, 991 redirect_uri='postmessage', 992 http=None, 993 cache=None):
994 """Returns OAuth2Credentials from a clientsecrets file and an auth code. 995 996 Will create the right kind of Flow based on the contents of the clientsecrets 997 file or will raise InvalidClientSecretsError for unknown types of Flows. 998 999 Args: 1000 filename: string, File name of clientsecrets. 1001 scope: string or iterable of strings, scope(s) to request. 1002 code: string, An authorization code, most likely passed down from 1003 the client 1004 message: string, A friendly string to display to the user if the 1005 clientsecrets file is missing or invalid. If message is provided then 1006 sys.exit will be called in the case of an error. If message in not 1007 provided then clientsecrets.InvalidClientSecretsError will be raised. 1008 redirect_uri: string, this is generally set to 'postmessage' to match the 1009 redirect_uri that the client specified 1010 http: httplib2.Http, optional http instance to use to do the fetch 1011 cache: An optional cache service client that implements get() and set() 1012 methods. See clientsecrets.loadfile() for details. 1013 1014 Returns: 1015 An OAuth2Credentials object. 1016 1017 Raises: 1018 FlowExchangeError if the authorization code cannot be exchanged for an 1019 access token 1020 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1021 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1022 invalid. 1023 """ 1024 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, 1025 redirect_uri=redirect_uri) 1026 credentials = flow.step2_exchange(code, http=http) 1027 return credentials
1028
1029 1030 -class OAuth2WebServerFlow(Flow):
1031 """Does the Web Server Flow for OAuth 2.0. 1032 1033 OAuth2WebServerFlow objects may be safely pickled and unpickled. 1034 """ 1035 1036 @util.positional(4)
1037 - def __init__(self, client_id, client_secret, scope, 1038 redirect_uri=None, 1039 user_agent=None, 1040 auth_uri='https://accounts.google.com/o/oauth2/auth', 1041 token_uri='https://accounts.google.com/o/oauth2/token', 1042 **kwargs):
1043 """Constructor for OAuth2WebServerFlow. 1044 1045 The kwargs argument is used to set extra query parameters on the 1046 auth_uri. For example, the access_type and approval_prompt 1047 query parameters can be set via kwargs. 1048 1049 Args: 1050 client_id: string, client identifier. 1051 client_secret: string client secret. 1052 scope: string or iterable of strings, scope(s) of the credentials being 1053 requested. 1054 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1055 a non-web-based application, or a URI that handles the callback from 1056 the authorization server. 1057 user_agent: string, HTTP User-Agent to provide for this application. 1058 auth_uri: string, URI for authorization endpoint. For convenience 1059 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1060 token_uri: string, URI for token endpoint. For convenience 1061 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1062 **kwargs: dict, The keyword arguments are all optional and required 1063 parameters for the OAuth calls. 1064 """ 1065 self.client_id = client_id 1066 self.client_secret = client_secret 1067 self.scope = util.scopes_to_string(scope) 1068 self.redirect_uri = redirect_uri 1069 self.user_agent = user_agent 1070 self.auth_uri = auth_uri 1071 self.token_uri = token_uri 1072 self.params = { 1073 'access_type': 'offline', 1074 'response_type': 'code', 1075 } 1076 self.params.update(kwargs)
1077 1078 @util.positional(1)
1079 - def step1_get_authorize_url(self, redirect_uri=None):
1080 """Returns a URI to redirect to the provider. 1081 1082 Args: 1083 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1084 a non-web-based application, or a URI that handles the callback from 1085 the authorization server. This parameter is deprecated, please move to 1086 passing the redirect_uri in via the constructor. 1087 1088 Returns: 1089 A URI as a string to redirect the user to begin the authorization flow. 1090 """ 1091 if redirect_uri is not None: 1092 logger.warning(('The redirect_uri parameter for' 1093 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please' 1094 'move to passing the redirect_uri in via the constructor.')) 1095 self.redirect_uri = redirect_uri 1096 1097 if self.redirect_uri is None: 1098 raise ValueError('The value of redirect_uri must not be None.') 1099 1100 query = { 1101 'client_id': self.client_id, 1102 'redirect_uri': self.redirect_uri, 1103 'scope': self.scope, 1104 } 1105 query.update(self.params) 1106 parts = list(urlparse.urlparse(self.auth_uri)) 1107 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part 1108 parts[4] = urllib.urlencode(query) 1109 return urlparse.urlunparse(parts)
1110 1111 @util.positional(2)
1112 - def step2_exchange(self, code, http=None):
1113 """Exhanges a code for OAuth2Credentials. 1114 1115 Args: 1116 code: string or dict, either the code as a string, or a dictionary 1117 of the query parameters to the redirect_uri, which contains 1118 the code. 1119 http: httplib2.Http, optional http instance to use to do the fetch 1120 1121 Returns: 1122 An OAuth2Credentials object that can be used to authorize requests. 1123 1124 Raises: 1125 FlowExchangeError if a problem occured exchanging the code for a 1126 refresh_token. 1127 """ 1128 1129 if not (isinstance(code, str) or isinstance(code, unicode)): 1130 if 'code' not in code: 1131 if 'error' in code: 1132 error_msg = code['error'] 1133 else: 1134 error_msg = 'No code was supplied in the query parameters.' 1135 raise FlowExchangeError(error_msg) 1136 else: 1137 code = code['code'] 1138 1139 body = urllib.urlencode({ 1140 'grant_type': 'authorization_code', 1141 'client_id': self.client_id, 1142 'client_secret': self.client_secret, 1143 'code': code, 1144 'redirect_uri': self.redirect_uri, 1145 'scope': self.scope, 1146 }) 1147 headers = { 1148 'content-type': 'application/x-www-form-urlencoded', 1149 } 1150 1151 if self.user_agent is not None: 1152 headers['user-agent'] = self.user_agent 1153 1154 if http is None: 1155 http = httplib2.Http() 1156 1157 resp, content = http.request(self.token_uri, method='POST', body=body, 1158 headers=headers) 1159 d = _parse_exchange_token_response(content) 1160 if resp.status == 200 and 'access_token' in d: 1161 access_token = d['access_token'] 1162 refresh_token = d.get('refresh_token', None) 1163 token_expiry = None 1164 if 'expires_in' in d: 1165 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( 1166 seconds=int(d['expires_in'])) 1167 1168 if 'id_token' in d: 1169 d['id_token'] = _extract_id_token(d['id_token']) 1170 1171 logger.info('Successfully retrieved access token') 1172 return OAuth2Credentials(access_token, self.client_id, 1173 self.client_secret, refresh_token, token_expiry, 1174 self.token_uri, self.user_agent, 1175 id_token=d.get('id_token', None)) 1176 else: 1177 logger.info('Failed to retrieve access token: %s' % content) 1178 if 'error' in d: 1179 # you never know what those providers got to say 1180 error_msg = unicode(d['error']) 1181 else: 1182 error_msg = 'Invalid response: %s.' % str(resp.status) 1183 raise FlowExchangeError(error_msg)
1184
1185 1186 @util.positional(2) 1187 -def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
1188 """Create a Flow from a clientsecrets file. 1189 1190 Will create the right kind of Flow based on the contents of the clientsecrets 1191 file or will raise InvalidClientSecretsError for unknown types of Flows. 1192 1193 Args: 1194 filename: string, File name of client secrets. 1195 scope: string or iterable of strings, scope(s) to request. 1196 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1197 a non-web-based application, or a URI that handles the callback from 1198 the authorization server. 1199 message: string, A friendly string to display to the user if the 1200 clientsecrets file is missing or invalid. If message is provided then 1201 sys.exit will be called in the case of an error. If message in not 1202 provided then clientsecrets.InvalidClientSecretsError will be raised. 1203 cache: An optional cache service client that implements get() and set() 1204 methods. See clientsecrets.loadfile() for details. 1205 1206 Returns: 1207 A Flow object. 1208 1209 Raises: 1210 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1211 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1212 invalid. 1213 """ 1214 try: 1215 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 1216 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 1217 return OAuth2WebServerFlow( 1218 client_info['client_id'], 1219 client_info['client_secret'], 1220 scope, 1221 redirect_uri=redirect_uri, 1222 user_agent=None, 1223 auth_uri=client_info['auth_uri'], 1224 token_uri=client_info['token_uri']) 1225 1226 except clientsecrets.InvalidClientSecretsError: 1227 if message: 1228 sys.exit(message) 1229 else: 1230 raise 1231 else: 1232 raise UnknownClientSecretsFlowError( 1233 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
1234