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

Source Code for Module oauth2client.client

   1  # Copyright 2014 Google Inc. All rights reserved. 
   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 collections 
  24  import copy 
  25  import datetime 
  26  import json 
  27  import logging 
  28  import os 
  29  import sys 
  30  import time 
  31  import urllib 
  32  import urlparse 
  33   
  34  import httplib2 
  35  from oauth2client import clientsecrets 
  36  from oauth2client import GOOGLE_AUTH_URI 
  37  from oauth2client import GOOGLE_DEVICE_URI 
  38  from oauth2client import GOOGLE_REVOKE_URI 
  39  from oauth2client import GOOGLE_TOKEN_URI 
  40  from oauth2client import util 
  41   
  42  HAS_OPENSSL = False 
  43  HAS_CRYPTO = False 
  44  try: 
  45    from oauth2client import crypt 
  46    HAS_CRYPTO = True 
  47    if crypt.OpenSSLVerifier is not None: 
  48      HAS_OPENSSL = True 
  49  except ImportError: 
  50    pass 
  51   
  52  logger = logging.getLogger(__name__) 
  53   
  54  # Expiry is stored in RFC3339 UTC format 
  55  EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ' 
  56   
  57  # Which certs to use to validate id_tokens received. 
  58  ID_TOKEN_VERIFICATION_CERTS = 'https://www.googleapis.com/oauth2/v1/certs' 
  59  # This symbol previously had a typo in the name; we keep the old name 
  60  # around for now, but will remove it in the future. 
  61  ID_TOKEN_VERIFICATON_CERTS = ID_TOKEN_VERIFICATION_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  # The value representing user credentials. 
  70  AUTHORIZED_USER = 'authorized_user' 
  71   
  72  # The value representing service account credentials. 
  73  SERVICE_ACCOUNT = 'service_account' 
  74   
  75  # The environment variable pointing the file with local 
  76  # Application Default Credentials. 
  77  GOOGLE_APPLICATION_CREDENTIALS = 'GOOGLE_APPLICATION_CREDENTIALS' 
  78   
  79  # The error message we show users when we can't find the Application 
  80  # Default Credentials. 
  81  ADC_HELP_MSG = ( 
  82      'The Application Default Credentials are not available. They are available ' 
  83      'if running in Google Compute Engine. Otherwise, the environment variable ' 
  84      + GOOGLE_APPLICATION_CREDENTIALS + 
  85      ' must be defined pointing to a file defining the credentials. See ' 
  86      'https://developers.google.com/accounts/docs/application-default-credentials'  # pylint:disable=line-too-long 
  87      ' for more information.') 
  88   
  89  # The access token along with the seconds in which it expires. 
  90  AccessTokenInfo = collections.namedtuple( 
  91      'AccessTokenInfo', ['access_token', 'expires_in']) 
92 93 94 -class Error(Exception):
95 """Base error for this module."""
96
97 98 -class FlowExchangeError(Error):
99 """Error trying to exchange an authorization grant for an access token."""
100
101 102 -class AccessTokenRefreshError(Error):
103 """Error trying to refresh an expired access token."""
104
105 106 -class TokenRevokeError(Error):
107 """Error trying to revoke a token."""
108
109 110 -class UnknownClientSecretsFlowError(Error):
111 """The client secrets file called for an unknown type of OAuth 2.0 flow. """
112
113 114 -class AccessTokenCredentialsError(Error):
115 """Having only the access_token means no refresh is possible."""
116
117 118 -class VerifyJwtTokenError(Error):
119 """Could not retrieve certificates for validation."""
120
121 122 -class NonAsciiHeaderError(Error):
123 """Header names and values must be ASCII strings."""
124
125 126 -class ApplicationDefaultCredentialsError(Error):
127 """Error retrieving the Application Default Credentials."""
128
129 130 -class OAuth2DeviceCodeError(Error):
131 """Error trying to retrieve a device code."""
132
133 134 -class CryptoUnavailableError(Error, NotImplementedError):
135 """Raised when a crypto library is required, but none is available."""
136
137 138 -def _abstract():
139 raise NotImplementedError('You need to override this function')
140
141 142 -class MemoryCache(object):
143 """httplib2 Cache implementation which only caches locally.""" 144
145 - def __init__(self):
146 self.cache = {}
147
148 - def get(self, key):
149 return self.cache.get(key)
150
151 - def set(self, key, value):
152 self.cache[key] = value
153
154 - def delete(self, key):
155 self.cache.pop(key, None)
156
157 158 -class Credentials(object):
159 """Base class for all Credentials objects. 160 161 Subclasses must define an authorize() method that applies the credentials to 162 an HTTP transport. 163 164 Subclasses must also specify a classmethod named 'from_json' that takes a JSON 165 string as input and returns an instantiated Credentials object. 166 """ 167 168 NON_SERIALIZED_MEMBERS = ['store'] 169 170
171 - def authorize(self, http):
172 """Take an httplib2.Http instance (or equivalent) and authorizes it. 173 174 Authorizes it for the set of credentials, usually by replacing 175 http.request() with a method that adds in the appropriate headers and then 176 delegates to the original Http.request() method. 177 178 Args: 179 http: httplib2.Http, an http object to be used to make the refresh 180 request. 181 """ 182 _abstract()
183 184
185 - def refresh(self, http):
186 """Forces a refresh of the access_token. 187 188 Args: 189 http: httplib2.Http, an http object to be used to make the refresh 190 request. 191 """ 192 _abstract()
193 194
195 - def revoke(self, http):
196 """Revokes a refresh_token and makes the credentials void. 197 198 Args: 199 http: httplib2.Http, an http object to be used to make the revoke 200 request. 201 """ 202 _abstract()
203 204
205 - def apply(self, headers):
206 """Add the authorization to the headers. 207 208 Args: 209 headers: dict, the headers to add the Authorization header to. 210 """ 211 _abstract()
212
213 - def _to_json(self, strip):
214 """Utility function that creates JSON repr. of a Credentials object. 215 216 Args: 217 strip: array, An array of names of members to not include in the JSON. 218 219 Returns: 220 string, a JSON representation of this instance, suitable to pass to 221 from_json(). 222 """ 223 t = type(self) 224 d = copy.copy(self.__dict__) 225 for member in strip: 226 if member in d: 227 del d[member] 228 if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime): 229 d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) 230 # Add in information we will need later to reconsistitue this instance. 231 d['_class'] = t.__name__ 232 d['_module'] = t.__module__ 233 return json.dumps(d)
234
235 - def to_json(self):
236 """Creating a JSON representation of an instance of Credentials. 237 238 Returns: 239 string, a JSON representation of this instance, suitable to pass to 240 from_json(). 241 """ 242 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
243 244 @classmethod
245 - def new_from_json(cls, s):
246 """Utility class method to instantiate a Credentials subclass from a JSON 247 representation produced by to_json(). 248 249 Args: 250 s: string, JSON from to_json(). 251 252 Returns: 253 An instance of the subclass of Credentials that was serialized with 254 to_json(). 255 """ 256 data = json.loads(s) 257 # Find and call the right classmethod from_json() to restore the object. 258 module = data['_module'] 259 try: 260 m = __import__(module) 261 except ImportError: 262 # In case there's an object from the old package structure, update it 263 module = module.replace('.googleapiclient', '') 264 m = __import__(module) 265 266 m = __import__(module, fromlist=module.split('.')[:-1]) 267 kls = getattr(m, data['_class']) 268 from_json = getattr(kls, 'from_json') 269 return from_json(s)
270 271 @classmethod
272 - def from_json(cls, unused_data):
273 """Instantiate a Credentials object from a JSON description of it. 274 275 The JSON should have been produced by calling .to_json() on the object. 276 277 Args: 278 unused_data: dict, A deserialized JSON object. 279 280 Returns: 281 An instance of a Credentials subclass. 282 """ 283 return Credentials()
284
285 286 -class Flow(object):
287 """Base class for all Flow objects.""" 288 pass
289
290 291 -class Storage(object):
292 """Base class for all Storage objects. 293 294 Store and retrieve a single credential. This class supports locking 295 such that multiple processes and threads can operate on a single 296 store. 297 """ 298
299 - def acquire_lock(self):
300 """Acquires any lock necessary to access this Storage. 301 302 This lock is not reentrant. 303 """ 304 pass
305
306 - def release_lock(self):
307 """Release the Storage lock. 308 309 Trying to release a lock that isn't held will result in a 310 RuntimeError. 311 """ 312 pass
313
314 - def locked_get(self):
315 """Retrieve credential. 316 317 The Storage lock must be held when this is called. 318 319 Returns: 320 oauth2client.client.Credentials 321 """ 322 _abstract()
323
324 - def locked_put(self, credentials):
325 """Write a credential. 326 327 The Storage lock must be held when this is called. 328 329 Args: 330 credentials: Credentials, the credentials to store. 331 """ 332 _abstract()
333
334 - def locked_delete(self):
335 """Delete a credential. 336 337 The Storage lock must be held when this is called. 338 """ 339 _abstract()
340
341 - def get(self):
342 """Retrieve credential. 343 344 The Storage lock must *not* be held when this is called. 345 346 Returns: 347 oauth2client.client.Credentials 348 """ 349 self.acquire_lock() 350 try: 351 return self.locked_get() 352 finally: 353 self.release_lock()
354
355 - def put(self, credentials):
356 """Write a credential. 357 358 The Storage lock must be held when this is called. 359 360 Args: 361 credentials: Credentials, the credentials to store. 362 """ 363 self.acquire_lock() 364 try: 365 self.locked_put(credentials) 366 finally: 367 self.release_lock()
368
369 - def delete(self):
370 """Delete credential. 371 372 Frees any resources associated with storing the credential. 373 The Storage lock must *not* be held when this is called. 374 375 Returns: 376 None 377 """ 378 self.acquire_lock() 379 try: 380 return self.locked_delete() 381 finally: 382 self.release_lock()
383
384 385 -def clean_headers(headers):
386 """Forces header keys and values to be strings, i.e not unicode. 387 388 The httplib module just concats the header keys and values in a way that may 389 make the message header a unicode string, which, if it then tries to 390 contatenate to a binary request body may result in a unicode decode error. 391 392 Args: 393 headers: dict, A dictionary of headers. 394 395 Returns: 396 The same dictionary but with all the keys converted to strings. 397 """ 398 clean = {} 399 try: 400 for k, v in headers.iteritems(): 401 clean[str(k)] = str(v) 402 except UnicodeEncodeError: 403 raise NonAsciiHeaderError(k + ': ' + v) 404 return clean
405
406 407 -def _update_query_params(uri, params):
408 """Updates a URI with new query parameters. 409 410 Args: 411 uri: string, A valid URI, with potential existing query parameters. 412 params: dict, A dictionary of query parameters. 413 414 Returns: 415 The same URI but with the new query parameters added. 416 """ 417 parts = urlparse.urlparse(uri) 418 query_params = dict(urlparse.parse_qsl(parts.query)) 419 query_params.update(params) 420 new_parts = parts._replace(query=urllib.urlencode(query_params)) 421 return urlparse.urlunparse(new_parts)
422
423 424 -class OAuth2Credentials(Credentials):
425 """Credentials object for OAuth 2.0. 426 427 Credentials can be applied to an httplib2.Http object using the authorize() 428 method, which then adds the OAuth 2.0 access token to each request. 429 430 OAuth2Credentials objects may be safely pickled and unpickled. 431 """ 432 433 @util.positional(8)
434 - def __init__(self, access_token, client_id, client_secret, refresh_token, 435 token_expiry, token_uri, user_agent, revoke_uri=None, 436 id_token=None, token_response=None):
437 """Create an instance of OAuth2Credentials. 438 439 This constructor is not usually called by the user, instead 440 OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow. 441 442 Args: 443 access_token: string, access token. 444 client_id: string, client identifier. 445 client_secret: string, client secret. 446 refresh_token: string, refresh token. 447 token_expiry: datetime, when the access_token expires. 448 token_uri: string, URI of token endpoint. 449 user_agent: string, The HTTP User-Agent to provide for this application. 450 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token 451 can't be revoked if this is None. 452 id_token: object, The identity of the resource owner. 453 token_response: dict, the decoded response to the token request. None 454 if a token hasn't been requested yet. Stored because some providers 455 (e.g. wordpress.com) include extra fields that clients may want. 456 457 Notes: 458 store: callable, A callable that when passed a Credential 459 will store the credential back to where it came from. 460 This is needed to store the latest access_token if it 461 has expired and been refreshed. 462 """ 463 self.access_token = access_token 464 self.client_id = client_id 465 self.client_secret = client_secret 466 self.refresh_token = refresh_token 467 self.store = None 468 self.token_expiry = token_expiry 469 self.token_uri = token_uri 470 self.user_agent = user_agent 471 self.revoke_uri = revoke_uri 472 self.id_token = id_token 473 self.token_response = token_response 474 475 # True if the credentials have been revoked or expired and can't be 476 # refreshed. 477 self.invalid = False
478
479 - def authorize(self, http):
480 """Authorize an httplib2.Http instance with these credentials. 481 482 The modified http.request method will add authentication headers to each 483 request and will refresh access_tokens when a 401 is received on a 484 request. In addition the http.request method has a credentials property, 485 http.request.credentials, which is the Credentials object that authorized 486 it. 487 488 Args: 489 http: An instance of httplib2.Http 490 or something that acts like it. 491 492 Returns: 493 A modified instance of http that was passed in. 494 495 Example: 496 497 h = httplib2.Http() 498 h = credentials.authorize(h) 499 500 You can't create a new OAuth subclass of httplib2.Authentication 501 because it never gets passed the absolute URI, which is needed for 502 signing. So instead we have to overload 'request' with a closure 503 that adds in the Authorization header and then calls the original 504 version of 'request()'. 505 """ 506 request_orig = http.request 507 508 # The closure that will replace 'httplib2.Http.request'. 509 @util.positional(1) 510 def new_request(uri, method='GET', body=None, headers=None, 511 redirections=httplib2.DEFAULT_MAX_REDIRECTS, 512 connection_type=None): 513 if not self.access_token: 514 logger.info('Attempting refresh to obtain initial access_token') 515 self._refresh(request_orig) 516 517 # Clone and modify the request headers to add the appropriate 518 # Authorization header. 519 if headers is None: 520 headers = {} 521 else: 522 headers = dict(headers) 523 self.apply(headers) 524 525 if self.user_agent is not None: 526 if 'user-agent' in headers: 527 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] 528 else: 529 headers['user-agent'] = self.user_agent 530 531 resp, content = request_orig(uri, method, body, clean_headers(headers), 532 redirections, connection_type) 533 534 if resp.status in REFRESH_STATUS_CODES: 535 logger.info('Refreshing due to a %s', resp.status) 536 self._refresh(request_orig) 537 self.apply(headers) 538 return request_orig(uri, method, body, clean_headers(headers), 539 redirections, connection_type) 540 else: 541 return (resp, content)
542 543 # Replace the request method with our own closure. 544 http.request = new_request 545 546 # Set credentials as a property of the request method. 547 setattr(http.request, 'credentials', self) 548 549 return http
550
551 - def refresh(self, http):
552 """Forces a refresh of the access_token. 553 554 Args: 555 http: httplib2.Http, an http object to be used to make the refresh 556 request. 557 """ 558 self._refresh(http.request)
559
560 - def revoke(self, http):
561 """Revokes a refresh_token and makes the credentials void. 562 563 Args: 564 http: httplib2.Http, an http object to be used to make the revoke 565 request. 566 """ 567 self._revoke(http.request)
568
569 - def apply(self, headers):
570 """Add the authorization to the headers. 571 572 Args: 573 headers: dict, the headers to add the Authorization header to. 574 """ 575 headers['Authorization'] = 'Bearer ' + self.access_token
576
577 - def to_json(self):
578 return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
579 580 @classmethod
581 - def from_json(cls, s):
582 """Instantiate a Credentials object from a JSON description of it. The JSON 583 should have been produced by calling .to_json() on the object. 584 585 Args: 586 data: dict, A deserialized JSON object. 587 588 Returns: 589 An instance of a Credentials subclass. 590 """ 591 data = json.loads(s) 592 if ('token_expiry' in data and 593 not isinstance(data['token_expiry'], datetime.datetime)): 594 try: 595 data['token_expiry'] = datetime.datetime.strptime( 596 data['token_expiry'], EXPIRY_FORMAT) 597 except ValueError: 598 data['token_expiry'] = None 599 retval = cls( 600 data['access_token'], 601 data['client_id'], 602 data['client_secret'], 603 data['refresh_token'], 604 data['token_expiry'], 605 data['token_uri'], 606 data['user_agent'], 607 revoke_uri=data.get('revoke_uri', None), 608 id_token=data.get('id_token', None), 609 token_response=data.get('token_response', None)) 610 retval.invalid = data['invalid'] 611 return retval
612 613 @property
614 - def access_token_expired(self):
615 """True if the credential is expired or invalid. 616 617 If the token_expiry isn't set, we assume the token doesn't expire. 618 """ 619 if self.invalid: 620 return True 621 622 if not self.token_expiry: 623 return False 624 625 now = datetime.datetime.utcnow() 626 if now >= self.token_expiry: 627 logger.info('access_token is expired. Now: %s, token_expiry: %s', 628 now, self.token_expiry) 629 return True 630 return False
631
632 - def get_access_token(self, http=None):
633 """Return the access token and its expiration information. 634 635 If the token does not exist, get one. 636 If the token expired, refresh it. 637 """ 638 if not self.access_token or self.access_token_expired: 639 if not http: 640 http = httplib2.Http() 641 self.refresh(http) 642 return AccessTokenInfo(access_token=self.access_token, 643 expires_in=self._expires_in())
644
645 - def set_store(self, store):
646 """Set the Storage for the credential. 647 648 Args: 649 store: Storage, an implementation of Storage object. 650 This is needed to store the latest access_token if it 651 has expired and been refreshed. This implementation uses 652 locking to check for updates before updating the 653 access_token. 654 """ 655 self.store = store
656
657 - def _expires_in(self):
658 """Return the number of seconds until this token expires. 659 660 If token_expiry is in the past, this method will return 0, meaning the 661 token has already expired. 662 If token_expiry is None, this method will return None. Note that returning 663 0 in such a case would not be fair: the token may still be valid; 664 we just don't know anything about it. 665 """ 666 if self.token_expiry: 667 now = datetime.datetime.utcnow() 668 if self.token_expiry > now: 669 time_delta = self.token_expiry - now 670 # TODO(orestica): return time_delta.total_seconds() 671 # once dropping support for Python 2.6 672 return time_delta.days * 86400 + time_delta.seconds 673 else: 674 return 0
675
676 - def _updateFromCredential(self, other):
677 """Update this Credential from another instance.""" 678 self.__dict__.update(other.__getstate__())
679
680 - def __getstate__(self):
681 """Trim the state down to something that can be pickled.""" 682 d = copy.copy(self.__dict__) 683 del d['store'] 684 return d
685
686 - def __setstate__(self, state):
687 """Reconstitute the state of the object from being pickled.""" 688 self.__dict__.update(state) 689 self.store = None
690
691 - def _generate_refresh_request_body(self):
692 """Generate the body that will be used in the refresh request.""" 693 body = urllib.urlencode({ 694 'grant_type': 'refresh_token', 695 'client_id': self.client_id, 696 'client_secret': self.client_secret, 697 'refresh_token': self.refresh_token, 698 }) 699 return body
700
701 - def _generate_refresh_request_headers(self):
702 """Generate the headers that will be used in the refresh request.""" 703 headers = { 704 'content-type': 'application/x-www-form-urlencoded', 705 } 706 707 if self.user_agent is not None: 708 headers['user-agent'] = self.user_agent 709 710 return headers
711
712 - def _refresh(self, http_request):
713 """Refreshes the access_token. 714 715 This method first checks by reading the Storage object if available. 716 If a refresh is still needed, it holds the Storage lock until the 717 refresh is completed. 718 719 Args: 720 http_request: callable, a callable that matches the method signature of 721 httplib2.Http.request, used to make the refresh request. 722 723 Raises: 724 AccessTokenRefreshError: When the refresh fails. 725 """ 726 if not self.store: 727 self._do_refresh_request(http_request) 728 else: 729 self.store.acquire_lock() 730 try: 731 new_cred = self.store.locked_get() 732 if (new_cred and not new_cred.invalid and 733 new_cred.access_token != self.access_token): 734 logger.info('Updated access_token read from Storage') 735 self._updateFromCredential(new_cred) 736 else: 737 self._do_refresh_request(http_request) 738 finally: 739 self.store.release_lock()
740
741 - def _do_refresh_request(self, http_request):
742 """Refresh the access_token using the refresh_token. 743 744 Args: 745 http_request: callable, a callable that matches the method signature of 746 httplib2.Http.request, used to make the refresh request. 747 748 Raises: 749 AccessTokenRefreshError: When the refresh fails. 750 """ 751 body = self._generate_refresh_request_body() 752 headers = self._generate_refresh_request_headers() 753 754 logger.info('Refreshing access_token') 755 resp, content = http_request( 756 self.token_uri, method='POST', body=body, headers=headers) 757 if resp.status == 200: 758 # TODO(jcgregorio) Raise an error if loads fails? 759 d = json.loads(content) 760 self.token_response = d 761 self.access_token = d['access_token'] 762 self.refresh_token = d.get('refresh_token', self.refresh_token) 763 if 'expires_in' in d: 764 self.token_expiry = datetime.timedelta( 765 seconds=int(d['expires_in'])) + datetime.datetime.utcnow() 766 else: 767 self.token_expiry = None 768 # On temporary refresh errors, the user does not actually have to 769 # re-authorize, so we unflag here. 770 self.invalid = False 771 if self.store: 772 self.store.locked_put(self) 773 else: 774 # An {'error':...} response body means the token is expired or revoked, 775 # so we flag the credentials as such. 776 logger.info('Failed to retrieve access token: %s', content) 777 error_msg = 'Invalid response %s.' % resp['status'] 778 try: 779 d = json.loads(content) 780 if 'error' in d: 781 error_msg = d['error'] 782 if 'error_description' in d: 783 error_msg += ': ' + d['error_description'] 784 self.invalid = True 785 if self.store: 786 self.store.locked_put(self) 787 except StandardError: 788 pass 789 raise AccessTokenRefreshError(error_msg)
790
791 - def _revoke(self, http_request):
792 """Revokes the refresh_token and deletes the store if available. 793 794 Args: 795 http_request: callable, a callable that matches the method signature of 796 httplib2.Http.request, used to make the revoke request. 797 """ 798 self._do_revoke(http_request, self.refresh_token)
799
800 - def _do_revoke(self, http_request, token):
801 """Revokes the credentials and deletes the store if available. 802 803 Args: 804 http_request: callable, a callable that matches the method signature of 805 httplib2.Http.request, used to make the refresh request. 806 token: A string used as the token to be revoked. Can be either an 807 access_token or refresh_token. 808 809 Raises: 810 TokenRevokeError: If the revoke request does not return with a 200 OK. 811 """ 812 logger.info('Revoking token') 813 query_params = {'token': token} 814 token_revoke_uri = _update_query_params(self.revoke_uri, query_params) 815 resp, content = http_request(token_revoke_uri) 816 if resp.status == 200: 817 self.invalid = True 818 else: 819 error_msg = 'Invalid response %s.' % resp.status 820 try: 821 d = json.loads(content) 822 if 'error' in d: 823 error_msg = d['error'] 824 except StandardError: 825 pass 826 raise TokenRevokeError(error_msg) 827 828 if self.store: 829 self.store.delete()
830
831 832 -class AccessTokenCredentials(OAuth2Credentials):
833 """Credentials object for OAuth 2.0. 834 835 Credentials can be applied to an httplib2.Http object using the 836 authorize() method, which then signs each request from that object 837 with the OAuth 2.0 access token. This set of credentials is for the 838 use case where you have acquired an OAuth 2.0 access_token from 839 another place such as a JavaScript client or another web 840 application, and wish to use it from Python. Because only the 841 access_token is present it can not be refreshed and will in time 842 expire. 843 844 AccessTokenCredentials objects may be safely pickled and unpickled. 845 846 Usage: 847 credentials = AccessTokenCredentials('<an access token>', 848 'my-user-agent/1.0') 849 http = httplib2.Http() 850 http = credentials.authorize(http) 851 852 Exceptions: 853 AccessTokenCredentialsExpired: raised when the access_token expires or is 854 revoked. 855 """ 856
857 - def __init__(self, access_token, user_agent, revoke_uri=None):
858 """Create an instance of OAuth2Credentials 859 860 This is one of the few types if Credentials that you should contrust, 861 Credentials objects are usually instantiated by a Flow. 862 863 Args: 864 access_token: string, access token. 865 user_agent: string, The HTTP User-Agent to provide for this application. 866 revoke_uri: string, URI for revoke endpoint. Defaults to None; a token 867 can't be revoked if this is None. 868 """ 869 super(AccessTokenCredentials, self).__init__( 870 access_token, 871 None, 872 None, 873 None, 874 None, 875 None, 876 user_agent, 877 revoke_uri=revoke_uri)
878 879 880 @classmethod
881 - def from_json(cls, s):
882 data = json.loads(s) 883 retval = AccessTokenCredentials( 884 data['access_token'], 885 data['user_agent']) 886 return retval
887
888 - def _refresh(self, http_request):
889 raise AccessTokenCredentialsError( 890 'The access_token is expired or invalid and can\'t be refreshed.')
891
892 - def _revoke(self, http_request):
893 """Revokes the access_token and deletes the store if available. 894 895 Args: 896 http_request: callable, a callable that matches the method signature of 897 httplib2.Http.request, used to make the revoke request. 898 """ 899 self._do_revoke(http_request, self.access_token)
900 901 902 _env_name = None
903 904 905 -def _get_environment(urllib2_urlopen=None):
906 """Detect the environment the code is being run on.""" 907 908 global _env_name 909 910 if _env_name: 911 return _env_name 912 913 server_software = os.environ.get('SERVER_SOFTWARE', '') 914 if server_software.startswith('Google App Engine/'): 915 _env_name = 'GAE_PRODUCTION' 916 elif server_software.startswith('Development/'): 917 _env_name = 'GAE_LOCAL' 918 else: 919 import urllib2 920 try: 921 if urllib2_urlopen is None: 922 urllib2_urlopen = urllib2.urlopen 923 response = urllib2_urlopen('http://metadata.google.internal') 924 if any('Metadata-Flavor: Google' in h for h in response.info().headers): 925 _env_name = 'GCE_PRODUCTION' 926 else: 927 _env_name = 'UNKNOWN' 928 except urllib2.URLError: 929 _env_name = 'UNKNOWN' 930 931 return _env_name
932
933 934 -class GoogleCredentials(OAuth2Credentials):
935 """Application Default Credentials for use in calling Google APIs. 936 937 The Application Default Credentials are being constructed as a function of 938 the environment where the code is being run. 939 More details can be found on this page: 940 https://developers.google.com/accounts/docs/application-default-credentials 941 942 Here is an example of how to use the Application Default Credentials for a 943 service that requires authentication: 944 945 <code> 946 from googleapiclient.discovery import build 947 from oauth2client.client import GoogleCredentials 948 949 PROJECT = 'bamboo-machine-422' # replace this with one of your projects 950 ZONE = 'us-central1-a' # replace this with the zone you care about 951 952 credentials = GoogleCredentials.get_application_default() 953 service = build('compute', 'v1', credentials=credentials) 954 955 request = service.instances().list(project=PROJECT, zone=ZONE) 956 response = request.execute() 957 958 print response 959 </code> 960 961 A service that does not require authentication does not need credentials 962 to be passed in: 963 964 <code> 965 from googleapiclient.discovery import build 966 967 service = build('discovery', 'v1') 968 969 request = service.apis().list() 970 response = request.execute() 971 972 print response 973 </code> 974 """ 975
976 - def __init__(self, access_token, client_id, client_secret, refresh_token, 977 token_expiry, token_uri, user_agent, 978 revoke_uri=GOOGLE_REVOKE_URI):
979 """Create an instance of GoogleCredentials. 980 981 This constructor is not usually called by the user, instead 982 GoogleCredentials objects are instantiated by 983 GoogleCredentials.from_stream() or 984 GoogleCredentials.get_application_default(). 985 986 Args: 987 access_token: string, access token. 988 client_id: string, client identifier. 989 client_secret: string, client secret. 990 refresh_token: string, refresh token. 991 token_expiry: datetime, when the access_token expires. 992 token_uri: string, URI of token endpoint. 993 user_agent: string, The HTTP User-Agent to provide for this application. 994 revoke_uri: string, URI for revoke endpoint. 995 Defaults to GOOGLE_REVOKE_URI; a token can't be revoked if this is None. 996 """ 997 super(GoogleCredentials, self).__init__( 998 access_token, client_id, client_secret, refresh_token, token_expiry, 999 token_uri, user_agent, revoke_uri=revoke_uri)
1000
1001 - def create_scoped_required(self):
1002 """Whether this Credentials object is scopeless. 1003 1004 create_scoped(scopes) method needs to be called in order to create 1005 a Credentials object for API calls. 1006 """ 1007 return False
1008
1009 - def create_scoped(self, scopes):
1010 """Create a Credentials object for the given scopes. 1011 1012 The Credentials type is preserved. 1013 """ 1014 return self
1015 1016 @property
1017 - def serialization_data(self):
1018 """Get the fields and their values identifying the current credentials.""" 1019 return { 1020 'type': 'authorized_user', 1021 'client_id': self.client_id, 1022 'client_secret': self.client_secret, 1023 'refresh_token': self.refresh_token 1024 }
1025 1026 @staticmethod
1028 """Get the Application Default Credentials for the current environment. 1029 1030 Exceptions: 1031 ApplicationDefaultCredentialsError: raised when the credentials fail 1032 to be retrieved. 1033 """ 1034 1035 env_name = _get_environment() 1036 1037 if env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'): 1038 # if we are running inside Google App Engine 1039 # there is no need to look for credentials in local files 1040 application_default_credential_filename = None 1041 well_known_file = None 1042 else: 1043 application_default_credential_filename = _get_environment_variable_file() 1044 well_known_file = _get_well_known_file() 1045 if not os.path.isfile(well_known_file): 1046 well_known_file = None 1047 1048 if application_default_credential_filename: 1049 try: 1050 return _get_application_default_credential_from_file( 1051 application_default_credential_filename) 1052 except (ApplicationDefaultCredentialsError, ValueError) as error: 1053 extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + 1054 ' environment variable)') 1055 _raise_exception_for_reading_json( 1056 application_default_credential_filename, extra_help, error) 1057 elif well_known_file: 1058 try: 1059 return _get_application_default_credential_from_file(well_known_file) 1060 except (ApplicationDefaultCredentialsError, ValueError) as error: 1061 extra_help = (' (produced automatically when running' 1062 ' "gcloud auth login" command)') 1063 _raise_exception_for_reading_json(well_known_file, extra_help, error) 1064 elif env_name in ('GAE_PRODUCTION', 'GAE_LOCAL'): 1065 return _get_application_default_credential_GAE() 1066 elif env_name == 'GCE_PRODUCTION': 1067 return _get_application_default_credential_GCE() 1068 else: 1069 raise ApplicationDefaultCredentialsError(ADC_HELP_MSG)
1070 1071 @staticmethod
1072 - def from_stream(credential_filename):
1073 """Create a Credentials object by reading the information from a given file. 1074 1075 It returns an object of type GoogleCredentials. 1076 1077 Args: 1078 credential_filename: the path to the file from where the credentials 1079 are to be read 1080 1081 Exceptions: 1082 ApplicationDefaultCredentialsError: raised when the credentials fail 1083 to be retrieved. 1084 """ 1085 1086 if credential_filename and os.path.isfile(credential_filename): 1087 try: 1088 return _get_application_default_credential_from_file( 1089 credential_filename) 1090 except (ApplicationDefaultCredentialsError, ValueError) as error: 1091 extra_help = ' (provided as parameter to the from_stream() method)' 1092 _raise_exception_for_reading_json(credential_filename, 1093 extra_help, 1094 error) 1095 else: 1096 raise ApplicationDefaultCredentialsError( 1097 'The parameter passed to the from_stream() ' 1098 'method should point to a file.')
1099
1100 1101 -def save_to_well_known_file(credentials, well_known_file=None):
1102 """Save the provided GoogleCredentials to the well known file. 1103 1104 Args: 1105 credentials: 1106 the credentials to be saved to the well known file; 1107 it should be an instance of GoogleCredentials 1108 well_known_file: 1109 the name of the file where the credentials are to be saved; 1110 this parameter is supposed to be used for testing only 1111 """ 1112 # TODO(orestica): move this method to tools.py 1113 # once the argparse import gets fixed (it is not present in Python 2.6) 1114 1115 if well_known_file is None: 1116 well_known_file = _get_well_known_file() 1117 1118 credentials_data = credentials.serialization_data 1119 1120 with open(well_known_file, 'w') as f: 1121 json.dump(credentials_data, f, sort_keys=True, indent=2)
1122
1123 1124 -def _get_environment_variable_file():
1125 application_default_credential_filename = ( 1126 os.environ.get(GOOGLE_APPLICATION_CREDENTIALS, 1127 None)) 1128 1129 if application_default_credential_filename: 1130 if os.path.isfile(application_default_credential_filename): 1131 return application_default_credential_filename 1132 else: 1133 raise ApplicationDefaultCredentialsError( 1134 'File ' + application_default_credential_filename + ' (pointed by ' + 1135 GOOGLE_APPLICATION_CREDENTIALS + 1136 ' environment variable) does not exist!')
1137
1138 1139 -def _get_well_known_file():
1140 """Get the well known file produced by command 'gcloud auth login'.""" 1141 # TODO(orestica): Revisit this method once gcloud provides a better way 1142 # of pinpointing the exact location of the file. 1143 1144 WELL_KNOWN_CREDENTIALS_FILE = 'application_default_credentials.json' 1145 CLOUDSDK_CONFIG_DIRECTORY = 'gcloud' 1146 1147 if os.name == 'nt': 1148 try: 1149 default_config_path = os.path.join(os.environ['APPDATA'], 1150 CLOUDSDK_CONFIG_DIRECTORY) 1151 except KeyError: 1152 # This should never happen unless someone is really messing with things. 1153 drive = os.environ.get('SystemDrive', 'C:') 1154 default_config_path = os.path.join(drive, '\\', CLOUDSDK_CONFIG_DIRECTORY) 1155 else: 1156 default_config_path = os.path.join(os.path.expanduser('~'), 1157 '.config', 1158 CLOUDSDK_CONFIG_DIRECTORY) 1159 1160 default_config_path = os.path.join(default_config_path, 1161 WELL_KNOWN_CREDENTIALS_FILE) 1162 1163 return default_config_path
1164
1165 1166 -def _get_application_default_credential_from_file( 1167 application_default_credential_filename):
1168 """Build the Application Default Credentials from file.""" 1169 1170 import service_account 1171 1172 # read the credentials from the file 1173 with open(application_default_credential_filename) as ( 1174 application_default_credential): 1175 client_credentials = json.load(application_default_credential) 1176 1177 credentials_type = client_credentials.get('type') 1178 if credentials_type == AUTHORIZED_USER: 1179 required_fields = set(['client_id', 'client_secret', 'refresh_token']) 1180 elif credentials_type == SERVICE_ACCOUNT: 1181 required_fields = set(['client_id', 'client_email', 'private_key_id', 1182 'private_key']) 1183 else: 1184 raise ApplicationDefaultCredentialsError( 1185 "'type' field should be defined (and have one of the '" + 1186 AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") 1187 1188 missing_fields = required_fields.difference(client_credentials.keys()) 1189 1190 if missing_fields: 1191 _raise_exception_for_missing_fields(missing_fields) 1192 1193 if client_credentials['type'] == AUTHORIZED_USER: 1194 return GoogleCredentials( 1195 access_token=None, 1196 client_id=client_credentials['client_id'], 1197 client_secret=client_credentials['client_secret'], 1198 refresh_token=client_credentials['refresh_token'], 1199 token_expiry=None, 1200 token_uri=GOOGLE_TOKEN_URI, 1201 user_agent='Python client library') 1202 else: # client_credentials['type'] == SERVICE_ACCOUNT 1203 return service_account._ServiceAccountCredentials( 1204 service_account_id=client_credentials['client_id'], 1205 service_account_email=client_credentials['client_email'], 1206 private_key_id=client_credentials['private_key_id'], 1207 private_key_pkcs8_text=client_credentials['private_key'], 1208 scopes=[])
1209
1210 1211 -def _raise_exception_for_missing_fields(missing_fields):
1212 raise ApplicationDefaultCredentialsError( 1213 'The following field(s) must be defined: ' + ', '.join(missing_fields))
1214
1215 1216 -def _raise_exception_for_reading_json(credential_file, 1217 extra_help, 1218 error):
1219 raise ApplicationDefaultCredentialsError( 1220 'An error was encountered while reading json file: '+ 1221 credential_file + extra_help + ': ' + str(error))
1222
1223 1224 -def _get_application_default_credential_GAE():
1225 from oauth2client.appengine import AppAssertionCredentials 1226 1227 return AppAssertionCredentials([])
1228
1229 1230 -def _get_application_default_credential_GCE():
1231 from oauth2client.gce import AppAssertionCredentials 1232 1233 return AppAssertionCredentials([])
1234
1235 1236 -class AssertionCredentials(GoogleCredentials):
1237 """Abstract Credentials object used for OAuth 2.0 assertion grants. 1238 1239 This credential does not require a flow to instantiate because it 1240 represents a two legged flow, and therefore has all of the required 1241 information to generate and refresh its own access tokens. It must 1242 be subclassed to generate the appropriate assertion string. 1243 1244 AssertionCredentials objects may be safely pickled and unpickled. 1245 """ 1246 1247 @util.positional(2)
1248 - def __init__(self, assertion_type, user_agent=None, 1249 token_uri=GOOGLE_TOKEN_URI, 1250 revoke_uri=GOOGLE_REVOKE_URI, 1251 **unused_kwargs):
1252 """Constructor for AssertionFlowCredentials. 1253 1254 Args: 1255 assertion_type: string, assertion type that will be declared to the auth 1256 server 1257 user_agent: string, The HTTP User-Agent to provide for this application. 1258 token_uri: string, URI for token endpoint. For convenience 1259 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1260 revoke_uri: string, URI for revoke endpoint. 1261 """ 1262 super(AssertionCredentials, self).__init__( 1263 None, 1264 None, 1265 None, 1266 None, 1267 None, 1268 token_uri, 1269 user_agent, 1270 revoke_uri=revoke_uri) 1271 self.assertion_type = assertion_type
1272
1274 assertion = self._generate_assertion() 1275 1276 body = urllib.urlencode({ 1277 'assertion': assertion, 1278 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 1279 }) 1280 1281 return body
1282
1283 - def _generate_assertion(self):
1284 """Generate the assertion string that will be used in the access token 1285 request. 1286 """ 1287 _abstract()
1288
1289 - def _revoke(self, http_request):
1290 """Revokes the access_token and deletes the store if available. 1291 1292 Args: 1293 http_request: callable, a callable that matches the method signature of 1294 httplib2.Http.request, used to make the revoke request. 1295 """ 1296 self._do_revoke(http_request, self.access_token)
1297
1298 1299 -def _RequireCryptoOrDie():
1300 """Ensure we have a crypto library, or throw CryptoUnavailableError. 1301 1302 The oauth2client.crypt module requires either PyCrypto or PyOpenSSL 1303 to be available in order to function, but these are optional 1304 dependencies. 1305 """ 1306 if not HAS_CRYPTO: 1307 raise CryptoUnavailableError('No crypto library available')
1308
1309 1310 -class SignedJwtAssertionCredentials(AssertionCredentials):
1311 """Credentials object used for OAuth 2.0 Signed JWT assertion grants. 1312 1313 This credential does not require a flow to instantiate because it 1314 represents a two legged flow, and therefore has all of the required 1315 information to generate and refresh its own access tokens. 1316 1317 SignedJwtAssertionCredentials requires either PyOpenSSL, or PyCrypto 1318 2.6 or later. For App Engine you may also consider using 1319 AppAssertionCredentials. 1320 """ 1321 1322 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 1323 1324 @util.positional(4)
1325 - def __init__(self, 1326 service_account_name, 1327 private_key, 1328 scope, 1329 private_key_password='notasecret', 1330 user_agent=None, 1331 token_uri=GOOGLE_TOKEN_URI, 1332 revoke_uri=GOOGLE_REVOKE_URI, 1333 **kwargs):
1334 """Constructor for SignedJwtAssertionCredentials. 1335 1336 Args: 1337 service_account_name: string, id for account, usually an email address. 1338 private_key: string, private key in PKCS12 or PEM format. 1339 scope: string or iterable of strings, scope(s) of the credentials being 1340 requested. 1341 private_key_password: string, password for private_key, unused if 1342 private_key is in PEM format. 1343 user_agent: string, HTTP User-Agent to provide for this application. 1344 token_uri: string, URI for token endpoint. For convenience 1345 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1346 revoke_uri: string, URI for revoke endpoint. 1347 kwargs: kwargs, Additional parameters to add to the JWT token, for 1348 example sub=joe@xample.org. 1349 1350 Raises: 1351 CryptoUnavailableError if no crypto library is available. 1352 """ 1353 _RequireCryptoOrDie() 1354 super(SignedJwtAssertionCredentials, self).__init__( 1355 None, 1356 user_agent=user_agent, 1357 token_uri=token_uri, 1358 revoke_uri=revoke_uri, 1359 ) 1360 1361 self.scope = util.scopes_to_string(scope) 1362 1363 # Keep base64 encoded so it can be stored in JSON. 1364 self.private_key = base64.b64encode(private_key) 1365 1366 self.private_key_password = private_key_password 1367 self.service_account_name = service_account_name 1368 self.kwargs = kwargs
1369 1370 @classmethod
1371 - def from_json(cls, s):
1372 data = json.loads(s) 1373 retval = SignedJwtAssertionCredentials( 1374 data['service_account_name'], 1375 base64.b64decode(data['private_key']), 1376 data['scope'], 1377 private_key_password=data['private_key_password'], 1378 user_agent=data['user_agent'], 1379 token_uri=data['token_uri'], 1380 **data['kwargs'] 1381 ) 1382 retval.invalid = data['invalid'] 1383 retval.access_token = data['access_token'] 1384 return retval
1385
1386 - def _generate_assertion(self):
1387 """Generate the assertion that will be used in the request.""" 1388 now = long(time.time()) 1389 payload = { 1390 'aud': self.token_uri, 1391 'scope': self.scope, 1392 'iat': now, 1393 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, 1394 'iss': self.service_account_name 1395 } 1396 payload.update(self.kwargs) 1397 logger.debug(str(payload)) 1398 1399 private_key = base64.b64decode(self.private_key) 1400 return crypt.make_signed_jwt(crypt.Signer.from_string( 1401 private_key, self.private_key_password), payload)
1402 1403 # Only used in verify_id_token(), which is always calling to the same URI 1404 # for the certs. 1405 _cached_http = httplib2.Http(MemoryCache())
1406 1407 @util.positional(2) 1408 -def verify_id_token(id_token, audience, http=None, 1409 cert_uri=ID_TOKEN_VERIFICATION_CERTS):
1410 """Verifies a signed JWT id_token. 1411 1412 This function requires PyOpenSSL and because of that it does not work on 1413 App Engine. 1414 1415 Args: 1416 id_token: string, A Signed JWT. 1417 audience: string, The audience 'aud' that the token should be for. 1418 http: httplib2.Http, instance to use to make the HTTP request. Callers 1419 should supply an instance that has caching enabled. 1420 cert_uri: string, URI of the certificates in JSON format to 1421 verify the JWT against. 1422 1423 Returns: 1424 The deserialized JSON in the JWT. 1425 1426 Raises: 1427 oauth2client.crypt.AppIdentityError: if the JWT fails to verify. 1428 CryptoUnavailableError: if no crypto library is available. 1429 """ 1430 _RequireCryptoOrDie() 1431 if http is None: 1432 http = _cached_http 1433 1434 resp, content = http.request(cert_uri) 1435 1436 if resp.status == 200: 1437 certs = json.loads(content) 1438 return crypt.verify_signed_jwt_with_certs(id_token, certs, audience) 1439 else: 1440 raise VerifyJwtTokenError('Status code: %d' % resp.status)
1441
1442 1443 -def _urlsafe_b64decode(b64string):
1444 # Guard against unicode strings, which base64 can't handle. 1445 b64string = b64string.encode('ascii') 1446 padded = b64string + '=' * (4 - len(b64string) % 4) 1447 return base64.urlsafe_b64decode(padded)
1448
1449 1450 -def _extract_id_token(id_token):
1451 """Extract the JSON payload from a JWT. 1452 1453 Does the extraction w/o checking the signature. 1454 1455 Args: 1456 id_token: string, OAuth 2.0 id_token. 1457 1458 Returns: 1459 object, The deserialized JSON payload. 1460 """ 1461 segments = id_token.split('.') 1462 1463 if len(segments) != 3: 1464 raise VerifyJwtTokenError( 1465 'Wrong number of segments in token: %s' % id_token) 1466 1467 return json.loads(_urlsafe_b64decode(segments[1]))
1468
1469 1470 -def _parse_exchange_token_response(content):
1471 """Parses response of an exchange token request. 1472 1473 Most providers return JSON but some (e.g. Facebook) return a 1474 url-encoded string. 1475 1476 Args: 1477 content: The body of a response 1478 1479 Returns: 1480 Content as a dictionary object. Note that the dict could be empty, 1481 i.e. {}. That basically indicates a failure. 1482 """ 1483 resp = {} 1484 try: 1485 resp = json.loads(content) 1486 except StandardError: 1487 # different JSON libs raise different exceptions, 1488 # so we just do a catch-all here 1489 resp = dict(urlparse.parse_qsl(content)) 1490 1491 # some providers respond with 'expires', others with 'expires_in' 1492 if resp and 'expires' in resp: 1493 resp['expires_in'] = resp.pop('expires') 1494 1495 return resp
1496
1497 1498 @util.positional(4) 1499 -def credentials_from_code(client_id, client_secret, scope, code, 1500 redirect_uri='postmessage', http=None, 1501 user_agent=None, token_uri=GOOGLE_TOKEN_URI, 1502 auth_uri=GOOGLE_AUTH_URI, 1503 revoke_uri=GOOGLE_REVOKE_URI, 1504 device_uri=GOOGLE_DEVICE_URI):
1505 """Exchanges an authorization code for an OAuth2Credentials object. 1506 1507 Args: 1508 client_id: string, client identifier. 1509 client_secret: string, client secret. 1510 scope: string or iterable of strings, scope(s) to request. 1511 code: string, An authroization code, most likely passed down from 1512 the client 1513 redirect_uri: string, this is generally set to 'postmessage' to match the 1514 redirect_uri that the client specified 1515 http: httplib2.Http, optional http instance to use to do the fetch 1516 token_uri: string, URI for token endpoint. For convenience 1517 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1518 auth_uri: string, URI for authorization endpoint. For convenience 1519 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1520 revoke_uri: string, URI for revoke endpoint. For convenience 1521 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1522 device_uri: string, URI for device authorization endpoint. For convenience 1523 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1524 1525 Returns: 1526 An OAuth2Credentials object. 1527 1528 Raises: 1529 FlowExchangeError if the authorization code cannot be exchanged for an 1530 access token 1531 """ 1532 flow = OAuth2WebServerFlow(client_id, client_secret, scope, 1533 redirect_uri=redirect_uri, user_agent=user_agent, 1534 auth_uri=auth_uri, token_uri=token_uri, 1535 revoke_uri=revoke_uri, device_uri=device_uri) 1536 1537 credentials = flow.step2_exchange(code, http=http) 1538 return credentials
1539
1540 1541 @util.positional(3) 1542 -def credentials_from_clientsecrets_and_code(filename, scope, code, 1543 message = None, 1544 redirect_uri='postmessage', 1545 http=None, 1546 cache=None, 1547 device_uri=None):
1548 """Returns OAuth2Credentials from a clientsecrets file and an auth code. 1549 1550 Will create the right kind of Flow based on the contents of the clientsecrets 1551 file or will raise InvalidClientSecretsError for unknown types of Flows. 1552 1553 Args: 1554 filename: string, File name of clientsecrets. 1555 scope: string or iterable of strings, scope(s) to request. 1556 code: string, An authorization code, most likely passed down from 1557 the client 1558 message: string, A friendly string to display to the user if the 1559 clientsecrets file is missing or invalid. If message is provided then 1560 sys.exit will be called in the case of an error. If message in not 1561 provided then clientsecrets.InvalidClientSecretsError will be raised. 1562 redirect_uri: string, this is generally set to 'postmessage' to match the 1563 redirect_uri that the client specified 1564 http: httplib2.Http, optional http instance to use to do the fetch 1565 cache: An optional cache service client that implements get() and set() 1566 methods. See clientsecrets.loadfile() for details. 1567 device_uri: string, OAuth 2.0 device authorization endpoint 1568 1569 Returns: 1570 An OAuth2Credentials object. 1571 1572 Raises: 1573 FlowExchangeError if the authorization code cannot be exchanged for an 1574 access token 1575 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1576 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1577 invalid. 1578 """ 1579 flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, 1580 redirect_uri=redirect_uri, 1581 device_uri=device_uri) 1582 credentials = flow.step2_exchange(code, http=http) 1583 return credentials
1584
1585 1586 -class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( 1587 'device_code', 'user_code', 'interval', 'verification_url', 1588 'user_code_expiry'))):
1589 """Intermediate information the OAuth2 for devices flow.""" 1590 1591 @classmethod
1592 - def FromResponse(cls, response):
1593 """Create a DeviceFlowInfo from a server response. 1594 1595 The response should be a dict containing entries as described 1596 here: 1597 http://tools.ietf.org/html/draft-ietf-oauth-v2-05#section-3.7.1 1598 """ 1599 # device_code, user_code, and verification_url are required. 1600 kwargs = { 1601 'device_code': response['device_code'], 1602 'user_code': response['user_code'], 1603 } 1604 # The response may list the verification address as either 1605 # verification_url or verification_uri, so we check for both. 1606 verification_url = response.get( 1607 'verification_url', response.get('verification_uri')) 1608 if verification_url is None: 1609 raise OAuth2DeviceCodeError( 1610 'No verification_url provided in server response') 1611 kwargs['verification_url'] = verification_url 1612 # expires_in and interval are optional. 1613 kwargs.update({ 1614 'interval': response.get('interval'), 1615 'user_code_expiry': None, 1616 }) 1617 if 'expires_in' in response: 1618 kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta( 1619 seconds=int(response['expires_in'])) 1620 1621 return cls(**kwargs)
1622
1623 -class OAuth2WebServerFlow(Flow):
1624 """Does the Web Server Flow for OAuth 2.0. 1625 1626 OAuth2WebServerFlow objects may be safely pickled and unpickled. 1627 """ 1628 1629 @util.positional(4)
1630 - def __init__(self, client_id, client_secret, scope, 1631 redirect_uri=None, 1632 user_agent=None, 1633 auth_uri=GOOGLE_AUTH_URI, 1634 token_uri=GOOGLE_TOKEN_URI, 1635 revoke_uri=GOOGLE_REVOKE_URI, 1636 login_hint=None, 1637 device_uri=GOOGLE_DEVICE_URI, 1638 **kwargs):
1639 """Constructor for OAuth2WebServerFlow. 1640 1641 The kwargs argument is used to set extra query parameters on the 1642 auth_uri. For example, the access_type and approval_prompt 1643 query parameters can be set via kwargs. 1644 1645 Args: 1646 client_id: string, client identifier. 1647 client_secret: string client secret. 1648 scope: string or iterable of strings, scope(s) of the credentials being 1649 requested. 1650 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1651 a non-web-based application, or a URI that handles the callback from 1652 the authorization server. 1653 user_agent: string, HTTP User-Agent to provide for this application. 1654 auth_uri: string, URI for authorization endpoint. For convenience 1655 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1656 token_uri: string, URI for token endpoint. For convenience 1657 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1658 revoke_uri: string, URI for revoke endpoint. For convenience 1659 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1660 login_hint: string, Either an email address or domain. Passing this hint 1661 will either pre-fill the email box on the sign-in form or select the 1662 proper multi-login session, thereby simplifying the login flow. 1663 device_uri: string, URI for device authorization endpoint. For convenience 1664 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1665 **kwargs: dict, The keyword arguments are all optional and required 1666 parameters for the OAuth calls. 1667 """ 1668 self.client_id = client_id 1669 self.client_secret = client_secret 1670 self.scope = util.scopes_to_string(scope) 1671 self.redirect_uri = redirect_uri 1672 self.login_hint = login_hint 1673 self.user_agent = user_agent 1674 self.auth_uri = auth_uri 1675 self.token_uri = token_uri 1676 self.revoke_uri = revoke_uri 1677 self.device_uri = device_uri 1678 self.params = { 1679 'access_type': 'offline', 1680 'response_type': 'code', 1681 } 1682 self.params.update(kwargs)
1683 1684 @util.positional(1)
1685 - def step1_get_authorize_url(self, redirect_uri=None):
1686 """Returns a URI to redirect to the provider. 1687 1688 Args: 1689 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1690 a non-web-based application, or a URI that handles the callback from 1691 the authorization server. This parameter is deprecated, please move to 1692 passing the redirect_uri in via the constructor. 1693 1694 Returns: 1695 A URI as a string to redirect the user to begin the authorization flow. 1696 """ 1697 if redirect_uri is not None: 1698 logger.warning(( 1699 'The redirect_uri parameter for ' 1700 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please ' 1701 'move to passing the redirect_uri in via the constructor.')) 1702 self.redirect_uri = redirect_uri 1703 1704 if self.redirect_uri is None: 1705 raise ValueError('The value of redirect_uri must not be None.') 1706 1707 query_params = { 1708 'client_id': self.client_id, 1709 'redirect_uri': self.redirect_uri, 1710 'scope': self.scope, 1711 } 1712 if self.login_hint is not None: 1713 query_params['login_hint'] = self.login_hint 1714 query_params.update(self.params) 1715 return _update_query_params(self.auth_uri, query_params)
1716 1717 @util.positional(1)
1718 - def step1_get_device_and_user_codes(self, http=None):
1719 """Returns a user code and the verification URL where to enter it 1720 1721 Returns: 1722 A user code as a string for the user to authorize the application 1723 An URL as a string where the user has to enter the code 1724 """ 1725 if self.device_uri is None: 1726 raise ValueError('The value of device_uri must not be None.') 1727 1728 body = urllib.urlencode({ 1729 'client_id': self.client_id, 1730 'scope': self.scope, 1731 }) 1732 headers = { 1733 'content-type': 'application/x-www-form-urlencoded', 1734 } 1735 1736 if self.user_agent is not None: 1737 headers['user-agent'] = self.user_agent 1738 1739 if http is None: 1740 http = httplib2.Http() 1741 1742 resp, content = http.request(self.device_uri, method='POST', body=body, 1743 headers=headers) 1744 if resp.status == 200: 1745 try: 1746 flow_info = json.loads(content) 1747 except ValueError as e: 1748 raise OAuth2DeviceCodeError( 1749 'Could not parse server response as JSON: "%s", error: "%s"' % ( 1750 content, e)) 1751 return DeviceFlowInfo.FromResponse(flow_info) 1752 else: 1753 error_msg = 'Invalid response %s.' % resp.status 1754 try: 1755 d = json.loads(content) 1756 if 'error' in d: 1757 error_msg += ' Error: %s' % d['error'] 1758 except ValueError: 1759 # Couldn't decode a JSON response, stick with the default message. 1760 pass 1761 raise OAuth2DeviceCodeError(error_msg)
1762 1763 @util.positional(2)
1764 - def step2_exchange(self, code=None, http=None, device_flow_info=None):
1765 """Exchanges a code for OAuth2Credentials. 1766 1767 Args: 1768 1769 code: string, dict or None. For a non-device flow, this is 1770 either the response code as a string, or a dictionary of 1771 query parameters to the redirect_uri. For a device flow, 1772 this should be None. 1773 http: httplib2.Http, optional http instance to use when fetching 1774 credentials. 1775 device_flow_info: DeviceFlowInfo, return value from step1 in the 1776 case of a device flow. 1777 1778 Returns: 1779 An OAuth2Credentials object that can be used to authorize requests. 1780 1781 Raises: 1782 FlowExchangeError: if a problem occured exchanging the code for a 1783 refresh_token. 1784 ValueError: if code and device_flow_info are both provided or both 1785 missing. 1786 1787 """ 1788 if code is None and device_flow_info is None: 1789 raise ValueError('No code or device_flow_info provided.') 1790 if code is not None and device_flow_info is not None: 1791 raise ValueError('Cannot provide both code and device_flow_info.') 1792 1793 if code is None: 1794 code = device_flow_info.device_code 1795 elif isinstance(code, dict): 1796 if 'code' not in code: 1797 raise FlowExchangeError(code.get( 1798 'error', 'No code was supplied in the query parameters.')) 1799 code = code['code'] 1800 1801 post_data = { 1802 'client_id': self.client_id, 1803 'client_secret': self.client_secret, 1804 'code': code, 1805 'scope': self.scope, 1806 } 1807 if device_flow_info is not None: 1808 post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' 1809 else: 1810 post_data['grant_type'] = 'authorization_code' 1811 post_data['redirect_uri'] = self.redirect_uri 1812 body = urllib.urlencode(post_data) 1813 headers = { 1814 'content-type': 'application/x-www-form-urlencoded', 1815 } 1816 1817 if self.user_agent is not None: 1818 headers['user-agent'] = self.user_agent 1819 1820 if http is None: 1821 http = httplib2.Http() 1822 1823 resp, content = http.request(self.token_uri, method='POST', body=body, 1824 headers=headers) 1825 d = _parse_exchange_token_response(content) 1826 if resp.status == 200 and 'access_token' in d: 1827 access_token = d['access_token'] 1828 refresh_token = d.get('refresh_token', None) 1829 if not refresh_token: 1830 logger.info( 1831 'Received token response with no refresh_token. Consider ' 1832 "reauthenticating with approval_prompt='force'.") 1833 token_expiry = None 1834 if 'expires_in' in d: 1835 token_expiry = datetime.datetime.utcnow() + datetime.timedelta( 1836 seconds=int(d['expires_in'])) 1837 1838 if 'id_token' in d: 1839 d['id_token'] = _extract_id_token(d['id_token']) 1840 1841 logger.info('Successfully retrieved access token') 1842 return OAuth2Credentials(access_token, self.client_id, 1843 self.client_secret, refresh_token, token_expiry, 1844 self.token_uri, self.user_agent, 1845 revoke_uri=self.revoke_uri, 1846 id_token=d.get('id_token', None), 1847 token_response=d) 1848 else: 1849 logger.info('Failed to retrieve access token: %s', content) 1850 if 'error' in d: 1851 # you never know what those providers got to say 1852 error_msg = unicode(d['error']) 1853 else: 1854 error_msg = 'Invalid response: %s.' % str(resp.status) 1855 raise FlowExchangeError(error_msg)
1856
1857 1858 @util.positional(2) 1859 -def flow_from_clientsecrets(filename, scope, redirect_uri=None, 1860 message=None, cache=None, login_hint=None, 1861 device_uri=None):
1862 """Create a Flow from a clientsecrets file. 1863 1864 Will create the right kind of Flow based on the contents of the clientsecrets 1865 file or will raise InvalidClientSecretsError for unknown types of Flows. 1866 1867 Args: 1868 filename: string, File name of client secrets. 1869 scope: string or iterable of strings, scope(s) to request. 1870 redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for 1871 a non-web-based application, or a URI that handles the callback from 1872 the authorization server. 1873 message: string, A friendly string to display to the user if the 1874 clientsecrets file is missing or invalid. If message is provided then 1875 sys.exit will be called in the case of an error. If message in not 1876 provided then clientsecrets.InvalidClientSecretsError will be raised. 1877 cache: An optional cache service client that implements get() and set() 1878 methods. See clientsecrets.loadfile() for details. 1879 login_hint: string, Either an email address or domain. Passing this hint 1880 will either pre-fill the email box on the sign-in form or select the 1881 proper multi-login session, thereby simplifying the login flow. 1882 device_uri: string, URI for device authorization endpoint. For convenience 1883 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 1884 1885 Returns: 1886 A Flow object. 1887 1888 Raises: 1889 UnknownClientSecretsFlowError if the file describes an unknown kind of Flow. 1890 clientsecrets.InvalidClientSecretsError if the clientsecrets file is 1891 invalid. 1892 """ 1893 try: 1894 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 1895 if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): 1896 constructor_kwargs = { 1897 'redirect_uri': redirect_uri, 1898 'auth_uri': client_info['auth_uri'], 1899 'token_uri': client_info['token_uri'], 1900 'login_hint': login_hint, 1901 } 1902 revoke_uri = client_info.get('revoke_uri') 1903 if revoke_uri is not None: 1904 constructor_kwargs['revoke_uri'] = revoke_uri 1905 if device_uri is not None: 1906 constructor_kwargs['device_uri'] = device_uri 1907 return OAuth2WebServerFlow( 1908 client_info['client_id'], client_info['client_secret'], 1909 scope, **constructor_kwargs) 1910 1911 except clientsecrets.InvalidClientSecretsError: 1912 if message: 1913 sys.exit(message) 1914 else: 1915 raise 1916 else: 1917 raise UnknownClientSecretsFlowError( 1918 'This OAuth 2.0 flow is unsupported: %r' % client_type)
1919