Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(97)

Side by Side Diff: third_party/gsutil/oauth2_plugin/oauth2_client.py

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

Powered by Google App Engine
This is Rietveld 408576698