| OLD | NEW | 
| (Empty) |  | 
 |    1 # Copyright 2010 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 OAuth2 client library. | 
 |   16  | 
 |   17 This library provides a client implementation of the OAuth2 protocol (see | 
 |   18 http://code.google.com/apis/accounts/docs/OAuth2.html). | 
 |   19  | 
 |   20 **** Experimental API **** | 
 |   21  | 
 |   22 This module is experimental and is subject to modification or removal without | 
 |   23 notice. | 
 |   24 """ | 
 |   25  | 
 |   26 # This implementation is inspired by the implementation in | 
 |   27 # http://code.google.com/p/google-api-python-client/source/browse/oauth2client/, | 
 |   28 # with the following main differences: | 
 |   29 # - This library uses the fancy_urllib monkey patch for urllib to correctly | 
 |   30 #   implement SSL certificate validation. | 
 |   31 # - This library does not assume that client code is using the httplib2 library | 
 |   32 #   to make HTTP requests. | 
 |   33 # - This library implements caching of access tokens independent of refresh | 
 |   34 #   tokens (in the python API client oauth2client, there is a single class that | 
 |   35 #   encapsulates both refresh and access tokens). | 
 |   36  | 
 |   37  | 
 |   38 import cgi | 
 |   39 import datetime | 
 |   40 import errno | 
 |   41 from hashlib import sha1 | 
 |   42 import logging | 
 |   43 import os | 
 |   44 import tempfile | 
 |   45 import threading | 
 |   46 import urllib | 
 |   47 import urllib2 | 
 |   48 import urlparse | 
 |   49  | 
 |   50 from boto import cacerts | 
 |   51 from third_party import fancy_urllib | 
 |   52  | 
 |   53 try: | 
 |   54   import json | 
 |   55 except ImportError: | 
 |   56   try: | 
 |   57     # Try to import from django, should work on App Engine | 
 |   58     from django.utils import simplejson as json | 
 |   59   except ImportError: | 
 |   60     # Try for simplejson | 
 |   61     import simplejson as json | 
 |   62  | 
 |   63 LOG = logging.getLogger('oauth2_client') | 
 |   64 # Lock used for checking/exchanging refresh token, so multithreaded | 
 |   65 # operation doesn't attempt concurrent refreshes. | 
 |   66 token_exchange_lock = threading.Lock() | 
 |   67  | 
 |   68 # SHA1 sum of the CA certificates file imported from boto. | 
 |   69 CACERTS_FILE_SHA1SUM = 'ed024a78d9327f8669b3b117d9eac9e3c9460e9b' | 
 |   70  | 
 |   71 class Error(Exception): | 
 |   72   """Base exception for the OAuth2 module.""" | 
 |   73   pass | 
 |   74  | 
 |   75  | 
 |   76 class AccessTokenRefreshError(Error): | 
 |   77   """Error trying to exchange a refresh token into an access token.""" | 
 |   78   pass | 
 |   79  | 
 |   80  | 
 |   81 class AuthorizationCodeExchangeError(Error): | 
 |   82   """Error trying to exchange an authorization code into a refresh token.""" | 
 |   83   pass | 
 |   84  | 
 |   85  | 
 |   86 class TokenCache(object): | 
 |   87   """Interface for OAuth2 token caches.""" | 
 |   88  | 
 |   89   def PutToken(self, key, value): | 
 |   90     raise NotImplementedError | 
 |   91  | 
 |   92   def GetToken(self, key): | 
 |   93     raise NotImplementedError | 
 |   94  | 
 |   95  | 
 |   96 class NoopTokenCache(TokenCache): | 
 |   97   """A stub implementation of TokenCache that does nothing.""" | 
 |   98  | 
 |   99   def PutToken(self, key, value): | 
 |  100     pass | 
 |  101  | 
 |  102   def GetToken(self, key): | 
 |  103     return None | 
 |  104  | 
 |  105  | 
 |  106 class InMemoryTokenCache(TokenCache): | 
 |  107   """An in-memory token cache. | 
 |  108  | 
 |  109   The cache is implemented by a python dict, and inherits the thread-safety | 
 |  110   properties of dict. | 
 |  111   """ | 
 |  112  | 
 |  113   def __init__(self): | 
 |  114     super(InMemoryTokenCache, self).__init__() | 
 |  115     self.cache = dict() | 
 |  116  | 
 |  117   def PutToken(self, key, value): | 
 |  118     LOG.info('InMemoryTokenCache.PutToken: key=%s', key) | 
 |  119     self.cache[key] = value | 
 |  120  | 
 |  121   def GetToken(self, key): | 
 |  122     value = self.cache.get(key, None) | 
 |  123     LOG.info('InMemoryTokenCache.GetToken: key=%s%s present', | 
 |  124              key, ' not' if value is None else '') | 
 |  125     return value | 
 |  126  | 
 |  127  | 
 |  128 class FileSystemTokenCache(TokenCache): | 
 |  129   """An implementation of a token cache that persists tokens on disk. | 
 |  130  | 
 |  131   Each token object in the cache is stored in serialized form in a separate | 
 |  132   file. The cache file's name can be configured via a path pattern that is | 
 |  133   parameterized by the key under which a value is cached and optionally the | 
 |  134   current processes uid as obtained by os.getuid(). | 
 |  135  | 
 |  136   Since file names are generally publicly visible in the system, it is important | 
 |  137   that the cache key does not leak information about the token's value.  If | 
 |  138   client code computes cache keys from token values, a cryptographically strong | 
 |  139   one-way function must be used. | 
 |  140   """ | 
 |  141  | 
 |  142   def __init__(self, path_pattern=None): | 
 |  143     """Creates a FileSystemTokenCache. | 
 |  144  | 
 |  145     Args: | 
 |  146       path_pattern: Optional string argument to specify the path pattern for | 
 |  147           cache files.  The argument should be a path with format placeholders | 
 |  148           '%(key)s' and optionally '%(uid)s'.  If the argument is omitted, the | 
 |  149           default pattern | 
 |  150             <tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s | 
 |  151           is used, where <tmpdir> is replaced with the system temp dir as | 
 |  152           obtained from tempfile.gettempdir(). | 
 |  153     """ | 
 |  154     super(FileSystemTokenCache, self).__init__() | 
 |  155     self.path_pattern = path_pattern | 
 |  156     if not path_pattern: | 
 |  157       self.path_pattern = os.path.join( | 
 |  158           tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s') | 
 |  159  | 
 |  160   def CacheFileName(self, key): | 
 |  161     uid = '_' | 
 |  162     try: | 
 |  163       # os.getuid() doesn't seem to work in Windows | 
 |  164       uid = str(os.getuid()) | 
 |  165     except: | 
 |  166       pass | 
 |  167     return self.path_pattern % {'key': key, 'uid': uid} | 
 |  168  | 
 |  169   def PutToken(self, key, value): | 
 |  170     """Serializes the value to the key's filename. | 
 |  171  | 
 |  172     To ensure that written tokens aren't leaked to a different users, we | 
 |  173      a) unlink an existing cache file, if any (to ensure we don't fall victim | 
 |  174         to symlink attacks and the like), | 
 |  175      b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to | 
 |  176         race us) | 
 |  177      If either of these steps fail, we simply give up (but log a warning). Not | 
 |  178      caching access tokens is not catastrophic, and failure to create a file | 
 |  179      can happen for either of the following reasons: | 
 |  180       - someone is attacking us as above, in which case we want to default to | 
 |  181         safe operation (not write the token); | 
 |  182       - another legitimate process is racing us; in this case one of the two | 
 |  183         will win and write the access token, which is fine; | 
 |  184       - we don't have permission to remove the old file or write to the | 
 |  185         specified directory, in which case we can't recover | 
 |  186  | 
 |  187     Args: | 
 |  188       key: the refresh_token hash key to store. | 
 |  189       value: the access_token value to serialize. | 
 |  190     """ | 
 |  191  | 
 |  192     cache_file = self.CacheFileName(key) | 
 |  193     LOG.info('FileSystemTokenCache.PutToken: key=%s, cache_file=%s', | 
 |  194              key, cache_file) | 
 |  195     try: | 
 |  196       os.unlink(cache_file) | 
 |  197     except: | 
 |  198       # Ignore failure to unlink the file; if the file exists and can't be | 
 |  199       # unlinked, the subsequent open with O_CREAT | O_EXCL will fail. | 
 |  200       pass | 
 |  201  | 
 |  202     flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | 
 |  203  | 
 |  204     # Accommodate Windows; stolen from python2.6/tempfile.py. | 
 |  205     if hasattr(os, 'O_NOINHERIT'): | 
 |  206       flags |= os.O_NOINHERIT | 
 |  207     if hasattr(os, 'O_BINARY'): | 
 |  208       flags |= os.O_BINARY | 
 |  209  | 
 |  210     try: | 
 |  211       fd = os.open(cache_file, flags, 0600) | 
 |  212     except (OSError, IOError), e: | 
 |  213       LOG.warning('FileSystemTokenCache.PutToken: ' | 
 |  214                   'Failed to create cache file %s: %s', cache_file, e) | 
 |  215       return | 
 |  216     f = os.fdopen(fd, 'w+b') | 
 |  217     f.write(value.Serialize()) | 
 |  218     f.close() | 
 |  219  | 
 |  220   def GetToken(self, key): | 
 |  221     """Returns a deserialized access token from the key's filename.""" | 
 |  222     value = None | 
 |  223     cache_file = self.CacheFileName(key) | 
 |  224     try: | 
 |  225       f = open(cache_file) | 
 |  226       value = AccessToken.UnSerialize(f.read()) | 
 |  227       f.close() | 
 |  228     except (IOError, OSError), e: | 
 |  229       if e.errno != errno.ENOENT: | 
 |  230         LOG.warning('FileSystemTokenCache.GetToken: ' | 
 |  231                     'Failed to read cache file %s: %s', cache_file, e) | 
 |  232     except Exception, e: | 
 |  233       LOG.warning('FileSystemTokenCache.GetToken: ' | 
 |  234                   'Failed to read cache file %s (possibly corrupted): %s', | 
 |  235                   cache_file, e) | 
 |  236  | 
 |  237     LOG.info('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)', | 
 |  238              key, ' not' if value is None else '', cache_file) | 
 |  239     return value | 
 |  240  | 
 |  241  | 
 |  242 class OAuth2Provider(object): | 
 |  243   """Encapsulates information about an OAuth2 provider.""" | 
 |  244  | 
 |  245   def __init__(self, label, authorization_uri, token_uri): | 
 |  246     """Creates an OAuth2Provider. | 
 |  247  | 
 |  248     Args: | 
 |  249       label: A string identifying this oauth2 provider, e.g. "Google". | 
 |  250       authorization_uri: The provider's authorization URI. | 
 |  251       token_uri: The provider's token endpoint URI. | 
 |  252     """ | 
 |  253     self.label = label | 
 |  254     self.authorization_uri = authorization_uri | 
 |  255     self.token_uri = token_uri | 
 |  256  | 
 |  257  | 
 |  258 class OAuth2Client(object): | 
 |  259   """An OAuth2 client.""" | 
 |  260  | 
 |  261   def __init__(self, provider, client_id, client_secret, | 
 |  262                url_opener=None, | 
 |  263                proxy=None, | 
 |  264                access_token_cache=None, | 
 |  265                datetime_strategy=datetime.datetime): | 
 |  266     """Creates an OAuth2Client. | 
 |  267  | 
 |  268     Args: | 
 |  269       provider: The OAuth2Provider provider this client will authenticate | 
 |  270           against. | 
 |  271       client_id: The OAuth2 client ID of this client. | 
 |  272       client_secret: The OAuth2 client secret of this client. | 
 |  273       url_opener: An optinal urllib2.OpenerDirector to use for making HTTP | 
 |  274           requests to the OAuth2 provider's token endpoint.  The provided | 
 |  275           url_opener *must* be configured to validate server SSL certificates | 
 |  276           for requests to https connections, and to correctly handle proxying of | 
 |  277           https requests.  If this argument is omitted or None, a suitable | 
 |  278           opener based on fancy_urllib is used. | 
 |  279       proxy: An optional string specifying a HTTP proxy to be used, in the form | 
 |  280           '<proxy>:<port>'.  This option is only effective if the url_opener has | 
 |  281           been configured with a fancy_urllib.FancyProxyHandler (this is the | 
 |  282           case for the default url_opener). | 
 |  283       access_token_cache: An optional instance of a TokenCache. If omitted or | 
 |  284           None, an InMemoryTokenCache is used. | 
 |  285       datetime_strategy: datetime module strategy to use. | 
 |  286     """ | 
 |  287     self.provider = provider | 
 |  288     self.client_id = client_id | 
 |  289     self.client_secret = client_secret | 
 |  290     # datetime_strategy is used to invoke utcnow() on; it is injected into the | 
 |  291     # constructor for unit testing purposes. | 
 |  292     self.datetime_strategy = datetime_strategy | 
 |  293     self._proxy = proxy | 
 |  294  | 
 |  295     self.access_token_cache = access_token_cache or InMemoryTokenCache() | 
 |  296  | 
 |  297     self.ca_certs_file = os.path.join( | 
 |  298         os.path.dirname(os.path.abspath(cacerts.__file__)), 'cacerts.txt') | 
 |  299  | 
 |  300     if url_opener is None: | 
 |  301       # Check that the cert file distributed with boto has not been tampered | 
 |  302       # with. | 
 |  303       h = sha1() | 
 |  304       h.update(file(self.ca_certs_file).read()) | 
 |  305       actual_sha1 = h.hexdigest() | 
 |  306       if actual_sha1 != CACERTS_FILE_SHA1SUM: | 
 |  307         raise Error( | 
 |  308             'CA certificates file does not have expected SHA1 sum; ' | 
 |  309             'expected: %s, actual: %s' % (CACERTS_FILE_SHA1SUM, actual_sha1)) | 
 |  310       # TODO(Google): set user agent? | 
 |  311       url_opener = urllib2.build_opener( | 
 |  312           fancy_urllib.FancyProxyHandler(), | 
 |  313           fancy_urllib.FancyRedirectHandler(), | 
 |  314           fancy_urllib.FancyHTTPSHandler()) | 
 |  315     self.url_opener = url_opener | 
 |  316  | 
 |  317   def _TokenRequest(self, request): | 
 |  318     """Make a requst to this client's provider's token endpoint. | 
 |  319  | 
 |  320     Args: | 
 |  321       request: A dict with the request parameteres. | 
 |  322     Returns: | 
 |  323       A tuple (response, error) where, | 
 |  324       - response is the parsed JSON response received from the token endpoint, | 
 |  325             or None if no parseable response was received, and | 
 |  326       - error is None if the request succeeded or | 
 |  327             an Exception if an error occurred. | 
 |  328     """ | 
 |  329  | 
 |  330     body = urllib.urlencode(request) | 
 |  331     LOG.debug('_TokenRequest request: %s', body) | 
 |  332     response = None | 
 |  333     try: | 
 |  334       request = fancy_urllib.FancyRequest( | 
 |  335           self.provider.token_uri, data=body) | 
 |  336       if self._proxy: | 
 |  337         request.set_proxy(self._proxy, 'http') | 
 |  338  | 
 |  339       request.set_ssl_info(ca_certs=self.ca_certs_file) | 
 |  340       result = self.url_opener.open(request) | 
 |  341       resp_body = result.read() | 
 |  342       LOG.debug('_TokenRequest response: %s', resp_body) | 
 |  343     except urllib2.HTTPError, e: | 
 |  344       try: | 
 |  345         response = json.loads(e.read()) | 
 |  346       except: | 
 |  347         pass | 
 |  348       return (response, e) | 
 |  349  | 
 |  350     try: | 
 |  351       response = json.loads(resp_body) | 
 |  352     except ValueError, e: | 
 |  353       return (None, e) | 
 |  354  | 
 |  355     return (response, None) | 
 |  356  | 
 |  357   def GetAccessToken(self, refresh_token): | 
 |  358     """Given a RefreshToken, obtains a corresponding access token. | 
 |  359  | 
 |  360     First, this client's access token cache is checked for an existing, | 
 |  361     not-yet-expired access token for the provided refresh token.  If none is | 
 |  362     found, the client obtains a fresh access token for the provided refresh | 
 |  363     token from the OAuth2 provider's token endpoint. | 
 |  364  | 
 |  365     Args: | 
 |  366       refresh_token: The RefreshToken object which to get an access token for. | 
 |  367     Returns: | 
 |  368       The cached or freshly obtained AccessToken. | 
 |  369     Raises: | 
 |  370       AccessTokenRefreshError if an error occurs. | 
 |  371     """ | 
 |  372     # Ensure only one thread at a time attempts to get (and possibly refresh) | 
 |  373     # the access token. This doesn't prevent concurrent refresh attempts across | 
 |  374     # multiple gsutil instances, but at least protects against multiple threads | 
 |  375     # simultaneously attempting to refresh when gsutil -m is used. | 
 |  376     token_exchange_lock.acquire() | 
 |  377     try: | 
 |  378       cache_key = refresh_token.CacheKey() | 
 |  379       LOG.info('GetAccessToken: checking cache for key %s', cache_key) | 
 |  380       access_token = self.access_token_cache.GetToken(cache_key) | 
 |  381       LOG.debug('GetAccessToken: token from cache: %s', access_token) | 
 |  382       if access_token is None or access_token.ShouldRefresh(): | 
 |  383         LOG.info('GetAccessToken: fetching fresh access token...') | 
 |  384         access_token = self.FetchAccessToken(refresh_token) | 
 |  385         LOG.debug('GetAccessToken: fresh access token: %s', access_token) | 
 |  386         self.access_token_cache.PutToken(cache_key, access_token) | 
 |  387       return access_token | 
 |  388     finally: | 
 |  389       token_exchange_lock.release() | 
 |  390  | 
 |  391   def FetchAccessToken(self, refresh_token): | 
 |  392     """Fetches an access token from the provider's token endpoint. | 
 |  393  | 
 |  394     Given a RefreshToken, fetches an access token from this client's OAuth2 | 
 |  395     provider's token endpoint. | 
 |  396  | 
 |  397     Args: | 
 |  398       refresh_token: The RefreshToken object which to get an access token for. | 
 |  399     Returns: | 
 |  400       The fetched AccessToken. | 
 |  401     Raises: | 
 |  402       AccessTokenRefreshError: if an error occurs. | 
 |  403     """ | 
 |  404     request = { | 
 |  405         'grant_type': 'refresh_token', | 
 |  406         'client_id': self.client_id, | 
 |  407         'client_secret': self.client_secret, | 
 |  408         'refresh_token': refresh_token.refresh_token, | 
 |  409         } | 
 |  410     LOG.debug('FetchAccessToken request: %s', request) | 
 |  411  | 
 |  412     response, error = self._TokenRequest(request) | 
 |  413     LOG.debug( | 
 |  414         'FetchAccessToken response (error = %s): %s', error, response) | 
 |  415  | 
 |  416     if error: | 
 |  417       oauth2_error = '' | 
 |  418       if response and response['error']: | 
 |  419         oauth2_error = '; OAuth2 error: %s' % response['error'] | 
 |  420       raise AccessTokenRefreshError( | 
 |  421           'Failed to exchange refresh token into access token; ' | 
 |  422           'request failed: %s%s' % (error, oauth2_error)) | 
 |  423  | 
 |  424     if 'access_token' not in response: | 
 |  425       raise AccessTokenRefreshError( | 
 |  426           'Failed to exchange refresh token into access token; response: %s' % | 
 |  427           response) | 
 |  428  | 
 |  429     token_expiry = None | 
 |  430     if 'expires_in' in response: | 
 |  431       token_expiry = ( | 
 |  432           self.datetime_strategy.utcnow() + | 
 |  433           datetime.timedelta(seconds=int(response['expires_in']))) | 
 |  434  | 
 |  435     return AccessToken(response['access_token'], token_expiry, | 
 |  436                        datetime_strategy=self.datetime_strategy) | 
 |  437  | 
 |  438   def GetAuthorizationUri(self, redirect_uri, scopes, extra_params=None): | 
 |  439     """Gets the OAuth2 authorization URI and the specified scope(s). | 
 |  440  | 
 |  441     Applications should navigate/redirect the user's user agent to this URI. The | 
 |  442     user will be shown an approval UI requesting the user to approve access of | 
 |  443     this client to the requested scopes under the identity of the authenticated | 
 |  444     end user. | 
 |  445  | 
 |  446     The application should expect the user agent to be redirected to the | 
 |  447     specified redirect_uri after the user's approval/disapproval. | 
 |  448  | 
 |  449     Installed applications may use the special redirect_uri | 
 |  450     'urn:ietf:wg:oauth:2.0:oob' to indicate that instead of redirecting the | 
 |  451     browser, the user be shown a confirmation page with a verification code. | 
 |  452     The application should query the user for this code. | 
 |  453  | 
 |  454     Args: | 
 |  455       redirect_uri: Either the string 'urn:ietf:wg:oauth:2.0:oob' for a | 
 |  456           non-web-based application, or a URI that handles the callback from the | 
 |  457           authorization server. | 
 |  458       scopes: A list of strings specifying the OAuth scopes the application | 
 |  459           requests access to. | 
 |  460       extra_params: Optional dictionary of additional parameters to be passed to | 
 |  461           the OAuth2 authorization URI. | 
 |  462     Returns: | 
 |  463       The authorization URI for the specified scopes as a string. | 
 |  464     """ | 
 |  465  | 
 |  466     request = { | 
 |  467         'response_type': 'code', | 
 |  468         'client_id': self.client_id, | 
 |  469         'redirect_uri': redirect_uri, | 
 |  470         'scope': ' '.join(scopes), | 
 |  471         } | 
 |  472  | 
 |  473     if extra_params: | 
 |  474       request.update(extra_params) | 
 |  475     url_parts = list(urlparse.urlparse(self.provider.authorization_uri)) | 
 |  476     # 4 is the index of the query part | 
 |  477     request.update(dict(cgi.parse_qsl(url_parts[4]))) | 
 |  478     url_parts[4] = urllib.urlencode(request) | 
 |  479     return urlparse.urlunparse(url_parts) | 
 |  480  | 
 |  481   def ExchangeAuthorizationCode(self, code, redirect_uri, scopes): | 
 |  482     """Exchanges an authorization code for a refresh token. | 
 |  483  | 
 |  484     Invokes this client's OAuth2 provider's token endpoint to exchange an | 
 |  485     authorization code into a refresh token. | 
 |  486  | 
 |  487     Args: | 
 |  488       code: the authrorization code. | 
 |  489       redirect_uri: Either the string 'urn:ietf:wg:oauth:2.0:oob' for a | 
 |  490           non-web-based application, or a URI that handles the callback from the | 
 |  491           authorization server. | 
 |  492       scopes: A list of strings specifying the OAuth scopes the application | 
 |  493           requests access to. | 
 |  494     Returns: | 
 |  495       A tuple consting of the resulting RefreshToken and AccessToken. | 
 |  496     Raises: | 
 |  497       AuthorizationCodeExchangeError: if an error occurs. | 
 |  498     """ | 
 |  499     request = { | 
 |  500         'grant_type': 'authorization_code', | 
 |  501         'client_id': self.client_id, | 
 |  502         'client_secret': self.client_secret, | 
 |  503         'code': code, | 
 |  504         'redirect_uri': redirect_uri, | 
 |  505         'scope': ' '.join(scopes), | 
 |  506         } | 
 |  507     LOG.debug('ExchangeAuthorizationCode request: %s', request) | 
 |  508  | 
 |  509     response, error = self._TokenRequest(request) | 
 |  510     LOG.debug( | 
 |  511         'ExchangeAuthorizationCode response (error = %s): %s', | 
 |  512         error, response) | 
 |  513  | 
 |  514     if error: | 
 |  515       oauth2_error = '' | 
 |  516       if response and response['error']: | 
 |  517         oauth2_error = '; OAuth2 error: %s' % response['error'] | 
 |  518       raise AuthorizationCodeExchangeError( | 
 |  519           'Failed to exchange refresh token into access token; ' | 
 |  520           'request failed: %s%s' % (str(error), oauth2_error)) | 
 |  521  | 
 |  522     if not 'access_token' in response: | 
 |  523       raise AuthorizationCodeExchangeError( | 
 |  524           'Failed to exchange authorization code into access token; ' | 
 |  525           'response: %s' % response) | 
 |  526  | 
 |  527     token_expiry = None | 
 |  528     if 'expires_in' in response: | 
 |  529       token_expiry = ( | 
 |  530           self.datetime_strategy.utcnow() + | 
 |  531           datetime.timedelta(seconds=int(response['expires_in']))) | 
 |  532  | 
 |  533     access_token = AccessToken(response['access_token'], token_expiry, | 
 |  534                                datetime_strategy=self.datetime_strategy) | 
 |  535  | 
 |  536     refresh_token = None | 
 |  537     refresh_token_string = response.get('refresh_token', None) | 
 |  538  | 
 |  539     token_exchange_lock.acquire() | 
 |  540     try: | 
 |  541       if refresh_token_string: | 
 |  542         refresh_token = RefreshToken(self, refresh_token_string) | 
 |  543         self.access_token_cache.PutToken(refresh_token.CacheKey(), access_token) | 
 |  544     finally: | 
 |  545       token_exchange_lock.release() | 
 |  546  | 
 |  547     return (refresh_token, access_token) | 
 |  548  | 
 |  549  | 
 |  550 class AccessToken(object): | 
 |  551   """Encapsulates an OAuth2 access token.""" | 
 |  552  | 
 |  553   def __init__(self, token, expiry, datetime_strategy=datetime.datetime): | 
 |  554     self.token = token | 
 |  555     self.expiry = expiry | 
 |  556     self.datetime_strategy = datetime_strategy | 
 |  557  | 
 |  558   @staticmethod | 
 |  559   def UnSerialize(query): | 
 |  560     """Creates an AccessToken object from its serialized form.""" | 
 |  561  | 
 |  562     def GetValue(d, key): | 
 |  563       return (d.get(key, [None]))[0] | 
 |  564     kv = cgi.parse_qs(query) | 
 |  565     if not kv['token']: | 
 |  566       return None | 
 |  567     expiry = None | 
 |  568     expiry_tuple = GetValue(kv, 'expiry') | 
 |  569     if expiry_tuple: | 
 |  570       try: | 
 |  571         expiry = datetime.datetime( | 
 |  572             *[int(n) for n in expiry_tuple.split(',')]) | 
 |  573       except: | 
 |  574         return None | 
 |  575     return AccessToken(GetValue(kv, 'token'), expiry) | 
 |  576  | 
 |  577   def Serialize(self): | 
 |  578     """Serializes this object as URI-encoded key-value pairs.""" | 
 |  579     # There's got to be a better way to serialize a datetime. Unfortunately, | 
 |  580     # there is no reliable way to convert into a unix epoch. | 
 |  581     kv = {'token': self.token} | 
 |  582     if self.expiry: | 
 |  583       t = self.expiry | 
 |  584       tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond) | 
 |  585       kv['expiry'] = ','.join([str(i) for i in tupl]) | 
 |  586     return urllib.urlencode(kv) | 
 |  587  | 
 |  588   def ShouldRefresh(self, time_delta=300): | 
 |  589     """Whether the access token needs to be refreshed. | 
 |  590  | 
 |  591     Args: | 
 |  592       time_delta: refresh access token when it expires within time_delta secs. | 
 |  593  | 
 |  594     Returns: | 
 |  595       True if the token is expired or about to expire, False if the | 
 |  596       token should be expected to work.  Note that the token may still | 
 |  597       be rejected, e.g. if it has been revoked server-side. | 
 |  598     """ | 
 |  599     if self.expiry is None: | 
 |  600       return False | 
 |  601     return (self.datetime_strategy.utcnow() | 
 |  602             + datetime.timedelta(seconds=time_delta) > self.expiry) | 
 |  603  | 
 |  604   def __eq__(self, other): | 
 |  605     return self.token == other.token and self.expiry == other.expiry | 
 |  606  | 
 |  607   def __ne__(self, other): | 
 |  608     return not self.__eq__(other) | 
 |  609  | 
 |  610   def __str__(self): | 
 |  611     return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry) | 
 |  612  | 
 |  613  | 
 |  614 class RefreshToken(object): | 
 |  615   """Encapsulates an OAuth2 refresh token.""" | 
 |  616  | 
 |  617   def __init__(self, oauth2_client, refresh_token): | 
 |  618     self.oauth2_client = oauth2_client | 
 |  619     self.refresh_token = refresh_token | 
 |  620  | 
 |  621   def CacheKey(self): | 
 |  622     """Computes a cache key for this refresh token. | 
 |  623  | 
 |  624     The cache key is computed as the SHA1 hash of the token, and as such | 
 |  625     satisfies the FileSystemTokenCache requirement that cache keys do not leak | 
 |  626     information about token values. | 
 |  627  | 
 |  628     Returns: | 
 |  629       A hash key for this refresh token. | 
 |  630     """ | 
 |  631     h = sha1() | 
 |  632     h.update(self.refresh_token) | 
 |  633     return h.hexdigest() | 
 |  634  | 
 |  635   def GetAuthorizationHeader(self): | 
 |  636     """Gets the access token HTTP authorication header value. | 
 |  637  | 
 |  638     Returns: | 
 |  639       The value of an Authorization HTTP header that authenticates | 
 |  640       requests with an OAuth2 access token based on this refresh token. | 
 |  641     """ | 
 |  642     return 'Bearer %s' % self.oauth2_client.GetAccessToken(self).token | 
| OLD | NEW |