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 if resp.status == 401: 420 logger.info('Refreshing due to a 401') 421 self._refresh(request_orig) 422 self.apply(headers) 423 return request_orig(uri, method, body, headers, 424 redirections, connection_type) 425 else: 426 return (resp, content)
427 428 # Replace the request method with our own closure. 429 http.request = new_request 430 431 # Set credentials as a property of the request method. 432 setattr(http.request, 'credentials', self) 433 434 return http
435
436 - def refresh(self, http):
437 """Forces a refresh of the access_token. 438 439 Args: 440 http: httplib2.Http, an http object to be used to make the refresh 441 request. 442 """ 443 self._refresh(http.request)
444
445 - def apply(self, headers):
446 """Add the authorization to the headers. 447 448 Args: 449 headers: dict, the headers to add the Authorization header to. 450 """ 451 headers['Authorization'] = 'Bearer ' + self.access_token
452
453 - def to_json(self):
454 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
455 456 @classmethod
457 - def from_json(cls, s):
458 """Instantiate a Credentials object from a JSON description of it. The JSON 459 should have been produced by calling .to_json() on the object. 460 461 Args: 462 data: dict, A deserialized JSON object. 463 464 Returns: 465 An instance of a Credentials subclass. 466 """ 467 data = simplejson.loads(s) 468 if 'token_expiry' in data and not isinstance(data['token_expiry'], 469 datetime.datetime): 470 try: 471 data['token_expiry'] = datetime.datetime.strptime( 472 data['token_expiry'], EXPIRY_FORMAT) 473 except: 474 data['token_expiry'] = None 475 retval = OAuth2Credentials( 476 data['access_token'], 477 data['client_id'], 478 data['client_secret'], 479 data['refresh_token'], 480 data['token_expiry'], 481 data['token_uri'], 482 data['user_agent'], 483 data.get('id_token', None)) 484 retval.invalid = data['invalid'] 485 return retval
486 487 @property
488 - def access_token_expired(self):
489 """True if the credential is expired or invalid. 490 491 If the token_expiry isn't set, we assume the token doesn't expire. 492 """ 493 if self.invalid: 494 return True 495 496 if not self.token_expiry: 497 return False 498 499 now = datetime.datetime.utcnow() 500 if now >= self.token_expiry: 501 logger.info('access_token is expired. Now: %s, token_expiry: %s', 502 now, self.token_expiry) 503 return True 504 return False
505
506 - def set_store(self, store):
507 """Set the Storage for the credential. 508 509 Args: 510 store: Storage, an implementation of Stroage object. 511 This is needed to store the latest access_token if it 512 has expired and been refreshed. This implementation uses 513 locking to check for updates before updating the 514 access_token. 515 """ 516 self.store = store
517
518 - def _updateFromCredential(self, other):
519 """Update this Credential from another instance.""" 520 self.__dict__.update(other.__getstate__())
521
522 - def __getstate__(self):
523 """Trim the state down to something that can be pickled.""" 524 d = copy.copy(self.__dict__) 525 del d['store'] 526 return d
527
528 - def __setstate__(self, state):
529 """Reconstitute the state of the object from being pickled.""" 530 self.__dict__.update(state) 531 self.store = None
532
533 - def _generate_refresh_request_body(self):
534 """Generate the body that will be used in the refresh request.""" 535 body = urllib.urlencode({ 536 'grant_type': 'refresh_token', 537 'client_id': self.client_id, 538 'client_secret': self.client_secret, 539 'refresh_token': self.refresh_token, 540 }) 541 return body
542
543 - def _generate_refresh_request_headers(self):
544 """Generate the headers that will be used in the refresh request.""" 545 headers = { 546 'content-type': 'application/x-www-form-urlencoded', 547 } 548 549 if self.user_agent is not None: 550 headers['user-agent'] = self.user_agent 551 552 return headers
553
554 - def _refresh(self, http_request):
555 """Refreshes the access_token. 556 557 This method first checks by reading the Storage object if available. 558 If a refresh is still needed, it holds the Storage lock until the 559 refresh is completed. 560 561 Args: 562 http_request: callable, a callable that matches the method signature of 563 httplib2.Http.request, used to make the refresh request. 564 565 Raises: 566 AccessTokenRefreshError: When the refresh fails. 567 """ 568 if not self.store: 569 self._do_refresh_request(http_request) 570 else: 571 self.store.acquire_lock() 572 try: 573 new_cred = self.store.locked_get() 574 if (new_cred and not new_cred.invalid and 575 new_cred.access_token != self.access_token): 576 logger.info('Updated access_token read from Storage') 577 self._updateFromCredential(new_cred) 578 else: 579 self._do_refresh_request(http_request) 580 finally: 581 self.store.release_lock()
582
583 - def _do_refresh_request(self, http_request):
584 """Refresh the access_token using the refresh_token. 585 586 Args: 587 http_request: callable, a callable that matches the method signature of 588 httplib2.Http.request, used to make the refresh request. 589 590 Raises: 591 AccessTokenRefreshError: When the refresh fails. 592 """ 593 body = self._generate_refresh_request_body() 594 headers = self._generate_refresh_request_headers() 595 596 logger.info('Refreshing access_token') 597 resp, content = http_request( 598 self.token_uri, method='POST', body=body, headers=headers) 599 if resp.status == 200: 600 # TODO(jcgregorio) Raise an error if loads fails? 601 d = simplejson.loads(content) 602 self.access_token = d['access_token'] 603 self.refresh_token = d.get('refresh_token', self.refresh_token) 604 if 'expires_in' in d: 605 self.token_expiry = datetime.timedelta( 606 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() 607 else: 608 self.token_expiry = None 609 if self.store: 610 self.store.locked_put(self) 611 else: 612 # An {'error':...} response body means the token is expired or revoked, 613 # so we flag the credentials as such. 614 logger.info('Failed to retrieve access token: %s' % content) 615 error_msg = 'Invalid response %s.' % resp['status'] 616 try: 617 d = simplejson.loads(content) 618 if 'error' in d: 619 error_msg = d['error'] 620 self.invalid = True 621 if self.store: 622 self.store.locked_put(self) 623 except: 624 pass 625 raise AccessTokenRefreshError(error_msg)
626
627 628 -class AccessTokenCredentials(OAuth2Credentials):
629 """Credentials object for OAuth 2.0. 630 631 Credentials can be applied to an httplib2.Http object using the 632 authorize() method, which then signs each request from that object 633 with the OAuth 2.0 access token. This set of credentials is for the 634 use case where you have acquired an OAuth 2.0 access_token from 635 another place such as a JavaScript client or another web 636 application, and wish to use it from Python. Because only the 637 access_token is present it can not be refreshed and will in time 638 expire. 639 640 AccessTokenCredentials objects may be safely pickled and unpickled. 641 642 Usage: 643 credentials = AccessTokenCredentials('<an access token>', 644 'my-user-agent/1.0') 645 http = httplib2.Http() 646 http = credentials.authorize(http) 647 648 Exceptions: 649 AccessTokenCredentialsExpired: raised when the access_token expires or is 650 revoked. 651 """ 652
653 - def __init__(self, access_token, user_agent):
654 """Create an instance of OAuth2Credentials 655 656 This is one of the few types if Credentials that you should contrust, 657 Credentials objects are usually instantiated by a Flow. 658 659 Args: 660 access_token: string, access token. 661 user_agent: string, The HTTP User-Agent to provide for this application. 662 663 Notes: 664 store: callable, a callable that when passed a Credential 665 will store the credential back to where it came from. 666 """ 667 super(AccessTokenCredentials, self).__init__( 668 access_token, 669 None, 670 None, 671 None, 672 None, 673 None, 674 user_agent)
675 676 677 @classmethod
678 - def from_json(cls, s):
679 data = simplejson.loads(s) 680 retval = AccessTokenCredentials( 681 data['access_token'], 682 data['user_agent']) 683 return retval
684
685 - def _refresh(self, http_request):
686 raise AccessTokenCredentialsError( 687 "The access_token is expired or invalid and can't be refreshed.")
688
689 690 -class AssertionCredentials(OAuth2Credentials):
691 """Abstract Credentials object used for OAuth 2.0 assertion grants. 692 693 This credential does not require a flow to instantiate because it 694 represents a two legged flow, and therefore has all of the required 695 information to generate and refresh its own access tokens. It must 696 be subclassed to generate the appropriate assertion string. 697 698 AssertionCredentials objects may be safely pickled and unpickled. 699 """ 700
701 - def __init__(self, assertion_type, user_agent, 702 token_uri='https://accounts.google.com/o/oauth2/token', 703 **unused_kwargs):
704 """Constructor for AssertionFlowCredentials. 705 706 Args: 707 assertion_type: string, assertion type that will be declared to the auth 708 server 709 user_agent: string, The HTTP User-Agent to provide for this application. 710 token_uri: string, URI for token endpoint. For convenience 711 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 712 """ 713 super(AssertionCredentials, self).__init__( 714 None, 715 None, 716 None, 717 None, 718 None, 719 token_uri, 720 user_agent) 721 self.assertion_type = assertion_type
722
724 assertion = self._generate_assertion() 725 726 body = urllib.urlencode({ 727 'assertion_type': self.assertion_type, 728 'assertion': assertion, 729 'grant_type': 'assertion', 730 }) 731 732 return body
733
734 - def _generate_assertion(self):
735 """Generate the assertion string that will be used in the access token 736 request. 737 """ 738 _abstract()
739 740 if HAS_OPENSSL:
741 # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then 742 # don't create the SignedJwtAssertionCredentials or the verify_id_token() 743 # method. 744 745 - class SignedJwtAssertionCredentials(AssertionCredentials):
746 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. 747 748 This credential does not require a flow to instantiate because it represents 749 a two legged flow, and therefore has all of the required information to 750 generate and refresh its own access tokens. 751 752 SignedJwtAssertionCredentials requires PyOpenSSL and because of that it does 753 not work on App Engine. For App Engine you may consider using 754 AppAssertionCredentials. 755 """ 756 757 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 758
759 - def __init__(self, 760 service_account_name, 761 private_key, 762 scope, 763 private_key_password='notasecret', 764 user_agent=None, 765 token_uri='https://accounts.google.com/o/oauth2/token', 766 **kwargs):
767 """Constructor for SignedJwtAssertionCredentials. 768 769 Args: 770 service_account_name: string, id for account, usually an email address. 771 private_key: string, private key in P12 format. 772 scope: string or list of strings, scope(s) of the credentials being 773 requested. 774 private_key_password: string, password for private_key. 775 user_agent: string, HTTP User-Agent to provide for this application. 776 token_uri: string, URI for token endpoint. For convenience 777 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 778 kwargs: kwargs, Additional parameters to add to the JWT token, for 779 example prn=joe@xample.org.""" 780 781 super(SignedJwtAssertionCredentials, self).__init__( 782 'http://oauth.net/grant_type/jwt/1.0/bearer', 783 user_agent, 784 token_uri=token_uri, 785 ) 786 787 if type(scope) is list: 788 scope = ' '.join(scope) 789 self.scope = scope 790 791 self.private_key = private_key 792 self.private_key_password = private_key_password 793 self.service_account_name = service_account_name 794 self.kwargs = kwargs
795 796 @classmethod
797 - def from_json(cls, s):
798 data = simplejson.loads(s) 799 retval = SignedJwtAssertionCredentials( 800 data['service_account_name'], 801 data['private_key'], 802 data['private_key_password'], 803 data['scope'], 804 data['user_agent'], 805 data['token_uri'], 806 data['kwargs'] 807 ) 808 retval.invalid = data['invalid'] 809 return retval
810
811 - def _generate_assertion(self):
812 """Generate the assertion that will be used in the request.""" 813 now = long(time.time()) 814 payload = { 815 'aud': self.token_uri, 816 'scope': self.scope, 817 'iat': now, 818 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, 819 'iss': self.service_account_name 820 } 821 payload.update(self.kwargs) 822 logger.debug(str(payload)) 823 824 return make_signed_jwt( 825 Signer.from_string(self.private_key, self.private_key_password), 826 payload)
827 828 # Only used in verify_id_token(), which is always calling to the same URI 829 # for the certs. 830 _cached_http = httplib2.Http(MemoryCache())
831 832 - def verify_id_token(id_token, audience, http=None, 833 cert_uri=ID_TOKEN_VERIFICATON_CERTS):
834 """Verifies a signed JWT id_token. 835 836 This function requires PyOpenSSL and because of that it does not work on 837 App Engine. For App Engine you may consider using AppAssertionCredentials. 838 839 Args: 840 id_token: string, A Signed JWT. 841 audience: string, The audience 'aud' that the token should be for. 842 http: httplib2.Http, instance to use to make the HTTP request. Callers 843 should supply an instance that has caching enabled. 844 cert_uri: string, URI of the certificates in JSON format to 845 verify the JWT against. 846 847 Returns: 848 The deserialized JSON in the JWT. 849 850 Raises: 851 oauth2client.crypt.AppIdentityError if the JWT fails to verify. 852 """ 853 if http is None: 854 http = _cached_http 855 856 resp, content = http.request(cert_uri) 857 858 if resp.status == 200: 859 certs = simplejson.loads(content) 860 return verify_signed_jwt_with_certs(id_token, certs, audience) 861 else: 862 raise VerifyJwtTokenError('Status code: %d' % resp.status)
863
864 865 -def _urlsafe_b64decode(b64string):
866 # Guard against unicode strings, which base64 can't handle. 867 b64string = b64string.encode('ascii') 868 padded = b64string + '=' * (4 - len(b64string) % 4) 869 return base64.urlsafe_b64decode(padded)
870
871 872 -def _extract_id_token(id_token):
873 """Extract the JSON payload from a JWT. 874 875 Does the extraction w/o checking the signature. 876 877 Args: 878 id_token: string, OAuth 2.0 id_token. 879 880 Returns: 881 object, The deserialized JSON payload. 882 """ 883 segments = id_token.split('.') 884 885 if (len(segments) != 3): 886 raise VerifyJwtTokenError( 887 'Wrong number of segments in token: %s' % id_token) 888 889 return simplejson.loads(_urlsafe_b64decode(segments[1]))
890
891 -def credentials_from_code(client_id, client_secret, scope, code, 892 redirect_uri = 'postmessage', 893 http=None, user_agent=None, 894 token_uri='https://accounts.google.com/o/oauth2/token'):
895 """Exchanges an authorization code for an OAuth2Credentials object. 896 897 Args: 898 client_id: string, client identifier. 899 client_secret: string, client secret. 900 scope: string or list of strings, scope(s) to request. 901 code: string, An authroization code, most likely passed down from 902 the client 903 redirect_uri: string, this is generally set to 'postmessage' to match the 904 redirect_uri that the client specified 905 http: httplib2.Http, optional http instance to use to do the fetch 906 token_uri: string, URI for token endpoint. For convenience 907 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 908 Returns: 909 An OAuth2Credentials object. 910 911 Raises: 912 FlowExchangeError if the authorization code cannot be exchanged for an 913 access token 914 """ 915 flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent, 916 'https://accounts.google.com/o/oauth2/auth', 917 token_uri) 918 919 # We primarily make this call to set up the redirect_uri in the flow object 920 uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri) 921 credentials = flow.step2_exchange(code, http) 922 return credentials
923
924 925 -def credentials_from_clientsecrets_and_code(filename, scope, code, 926 message = None, 927 redirect_uri = 'postmessage', 928 http=None):
929 """Returns OAuth2Credentials from a clientsecrets file and an auth code. 930 931 Will create the right kind of Flow based on the contents of the clientsecrets 932 file or will raise InvalidClientSecretsError for unknown types of Flows. 933 934 Args: 935 filename: string, File name of clientsecrets. 936 scope: string or list of strings, scope(s) to request. 937 code: string, An authroization code, most likely passed down from 938 the client 939 message: string, A friendly string to display to the user if the 940 clientsecrets file is missing or invalid. If message is provided then 941 sys.exit will be called in the case of an error. If message in not 942 provided then clientsecrets.InvalidClientSecretsError will be raised. 943 redirect_uri: string, this is generally set to 'postmessage' to match the 944 redirect_uri that the client specified 945 http: httplib2.Http, optional http instance to use to do the fetch 946 947 Returns: 948 An OAuth2Credentials object. 949 950 Raises: 951 FlowExchangeError if the authorization code cannot be exchanged for an 952 access token 953 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 954 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 955 invalid. 956 """ 957 flow = flow_from_clientsecrets(filename, scope, message) 958 # We primarily make this call to set up the redirect_uri in the flow object 959 uriThatWeDontReallyUse = flow.step1_get_authorize_url(redirect_uri) 960 credentials = flow.step2_exchange(code, http) 961 return credentials
962
963 964 -class OAuth2WebServerFlow(Flow):
965 """Does the Web Server Flow for OAuth 2.0. 966 967 OAuth2Credentials objects may be safely pickled and unpickled. 968 """ 969
970 - def __init__(self, client_id, client_secret, scope, user_agent=None, 971 auth_uri='https://accounts.google.com/o/oauth2/auth', 972 token_uri='https://accounts.google.com/o/oauth2/token', 973 **kwargs):
974 """Constructor for OAuth2WebServerFlow. 975 976 Args: 977 client_id: string, client identifier. 978 client_secret: string client secret. 979 scope: string or list of strings, scope(s) of the credentials being 980 requested. 981 user_agent: string, HTTP User-Agent to provide for this application. 982 auth_uri: string, URI for authorization endpoint. For convenience 983 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 984 token_uri: string, URI for token endpoint. For convenience 985 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 986 **kwargs: dict, The keyword arguments are all optional and required 987 parameters for the OAuth calls. 988 """ 989 self.client_id = client_id 990 self.client_secret = client_secret 991 if type(scope) is list: 992 scope = ' '.join(scope) 993 self.scope = scope 994 self.user_agent = user_agent 995 self.auth_uri = auth_uri 996 self.token_uri = token_uri 997 self.params = { 998 'access_type': 'offline', 999 } 1000 self.params.update(kwargs) 1001 self.redirect_uri = None
1002
1003 - def step1_get_authorize_url(self, redirect_uri=OOB_CALLBACK_URN):
1004 """Returns a URI to redirect to the provider. 1005 1006 Args: 1007 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1008 a non-web-based application, or a URI that handles the callback from 1009 the authorization server. 1010 1011 If redirect_uri is 'urn:ietf:wg:oauth:2.0:oob' then pass in the 1012 generated verification code to step2_exchange, 1013 otherwise pass in the query parameters received 1014 at the callback uri to step2_exchange. 1015 """ 1016 1017 self.redirect_uri = redirect_uri 1018 query = { 1019 'response_type': 'code', 1020 'client_id': self.client_id, 1021 'redirect_uri': redirect_uri, 1022 'scope': self.scope, 1023 } 1024 query.update(self.params) 1025 parts = list(urlparse.urlparse(self.auth_uri)) 1026 query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part 1027 parts[4] = urllib.urlencode(query) 1028 return urlparse.urlunparse(parts)
1029
1030 - def step2_exchange(self, code, http=None):
1031 """Exhanges a code for OAuth2Credentials. 1032 1033 Args: 1034 code: string or dict, either the code as a string, or a dictionary 1035 of the query parameters to the redirect_uri, which contains 1036 the code. 1037 http: httplib2.Http, optional http instance to use to do the fetch 1038 1039 Returns: 1040 An OAuth2Credentials object that can be used to authorize requests. 1041 1042 Raises: 1043 FlowExchangeError if a problem occured exchanging the code for a 1044 refresh_token. 1045 """ 1046 1047 if not (isinstance(code, str) or isinstance(code, unicode)): 1048 if 'code' not in code: 1049 if 'error' in code: 1050 error_msg = code['error'] 1051 else: 1052 error_msg = 'No code was supplied in the query parameters.' 1053 raise FlowExchangeError(error_msg) 1054 else: 1055 code = code['code'] 1056 1057 body = urllib.urlencode({ 1058 'grant_type': 'authorization_code', 1059 'client_id': self.client_id, 1060 'client_secret': self.client_secret, 1061 'code': code, 1062 'redirect_uri': self.redirect_uri, 1063 'scope': self.scope, 1064 }) 1065 headers = { 1066 'content-type': 'application/x-www-form-urlencoded', 1067 } 1068 1069 if self.user_agent is not None: 1070 headers['user-agent'] = self.user_agent 1071 1072 if http is None: 1073 http = httplib2.Http() 1074 1075 resp, content = http.request(self.token_uri, method='POST', body=body, 1076 headers=headers) 1077 if resp.status == 200: 1078 # TODO(jcgregorio) Raise an error if simplejson.loads fails? 1079 d = simplejson.loads(content) 1080 access_token = d['access_token'] 1081 refresh_token = d.get('refresh_token', None) 1082 token_expiry = None 1083 if 'expires_in' in d: 1084 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( 1085 seconds=int(d['expires_in'])) 1086 1087 if 'id_token' in d: 1088 d['id_token'] = _extract_id_token(d['id_token']) 1089 1090 logger.info('Successfully retrieved access token: %s' % content) 1091 return OAuth2Credentials(access_token, self.client_id, 1092 self.client_secret, refresh_token, token_expiry, 1093 self.token_uri, self.user_agent, 1094 id_token=d.get('id_token', None)) 1095 else: 1096 logger.info('Failed to retrieve access token: %s' % content) 1097 error_msg = 'Invalid response %s.' % resp['status'] 1098 try: 1099 d = simplejson.loads(content) 1100 if 'error' in d: 1101 error_msg = d['error'] 1102 except: 1103 pass 1104 1105 raise FlowExchangeError(error_msg)
1106
1107 -def flow_from_clientsecrets(filename, scope, message=None):
1108 """Create a Flow from a clientsecrets file. 1109 1110 Will create the right kind of Flow based on the contents of the clientsecrets 1111 file or will raise InvalidClientSecretsError for unknown types of Flows. 1112 1113 Args: 1114 filename: string, File name of client secrets. 1115 scope: string or list of strings, scope(s) to request. 1116 message: string, A friendly string to display to the user if the 1117 clientsecrets file is missing or invalid. If message is provided then 1118 sys.exit will be called in the case of an error. If message in not 1119 provided then clientsecrets.InvalidClientSecretsError will be raised. 1120 1121 Returns: 1122 A Flow object. 1123 1124 Raises: 1125 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1126 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1127 invalid. 1128 """ 1129 try: 1130 client_type, client_info = clientsecrets.loadfile(filename) 1131 if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 1132 return OAuth2WebServerFlow( 1133 client_info['client_id'], 1134 client_info['client_secret'], 1135 scope, 1136 None, # user_agent 1137 client_info['auth_uri'], 1138 client_info['token_uri']) 1139 except clientsecrets.InvalidClientSecretsError: 1140 if message: 1141 sys.exit(message) 1142 else: 1143 raise 1144 else: 1145 raise UnknownClientSecretsFlowError( 1146 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
1147