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

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

Issue 10199002: Upgrade gsutil to 3.4 (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Addressed comments Created 8 years, 7 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 | Annotate | Revision Log
OLDNEW
(Empty)
1 # Copyright 2010 Google Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """An 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 urllib
46 import urllib2
47 import urlparse
48
49 from boto import cacerts
50 from third_party import fancy_urllib
51
52 try:
53 import json
54 except ImportError:
55 try:
56 # Try to import from django, should work on App Engine
57 from django.utils import simplejson as json
58 except ImportError:
59 # Try for simplejson
60 import simplejson as json
61
62 LOG = logging.getLogger('oauth2_client')
63
64 # SHA1 sum of the CA certificates file imported from boto.
65 CACERTS_FILE_SHA1SUM = 'ed024a78d9327f8669b3b117d9eac9e3c9460e9b'
66
67 class Error(Exception):
68 """Base exception for the OAuth2 module."""
69 pass
70
71
72 class AccessTokenRefreshError(Error):
73 """Error trying to exchange a refresh token into an access token."""
74 pass
75
76
77 class AuthorizationCodeExchangeError(Error):
78 """Error trying to exchange an authorization code into a refresh token."""
79 pass
80
81
82 class TokenCache(object):
83 """Interface for OAuth2 token caches."""
84
85 def PutToken(self, key, value):
86 raise NotImplementedError
87
88 def GetToken(self, key):
89 raise NotImplementedError
90
91
92 class NoopTokenCache(TokenCache):
93 """A stub implementation of TokenCache that does nothing."""
94
95 def PutToken(self, key, value):
96 pass
97
98 def GetToken(self, key):
99 return None
100
101
102 class InMemoryTokenCache(TokenCache):
103 """An in-memory token cache.
104
105 The cache is implemented by a python dict, and inherits the thread-safety
106 properties of dict.
107 """
108
109 def __init__(self):
110 super(InMemoryTokenCache, self).__init__()
111 self.cache = dict()
112
113 def PutToken(self, key, value):
114 LOG.info('InMemoryTokenCache.PutToken: key=%s', key)
115 self.cache[key] = value
116
117 def GetToken(self, key):
118 value = self.cache.get(key, None)
119 LOG.info('InMemoryTokenCache.GetToken: key=%s%s present',
120 key, ' not' if value is None else '')
121 return value
122
123
124 class FileSystemTokenCache(TokenCache):
125 """An implementation of a token cache that persists tokens on disk.
126
127 Each token object in the cache is stored in serialized form in a separate
128 file. The cache file's name can be configured via a path pattern that is
129 parameterized by the key under which a value is cached and optionally the
130 current processes uid as obtained by os.getuid().
131
132 Since file names are generally publicly visible in the system, it is important
133 that the cache key does not leak information about the token's value. If
134 client code computes cache keys from token values, a cryptographically strong
135 one-way function must be used.
136 """
137
138 def __init__(self, path_pattern=None):
139 """Creates a FileSystemTokenCache.
140
141 Args:
142 path_pattern: Optional string argument to specify the path pattern for
143 cache files. The argument should be a path with format placeholders
144 '%(key)s' and optionally '%(uid)s'. If the argument is omitted, the
145 default pattern
146 <tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s
147 is used, where <tmpdir> is replaced with the system temp dir as
148 obtained from tempfile.gettempdir().
149 """
150 super(FileSystemTokenCache, self).__init__()
151 self.path_pattern = path_pattern
152 if not path_pattern:
153 self.path_pattern = os.path.join(
154 tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s')
155
156 def CacheFileName(self, key):
157 uid = '_'
158 try:
159 # os.getuid() doesn't seem to work in Windows
160 uid = str(os.getuid())
161 except:
162 pass
163 return self.path_pattern % {'key': key, 'uid': uid}
164
165 def PutToken(self, key, value):
166 """Serializes the value to the key's filename.
167
168 To ensure that written tokens aren't leaked to a different users, we
169 a) unlink an existing cache file, if any (to ensure we don't fall victim
170 to symlink attacks and the like),
171 b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to
172 race us)
173 If either of these steps fail, we simply give up (but log a warning). Not
174 caching access tokens is not catastrophic, and failure to create a file
175 can happen for either of the following reasons:
176 - someone is attacking us as above, in which case we want to default to
177 safe operation (not write the token);
178 - another legitimate process is racing us; in this case one of the two
179 will win and write the access token, which is fine;
180 - we don't have permission to remove the old file or write to the
181 specified directory, in which case we can't recover
182
183 Args:
184 key: the refresh_token hash key to store.
185 value: the access_token value to serialize.
186 """
187
188 cache_file = self.CacheFileName(key)
189 LOG.info('FileSystemTokenCache.PutToken: key=%s, cache_file=%s',
190 key, cache_file)
191 try:
192 os.unlink(cache_file)
193 except:
194 # Ignore failure to unlink the file; if the file exists and can't be
195 # unlinked, the subsequent open with O_CREAT | O_EXCL will fail.
196 pass
197
198 flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
199
200 # Accommodate Windows; stolen from python2.6/tempfile.py.
201 if hasattr(os, 'O_NOINHERIT'):
202 flags |= os.O_NOINHERIT
203 if hasattr(os, 'O_BINARY'):
204 flags |= os.O_BINARY
205
206 try:
207 fd = os.open(cache_file, flags, 0600)
208 except (OSError, IOError), e:
209 LOG.warning('FileSystemTokenCache.PutToken: '
210 'Failed to create cache file %s: %s', cache_file, e)
211 return
212 f = os.fdopen(fd, 'w+b')
213 f.write(value.Serialize())
214 f.close()
215
216 def GetToken(self, key):
217 """Returns a deserialized access token from the key's filename."""
218 value = None
219 cache_file = self.CacheFileName(key)
220 try:
221 f = open(cache_file)
222 value = AccessToken.UnSerialize(f.read())
223 f.close()
224 except (IOError, OSError), e:
225 if e.errno != errno.ENOENT:
226 LOG.warning('FileSystemTokenCache.GetToken: '
227 'Failed to read cache file %s: %s', cache_file, e)
228 except Exception, e:
229 LOG.warning('FileSystemTokenCache.GetToken: '
230 'Failed to read cache file %s (possibly corrupted): %s',
231 cache_file, e)
232
233 LOG.info('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)',
234 key, ' not' if value is None else '', cache_file)
235 return value
236
237
238 class OAuth2Provider(object):
239 """Encapsulates information about an OAuth2 provider."""
240
241 def __init__(self, label, authorization_uri, token_uri):
242 """Creates an OAuth2Provider.
243
244 Args:
245 label: A string identifying this oauth2 provider, e.g. "Google".
246 authorization_uri: The provider's authorization URI.
247 token_uri: The provider's token endpoint URI.
248 """
249 self.label = label
250 self.authorization_uri = authorization_uri
251 self.token_uri = token_uri
252
253
254 class OAuth2Client(object):
255 """An OAuth2 client."""
256
257 def __init__(self, provider, client_id, client_secret,
258 url_opener=None,
259 proxy=None,
260 access_token_cache=None,
261 datetime_strategy=datetime.datetime):
262 """Creates an OAuth2Client.
263
264 Args:
265 provider: The OAuth2Provider provider this client will authenticate
266 against.
267 client_id: The OAuth2 client ID of this client.
268 client_secret: The OAuth2 client secret of this client.
269 url_opener: An optinal urllib2.OpenerDirector to use for making HTTP
270 requests to the OAuth2 provider's token endpoint. The provided
271 url_opener *must* be configured to validate server SSL certificates
272 for requests to https connections, and to correctly handle proxying of
273 https requests. If this argument is omitted or None, a suitable
274 opener based on fancy_urllib is used.
275 proxy: An optional string specifying a HTTP proxy to be used, in the form
276 '<proxy>:<port>'. This option is only effective if the url_opener has
277 been configured with a fancy_urllib.FancyProxyHandler (this is the
278 case for the default url_opener).
279 access_token_cache: An optional instance of a TokenCache. If omitted or
280 None, an InMemoryTokenCache is used.
281 datetime_strategy: datetime module strategy to use.
282 """
283 self.provider = provider
284 self.client_id = client_id
285 self.client_secret = client_secret
286 # datetime_strategy is used to invoke utcnow() on; it is injected into the
287 # constructor for unit testing purposes.
288 self.datetime_strategy = datetime_strategy
289 self._proxy = proxy
290
291 self.access_token_cache = access_token_cache or InMemoryTokenCache()
292
293 self.ca_certs_file = os.path.join(
294 os.path.dirname(os.path.abspath(cacerts.__file__)), 'cacerts.txt')
295
296 if url_opener is None:
297 # Check that the cert file distributed with boto has not been tampered
298 # with.
299 h = sha1()
300 h.update(file(self.ca_certs_file).read())
301 actual_sha1 = h.hexdigest()
302 if actual_sha1 != CACERTS_FILE_SHA1SUM:
303 raise Error(
304 'CA certificates file does not have expected SHA1 sum; '
305 'expected: %s, actual: %s' % (CACERTS_FILE_SHA1SUM, actual_sha1))
306 # TODO(Google): set user agent?
307 url_opener = urllib2.build_opener(
308 fancy_urllib.FancyProxyHandler(),
309 fancy_urllib.FancyRedirectHandler(),
310 fancy_urllib.FancyHTTPSHandler())
311 self.url_opener = url_opener
312
313 def _TokenRequest(self, request):
314 """Make a requst to this client's provider's token endpoint.
315
316 Args:
317 request: A dict with the request parameteres.
318 Returns:
319 A tuple (response, error) where,
320 - response is the parsed JSON response received from the token endpoint,
321 or None if no parseable response was received, and
322 - error is None if the request succeeded or
323 an Exception if an error occurred.
324 """
325
326 body = urllib.urlencode(request)
327 LOG.debug('_TokenRequest request: %s', body)
328 response = None
329 try:
330 request = fancy_urllib.FancyRequest(
331 self.provider.token_uri, data=body)
332 if self._proxy:
333 request.set_proxy(self._proxy, 'http')
334
335 request.set_ssl_info(ca_certs=self.ca_certs_file)
336 result = self.url_opener.open(request)
337 resp_body = result.read()
338 LOG.debug('_TokenRequest response: %s', resp_body)
339 except urllib2.HTTPError, e:
340 try:
341 response = json.loads(e.read())
342 except:
343 pass
344 return (response, e)
345
346 try:
347 response = json.loads(resp_body)
348 except ValueError, e:
349 return (None, e)
350
351 return (response, None)
352
353 def GetAccessToken(self, refresh_token):
354 """Given a RefreshToken, obtains a corresponding access token.
355
356 First, this client's access token cache is checked for an existing,
357 not-yet-expired access token for the provided refresh token. If none is
358 found, the client obtains a fresh access token for the provided refresh
359 token from the OAuth2 provider's token endpoint.
360
361 Args:
362 refresh_token: The RefreshToken object which to get an access token for.
363 Returns:
364 The cached or freshly obtained AccessToken.
365 Raises:
366 AccessTokenRefreshError if an error occurs.
367 """
368 cache_key = refresh_token.CacheKey()
369 LOG.info('GetAccessToken: checking cache for key %s', cache_key)
370 access_token = self.access_token_cache.GetToken(cache_key)
371 LOG.debug('GetAccessToken: token from cache: %s', access_token)
372 if access_token is None or access_token.ShouldRefresh():
373 LOG.info('GetAccessToken: fetching fresh access token...')
374 access_token = self.FetchAccessToken(refresh_token)
375 LOG.debug('GetAccessToken: fresh access token: %s', access_token)
376 self.access_token_cache.PutToken(cache_key, access_token)
377 return access_token
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', 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 if refresh_token_string:
528 refresh_token = RefreshToken(self, refresh_token_string)
529 self.access_token_cache.PutToken(refresh_token.CacheKey(), access_token)
530
531 return (refresh_token, access_token)
532
533
534 class AccessToken(object):
535 """Encapsulates an OAuth2 access token."""
536
537 def __init__(self, token, expiry, datetime_strategy=datetime.datetime):
538 self.token = token
539 self.expiry = expiry
540 self.datetime_strategy = datetime_strategy
541
542 @staticmethod
543 def UnSerialize(query):
544 """Creates an AccessToken object from its serialized form."""
545
546 def GetValue(d, key):
547 return (d.get(key, [None]))[0]
548 kv = cgi.parse_qs(query)
549 if not kv['token']:
550 return None
551 expiry = None
552 expiry_tuple = GetValue(kv, 'expiry')
553 if expiry_tuple:
554 try:
555 expiry = datetime.datetime(
556 *[int(n) for n in expiry_tuple.split(',')])
557 except:
558 return None
559 return AccessToken(GetValue(kv, 'token'), expiry)
560
561 def Serialize(self):
562 """Serializes this object as URI-encoded key-value pairs."""
563 # There's got to be a better way to serialize a datetime. Unfortunately,
564 # there is no reliable way to convert into a unix epoch.
565 kv = {'token': self.token}
566 if self.expiry:
567 t = self.expiry
568 tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond)
569 kv['expiry'] = ','.join([str(i) for i in tupl])
570 return urllib.urlencode(kv)
571
572 def ShouldRefresh(self, time_delta=300):
573 """Whether the access token needs to be refreshed.
574
575 Args:
576 time_delta: refresh access token when it expires within time_delta secs.
577
578 Returns:
579 True if the token is expired or about to expire, False if the
580 token should be expected to work. Note that the token may still
581 be rejected, e.g. if it has been revoked server-side.
582 """
583 if self.expiry is None:
584 return False
585 return (self.datetime_strategy.utcnow()
586 + datetime.timedelta(seconds=time_delta) > self.expiry)
587
588 def __eq__(self, other):
589 return self.token == other.token and self.expiry == other.expiry
590
591 def __ne__(self, other):
592 return not self.__eq__(other)
593
594 def __str__(self):
595 return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry)
596
597
598 class RefreshToken(object):
599 """Encapsulates an OAuth2 refresh token."""
600
601 def __init__(self, oauth2_client, refresh_token):
602 self.oauth2_client = oauth2_client
603 self.refresh_token = refresh_token
604
605 def CacheKey(self):
606 """Computes a cache key for this refresh token.
607
608 The cache key is computed as the SHA1 hash of the token, and as such
609 satisfies the FileSystemTokenCache requirement that cache keys do not leak
610 information about token values.
611
612 Returns:
613 A hash key for this refresh token.
614 """
615 h = sha1()
616 h.update(self.refresh_token)
617 return h.hexdigest()
618
619 def GetAuthorizationHeader(self):
620 """Gets the access token HTTP authorication header value.
621
622 Returns:
623 The value of an Authorization HTTP header that authenticates
624 requests with an OAuth2 access token based on this refresh token.
625 """
626 return 'OAuth %s' % self.oauth2_client.GetAccessToken(self).token
OLDNEW
« no previous file with comments | « third_party/gsutil/20110627/oauth2_plugin/__init__.py ('k') | third_party/gsutil/20110627/oauth2_plugin/oauth2_client_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698