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