| OLD | NEW |
| 1 # Copyright 2010 Google Inc. | 1 # Copyright 2010 Google Inc. |
| 2 # | 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with 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 | 5 # You may obtain a copy of the License at |
| 6 # | 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| (...skipping 24 matching lines...) Expand all Loading... |
| 35 # encapsulates both refresh and access tokens). | 35 # encapsulates both refresh and access tokens). |
| 36 | 36 |
| 37 | 37 |
| 38 import cgi | 38 import cgi |
| 39 import datetime | 39 import datetime |
| 40 import errno | 40 import errno |
| 41 from hashlib import sha1 | 41 from hashlib import sha1 |
| 42 import logging | 42 import logging |
| 43 import os | 43 import os |
| 44 import tempfile | 44 import tempfile |
| 45 import threading |
| 45 import urllib | 46 import urllib |
| 46 import urllib2 | 47 import urllib2 |
| 47 import urlparse | 48 import urlparse |
| 48 | 49 |
| 49 from boto import cacerts | 50 from boto import cacerts |
| 50 from third_party import fancy_urllib | 51 from third_party import fancy_urllib |
| 51 | 52 |
| 52 try: | 53 try: |
| 53 import json | 54 import json |
| 54 except ImportError: | 55 except ImportError: |
| 55 try: | 56 try: |
| 56 # Try to import from django, should work on App Engine | 57 # Try to import from django, should work on App Engine |
| 57 from django.utils import simplejson as json | 58 from django.utils import simplejson as json |
| 58 except ImportError: | 59 except ImportError: |
| 59 # Try for simplejson | 60 # Try for simplejson |
| 60 import simplejson as json | 61 import simplejson as json |
| 61 | 62 |
| 62 LOG = logging.getLogger('oauth2_client') | 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() |
| 63 | 67 |
| 64 # SHA1 sum of the CA certificates file imported from boto. | 68 # SHA1 sum of the CA certificates file imported from boto. |
| 65 CACERTS_FILE_SHA1SUM = 'ed024a78d9327f8669b3b117d9eac9e3c9460e9b' | 69 CACERTS_FILE_SHA1SUM = 'ed024a78d9327f8669b3b117d9eac9e3c9460e9b' |
| 66 | 70 |
| 67 class Error(Exception): | 71 class Error(Exception): |
| 68 """Base exception for the OAuth2 module.""" | 72 """Base exception for the OAuth2 module.""" |
| 69 pass | 73 pass |
| 70 | 74 |
| 71 | 75 |
| 72 class AccessTokenRefreshError(Error): | 76 class AccessTokenRefreshError(Error): |
| (...skipping 285 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 358 found, the client obtains a fresh access token for the provided refresh | 362 found, the client obtains a fresh access token for the provided refresh |
| 359 token from the OAuth2 provider's token endpoint. | 363 token from the OAuth2 provider's token endpoint. |
| 360 | 364 |
| 361 Args: | 365 Args: |
| 362 refresh_token: The RefreshToken object which to get an access token for. | 366 refresh_token: The RefreshToken object which to get an access token for. |
| 363 Returns: | 367 Returns: |
| 364 The cached or freshly obtained AccessToken. | 368 The cached or freshly obtained AccessToken. |
| 365 Raises: | 369 Raises: |
| 366 AccessTokenRefreshError if an error occurs. | 370 AccessTokenRefreshError if an error occurs. |
| 367 """ | 371 """ |
| 368 cache_key = refresh_token.CacheKey() | 372 # Ensure only one thread at a time attempts to get (and possibly refresh) |
| 369 LOG.info('GetAccessToken: checking cache for key %s', cache_key) | 373 # the access token. This doesn't prevent concurrent refresh attempts across |
| 370 access_token = self.access_token_cache.GetToken(cache_key) | 374 # multiple gsutil instances, but at least protects against multiple threads |
| 371 LOG.debug('GetAccessToken: token from cache: %s', access_token) | 375 # simultaneously attempting to refresh when gsutil -m is used. |
| 372 if access_token is None or access_token.ShouldRefresh(): | 376 token_exchange_lock.acquire() |
| 373 LOG.info('GetAccessToken: fetching fresh access token...') | 377 try: |
| 374 access_token = self.FetchAccessToken(refresh_token) | 378 cache_key = refresh_token.CacheKey() |
| 375 LOG.debug('GetAccessToken: fresh access token: %s', access_token) | 379 LOG.info('GetAccessToken: checking cache for key %s', cache_key) |
| 376 self.access_token_cache.PutToken(cache_key, access_token) | 380 access_token = self.access_token_cache.GetToken(cache_key) |
| 377 return access_token | 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() |
| 378 | 390 |
| 379 def FetchAccessToken(self, refresh_token): | 391 def FetchAccessToken(self, refresh_token): |
| 380 """Fetches an access token from the provider's token endpoint. | 392 """Fetches an access token from the provider's token endpoint. |
| 381 | 393 |
| 382 Given a RefreshToken, fetches an access token from this client's OAuth2 | 394 Given a RefreshToken, fetches an access token from this client's OAuth2 |
| 383 provider's token endpoint. | 395 provider's token endpoint. |
| 384 | 396 |
| 385 Args: | 397 Args: |
| 386 refresh_token: The RefreshToken object which to get an access token for. | 398 refresh_token: The RefreshToken object which to get an access token for. |
| 387 Returns: | 399 Returns: |
| 388 The fetched AccessToken. | 400 The fetched AccessToken. |
| 389 Raises: | 401 Raises: |
| 390 AccessTokenRefreshError: if an error occurs. | 402 AccessTokenRefreshError: if an error occurs. |
| 391 """ | 403 """ |
| 392 request = { | 404 request = { |
| 393 'grant_type': 'refresh_token', | 405 'grant_type': 'refresh_token', |
| 394 'client_id': self.client_id, | 406 'client_id': self.client_id, |
| 395 'client_secret': self.client_secret, | 407 'client_secret': self.client_secret, |
| 396 'refresh_token': refresh_token.refresh_token, | 408 'refresh_token': refresh_token.refresh_token, |
| 397 } | 409 } |
| 398 LOG.debug('FetchAccessToken request: %s', request) | 410 LOG.debug('FetchAccessToken request: %s', request) |
| 399 | 411 |
| 400 response, error = self._TokenRequest(request) | 412 response, error = self._TokenRequest(request) |
| 401 LOG.debug( | 413 LOG.debug( |
| 402 'FetchAccessToken response (error = %s): %s', error, response) | 414 'FetchAccessToken response (error = %s): %s', error, response) |
| 403 | 415 |
| 404 if error: | 416 if error: |
| 405 oauth2_error = '' | 417 oauth2_error = '' |
| 406 if response and response['error']: | 418 if response and response['error']: |
| 407 oauth2_error = '; OAuth2 error: %s', response['error'] | 419 oauth2_error = '; OAuth2 error: %s' % response['error'] |
| 408 raise AccessTokenRefreshError( | 420 raise AccessTokenRefreshError( |
| 409 'Failed to exchange refresh token into access token; ' | 421 'Failed to exchange refresh token into access token; ' |
| 410 'request failed: %s%s', error, oauth2_error) | 422 'request failed: %s%s' % (error, oauth2_error)) |
| 411 | 423 |
| 412 if 'access_token' not in response: | 424 if 'access_token' not in response: |
| 413 raise AccessTokenRefreshError( | 425 raise AccessTokenRefreshError( |
| 414 'Failed to exchange refresh token into access token; response: %s', | 426 'Failed to exchange refresh token into access token; response: %s' % |
| 415 response) | 427 response) |
| 416 | 428 |
| 417 token_expiry = None | 429 token_expiry = None |
| 418 if 'expires_in' in response: | 430 if 'expires_in' in response: |
| 419 token_expiry = ( | 431 token_expiry = ( |
| 420 self.datetime_strategy.utcnow() + | 432 self.datetime_strategy.utcnow() + |
| 421 datetime.timedelta(seconds=int(response['expires_in']))) | 433 datetime.timedelta(seconds=int(response['expires_in']))) |
| 422 | 434 |
| 423 return AccessToken(response['access_token'], token_expiry, | 435 return AccessToken(response['access_token'], token_expiry, |
| 424 datetime_strategy=self.datetime_strategy) | 436 datetime_strategy=self.datetime_strategy) |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 495 LOG.debug('ExchangeAuthorizationCode request: %s', request) | 507 LOG.debug('ExchangeAuthorizationCode request: %s', request) |
| 496 | 508 |
| 497 response, error = self._TokenRequest(request) | 509 response, error = self._TokenRequest(request) |
| 498 LOG.debug( | 510 LOG.debug( |
| 499 'ExchangeAuthorizationCode response (error = %s): %s', | 511 'ExchangeAuthorizationCode response (error = %s): %s', |
| 500 error, response) | 512 error, response) |
| 501 | 513 |
| 502 if error: | 514 if error: |
| 503 oauth2_error = '' | 515 oauth2_error = '' |
| 504 if response and response['error']: | 516 if response and response['error']: |
| 505 oauth2_error = '; OAuth2 error: %s', response['error'] | 517 oauth2_error = '; OAuth2 error: %s' % response['error'] |
| 506 raise AuthorizationCodeExchangeError( | 518 raise AuthorizationCodeExchangeError( |
| 507 'Failed to exchange refresh token into access token; ' | 519 'Failed to exchange refresh token into access token; ' |
| 508 'request failed: %s%s', error, oauth2_error) | 520 'request failed: %s%s' % (str(error), oauth2_error)) |
| 509 | 521 |
| 510 if not 'access_token' in response: | 522 if not 'access_token' in response: |
| 511 raise AuthorizationCodeExchangeError( | 523 raise AuthorizationCodeExchangeError( |
| 512 'Failed to exchange authorization code into access token; ' | 524 'Failed to exchange authorization code into access token; ' |
| 513 'response: %s', response) | 525 'response: %s' % response) |
| 514 | 526 |
| 515 token_expiry = None | 527 token_expiry = None |
| 516 if 'expires_in' in response: | 528 if 'expires_in' in response: |
| 517 token_expiry = ( | 529 token_expiry = ( |
| 518 self.datetime_strategy.utcnow() + | 530 self.datetime_strategy.utcnow() + |
| 519 datetime.timedelta(seconds=int(response['expires_in']))) | 531 datetime.timedelta(seconds=int(response['expires_in']))) |
| 520 | 532 |
| 521 access_token = AccessToken(response['access_token'], token_expiry, | 533 access_token = AccessToken(response['access_token'], token_expiry, |
| 522 datetime_strategy=self.datetime_strategy) | 534 datetime_strategy=self.datetime_strategy) |
| 523 | 535 |
| (...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 616 h.update(self.refresh_token) | 628 h.update(self.refresh_token) |
| 617 return h.hexdigest() | 629 return h.hexdigest() |
| 618 | 630 |
| 619 def GetAuthorizationHeader(self): | 631 def GetAuthorizationHeader(self): |
| 620 """Gets the access token HTTP authorication header value. | 632 """Gets the access token HTTP authorication header value. |
| 621 | 633 |
| 622 Returns: | 634 Returns: |
| 623 The value of an Authorization HTTP header that authenticates | 635 The value of an Authorization HTTP header that authenticates |
| 624 requests with an OAuth2 access token based on this refresh token. | 636 requests with an OAuth2 access token based on this refresh token. |
| 625 """ | 637 """ |
| 626 return 'OAuth %s' % self.oauth2_client.GetAccessToken(self).token | 638 return 'Bearer %s' % self.oauth2_client.GetAccessToken(self).token |
| OLD | NEW |