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