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

Source Code for Module oauth2client.client

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