Making oauth2client/ files pass PEP8.

This commit is contained in:
Danny Hermes
2015-08-20 16:08:20 -07:00
parent b70baa4fab
commit d7c0c38b8d
20 changed files with 558 additions and 495 deletions

View File

@@ -63,11 +63,11 @@ def _to_bytes(value, encoding='ascii'):
ValueError if the value could not be converted to bytes. ValueError if the value could not be converted to bytes.
""" """
result = (value.encode(encoding) result = (value.encode(encoding)
if isinstance(value, six.text_type) else value) if isinstance(value, six.text_type) else value)
if isinstance(result, six.binary_type): if isinstance(result, six.binary_type):
return result return result
else: else:
raise ValueError('%r could not be converted to bytes' % (value, )) raise ValueError('%r could not be converted to bytes' % (value,))
def _from_bytes(value): def _from_bytes(value):
@@ -84,11 +84,11 @@ def _from_bytes(value):
ValueError if the value could not be converted to unicode. ValueError if the value could not be converted to unicode.
""" """
result = (value.decode('utf-8') result = (value.decode('utf-8')
if isinstance(value, six.binary_type) else value) if isinstance(value, six.binary_type) else value)
if isinstance(result, six.text_type): if isinstance(result, six.text_type):
return result return result
else: else:
raise ValueError('%r could not be converted to unicode' % (value, )) raise ValueError('%r could not be converted to unicode' % (value,))
def _urlsafe_b64encode(raw_bytes): def _urlsafe_b64encode(raw_bytes):

View File

@@ -54,7 +54,7 @@ class OpenSSLVerifier(object):
return False return False
@staticmethod @staticmethod
def from_string(key_pem, is_x509_cert): def from_string(key_pem, is_x509_cert):
"""Construct a Verified instance from a string. """Construct a Verified instance from a string.
Args: Args:
@@ -136,4 +136,4 @@ def pkcs12_key_as_pem(private_key_text, private_key_password):
pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password) pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
return crypto.dump_privatekey(crypto.FILETYPE_PEM, return crypto.dump_privatekey(crypto.FILETYPE_PEM,
pkcs12.get_privatekey()) pkcs12.get_privatekey())

View File

@@ -50,7 +50,7 @@ class PyCryptoVerifier(object):
""" """
message = _to_bytes(message, encoding='utf-8') message = _to_bytes(message, encoding='utf-8')
return PKCS1_v1_5.new(self._pubkey).verify( return PKCS1_v1_5.new(self._pubkey).verify(
SHA256.new(message), signature) SHA256.new(message), signature)
@staticmethod @staticmethod
def from_string(key_pem, is_x509_cert): def from_string(key_pem, is_x509_cert):
@@ -58,8 +58,8 @@ class PyCryptoVerifier(object):
Args: Args:
key_pem: string, public key in PEM format. key_pem: string, public key in PEM format.
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it
expected to be an RSA key in PEM format. is expected to be an RSA key in PEM format.
Returns: Returns:
Verifier instance. Verifier instance.
@@ -121,8 +121,9 @@ class PyCryptoSigner(object):
pkey = RSA.importKey(parsed_pem_key) pkey = RSA.importKey(parsed_pem_key)
else: else:
raise NotImplementedError( raise NotImplementedError(
'PKCS12 format is not supported by the PyCrypto library. ' 'PKCS12 format is not supported by the PyCrypto library. '
'Try converting to a "PEM" ' 'Try converting to a "PEM" '
'(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) ' '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > '
'or using PyOpenSSL if native code is an option.') 'privatekey.pem) '
'or using PyOpenSSL if native code is an option.')
return PyCryptoSigner(pkey) return PyCryptoSigner(pkey)

View File

@@ -132,7 +132,8 @@ def xsrf_secret_key():
model.secret = _generate_new_xsrf_secret_key() model.secret = _generate_new_xsrf_secret_key()
model.put() model.put()
secret = model.secret secret = model.secret
memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) memcache.add(XSRF_MEMCACHE_ID, secret,
namespace=OAUTH2CLIENT_NAMESPACE)
return str(secret) return str(secret)
@@ -166,7 +167,8 @@ class AppAssertionCredentials(AssertionCredentials):
self._kwargs = kwargs self._kwargs = kwargs
self.service_account_id = kwargs.get('service_account_id', None) self.service_account_id = kwargs.get('service_account_id', None)
# Assertion type is no longer used, but still in the parent class signature. # Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None) super(AppAssertionCredentials, self).__init__(None)
@classmethod @classmethod
@@ -192,14 +194,15 @@ class AppAssertionCredentials(AssertionCredentials):
try: try:
scopes = self.scope.split() scopes = self.scope.split()
(token, _) = app_identity.get_access_token( (token, _) = app_identity.get_access_token(
scopes, service_account_id=self.service_account_id) scopes, service_account_id=self.service_account_id)
except app_identity.Error as e: except app_identity.Error as e:
raise AccessTokenRefreshError(str(e)) raise AccessTokenRefreshError(str(e))
self.access_token = token self.access_token = token
@property @property
def serialization_data(self): def serialization_data(self):
raise NotImplementedError('Cannot serialize credentials for AppEngine.') raise NotImplementedError('Cannot serialize credentials '
'for Google App Engine.')
def create_scoped_required(self): def create_scoped_required(self):
return not self.scope return not self.scope
@@ -220,8 +223,8 @@ class FlowProperty(db.Property):
# For writing to datastore. # For writing to datastore.
def get_value_for_datastore(self, model_instance): def get_value_for_datastore(self, model_instance):
flow = super(FlowProperty, flow = super(FlowProperty, self).get_value_for_datastore(
self).get_value_for_datastore(model_instance) model_instance)
return db.Blob(pickle.dumps(flow)) return db.Blob(pickle.dumps(flow))
# For reading from datastore. # For reading from datastore.
@@ -233,8 +236,8 @@ class FlowProperty(db.Property):
def validate(self, value): def validate(self, value):
if value is not None and not isinstance(value, Flow): if value is not None and not isinstance(value, Flow):
raise db.BadValueError('Property %s must be convertible ' raise db.BadValueError('Property %s must be convertible '
'to a FlowThreeLegged instance (%s)' % 'to a FlowThreeLegged instance (%s)' %
(self.name, value)) (self.name, value))
return super(FlowProperty, self).validate(value) return super(FlowProperty, self).validate(value)
def empty(self, value): def empty(self, value):
@@ -266,7 +269,8 @@ if ndb is not None:
logger.info('validate: Got type %s', type(value)) logger.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, Flow): if value is not None and not isinstance(value, Flow):
raise TypeError('Property %s must be convertible to a flow ' raise TypeError('Property %s must be convertible to a flow '
'instance; received: %s.' % (self._name, value)) 'instance; received: %s.' % (self._name,
value))
class CredentialsProperty(db.Property): class CredentialsProperty(db.Property):
@@ -282,8 +286,8 @@ class CredentialsProperty(db.Property):
# For writing to datastore. # For writing to datastore.
def get_value_for_datastore(self, model_instance): def get_value_for_datastore(self, model_instance):
logger.info("get: Got type " + str(type(model_instance))) logger.info("get: Got type " + str(type(model_instance)))
cred = super(CredentialsProperty, cred = super(CredentialsProperty, self).get_value_for_datastore(
self).get_value_for_datastore(model_instance) model_instance)
if cred is None: if cred is None:
cred = '' cred = ''
else: else:
@@ -308,16 +312,12 @@ class CredentialsProperty(db.Property):
logger.info("validate: Got type " + str(type(value))) logger.info("validate: Got type " + str(type(value)))
if value is not None and not isinstance(value, Credentials): if value is not None and not isinstance(value, Credentials):
raise db.BadValueError('Property %s must be convertible ' raise db.BadValueError('Property %s must be convertible '
'to a Credentials instance (%s)' % 'to a Credentials instance (%s)' %
(self.name, value)) (self.name, value))
#if value is not None and not isinstance(value, Credentials):
# return None
return value return value
if ndb is not None: if ndb is not None:
# TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
# and subclass mechanics to use new_from_dict, to_dict, # and subclass mechanics to use new_from_dict, to_dict,
# from_dict, etc. # from_dict, etc.
@@ -344,8 +344,9 @@ if ndb is not None:
""" """
logger.info('validate: Got type %s', type(value)) logger.info('validate: Got type %s', type(value))
if value is not None and not isinstance(value, Credentials): if value is not None and not isinstance(value, Credentials):
raise TypeError('Property %s must be convertible to a credentials ' raise TypeError('Property %s must be convertible to a '
'instance; received: %s.' % (self._name, value)) 'credentials instance; received: %s.' %
(self._name, value))
def _to_base_type(self, value): def _to_base_type(self, value):
"""Converts our validated value to a JSON serialized string. """Converts our validated value to a JSON serialized string.
@@ -409,7 +410,8 @@ class StorageByKeyName(Storage):
""" """
if key_name is None: if key_name is None:
if user is None: if user is None:
raise ValueError('StorageByKeyName called with no key name or user.') raise ValueError('StorageByKeyName called with no '
'key name or user.')
key_name = user.user_id() key_name = user.user_id()
self._model = model self._model = model
@@ -423,15 +425,17 @@ class StorageByKeyName(Storage):
Returns: Returns:
Boolean indicating whether or not the model is an NDB or DB model. Boolean indicating whether or not the model is an NDB or DB model.
""" """
# issubclass will fail if one of the arguments is not a class, only need # issubclass will fail if one of the arguments is not a class, only
# worry about new-style classes since ndb and db models are new-style # need worry about new-style classes since ndb and db models are
# new-style
if isinstance(self._model, type): if isinstance(self._model, type):
if ndb is not None and issubclass(self._model, ndb.Model): if ndb is not None and issubclass(self._model, ndb.Model):
return True return True
elif issubclass(self._model, db.Model): elif issubclass(self._model, db.Model):
return False return False
raise TypeError('Model class not an NDB or DB model: %s.' % (self._model, )) raise TypeError('Model class not an NDB or DB model: %s.' %
(self._model,))
def _get_entity(self): def _get_entity(self):
"""Retrieve entity from datastore. """Retrieve entity from datastore.
@@ -460,7 +464,7 @@ class StorageByKeyName(Storage):
db.delete(entity_key) db.delete(entity_key)
@db.non_transactional(allow_existing=True) @db.non_transactional(allow_existing=True)
def locked_get(self): def locked_get(self):
"""Retrieve Credential from datastore. """Retrieve Credential from datastore.
Returns: Returns:
@@ -496,7 +500,7 @@ class StorageByKeyName(Storage):
self._cache.set(self._key_name, credentials.to_json()) self._cache.set(self._key_name, credentials.to_json())
@db.non_transactional(allow_existing=True) @db.non_transactional(allow_existing=True)
def locked_delete(self): def locked_delete(self):
"""Delete Credential from datastore.""" """Delete Credential from datastore."""
if self._cache: if self._cache:
@@ -548,8 +552,8 @@ def _build_state_value(request_handler, user):
""" """
uri = request_handler.request.url uri = request_handler.request.url
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
action_id=str(uri)) action_id=str(uri))
return uri + ':' + token return uri + ':' + token
def _parse_state_value(state, user): def _parse_state_value(state, user):
@@ -569,7 +573,7 @@ def _parse_state_value(state, user):
""" """
uri, token = state.rsplit(':', 1) uri, token = state.rsplit(':', 1)
if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
action_id=uri): action_id=uri):
raise InvalidXsrfTokenError() raise InvalidXsrfTokenError()
return uri return uri
@@ -629,18 +633,17 @@ class OAuth2Decorator(object):
@util.positional(4) @util.positional(4)
def __init__(self, client_id, client_secret, scope, def __init__(self, client_id, client_secret, scope,
auth_uri=GOOGLE_AUTH_URI, auth_uri=GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI, token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=GOOGLE_REVOKE_URI,
user_agent=None, user_agent=None,
message=None, message=None,
callback_path='/oauth2callback', callback_path='/oauth2callback',
token_response_param=None, token_response_param=None,
_storage_class=StorageByKeyName, _storage_class=StorageByKeyName,
_credentials_class=CredentialsModel, _credentials_class=CredentialsModel,
_credentials_property_name='credentials', _credentials_property_name='credentials',
**kwargs): **kwargs):
"""Constructor for OAuth2Decorator """Constructor for OAuth2Decorator
Args: Args:
@@ -659,13 +662,14 @@ class OAuth2Decorator(object):
provider can be used. provider can be used.
user_agent: string, User agent of your application, default to user_agent: string, User agent of your application, default to
None. None.
message: Message to display if there are problems with the OAuth 2.0 message: Message to display if there are problems with the
configuration. The message may contain HTML and will be OAuth 2.0 configuration. The message may contain HTML and
presented on the web interface for any method that uses will be presented on the web interface for any method that
the decorator. uses the decorator.
callback_path: string, The absolute path to use as the callback callback_path: string, The absolute path to use as the callback
URI. Note that this must match up with the URI given URI. Note that this must match up with the URI given
when registering the application in the APIs Console. when registering the application in the APIs
Console.
token_response_param: string. If provided, the full JSON response token_response_param: string. If provided, the full JSON response
to the access token request will be encoded to the access token request will be encoded
and included in this query parameter in the and included in this query parameter in the
@@ -730,19 +734,21 @@ class OAuth2Decorator(object):
return return
user = users.get_current_user() user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request. # Don't use @login_decorator as this could be used in a
# POST request.
if not user: if not user:
request_handler.redirect(users.create_login_url( request_handler.redirect(users.create_login_url(
request_handler.request.uri)) request_handler.request.uri))
return return
self._create_flow(request_handler) self._create_flow(request_handler)
# Store the request URI in 'state' so we can use it later # Store the request URI in 'state' so we can use it later
self.flow.params['state'] = _build_state_value(request_handler, user) self.flow.params['state'] = _build_state_value(
request_handler, user)
self.credentials = self._storage_class( self.credentials = self._storage_class(
self._credentials_class, None, self._credentials_class, None,
self._credentials_property_name, user=user).get() self._credentials_property_name, user=user).get()
if not self.has_credentials(): if not self.has_credentials():
return request_handler.redirect(self.authorize_url()) return request_handler.redirect(self.authorize_url())
@@ -768,14 +774,12 @@ class OAuth2Decorator(object):
""" """
if self.flow is None: if self.flow is None:
redirect_uri = request_handler.request.relative_url( redirect_uri = request_handler.request.relative_url(
self._callback_path) # Usually /oauth2callback self._callback_path) # Usually /oauth2callback
self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, self.flow = OAuth2WebServerFlow(
self._scope, redirect_uri=redirect_uri, self._client_id, self._client_secret, self._scope,
user_agent=self._user_agent, redirect_uri=redirect_uri, user_agent=self._user_agent,
auth_uri=self._auth_uri, auth_uri=self._auth_uri, token_uri=self._token_uri,
token_uri=self._token_uri, revoke_uri=self._revoke_uri, **self._kwargs)
revoke_uri=self._revoke_uri,
**self._kwargs)
def oauth_aware(self, method): def oauth_aware(self, method):
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it. """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
@@ -797,18 +801,20 @@ class OAuth2Decorator(object):
return return
user = users.get_current_user() user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request. # Don't use @login_decorator as this could be used in a
# POST request.
if not user: if not user:
request_handler.redirect(users.create_login_url( request_handler.redirect(users.create_login_url(
request_handler.request.uri)) request_handler.request.uri))
return return
self._create_flow(request_handler) self._create_flow(request_handler)
self.flow.params['state'] = _build_state_value(request_handler, user) self.flow.params['state'] = _build_state_value(request_handler,
user)
self.credentials = self._storage_class( self.credentials = self._storage_class(
self._credentials_class, None, self._credentials_class, None,
self._credentials_property_name, user=user).get() self._credentials_property_name, user=user).get()
try: try:
resp = method(request_handler, *args, **kwargs) resp = method(request_handler, *args, **kwargs)
finally: finally:
@@ -885,21 +891,26 @@ class OAuth2Decorator(object):
if error: if error:
errormsg = self.request.get('error_description', error) errormsg = self.request.get('error_description', error)
self.response.out.write( self.response.out.write(
'The authorization request failed: %s' % _safe_html(errormsg)) 'The authorization request failed: %s' %
_safe_html(errormsg))
else: else:
user = users.get_current_user() user = users.get_current_user()
decorator._create_flow(self) decorator._create_flow(self)
credentials = decorator.flow.step2_exchange(self.request.params) credentials = decorator.flow.step2_exchange(
self.request.params)
decorator._storage_class( decorator._storage_class(
decorator._credentials_class, None, decorator._credentials_class, None,
decorator._credentials_property_name, user=user).put(credentials) decorator._credentials_property_name,
redirect_uri = _parse_state_value(str(self.request.get('state')), user=user).put(credentials)
user) redirect_uri = _parse_state_value(
str(self.request.get('state')), user)
if decorator._token_response_param and credentials.token_response: if (decorator._token_response_param and
credentials.token_response):
resp_json = json.dumps(credentials.token_response) resp_json = json.dumps(credentials.token_response)
redirect_uri = util._add_query_parameter( redirect_uri = util._add_query_parameter(
redirect_uri, decorator._token_response_param, resp_json) redirect_uri, decorator._token_response_param,
resp_json)
self.redirect(redirect_uri) self.redirect(redirect_uri)
@@ -916,7 +927,7 @@ class OAuth2Decorator(object):
server during the OAuth 2.0 dance. server during the OAuth 2.0 dance.
""" """
return webapp.WSGIApplication([ return webapp.WSGIApplication([
(self.callback_path, self.callback_handler()) (self.callback_path, self.callback_handler())
]) ])
@@ -959,23 +970,25 @@ class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
**kwargs: dict, Keyword arguments are passed along as kwargs to **kwargs: dict, Keyword arguments are passed along as kwargs to
the OAuth2WebServerFlow constructor. the OAuth2WebServerFlow constructor.
""" """
client_type, client_info = clientsecrets.loadfile(filename, cache=cache) client_type, client_info = clientsecrets.loadfile(filename,
if client_type not in [ cache=cache)
clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: if client_type not in (clientsecrets.TYPE_WEB,
clientsecrets.TYPE_INSTALLED):
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
"OAuth2Decorator doesn't support this OAuth 2.0 flow.") "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
constructor_kwargs = dict(kwargs) constructor_kwargs = dict(kwargs)
constructor_kwargs.update({ constructor_kwargs.update({
'auth_uri': client_info['auth_uri'], 'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'], 'token_uri': client_info['token_uri'],
'message': message, 'message': message,
}) })
revoke_uri = client_info.get('revoke_uri') revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None: if revoke_uri is not None:
constructor_kwargs['revoke_uri'] = revoke_uri constructor_kwargs['revoke_uri'] = revoke_uri
super(OAuth2DecoratorFromClientSecrets, self).__init__( super(OAuth2DecoratorFromClientSecrets, self).__init__(
client_info['client_id'], client_info['client_secret'], client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs) scope, **constructor_kwargs)
if message is not None: if message is not None:
self._message = message self._message = message
else: else:
@@ -1001,4 +1014,4 @@ def oauth2decorator_from_clientsecrets(filename, scope,
Returns: An OAuth2Decorator Returns: An OAuth2Decorator
""" """
return OAuth2DecoratorFromClientSecrets(filename, scope, return OAuth2DecoratorFromClientSecrets(filename, scope,
message=message, cache=cache) message=message, cache=cache)

View File

@@ -93,12 +93,13 @@ _CLOUDSDK_CONFIG_ENV_VAR = 'CLOUDSDK_CONFIG'
# The error message we show users when we can't find the Application # The error message we show users when we can't find the Application
# Default Credentials. # Default Credentials.
ADC_HELP_MSG = ( ADC_HELP_MSG = (
'The Application Default Credentials are not available. They are available ' 'The Application Default Credentials are not available. They are '
'if running in Google Compute Engine. Otherwise, the environment variable ' 'available if running in Google Compute Engine. Otherwise, the '
+ GOOGLE_APPLICATION_CREDENTIALS + 'environment variable ' +
GOOGLE_APPLICATION_CREDENTIALS +
' must be defined pointing to a file defining the credentials. See ' ' must be defined pointing to a file defining the credentials. See '
'https://developers.google.com/accounts/docs/application-default-credentials' # pylint:disable=line-too-long 'https://developers.google.com/accounts/docs/'
' for more information.') 'application-default-credentials for more information.')
# The access token along with the seconds in which it expires. # The access token along with the seconds in which it expires.
AccessTokenInfo = collections.namedtuple( AccessTokenInfo = collections.namedtuple(
@@ -132,7 +133,7 @@ class TokenRevokeError(Error):
class UnknownClientSecretsFlowError(Error): class UnknownClientSecretsFlowError(Error):
"""The client secrets file called for an unknown type of OAuth 2.0 flow. """ """The client secrets file called for an unknown type of OAuth 2.0 flow."""
class AccessTokenCredentialsError(Error): class AccessTokenCredentialsError(Error):
@@ -247,7 +248,7 @@ class Credentials(object):
if member in d: if member in d:
del d[member] del d[member]
if (d.get('token_expiry') and if (d.get('token_expiry') and
isinstance(d['token_expiry'], datetime.datetime)): isinstance(d['token_expiry'], datetime.datetime)):
d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT) d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
# Add in information we will need later to reconsistitue this instance. # Add in information we will need later to reconsistitue this instance.
d['_class'] = t.__name__ d['_class'] = t.__name__
@@ -283,16 +284,19 @@ class Credentials(object):
""" """
json_string_as_unicode = _from_bytes(s) json_string_as_unicode = _from_bytes(s)
data = json.loads(json_string_as_unicode) data = json.loads(json_string_as_unicode)
# Find and call the right classmethod from_json() to restore the object. # Find and call the right classmethod from_json() to restore
# the object.
module_name = data['_module'] module_name = data['_module']
try: try:
module_obj = __import__(module_name) module_obj = __import__(module_name)
except ImportError: except ImportError:
# In case there's an object from the old package structure, update it # In case there's an object from the old package structure,
# update it
module_name = module_name.replace('.googleapiclient', '') module_name = module_name.replace('.googleapiclient', '')
module_obj = __import__(module_name) module_obj = __import__(module_name)
module_obj = __import__(module_name, fromlist=module_name.split('.')[:-1]) module_obj = __import__(module_name,
fromlist=module_name.split('.')[:-1])
kls = getattr(module_obj, data['_class']) kls = getattr(module_obj, data['_class'])
from_json = getattr(kls, 'from_json') from_json = getattr(kls, 'from_json')
return from_json(json_string_as_unicode) return from_json(json_string_as_unicode)
@@ -465,9 +469,9 @@ class OAuth2Credentials(Credentials):
@util.positional(8) @util.positional(8)
def __init__(self, access_token, client_id, client_secret, refresh_token, def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, revoke_uri=None, token_expiry, token_uri, user_agent, revoke_uri=None,
id_token=None, token_response=None, scopes=None, id_token=None, token_response=None, scopes=None,
token_info_uri=None): token_info_uri=None):
"""Create an instance of OAuth2Credentials. """Create an instance of OAuth2Credentials.
This constructor is not usually called by the user, instead This constructor is not usually called by the user, instead
@@ -522,10 +526,9 @@ class OAuth2Credentials(Credentials):
The modified http.request method will add authentication headers to The modified http.request method will add authentication headers to
each request and will refresh access_tokens when a 401 is received on a each request and will refresh access_tokens when a 401 is received on a
request. In addition the http.request method has a credentials property, request. In addition the http.request method has a credentials
http.request.credentials, which is the Credentials object that property, http.request.credentials, which is the Credentials object
authorized it. that authorized it.
Args: Args:
http: An instance of ``httplib2.Http`` or something that acts http: An instance of ``httplib2.Http`` or something that acts
@@ -549,10 +552,11 @@ class OAuth2Credentials(Credentials):
# The closure that will replace 'httplib2.Http.request'. # The closure that will replace 'httplib2.Http.request'.
def new_request(uri, method='GET', body=None, headers=None, def new_request(uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS, redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None): connection_type=None):
if not self.access_token: if not self.access_token:
logger.info('Attempting refresh to obtain initial access_token') logger.info('Attempting refresh to obtain '
'initial access_token')
self._refresh(request_orig) self._refresh(request_orig)
# Clone and modify the request headers to add the appropriate # Clone and modify the request headers to add the appropriate
@@ -565,33 +569,37 @@ class OAuth2Credentials(Credentials):
if self.user_agent is not None: if self.user_agent is not None:
if 'user-agent' in headers: if 'user-agent' in headers:
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] headers['user-agent'] = (self.user_agent + ' ' +
headers['user-agent'])
else: else:
headers['user-agent'] = self.user_agent headers['user-agent'] = self.user_agent
body_stream_position = None body_stream_position = None
if all(getattr(body, stream_prop, None) for stream_prop in if all(getattr(body, stream_prop, None) for stream_prop in
('read', 'seek', 'tell')): ('read', 'seek', 'tell')):
body_stream_position = body.tell() body_stream_position = body.tell()
resp, content = request_orig(uri, method, body, clean_headers(headers), resp, content = request_orig(uri, method, body,
redirections, connection_type) clean_headers(headers),
redirections, connection_type)
# A stored token may expire between the time it is retrieved and the time # A stored token may expire between the time it is retrieved and
# the request is made, so we may need to try twice. # the time the request is made, so we may need to try twice.
max_refresh_attempts = 2 max_refresh_attempts = 2
for refresh_attempt in range(max_refresh_attempts): for refresh_attempt in range(max_refresh_attempts):
if resp.status not in REFRESH_STATUS_CODES: if resp.status not in REFRESH_STATUS_CODES:
break break
logger.info('Refreshing due to a %s (attempt %s/%s)', resp.status, logger.info('Refreshing due to a %s (attempt %s/%s)',
refresh_attempt + 1, max_refresh_attempts) resp.status, refresh_attempt + 1,
max_refresh_attempts)
self._refresh(request_orig) self._refresh(request_orig)
self.apply(headers) self.apply(headers)
if body_stream_position is not None: if body_stream_position is not None:
body.seek(body_stream_position) body.seek(body_stream_position)
resp, content = request_orig(uri, method, body, clean_headers(headers), resp, content = request_orig(uri, method, body,
redirections, connection_type) clean_headers(headers),
redirections, connection_type)
return (resp, content) return (resp, content)
@@ -681,25 +689,25 @@ class OAuth2Credentials(Credentials):
s = _from_bytes(s) s = _from_bytes(s)
data = json.loads(s) data = json.loads(s)
if (data.get('token_expiry') and if (data.get('token_expiry') and
not isinstance(data['token_expiry'], datetime.datetime)): not isinstance(data['token_expiry'], datetime.datetime)):
try: try:
data['token_expiry'] = datetime.datetime.strptime( data['token_expiry'] = datetime.datetime.strptime(
data['token_expiry'], EXPIRY_FORMAT) data['token_expiry'], EXPIRY_FORMAT)
except ValueError: except ValueError:
data['token_expiry'] = None data['token_expiry'] = None
retval = cls( retval = cls(
data['access_token'], data['access_token'],
data['client_id'], data['client_id'],
data['client_secret'], data['client_secret'],
data['refresh_token'], data['refresh_token'],
data['token_expiry'], data['token_expiry'],
data['token_uri'], data['token_uri'],
data['user_agent'], data['user_agent'],
revoke_uri=data.get('revoke_uri', None), revoke_uri=data.get('revoke_uri', None),
id_token=data.get('id_token', None), id_token=data.get('id_token', None),
token_response=data.get('token_response', None), token_response=data.get('token_response', None),
scopes=data.get('scopes', None), scopes=data.get('scopes', None),
token_info_uri=data.get('token_info_uri', None)) token_info_uri=data.get('token_info_uri', None))
retval.invalid = data['invalid'] retval.invalid = data['invalid']
return retval return retval
@@ -718,7 +726,7 @@ class OAuth2Credentials(Credentials):
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
if now >= self.token_expiry: if now >= self.token_expiry:
logger.info('access_token is expired. Now: %s, token_expiry: %s', logger.info('access_token is expired. Now: %s, token_expiry: %s',
now, self.token_expiry) now, self.token_expiry)
return True return True
return False return False
@@ -733,7 +741,7 @@ class OAuth2Credentials(Credentials):
http = httplib2.Http() http = httplib2.Http()
self.refresh(http) self.refresh(http)
return AccessTokenInfo(access_token=self.access_token, return AccessTokenInfo(access_token=self.access_token,
expires_in=self._expires_in()) expires_in=self._expires_in())
def set_store(self, store): def set_store(self, store):
"""Set the Storage for the credential. """Set the Storage for the credential.
@@ -785,17 +793,17 @@ class OAuth2Credentials(Credentials):
def _generate_refresh_request_body(self): def _generate_refresh_request_body(self):
"""Generate the body that will be used in the refresh request.""" """Generate the body that will be used in the refresh request."""
body = urllib.parse.urlencode({ body = urllib.parse.urlencode({
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'refresh_token': self.refresh_token, 'refresh_token': self.refresh_token,
}) })
return body return body
def _generate_refresh_request_headers(self): def _generate_refresh_request_headers(self):
"""Generate the headers that will be used in the refresh request.""" """Generate the headers that will be used in the refresh request."""
headers = { headers = {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
} }
if self.user_agent is not None: if self.user_agent is not None:
@@ -826,8 +834,8 @@ class OAuth2Credentials(Credentials):
new_cred = self.store.locked_get() new_cred = self.store.locked_get()
if (new_cred and not new_cred.invalid and if (new_cred and not new_cred.invalid and
new_cred.access_token != self.access_token and new_cred.access_token != self.access_token and
not new_cred.access_token_expired): not new_cred.access_token_expired):
logger.info('Updated access_token read from Storage') logger.info('Updated access_token read from Storage')
self._updateFromCredential(new_cred) self._updateFromCredential(new_cred)
else: else:
@@ -851,7 +859,7 @@ class OAuth2Credentials(Credentials):
logger.info('Refreshing access_token') logger.info('Refreshing access_token')
resp, content = http_request( resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers) self.token_uri, method='POST', body=body, headers=headers)
content = _from_bytes(content) content = _from_bytes(content)
if resp.status == 200: if resp.status == 200:
d = json.loads(content) d = json.loads(content)
@@ -860,7 +868,7 @@ class OAuth2Credentials(Credentials):
self.refresh_token = d.get('refresh_token', self.refresh_token) self.refresh_token = d.get('refresh_token', self.refresh_token)
if 'expires_in' in d: if 'expires_in' in d:
self.token_expiry = datetime.timedelta( self.token_expiry = datetime.timedelta(
seconds=int(d['expires_in'])) + datetime.datetime.utcnow() seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
else: else:
self.token_expiry = None self.token_expiry = None
# On temporary refresh errors, the user does not actually have to # On temporary refresh errors, the user does not actually have to
@@ -869,8 +877,8 @@ class OAuth2Credentials(Credentials):
if self.store: if self.store:
self.store.locked_put(self) self.store.locked_put(self)
else: else:
# An {'error':...} response body means the token is expired or revoked, # An {'error':...} response body means the token is expired or
# so we flag the credentials as such. # revoked, so we flag the credentials as such.
logger.info('Failed to retrieve access token: %s', content) logger.info('Failed to retrieve access token: %s', content)
error_msg = 'Invalid response %s.' % resp['status'] error_msg = 'Invalid response %s.' % resp['status']
try: try:
@@ -955,14 +963,15 @@ class OAuth2Credentials(Credentials):
""" """
logger.info('Refreshing scopes') logger.info('Refreshing scopes')
query_params = {'access_token': token, 'fields': 'scope'} query_params = {'access_token': token, 'fields': 'scope'}
token_info_uri = _update_query_params(self.token_info_uri, query_params) token_info_uri = _update_query_params(self.token_info_uri,
query_params)
resp, content = http_request(token_info_uri) resp, content = http_request(token_info_uri)
content = _from_bytes(content) content = _from_bytes(content)
if resp.status == 200: if resp.status == 200:
d = json.loads(content) d = json.loads(content)
self.scopes = set(util.string_to_scopes(d.get('scope', ''))) self.scopes = set(util.string_to_scopes(d.get('scope', '')))
else: else:
error_msg = 'Invalid response %s.' % (resp.status, ) error_msg = 'Invalid response %s.' % (resp.status,)
try: try:
d = json.loads(content) d = json.loads(content)
if 'error_description' in d: if 'error_description' in d:
@@ -1012,26 +1021,26 @@ class AccessTokenCredentials(OAuth2Credentials):
token can't be revoked if this is None. token can't be revoked if this is None.
""" """
super(AccessTokenCredentials, self).__init__( super(AccessTokenCredentials, self).__init__(
access_token, access_token,
None, None,
None, None,
None, None,
None, None,
None, None,
user_agent, user_agent,
revoke_uri=revoke_uri) revoke_uri=revoke_uri)
@classmethod @classmethod
def from_json(cls, s): def from_json(cls, s):
data = json.loads(_from_bytes(s)) data = json.loads(_from_bytes(s))
retval = AccessTokenCredentials( retval = AccessTokenCredentials(
data['access_token'], data['access_token'],
data['user_agent']) data['user_agent'])
return retval return retval
def _refresh(self, http_request): def _refresh(self, http_request):
raise AccessTokenCredentialsError( raise AccessTokenCredentialsError(
'The access_token is expired or invalid and can\'t be refreshed.') 'The access_token is expired or invalid and can\'t be refreshed.')
def _revoke(self, http_request): def _revoke(self, http_request):
"""Revokes the access_token and deletes the store if available. """Revokes the access_token and deletes the store if available.
@@ -1143,8 +1152,8 @@ class GoogleCredentials(OAuth2Credentials):
""" """
def __init__(self, access_token, client_id, client_secret, refresh_token, def __init__(self, access_token, client_id, client_secret, refresh_token,
token_expiry, token_uri, user_agent, token_expiry, token_uri, user_agent,
revoke_uri=GOOGLE_REVOKE_URI): revoke_uri=GOOGLE_REVOKE_URI):
"""Create an instance of GoogleCredentials. """Create an instance of GoogleCredentials.
This constructor is not usually called by the user, instead This constructor is not usually called by the user, instead
@@ -1166,8 +1175,8 @@ class GoogleCredentials(OAuth2Credentials):
is None. is None.
""" """
super(GoogleCredentials, self).__init__( super(GoogleCredentials, self).__init__(
access_token, client_id, client_secret, refresh_token, token_expiry, access_token, client_id, client_secret, refresh_token,
token_uri, user_agent, revoke_uri=revoke_uri) token_expiry, token_uri, user_agent, revoke_uri=revoke_uri)
def create_scoped_required(self): def create_scoped_required(self):
"""Whether this Credentials object is scopeless. """Whether this Credentials object is scopeless.
@@ -1188,10 +1197,10 @@ class GoogleCredentials(OAuth2Credentials):
def serialization_data(self): def serialization_data(self):
"""Get the fields and values identifying the current credentials.""" """Get the fields and values identifying the current credentials."""
return { return {
'type': 'authorized_user', 'type': 'authorized_user',
'client_id': self.client_id, 'client_id': self.client_id,
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'refresh_token': self.refresh_token 'refresh_token': self.refresh_token
} }
@staticmethod @staticmethod
@@ -1247,27 +1256,29 @@ class GoogleCredentials(OAuth2Credentials):
credentials_filename = _get_well_known_file() credentials_filename = _get_well_known_file()
if os.path.isfile(credentials_filename): if os.path.isfile(credentials_filename):
extra_help = (' (produced automatically when running' extra_help = (' (produced automatically when running'
' "gcloud auth login" command)') ' "gcloud auth login" command)')
else: else:
credentials_filename = None credentials_filename = None
else: else:
extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS + extra_help = (' (pointed to by ' + GOOGLE_APPLICATION_CREDENTIALS +
' environment variable)') ' environment variable)')
if not credentials_filename: if not credentials_filename:
return return
# If we can read the credentials from a file, we don't need to know what # If we can read the credentials from a file, we don't need to know
# environment we are in. # what environment we are in.
SETTINGS.env_name = DEFAULT_ENV_NAME SETTINGS.env_name = DEFAULT_ENV_NAME
try: try:
return _get_application_default_credential_from_file(credentials_filename) return _get_application_default_credential_from_file(
credentials_filename)
except (ApplicationDefaultCredentialsError, ValueError) as error: except (ApplicationDefaultCredentialsError, ValueError) as error:
_raise_exception_for_reading_json(credentials_filename, extra_help, error) _raise_exception_for_reading_json(credentials_filename,
extra_help, error)
@classmethod @classmethod
def _get_implicit_credentials(cls): def _get_implicit_credentials(cls):
"""Gets credentials implicitly from the environment. """Gets credentials implicitly from the environment.
Checks environment in order of precedence: Checks environment in order of precedence:
@@ -1283,9 +1294,9 @@ class GoogleCredentials(OAuth2Credentials):
""" """
# Environ checks (in order). # Environ checks (in order).
environ_checkers = [ environ_checkers = [
cls._implicit_credentials_from_gae, cls._implicit_credentials_from_gae,
cls._implicit_credentials_from_files, cls._implicit_credentials_from_files,
cls._implicit_credentials_from_gce, cls._implicit_credentials_from_gce,
] ]
for checker in environ_checkers: for checker in environ_checkers:
@@ -1323,16 +1334,17 @@ class GoogleCredentials(OAuth2Credentials):
if credential_filename and os.path.isfile(credential_filename): if credential_filename and os.path.isfile(credential_filename):
try: try:
return _get_application_default_credential_from_file( return _get_application_default_credential_from_file(
credential_filename) credential_filename)
except (ApplicationDefaultCredentialsError, ValueError) as error: except (ApplicationDefaultCredentialsError, ValueError) as error:
extra_help = ' (provided as parameter to the from_stream() method)' extra_help = (' (provided as parameter to the '
'from_stream() method)')
_raise_exception_for_reading_json(credential_filename, _raise_exception_for_reading_json(credential_filename,
extra_help, extra_help,
error) error)
else: else:
raise ApplicationDefaultCredentialsError( raise ApplicationDefaultCredentialsError(
'The parameter passed to the from_stream() ' 'The parameter passed to the from_stream() '
'method should point to a file.') 'method should point to a file.')
def _save_private_file(filename, json_contents): def _save_private_file(filename, json_contents):
@@ -1346,7 +1358,7 @@ def _save_private_file(filename, json_contents):
file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600) file_desc = os.open(temp_filename, os.O_WRONLY | os.O_CREAT, 0o600)
with os.fdopen(file_desc, 'w') as file_handle: with os.fdopen(file_desc, 'w') as file_handle:
json.dump(json_contents, file_handle, sort_keys=True, json.dump(json_contents, file_handle, sort_keys=True,
indent=2, separators=(',', ': ')) indent=2, separators=(',', ': '))
shutil.move(temp_filename, filename) shutil.move(temp_filename, filename)
@@ -1384,9 +1396,10 @@ def _get_environment_variable_file():
return application_default_credential_filename return application_default_credential_filename
else: else:
raise ApplicationDefaultCredentialsError( raise ApplicationDefaultCredentialsError(
'File ' + application_default_credential_filename + ' (pointed by ' + 'File ' + application_default_credential_filename +
GOOGLE_APPLICATION_CREDENTIALS + ' (pointed by ' +
' environment variable) does not exist!') GOOGLE_APPLICATION_CREDENTIALS +
' environment variable) does not exist!')
def _get_well_known_file(): def _get_well_known_file():
@@ -1401,16 +1414,17 @@ def _get_well_known_file():
if os.name == 'nt': if os.name == 'nt':
try: try:
default_config_dir = os.path.join(os.environ['APPDATA'], default_config_dir = os.path.join(os.environ['APPDATA'],
_CLOUDSDK_CONFIG_DIRECTORY) _CLOUDSDK_CONFIG_DIRECTORY)
except KeyError: except KeyError:
# This should never happen unless someone is really messing with things. # This should never happen unless someone is really
# messing with things.
drive = os.environ.get('SystemDrive', 'C:') drive = os.environ.get('SystemDrive', 'C:')
default_config_dir = os.path.join(drive, '\\', default_config_dir = os.path.join(drive, '\\',
_CLOUDSDK_CONFIG_DIRECTORY) _CLOUDSDK_CONFIG_DIRECTORY)
else: else:
default_config_dir = os.path.join(os.path.expanduser('~'), default_config_dir = os.path.join(os.path.expanduser('~'),
'.config', '.config',
_CLOUDSDK_CONFIG_DIRECTORY) _CLOUDSDK_CONFIG_DIRECTORY)
return os.path.join(default_config_dir, WELL_KNOWN_CREDENTIALS_FILE) return os.path.join(default_config_dir, WELL_KNOWN_CREDENTIALS_FILE)
@@ -1429,11 +1443,11 @@ def _get_application_default_credential_from_file(filename):
required_fields = set(['client_id', 'client_secret', 'refresh_token']) required_fields = set(['client_id', 'client_secret', 'refresh_token'])
elif credentials_type == SERVICE_ACCOUNT: elif credentials_type == SERVICE_ACCOUNT:
required_fields = set(['client_id', 'client_email', 'private_key_id', required_fields = set(['client_id', 'client_email', 'private_key_id',
'private_key']) 'private_key'])
else: else:
raise ApplicationDefaultCredentialsError( raise ApplicationDefaultCredentialsError(
"'type' field should be defined (and have one of the '" + "'type' field should be defined (and have one of the '" +
AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)") AUTHORIZED_USER + "' or '" + SERVICE_ACCOUNT + "' values)")
missing_fields = required_fields.difference(client_credentials.keys()) missing_fields = required_fields.difference(client_credentials.keys())
@@ -1442,25 +1456,25 @@ def _get_application_default_credential_from_file(filename):
if client_credentials['type'] == AUTHORIZED_USER: if client_credentials['type'] == AUTHORIZED_USER:
return GoogleCredentials( return GoogleCredentials(
access_token=None, access_token=None,
client_id=client_credentials['client_id'], client_id=client_credentials['client_id'],
client_secret=client_credentials['client_secret'], client_secret=client_credentials['client_secret'],
refresh_token=client_credentials['refresh_token'], refresh_token=client_credentials['refresh_token'],
token_expiry=None, token_expiry=None,
token_uri=GOOGLE_TOKEN_URI, token_uri=GOOGLE_TOKEN_URI,
user_agent='Python client library') user_agent='Python client library')
else: # client_credentials['type'] == SERVICE_ACCOUNT else: # client_credentials['type'] == SERVICE_ACCOUNT
return service_account._ServiceAccountCredentials( return service_account._ServiceAccountCredentials(
service_account_id=client_credentials['client_id'], service_account_id=client_credentials['client_id'],
service_account_email=client_credentials['client_email'], service_account_email=client_credentials['client_email'],
private_key_id=client_credentials['private_key_id'], private_key_id=client_credentials['private_key_id'],
private_key_pkcs8_text=client_credentials['private_key'], private_key_pkcs8_text=client_credentials['private_key'],
scopes=[]) scopes=[])
def _raise_exception_for_missing_fields(missing_fields): def _raise_exception_for_missing_fields(missing_fields):
raise ApplicationDefaultCredentialsError( raise ApplicationDefaultCredentialsError(
'The following field(s) must be defined: ' + ', '.join(missing_fields)) 'The following field(s) must be defined: ' + ', '.join(missing_fields))
def _raise_exception_for_reading_json(credential_file, def _raise_exception_for_reading_json(credential_file,
@@ -1496,9 +1510,9 @@ class AssertionCredentials(GoogleCredentials):
@util.positional(2) @util.positional(2)
def __init__(self, assertion_type, user_agent=None, def __init__(self, assertion_type, user_agent=None,
token_uri=GOOGLE_TOKEN_URI, token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=GOOGLE_REVOKE_URI,
**unused_kwargs): **unused_kwargs):
"""Constructor for AssertionFlowCredentials. """Constructor for AssertionFlowCredentials.
Args: Args:
@@ -1512,22 +1526,22 @@ class AssertionCredentials(GoogleCredentials):
revoke_uri: string, URI for revoke endpoint. revoke_uri: string, URI for revoke endpoint.
""" """
super(AssertionCredentials, self).__init__( super(AssertionCredentials, self).__init__(
None, None,
None, None,
None, None,
None, None,
None, None,
token_uri, token_uri,
user_agent, user_agent,
revoke_uri=revoke_uri) revoke_uri=revoke_uri)
self.assertion_type = assertion_type self.assertion_type = assertion_type
def _generate_refresh_request_body(self): def _generate_refresh_request_body(self):
assertion = self._generate_assertion() assertion = self._generate_assertion()
body = urllib.parse.urlencode({ body = urllib.parse.urlencode({
'assertion': assertion, 'assertion': assertion,
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
}) })
return body return body
@@ -1574,14 +1588,14 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
@util.positional(4) @util.positional(4)
def __init__(self, def __init__(self,
service_account_name, service_account_name,
private_key, private_key,
scope, scope,
private_key_password='notasecret', private_key_password='notasecret',
user_agent=None, user_agent=None,
token_uri=GOOGLE_TOKEN_URI, token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=GOOGLE_REVOKE_URI,
**kwargs): **kwargs):
"""Constructor for SignedJwtAssertionCredentials. """Constructor for SignedJwtAssertionCredentials.
Args: Args:
@@ -1606,10 +1620,10 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
""" """
_RequireCryptoOrDie() _RequireCryptoOrDie()
super(SignedJwtAssertionCredentials, self).__init__( super(SignedJwtAssertionCredentials, self).__init__(
None, None,
user_agent=user_agent, user_agent=user_agent,
token_uri=token_uri, token_uri=token_uri,
revoke_uri=revoke_uri, revoke_uri=revoke_uri,
) )
self.scope = util.scopes_to_string(scope) self.scope = util.scopes_to_string(scope)
@@ -1625,13 +1639,13 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
def from_json(cls, s): def from_json(cls, s):
data = json.loads(_from_bytes(s)) data = json.loads(_from_bytes(s))
retval = SignedJwtAssertionCredentials( retval = SignedJwtAssertionCredentials(
data['service_account_name'], data['service_account_name'],
base64.b64decode(data['private_key']), base64.b64decode(data['private_key']),
data['scope'], data['scope'],
private_key_password=data['private_key_password'], private_key_password=data['private_key_password'],
user_agent=data['user_agent'], user_agent=data['user_agent'],
token_uri=data['token_uri'], token_uri=data['token_uri'],
**data['kwargs'] **data['kwargs']
) )
retval.invalid = data['invalid'] retval.invalid = data['invalid']
retval.access_token = data['access_token'] retval.access_token = data['access_token']
@@ -1641,18 +1655,18 @@ class SignedJwtAssertionCredentials(AssertionCredentials):
"""Generate the assertion that will be used in the request.""" """Generate the assertion that will be used in the request."""
now = int(time.time()) now = int(time.time())
payload = { payload = {
'aud': self.token_uri, 'aud': self.token_uri,
'scope': self.scope, 'scope': self.scope,
'iat': now, 'iat': now,
'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS, 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
'iss': self.service_account_name 'iss': self.service_account_name
} }
payload.update(self.kwargs) payload.update(self.kwargs)
logger.debug(str(payload)) logger.debug(str(payload))
private_key = base64.b64decode(self.private_key) private_key = base64.b64decode(self.private_key)
return crypt.make_signed_jwt(crypt.Signer.from_string( return crypt.make_signed_jwt(crypt.Signer.from_string(
private_key, self.private_key_password), payload) private_key, self.private_key_password), payload)
# Only used in verify_id_token(), which is always calling to the same URI # Only used in verify_id_token(), which is always calling to the same URI
# for the certs. # for the certs.
@@ -1712,7 +1726,7 @@ def _extract_id_token(id_token):
if len(segments) != 3: if len(segments) != 3:
raise VerifyJwtTokenError( raise VerifyJwtTokenError(
'Wrong number of segments in token: %s' % id_token) 'Wrong number of segments in token: %s' % id_token)
return json.loads(_from_bytes(_urlsafe_b64decode(segments[1]))) return json.loads(_from_bytes(_urlsafe_b64decode(segments[1])))
@@ -1786,10 +1800,11 @@ def credentials_from_code(client_id, client_secret, scope, code,
access token access token
""" """
flow = OAuth2WebServerFlow(client_id, client_secret, scope, flow = OAuth2WebServerFlow(client_id, client_secret, scope,
redirect_uri=redirect_uri, user_agent=user_agent, redirect_uri=redirect_uri,
auth_uri=auth_uri, token_uri=token_uri, user_agent=user_agent, auth_uri=auth_uri,
revoke_uri=revoke_uri, device_uri=device_uri, token_uri=token_uri, revoke_uri=revoke_uri,
token_info_uri=token_info_uri) device_uri=device_uri,
token_info_uri=token_info_uri)
credentials = flow.step2_exchange(code, http=http) credentials = flow.step2_exchange(code, http=http)
return credentials return credentials
@@ -1836,16 +1851,16 @@ def credentials_from_clientsecrets_and_code(filename, scope, code,
clientsecrets.InvalidClientSecretsError: if the clientsecrets file is clientsecrets.InvalidClientSecretsError: if the clientsecrets file is
invalid. invalid.
""" """
flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache, flow = flow_from_clientsecrets(filename, scope, message=message,
redirect_uri=redirect_uri, cache=cache, redirect_uri=redirect_uri,
device_uri=device_uri) device_uri=device_uri)
credentials = flow.step2_exchange(code, http=http) credentials = flow.step2_exchange(code, http=http)
return credentials return credentials
class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', ( class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
'device_code', 'user_code', 'interval', 'verification_url', 'device_code', 'user_code', 'interval', 'verification_url',
'user_code_expiry'))): 'user_code_expiry'))):
"""Intermediate information the OAuth2 for devices flow.""" """Intermediate information the OAuth2 for devices flow."""
@classmethod @classmethod
@@ -1858,26 +1873,26 @@ class DeviceFlowInfo(collections.namedtuple('DeviceFlowInfo', (
""" """
# device_code, user_code, and verification_url are required. # device_code, user_code, and verification_url are required.
kwargs = { kwargs = {
'device_code': response['device_code'], 'device_code': response['device_code'],
'user_code': response['user_code'], 'user_code': response['user_code'],
} }
# The response may list the verification address as either # The response may list the verification address as either
# verification_url or verification_uri, so we check for both. # verification_url or verification_uri, so we check for both.
verification_url = response.get( verification_url = response.get(
'verification_url', response.get('verification_uri')) 'verification_url', response.get('verification_uri'))
if verification_url is None: if verification_url is None:
raise OAuth2DeviceCodeError( raise OAuth2DeviceCodeError(
'No verification_url provided in server response') 'No verification_url provided in server response')
kwargs['verification_url'] = verification_url kwargs['verification_url'] = verification_url
# expires_in and interval are optional. # expires_in and interval are optional.
kwargs.update({ kwargs.update({
'interval': response.get('interval'), 'interval': response.get('interval'),
'user_code_expiry': None, 'user_code_expiry': None,
}) })
if 'expires_in' in response: if 'expires_in' in response:
kwargs['user_code_expiry'] = datetime.datetime.now() + datetime.timedelta( kwargs['user_code_expiry'] = (
seconds=int(response['expires_in'])) datetime.datetime.now() +
datetime.timedelta(seconds=int(response['expires_in'])))
return cls(**kwargs) return cls(**kwargs)
@@ -1889,18 +1904,18 @@ class OAuth2WebServerFlow(Flow):
@util.positional(4) @util.positional(4)
def __init__(self, client_id, def __init__(self, client_id,
client_secret=None, client_secret=None,
scope=None, scope=None,
redirect_uri=None, redirect_uri=None,
user_agent=None, user_agent=None,
auth_uri=GOOGLE_AUTH_URI, auth_uri=GOOGLE_AUTH_URI,
token_uri=GOOGLE_TOKEN_URI, token_uri=GOOGLE_TOKEN_URI,
revoke_uri=GOOGLE_REVOKE_URI, revoke_uri=GOOGLE_REVOKE_URI,
login_hint=None, login_hint=None,
device_uri=GOOGLE_DEVICE_URI, device_uri=GOOGLE_DEVICE_URI,
token_info_uri=GOOGLE_TOKEN_INFO_URI, token_info_uri=GOOGLE_TOKEN_INFO_URI,
authorization_header=None, authorization_header=None,
**kwargs): **kwargs):
"""Constructor for OAuth2WebServerFlow. """Constructor for OAuth2WebServerFlow.
The kwargs argument is used to set extra query parameters on the The kwargs argument is used to set extra query parameters on the
@@ -1915,7 +1930,8 @@ class OAuth2WebServerFlow(Flow):
redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob'
for a non-web-based application, or a URI that for a non-web-based application, or a URI that
handles the callback from the authorization server. handles the callback from the authorization server.
user_agent: string, HTTP User-Agent to provide for this application. user_agent: string, HTTP User-Agent to provide for this
application.
auth_uri: string, URI for authorization endpoint. For convenience auth_uri: string, URI for authorization endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider defaults to Google's endpoints but any OAuth 2.0 provider
can be used. can be used.
@@ -1956,8 +1972,8 @@ class OAuth2WebServerFlow(Flow):
self.token_info_uri = token_info_uri self.token_info_uri = token_info_uri
self.authorization_header = authorization_header self.authorization_header = authorization_header
self.params = { self.params = {
'access_type': 'offline', 'access_type': 'offline',
'response_type': 'code', 'response_type': 'code',
} }
self.params.update(kwargs) self.params.update(kwargs)
@@ -1981,18 +1997,19 @@ class OAuth2WebServerFlow(Flow):
""" """
if redirect_uri is not None: if redirect_uri is not None:
logger.warning(( logger.warning((
'The redirect_uri parameter for ' 'The redirect_uri parameter for '
'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please ' 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. '
'move to passing the redirect_uri in via the constructor.')) 'Please move to passing the redirect_uri in via the '
'constructor.'))
self.redirect_uri = redirect_uri self.redirect_uri = redirect_uri
if self.redirect_uri is None: if self.redirect_uri is None:
raise ValueError('The value of redirect_uri must not be None.') raise ValueError('The value of redirect_uri must not be None.')
query_params = { query_params = {
'client_id': self.client_id, 'client_id': self.client_id,
'redirect_uri': self.redirect_uri, 'redirect_uri': self.redirect_uri,
'scope': self.scope, 'scope': self.scope,
} }
if state is not None: if state is not None:
query_params['state'] = state query_params['state'] = state
@@ -2013,11 +2030,11 @@ class OAuth2WebServerFlow(Flow):
raise ValueError('The value of device_uri must not be None.') raise ValueError('The value of device_uri must not be None.')
body = urllib.parse.urlencode({ body = urllib.parse.urlencode({
'client_id': self.client_id, 'client_id': self.client_id,
'scope': self.scope, 'scope': self.scope,
}) })
headers = { headers = {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
} }
if self.user_agent is not None: if self.user_agent is not None:
@@ -2027,15 +2044,15 @@ class OAuth2WebServerFlow(Flow):
http = httplib2.Http() http = httplib2.Http()
resp, content = http.request(self.device_uri, method='POST', body=body, resp, content = http.request(self.device_uri, method='POST', body=body,
headers=headers) headers=headers)
content = _from_bytes(content) content = _from_bytes(content)
if resp.status == 200: if resp.status == 200:
try: try:
flow_info = json.loads(content) flow_info = json.loads(content)
except ValueError as e: except ValueError as e:
raise OAuth2DeviceCodeError( raise OAuth2DeviceCodeError(
'Could not parse server response as JSON: "%s", error: "%s"' % ( 'Could not parse server response as JSON: "%s", '
content, e)) 'error: "%s"' % (content, e))
return DeviceFlowInfo.FromResponse(flow_info) return DeviceFlowInfo.FromResponse(flow_info)
else: else:
error_msg = 'Invalid response %s.' % resp.status error_msg = 'Invalid response %s.' % resp.status
@@ -2044,12 +2061,13 @@ class OAuth2WebServerFlow(Flow):
if 'error' in d: if 'error' in d:
error_msg += ' Error: %s' % d['error'] error_msg += ' Error: %s' % d['error']
except ValueError: except ValueError:
# Couldn't decode a JSON response, stick with the default message. # Couldn't decode a JSON response, stick with the
# default message.
pass pass
raise OAuth2DeviceCodeError(error_msg) raise OAuth2DeviceCodeError(error_msg)
@util.positional(2) @util.positional(2)
def step2_exchange(self, code=None, http=None, device_flow_info=None): def step2_exchange(self, code=None, http=None, device_flow_info=None):
"""Exchanges a code for OAuth2Credentials. """Exchanges a code for OAuth2Credentials.
Args: Args:
@@ -2081,13 +2099,13 @@ class OAuth2WebServerFlow(Flow):
elif not isinstance(code, six.string_types): elif not isinstance(code, six.string_types):
if 'code' not in code: if 'code' not in code:
raise FlowExchangeError(code.get( raise FlowExchangeError(code.get(
'error', 'No code was supplied in the query parameters.')) 'error', 'No code was supplied in the query parameters.'))
code = code['code'] code = code['code']
post_data = { post_data = {
'client_id': self.client_id, 'client_id': self.client_id,
'code': code, 'code': code,
'scope': self.scope, 'scope': self.scope,
} }
if self.client_secret is not None: if self.client_secret is not None:
post_data['client_secret'] = self.client_secret post_data['client_secret'] = self.client_secret
@@ -2098,7 +2116,7 @@ class OAuth2WebServerFlow(Flow):
post_data['redirect_uri'] = self.redirect_uri post_data['redirect_uri'] = self.redirect_uri
body = urllib.parse.urlencode(post_data) body = urllib.parse.urlencode(post_data)
headers = { headers = {
'content-type': 'application/x-www-form-urlencoded', 'content-type': 'application/x-www-form-urlencoded',
} }
if self.authorization_header is not None: if self.authorization_header is not None:
headers['Authorization'] = self.authorization_header headers['Authorization'] = self.authorization_header
@@ -2109,38 +2127,38 @@ class OAuth2WebServerFlow(Flow):
http = httplib2.Http() http = httplib2.Http()
resp, content = http.request(self.token_uri, method='POST', body=body, resp, content = http.request(self.token_uri, method='POST', body=body,
headers=headers) headers=headers)
d = _parse_exchange_token_response(content) d = _parse_exchange_token_response(content)
if resp.status == 200 and 'access_token' in d: if resp.status == 200 and 'access_token' in d:
access_token = d['access_token'] access_token = d['access_token']
refresh_token = d.get('refresh_token', None) refresh_token = d.get('refresh_token', None)
if not refresh_token: if not refresh_token:
logger.info( logger.info(
'Received token response with no refresh_token. Consider ' 'Received token response with no refresh_token. Consider '
"reauthenticating with approval_prompt='force'.") "reauthenticating with approval_prompt='force'.")
token_expiry = None token_expiry = None
if 'expires_in' in d: if 'expires_in' in d:
token_expiry = datetime.datetime.utcnow() + datetime.timedelta( token_expiry = (
seconds=int(d['expires_in'])) datetime.datetime.utcnow() +
datetime.timedelta(seconds=int(d['expires_in'])))
extracted_id_token = None extracted_id_token = None
if 'id_token' in d: if 'id_token' in d:
extracted_id_token = _extract_id_token(d['id_token']) extracted_id_token = _extract_id_token(d['id_token'])
logger.info('Successfully retrieved access token') logger.info('Successfully retrieved access token')
return OAuth2Credentials(access_token, self.client_id, return OAuth2Credentials(
self.client_secret, refresh_token, token_expiry, access_token, self.client_id, self.client_secret,
self.token_uri, self.user_agent, refresh_token, token_expiry, self.token_uri, self.user_agent,
revoke_uri=self.revoke_uri, revoke_uri=self.revoke_uri, id_token=extracted_id_token,
id_token=extracted_id_token, token_response=d, scopes=self.scope,
token_response=d, token_info_uri=self.token_info_uri)
scopes=self.scope,
token_info_uri=self.token_info_uri)
else: else:
logger.info('Failed to retrieve access token: %s', content) logger.info('Failed to retrieve access token: %s', content)
if 'error' in d: if 'error' in d:
# you never know what those providers got to say # you never know what those providers got to say
error_msg = str(d['error']) + str(d.get('error_description', '')) error_msg = (str(d['error']) +
str(d.get('error_description', '')))
else: else:
error_msg = 'Invalid response: %s.' % str(resp.status) error_msg = 'Invalid response: %s.' % str(resp.status)
raise FlowExchangeError(error_msg) raise FlowExchangeError(error_msg)
@@ -2187,13 +2205,15 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
invalid. invalid.
""" """
try: try:
client_type, client_info = clientsecrets.loadfile(filename, cache=cache) client_type, client_info = clientsecrets.loadfile(filename,
if client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): cache=cache)
if client_type in (clientsecrets.TYPE_WEB,
clientsecrets.TYPE_INSTALLED):
constructor_kwargs = { constructor_kwargs = {
'redirect_uri': redirect_uri, 'redirect_uri': redirect_uri,
'auth_uri': client_info['auth_uri'], 'auth_uri': client_info['auth_uri'],
'token_uri': client_info['token_uri'], 'token_uri': client_info['token_uri'],
'login_hint': login_hint, 'login_hint': login_hint,
} }
revoke_uri = client_info.get('revoke_uri') revoke_uri = client_info.get('revoke_uri')
if revoke_uri is not None: if revoke_uri is not None:
@@ -2201,8 +2221,8 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
if device_uri is not None: if device_uri is not None:
constructor_kwargs['device_uri'] = device_uri constructor_kwargs['device_uri'] = device_uri
return OAuth2WebServerFlow( return OAuth2WebServerFlow(
client_info['client_id'], client_info['client_secret'], client_info['client_id'], client_info['client_secret'],
scope, **constructor_kwargs) scope, **constructor_kwargs)
except clientsecrets.InvalidClientSecretsError: except clientsecrets.InvalidClientSecretsError:
if message: if message:
@@ -2211,4 +2231,4 @@ def flow_from_clientsecrets(filename, scope, redirect_uri=None,
raise raise
else: else:
raise UnknownClientSecretsFlowError( raise UnknownClientSecretsFlowError(
'This OAuth 2.0 flow is unsupported: %r' % client_type) 'This OAuth 2.0 flow is unsupported: %r' % client_type)

View File

@@ -70,30 +70,31 @@ class InvalidClientSecretsError(Error):
def _validate_clientsecrets(obj): def _validate_clientsecrets(obj):
_INVALID_FILE_FORMAT_MSG = ( _INVALID_FILE_FORMAT_MSG = (
'Invalid file format. See ' 'Invalid file format. See '
'https://developers.google.com/api-client-library/' 'https://developers.google.com/api-client-library/'
'python/guide/aaa_client_secrets') 'python/guide/aaa_client_secrets')
if obj is None: if obj is None:
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG) raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
if len(obj) != 1: if len(obj) != 1:
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
_INVALID_FILE_FORMAT_MSG + ' ' _INVALID_FILE_FORMAT_MSG + ' '
'Expected a JSON object with a single property for a "web" or ' 'Expected a JSON object with a single property for a "web" or '
'"installed" application') '"installed" application')
client_type = tuple(obj)[0] client_type = tuple(obj)[0]
if client_type not in VALID_CLIENT: if client_type not in VALID_CLIENT:
raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type, )) raise InvalidClientSecretsError(
'Unknown client type: %s.' % (client_type,))
client_info = obj[client_type] client_info = obj[client_type]
for prop_name in VALID_CLIENT[client_type]['required']: for prop_name in VALID_CLIENT[client_type]['required']:
if prop_name not in client_info: if prop_name not in client_info:
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
'Missing property "%s" in a client type of "%s".' % (prop_name, 'Missing property "%s" in a client type of "%s".' %
client_type)) (prop_name, client_type))
for prop_name in VALID_CLIENT[client_type]['string']: for prop_name in VALID_CLIENT[client_type]['string']:
if client_info[prop_name].startswith('[['): if client_info[prop_name].startswith('[['):
raise InvalidClientSecretsError( raise InvalidClientSecretsError(
'Property "%s" is not configured.' % prop_name) 'Property "%s" is not configured.' % prop_name)
return client_type, client_info return client_type, client_info

View File

@@ -45,11 +45,9 @@ except ImportError:
OpenSSLVerifier = None OpenSSLVerifier = None
OpenSSLSigner = None OpenSSLSigner = None
def pkcs12_key_as_pem(*args, **kwargs): def pkcs12_key_as_pem(*args, **kwargs):
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.') raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
try: try:
from oauth2client._pycrypto_crypt import PyCryptoVerifier from oauth2client._pycrypto_crypt import PyCryptoVerifier
from oauth2client._pycrypto_crypt import PyCryptoSigner from oauth2client._pycrypto_crypt import PyCryptoSigner
@@ -66,7 +64,7 @@ elif PyCryptoSigner:
Verifier = PyCryptoVerifier Verifier = PyCryptoVerifier
else: else:
raise ImportError('No encryption library found. Please install either ' raise ImportError('No encryption library found. Please install either '
'PyOpenSSL, or PyCrypto 2.6 or later') 'PyOpenSSL, or PyCrypto 2.6 or later')
def make_signed_jwt(signer, payload): def make_signed_jwt(signer, payload):
@@ -157,10 +155,10 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
if now < earliest: if now < earliest:
raise AppIdentityError('Token used too early, %d < %d: %s' % raise AppIdentityError('Token used too early, %d < %d: %s' %
(now, earliest, json_body)) (now, earliest, json_body))
if now > latest: if now > latest:
raise AppIdentityError('Token used too late, %d > %d: %s' % raise AppIdentityError('Token used too late, %d > %d: %s' %
(now, latest, json_body)) (now, latest, json_body))
# Check audience. # Check audience.
if audience is not None: if audience is not None:
@@ -169,6 +167,6 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
raise AppIdentityError('No aud field in token: %s' % json_body) raise AppIdentityError('No aud field in token: %s' % json_body)
if aud != audience: if aud != audience:
raise AppIdentityError('Wrong recipient, %s != %s: %s' % raise AppIdentityError('Wrong recipient, %s != %s: %s' %
(aud, audience, json_body)) (aud, audience, json_body))
return parsed return parsed

View File

@@ -35,8 +35,9 @@ class CommunicationError(Error):
class NoDevshellServer(Error): class NoDevshellServer(Error):
"""Error when no Developer Shell server can be contacted.""" """Error when no Developer Shell server can be contacted."""
# The request for credential information to the Developer Shell client socket is # The request for credential information to the Developer Shell client socket
# always an empty PBLite-formatted JSON object, so just define it as a constant. # is always an empty PBLite-formatted JSON object, so just define it as a
# constant.
CREDENTIAL_INFO_REQUEST_JSON = '[]' CREDENTIAL_INFO_REQUEST_JSON = '[]'
@@ -44,7 +45,9 @@ class CredentialInfoResponse(object):
"""Credential information response from Developer Shell server. """Credential information response from Developer Shell server.
The credential information response from Developer Shell socket is a The credential information response from Developer Shell socket is a
PBLite-formatted JSON array with fields encoded by their index in the array: PBLite-formatted JSON array with fields encoded by their index in the
array:
* Index 0 - user email * Index 0 - user email
* Index 1 - default project ID. None if the project context is not known. * Index 1 - default project ID. None if the project context is not known.
* Index 2 - OAuth2 access token. None if there is no valid auth context. * Index 2 - OAuth2 access token. None if there is no valid auth context.
@@ -103,13 +106,13 @@ class DevshellCredentials(client.GoogleCredentials):
def __init__(self, user_agent=None): def __init__(self, user_agent=None):
super(DevshellCredentials, self).__init__( super(DevshellCredentials, self).__init__(
None, # access_token, initialized below None, # access_token, initialized below
None, # client_id None, # client_id
None, # client_secret None, # client_secret
None, # refresh_token None, # refresh_token
None, # token_expiry None, # token_expiry
None, # token_uri None, # token_uri
user_agent) user_agent)
self._refresh(None) self._refresh(None)
def _refresh(self, http_request): def _refresh(self, http_request):
@@ -127,9 +130,9 @@ class DevshellCredentials(client.GoogleCredentials):
@classmethod @classmethod
def from_json(cls, json_data): def from_json(cls, json_data):
raise NotImplementedError( raise NotImplementedError(
'Cannot load Developer Shell credentials from JSON.') 'Cannot load Developer Shell credentials from JSON.')
@property @property
def serialization_data(self): def serialization_data(self):
raise NotImplementedError( raise NotImplementedError(
'Cannot serialize Developer Shell credentials.') 'Cannot serialize Developer Shell credentials.')

View File

@@ -93,7 +93,8 @@ class Storage(BaseStorage):
Args: Args:
model: db.Model, model class model: db.Model, model class
key_name: string, key name for the entity that has the credentials key_name: string, key name for the entity that has the credentials
key_value: string, key value for the entity that has the credentials key_value: string, key value for the entity that has the
credentials
property_name: string, name of the property that is an property_name: string, name of the property that is an
CredentialsProperty CredentialsProperty
""" """
@@ -124,12 +125,14 @@ class Storage(BaseStorage):
Args: Args:
credentials: Credentials, the credentials to store. credentials: Credentials, the credentials to store.
overwrite: Boolean, indicates whether you would like these overwrite: Boolean, indicates whether you would like these
credentials to overwrite any existing stored credentials. credentials to overwrite any existing stored
credentials.
""" """
args = {self.key_name: self.key_value} args = {self.key_name: self.key_value}
if overwrite: if overwrite:
entity, unused_is_new = self.model_class.objects.get_or_create(**args) (entity,
unused_is_new) = self.model_class.objects.get_or_create(**args)
else: else:
entity = self.model_class(**args) entity = self.model_class(**args)

View File

@@ -42,7 +42,7 @@ class Storage(BaseStorage):
def _validate_file(self): def _validate_file(self):
if os.path.islink(self._filename): if os.path.islink(self._filename):
raise CredentialsFileSymbolicLinkError( raise CredentialsFileSymbolicLinkError(
'File: %s is a symbolic link.' % self._filename) 'File: %s is a symbolic link.' % self._filename)
def acquire_lock(self): def acquire_lock(self):
"""Acquires any lock necessary to access this Storage. """Acquires any lock necessary to access this Storage.

View File

@@ -46,8 +46,8 @@ apiui/credential>`__.
Usage Usage
===== =====
Once configured, you can use the :meth:`UserOAuth2.required` decorator to ensure Once configured, you can use the :meth:`UserOAuth2.required` decorator to
that credentials are available within a view. ensure that credentials are available within a view.
.. code-block:: python .. code-block:: python
:emphasize-lines: 3,7,10 :emphasize-lines: 3,7,10
@@ -190,7 +190,7 @@ from oauth2client import util
__author__ = 'jonwayne@google.com (Jon Wayne Parrott)' __author__ = 'jonwayne@google.com (Jon Wayne Parrott)'
DEFAULT_SCOPES = ('email', ) DEFAULT_SCOPES = ('email',)
class UserOAuth2(object): class UserOAuth2(object):
@@ -291,8 +291,9 @@ class UserOAuth2(object):
raise ValueError( raise ValueError(
'OAuth2 configuration could not be found. Either specify the ' 'OAuth2 configuration could not be found. Either specify the '
'client_secrets_file or client_id and client_secret or set the' 'client_secrets_file or client_id and client_secret or set the'
'app configuration variables GOOGLE_OAUTH2_CLIENT_SECRETS_FILE ' 'app configuration variables '
'or GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.') 'GOOGLE_OAUTH2_CLIENT_SECRETS_FILE or '
'GOOGLE_OAUTH2_CLIENT_ID and GOOGLE_OAUTH2_CLIENT_SECRET.')
def _load_client_secrets(self, filename): def _load_client_secrets(self, filename):
"""Loads client secrets from the given filename.""" """Loads client secrets from the given filename."""
@@ -392,7 +393,8 @@ class UserOAuth2(object):
credentials = flow.step2_exchange(code) credentials = flow.step2_exchange(code)
except FlowExchangeError as exchange_error: except FlowExchangeError as exchange_error:
current_app.logger.exception(exchange_error) current_app.logger.exception(exchange_error)
return 'An error occurred: %s' % exchange_error, httplib.BAD_REQUEST content = 'An error occurred: %s' % (exchange_error,)
return content, httplib.BAD_REQUEST
# Save the credentials to the storage. # Save the credentials to the storage.
self.storage.put(credentials) self.storage.put(credentials)
@@ -420,9 +422,9 @@ class UserOAuth2(object):
def email(self): def email(self):
"""Returns the user's email address or None if there are no credentials. """Returns the user's email address or None if there are no credentials.
The email address is provided by the current credentials' id_token. This The email address is provided by the current credentials' id_token.
should not be used as unique identifier as the user can change their This should not be used as unique identifier as the user can change
email. If you need a unique identifier, use user_id. their email. If you need a unique identifier, use user_id.
""" """
if not self.credentials: if not self.credentials:
return None return None
@@ -451,8 +453,9 @@ class UserOAuth2(object):
def authorize_url(self, return_url, **kwargs): def authorize_url(self, return_url, **kwargs):
"""Creates a URL that can be used to start the authorization flow. """Creates a URL that can be used to start the authorization flow.
When the user is directed to the URL, the authorization flow will begin. When the user is directed to the URL, the authorization flow will
Once complete, the user will be redirected to the specified return URL. begin. Once complete, the user will be redirected to the specified
return URL.
Any kwargs are passed into the flow constructor. Any kwargs are passed into the flow constructor.
""" """

View File

@@ -44,9 +44,9 @@ class AppAssertionCredentials(AssertionCredentials):
used for the purpose of accessing data stored under an account assigned to used for the purpose of accessing data stored under an account assigned to
the Compute Engine instance itself. the Compute Engine instance itself.
This credential does not require a flow to instantiate because it represents This credential does not require a flow to instantiate because it
a two legged flow, and therefore has all of the required information to represents a two legged flow, and therefore has all of the required
generate and refresh its own access tokens. information to generate and refresh its own access tokens.
""" """
@util.positional(2) @util.positional(2)
@@ -60,7 +60,8 @@ class AppAssertionCredentials(AssertionCredentials):
self.scope = util.scopes_to_string(scope) self.scope = util.scopes_to_string(scope)
self.kwargs = kwargs self.kwargs = kwargs
# Assertion type is no longer used, but still in the parent class signature. # Assertion type is no longer used, but still in the
# parent class signature.
super(AppAssertionCredentials, self).__init__(None) super(AppAssertionCredentials, self).__init__(None)
@classmethod @classmethod
@@ -74,9 +75,9 @@ class AppAssertionCredentials(AssertionCredentials):
Skip all the storage hoops and just refresh using the API. Skip all the storage hoops and just refresh using the API.
Args: Args:
http_request: callable, a callable that matches the method signature http_request: callable, a callable that matches the method
of httplib2.Http.request, used to make the refresh signature of httplib2.Http.request, used to make
request. the refresh request.
Raises: Raises:
AccessTokenRefreshError: When the refresh fails. AccessTokenRefreshError: When the refresh fails.
@@ -94,13 +95,13 @@ class AppAssertionCredentials(AssertionCredentials):
else: else:
if response.status == 404: if response.status == 404:
content += (' This can occur if a VM was created' content += (' This can occur if a VM was created'
' with no service account or scopes.') ' with no service account or scopes.')
raise AccessTokenRefreshError(content) raise AccessTokenRefreshError(content)
@property @property
def serialization_data(self): def serialization_data(self):
raise NotImplementedError( raise NotImplementedError(
'Cannot serialize credentials for GCE service accounts.') 'Cannot serialize credentials for GCE service accounts.')
def create_scoped_required(self): def create_scoped_required(self):
return not self.scope return not self.scope

View File

@@ -103,7 +103,7 @@ class Storage(BaseStorage):
credentials: Credentials, the credentials to store. credentials: Credentials, the credentials to store.
""" """
keyring.set_password(self._service_name, self._user_name, keyring.set_password(self._service_name, self._user_name,
credentials.to_json()) credentials.to_json())
def locked_delete(self): def locked_delete(self):
"""Delete Credentials file. """Delete Credentials file.

View File

@@ -57,7 +57,7 @@ class AlreadyLockedException(Exception):
def validate_file(filename): def validate_file(filename):
if os.path.islink(filename): if os.path.islink(filename):
raise CredentialsFileSymbolicLinkError( raise CredentialsFileSymbolicLinkError(
'File: %s is a symbolic link.' % filename) 'File: %s is a symbolic link.' % filename)
class _Opener(object): class _Opener(object):
@@ -123,7 +123,7 @@ class _PosixOpener(_Opener):
""" """
if self._locked: if self._locked:
raise AlreadyLockedException('File %s is already locked' % raise AlreadyLockedException('File %s is already locked' %
self._filename) self._filename)
self._locked = False self._locked = False
validate_file(self._filename) validate_file(self._filename)
@@ -140,7 +140,7 @@ class _PosixOpener(_Opener):
while True: while True:
try: try:
self._lock_fd = os.open(lock_filename, self._lock_fd = os.open(lock_filename,
os.O_CREAT | os.O_EXCL | os.O_RDWR) os.O_CREAT | os.O_EXCL | os.O_RDWR)
self._locked = True self._locked = True
break break
@@ -149,7 +149,7 @@ class _PosixOpener(_Opener):
raise raise
if (time.time() - start_time) >= timeout: if (time.time() - start_time) >= timeout:
logger.warn('Could not acquire lock %s in %s seconds', logger.warn('Could not acquire lock %s in %s seconds',
lock_filename, timeout) lock_filename, timeout)
# Close the file and open in fallback_mode. # Close the file and open in fallback_mode.
if self._fh: if self._fh:
self._fh.close() self._fh.close()
@@ -176,7 +176,6 @@ class _PosixOpener(_Opener):
try: try:
import fcntl import fcntl
class _FcntlOpener(_Opener): class _FcntlOpener(_Opener):
"""Open, lock, and unlock a file using fcntl.lockf.""" """Open, lock, and unlock a file using fcntl.lockf."""
@@ -190,18 +189,20 @@ try:
Raises: Raises:
AlreadyLockedException: if the lock is already acquired. AlreadyLockedException: if the lock is already acquired.
IOError: if the open fails. IOError: if the open fails.
CredentialsFileSymbolicLinkError if the file is a symbolic link. CredentialsFileSymbolicLinkError: if the file is a symbolic
link.
""" """
if self._locked: if self._locked:
raise AlreadyLockedException('File %s is already locked' % raise AlreadyLockedException('File %s is already locked' %
self._filename) self._filename)
start_time = time.time() start_time = time.time()
validate_file(self._filename) validate_file(self._filename)
try: try:
self._fh = open(self._filename, self._mode) self._fh = open(self._filename, self._mode)
except IOError as e: except IOError as e:
# If we can't access with _mode, try _fallback_mode and don't lock. # If we can't access with _mode, try _fallback_mode and
# don't lock.
if e.errno in (errno.EPERM, errno.EACCES): if e.errno in (errno.EPERM, errno.EACCES):
self._fh = open(self._filename, self._fallback_mode) self._fh = open(self._filename, self._fallback_mode)
return return
@@ -221,7 +222,7 @@ try:
# We could not acquire the lock. Try again. # We could not acquire the lock. Try again.
if (time.time() - start_time) >= timeout: if (time.time() - start_time) >= timeout:
logger.warn('Could not lock %s in %s seconds', logger.warn('Could not lock %s in %s seconds',
self._filename, timeout) self._filename, timeout)
if self._fh: if self._fh:
self._fh.close() self._fh.close()
self._fh = open(self._filename, self._fallback_mode) self._fh = open(self._filename, self._fallback_mode)
@@ -244,7 +245,6 @@ try:
import win32con import win32con
import win32file import win32file
class _Win32Opener(_Opener): class _Win32Opener(_Opener):
"""Open, lock, and unlock a file using windows primitives.""" """Open, lock, and unlock a file using windows primitives."""
@@ -271,14 +271,15 @@ try:
""" """
if self._locked: if self._locked:
raise AlreadyLockedException('File %s is already locked' % raise AlreadyLockedException('File %s is already locked' %
self._filename) self._filename)
start_time = time.time() start_time = time.time()
validate_file(self._filename) validate_file(self._filename)
try: try:
self._fh = open(self._filename, self._mode) self._fh = open(self._filename, self._mode)
except IOError as e: except IOError as e:
# If we can't access with _mode, try _fallback_mode and don't lock. # If we can't access with _mode, try _fallback_mode
# and don't lock.
if e.errno == errno.EACCES: if e.errno == errno.EACCES:
self._fh = open(self._filename, self._fallback_mode) self._fh = open(self._filename, self._fallback_mode)
return return
@@ -288,24 +289,25 @@ try:
try: try:
hfile = win32file._get_osfhandle(self._fh.fileno()) hfile = win32file._get_osfhandle(self._fh.fileno())
win32file.LockFileEx( win32file.LockFileEx(
hfile, hfile,
(win32con.LOCKFILE_FAIL_IMMEDIATELY | (win32con.LOCKFILE_FAIL_IMMEDIATELY |
win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000, win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
pywintypes.OVERLAPPED()) pywintypes.OVERLAPPED())
self._locked = True self._locked = True
return return
except pywintypes.error as e: except pywintypes.error as e:
if timeout == 0: if timeout == 0:
raise raise
# If the error is not that the file is already in use, raise. # If the error is not that the file is already
# in use, raise.
if e[0] != _Win32Opener.FILE_IN_USE_ERROR: if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
raise raise
# We could not acquire the lock. Try again. # We could not acquire the lock. Try again.
if (time.time() - start_time) >= timeout: if (time.time() - start_time) >= timeout:
logger.warn('Could not lock %s in %s seconds' % ( logger.warn('Could not lock %s in %s seconds' % (
self._filename, timeout)) self._filename, timeout))
if self._fh: if self._fh:
self._fh.close() self._fh.close()
self._fh = open(self._filename, self._fallback_mode) self._fh = open(self._filename, self._fallback_mode)
@@ -317,12 +319,13 @@ try:
if self._locked: if self._locked:
try: try:
hfile = win32file._get_osfhandle(self._fh.fileno()) hfile = win32file._get_osfhandle(self._fh.fileno())
win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED()) win32file.UnlockFileEx(hfile, 0, -0x10000,
pywintypes.OVERLAPPED())
except pywintypes.error as e: except pywintypes.error as e:
if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR: if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
raise raise
self._locked = False self._locked = False
if self._fh: if self._fh:
self._fh.close() self._fh.close()
except ImportError: except ImportError:
_Win32Opener = None _Win32Opener = None

View File

@@ -91,14 +91,14 @@ def get_credential_storage(filename, client_id, user_agent, scope,
""" """
# Recreate the legacy key with these specific parameters # Recreate the legacy key with these specific parameters
key = {'clientId': client_id, 'userAgent': user_agent, key = {'clientId': client_id, 'userAgent': user_agent,
'scope': util.scopes_to_string(scope)} 'scope': util.scopes_to_string(scope)}
return get_credential_storage_custom_key( return get_credential_storage_custom_key(
filename, key, warn_on_readonly=warn_on_readonly) filename, key, warn_on_readonly=warn_on_readonly)
@util.positional(2) @util.positional(2)
def get_credential_storage_custom_string_key( def get_credential_storage_custom_string_key(filename, key_string,
filename, key_string, warn_on_readonly=True): warn_on_readonly=True):
"""Get a Storage instance for a credential using a single string as a key. """Get a Storage instance for a credential using a single string as a key.
Allows you to provide a string as a custom key that will be used for Allows you to provide a string as a custom key that will be used for
@@ -120,8 +120,8 @@ def get_credential_storage_custom_string_key(
@util.positional(2) @util.positional(2)
def get_credential_storage_custom_key( def get_credential_storage_custom_key(filename, key_dict,
filename, key_dict, warn_on_readonly=True): warn_on_readonly=True):
"""Get a Storage instance for a credential using a dictionary as a key. """Get a Storage instance for a credential using a dictionary as a key.
Allows you to provide a dictionary as a custom key that will be used for Allows you to provide a dictionary as a custom key that will be used for
@@ -179,7 +179,7 @@ def _get_multistore(filename, warn_on_readonly=True):
_multistores_lock.acquire() _multistores_lock.acquire()
try: try:
multistore = _multistores.setdefault( multistore = _multistores.setdefault(
filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly)) filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
finally: finally:
_multistores_lock.release() _multistores_lock.release()
return multistore return multistore
@@ -211,7 +211,7 @@ class _MultiStore(object):
self._data = None self._data = None
class _Storage(BaseStorage): class _Storage(BaseStorage):
"""A Storage object that knows how to read/write a single credential.""" """A Storage object that can read/write a single credential."""
def __init__(self, multistore, key): def __init__(self, multistore, key):
self._multistore = multistore self._multistore = multistore
@@ -285,19 +285,20 @@ class _MultiStore(object):
self._file.open_and_lock() self._file.open_and_lock()
except IOError as e: except IOError as e:
if e.errno == errno.ENOSYS: if e.errno == errno.ENOSYS:
logger.warn('File system does not support locking the credentials ' logger.warn('File system does not support locking the '
'file.') 'credentials file.')
elif e.errno == errno.ENOLCK: elif e.errno == errno.ENOLCK:
logger.warn('File system is out of resources for writing the ' logger.warn('File system is out of resources for writing the '
'credentials file (is your disk full?).') 'credentials file (is your disk full?).')
else: else:
raise raise
if not self._file.is_locked(): if not self._file.is_locked():
self._read_only = True self._read_only = True
if self._warn_on_readonly: if self._warn_on_readonly:
logger.warn('The credentials file (%s) is not writable. Opening in ' logger.warn('The credentials file (%s) is not writable. '
'read-only mode. Any refreshed credentials will only be ' 'Opening in read-only mode. Any refreshed '
'valid for this run.', self._file.filename()) 'credentials will only be '
'valid for this run.', self._file.filename())
if os.path.getsize(self._file.filename()) == 0: if os.path.getsize(self._file.filename()) == 0:
logger.debug('Initializing empty multistore file') logger.debug('Initializing empty multistore file')
# The multistore is empty so write out an empty file. # The multistore is empty so write out an empty file.
@@ -340,7 +341,8 @@ class _MultiStore(object):
if self._read_only: if self._read_only:
return return
self._file.file_handle().seek(0) self._file.file_handle().seek(0)
json.dump(data, self._file.file_handle(), sort_keys=True, indent=2, separators=(',', ': ')) json.dump(data, self._file.file_handle(),
sort_keys=True, indent=2, separators=(',', ': '))
self._file.file_handle().truncate() self._file.file_handle().truncate()
def _refresh_data_cache(self): def _refresh_data_cache(self):
@@ -357,7 +359,7 @@ class _MultiStore(object):
raw_data = self._locked_json_read() raw_data = self._locked_json_read()
except Exception: except Exception:
logger.warn('Credential data store could not be loaded. ' logger.warn('Credential data store could not be loaded. '
'Will ignore and overwrite.') 'Will ignore and overwrite.')
return return
version = 0 version = 0
@@ -365,11 +367,11 @@ class _MultiStore(object):
version = raw_data['file_version'] version = raw_data['file_version']
except Exception: except Exception:
logger.warn('Missing version for credential data store. It may be ' logger.warn('Missing version for credential data store. It may be '
'corrupt or an old version. Overwriting.') 'corrupt or an old version. Overwriting.')
if version > 1: if version > 1:
raise NewerCredentialStoreError( raise NewerCredentialStoreError(
'Credential file has file_version of %d. ' 'Credential file has file_version of %d. '
'Only file_version of 1 is supported.' % version) 'Only file_version of 1 is supported.' % version)
credentials = [] credentials = []
try: try:
@@ -379,11 +381,12 @@ class _MultiStore(object):
for cred_entry in credentials: for cred_entry in credentials:
try: try:
(key, credential) = self._decode_credential_from_json(cred_entry) key, credential = self._decode_credential_from_json(cred_entry)
self._data[key] = credential self._data[key] = credential
except: except:
# If something goes wrong loading a credential, just ignore it # If something goes wrong loading a credential, just ignore it
logger.info('Error decoding credential, skipping', exc_info=True) logger.info('Error decoding credential, skipping',
exc_info=True)
def _decode_credential_from_json(self, cred_entry): def _decode_credential_from_json(self, cred_entry):
"""Load a credential from our JSON serialization. """Load a credential from our JSON serialization.
@@ -398,7 +401,8 @@ class _MultiStore(object):
raw_key = cred_entry['key'] raw_key = cred_entry['key']
key = util.dict_to_tuple_key(raw_key) key = util.dict_to_tuple_key(raw_key)
credential = None credential = None
credential = Credentials.new_from_json(json.dumps(cred_entry['credential'])) credential = Credentials.new_from_json(
json.dumps(cred_entry['credential']))
return (key, credential) return (key, credential)
def _write(self): def _write(self):

View File

@@ -72,7 +72,8 @@ def run(flow, storage, http=None):
of values. of values.
``--[no]auth_local_webserver`` (boolean, default: ``True``) ``--[no]auth_local_webserver`` (boolean, default: ``True``)
Run a local web server to handle redirects during OAuth authorization. Run a local web server to handle redirects during OAuth
authorization.
Since it uses flags make sure to initialize the ``gflags`` module before Since it uses flags make sure to initialize the ``gflags`` module before
calling ``run()``. calling ``run()``.
@@ -87,8 +88,8 @@ def run(flow, storage, http=None):
Credentials, the obtained credential. Credentials, the obtained credential.
""" """
logging.warning('This function, oauth2client.tools.run(), and the use of ' logging.warning('This function, oauth2client.tools.run(), and the use of '
'the gflags library are deprecated and will be removed in a future ' 'the gflags library are deprecated and will be removed in '
'version of the library.') 'a future version of the library.')
if FLAGS.auth_local_webserver: if FLAGS.auth_local_webserver:
success = False success = False
port_number = 0 port_number = 0
@@ -96,7 +97,7 @@ def run(flow, storage, http=None):
port_number = port port_number = port
try: try:
httpd = ClientRedirectServer((FLAGS.auth_host_name, port), httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
ClientRedirectHandler) ClientRedirectHandler)
except socket.error as e: except socket.error as e:
pass pass
else: else:
@@ -104,13 +105,14 @@ def run(flow, storage, http=None):
break break
FLAGS.auth_local_webserver = success FLAGS.auth_local_webserver = success
if not success: if not success:
print('Failed to start a local webserver listening on either port 8080') print('Failed to start a local webserver listening on '
print('or port 9090. Please check your firewall settings and locally') 'either port 8080')
print('running programs that may be blocking or using those ports.') print('or port 9090. Please check your firewall settings and locally')
print() print('running programs that may be blocking or using those ports.')
print('Falling back to --noauth_local_webserver and continuing with') print()
print('authorization.') print('Falling back to --noauth_local_webserver and continuing with')
print() print('authorization.')
print()
if FLAGS.auth_local_webserver: if FLAGS.auth_local_webserver:
oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number) oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
@@ -144,7 +146,8 @@ def run(flow, storage, http=None):
if 'code' in httpd.query_params: if 'code' in httpd.query_params:
code = httpd.query_params['code'] code = httpd.query_params['code']
else: else:
print('Failed to find "code" in the query parameters of the redirect.') print('Failed to find "code" in the query '
'parameters of the redirect.')
sys.exit('Try running with --noauth_local_webserver.') sys.exit('Try running with --noauth_local_webserver.')
else: else:
code = input('Enter verification code: ').strip() code = input('Enter verification code: ').strip()

View File

@@ -38,13 +38,14 @@ class _ServiceAccountCredentials(AssertionCredentials):
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
def __init__(self, service_account_id, service_account_email, private_key_id, def __init__(self, service_account_id, service_account_email,
private_key_pkcs8_text, scopes, user_agent=None, private_key_id, private_key_pkcs8_text, scopes,
token_uri=GOOGLE_TOKEN_URI, revoke_uri=GOOGLE_REVOKE_URI, user_agent=None, token_uri=GOOGLE_TOKEN_URI,
**kwargs): revoke_uri=GOOGLE_REVOKE_URI, **kwargs):
super(_ServiceAccountCredentials, self).__init__( super(_ServiceAccountCredentials, self).__init__(
None, user_agent=user_agent, token_uri=token_uri, revoke_uri=revoke_uri) None, user_agent=user_agent, token_uri=token_uri,
revoke_uri=revoke_uri)
self._service_account_id = service_account_id self._service_account_id = service_account_id
self._service_account_email = service_account_email self._service_account_email = service_account_email
@@ -61,18 +62,18 @@ class _ServiceAccountCredentials(AssertionCredentials):
"""Generate the assertion that will be used in the request.""" """Generate the assertion that will be used in the request."""
header = { header = {
'alg': 'RS256', 'alg': 'RS256',
'typ': 'JWT', 'typ': 'JWT',
'kid': self._private_key_id 'kid': self._private_key_id
} }
now = int(time.time()) now = int(time.time())
payload = { payload = {
'aud': self._token_uri, 'aud': self._token_uri,
'scope': self._scopes, 'scope': self._scopes,
'iat': now, 'iat': now,
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS, 'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
'iss': self._service_account_email 'iss': self._service_account_email
} }
payload.update(self._kwargs) payload.update(self._kwargs)
@@ -81,7 +82,8 @@ class _ServiceAccountCredentials(AssertionCredentials):
assertion_input = first_segment + b'.' + second_segment assertion_input = first_segment + b'.' + second_segment
# Sign the assertion. # Sign the assertion.
rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key, 'SHA-256') rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key,
'SHA-256')
signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=') signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')
return assertion_input + b'.' + signature return assertion_input + b'.' + signature
@@ -90,7 +92,7 @@ class _ServiceAccountCredentials(AssertionCredentials):
# Ensure that it is bytes # Ensure that it is bytes
blob = _to_bytes(blob, encoding='utf-8') blob = _to_bytes(blob, encoding='utf-8')
return (self._private_key_id, return (self._private_key_id,
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
@property @property
def service_account_email(self): def service_account_email(self):
@@ -99,11 +101,11 @@ class _ServiceAccountCredentials(AssertionCredentials):
@property @property
def serialization_data(self): def serialization_data(self):
return { return {
'type': 'service_account', 'type': 'service_account',
'client_id': self._service_account_id, 'client_id': self._service_account_id,
'client_email': self._service_account_email, 'client_email': self._service_account_email,
'private_key_id': self._private_key_id, 'private_key_id': self._private_key_id,
'private_key': self._private_key_pkcs8_text 'private_key': self._private_key_pkcs8_text
} }
def create_scoped_required(self): def create_scoped_required(self):
@@ -111,14 +113,14 @@ class _ServiceAccountCredentials(AssertionCredentials):
def create_scoped(self, scopes): def create_scoped(self, scopes):
return _ServiceAccountCredentials(self._service_account_id, return _ServiceAccountCredentials(self._service_account_id,
self._service_account_email, self._service_account_email,
self._private_key_id, self._private_key_id,
self._private_key_pkcs8_text, self._private_key_pkcs8_text,
scopes, scopes,
user_agent=self._user_agent, user_agent=self._user_agent,
token_uri=self._token_uri, token_uri=self._token_uri,
revoke_uri=self._revoke_uri, revoke_uri=self._revoke_uri,
**self._kwargs) **self._kwargs)
def _get_private_key(private_key_pkcs8_text): def _get_private_key(private_key_pkcs8_text):
@@ -127,5 +129,5 @@ def _get_private_key(private_key_pkcs8_text):
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY') der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo()) asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
return rsa.PrivateKey.load_pkcs1( return rsa.PrivateKey.load_pkcs1(
asn1_private_key.getComponentByName('privateKey').asOctets(), asn1_private_key.getComponentByName('privateKey').asOctets(),
format='DER') format='DER')

View File

@@ -55,14 +55,15 @@ def _CreateArgumentParser():
return None return None
parser = argparse.ArgumentParser(add_help=False) parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--auth_host_name', default='localhost', parser.add_argument('--auth_host_name', default='localhost',
help='Hostname when running a local web server.') help='Hostname when running a local web server.')
parser.add_argument('--noauth_local_webserver', action='store_true', parser.add_argument('--noauth_local_webserver', action='store_true',
default=False, help='Do not run a local web server.') default=False, help='Do not run a local web server.')
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int, parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
nargs='*', help='Port web server should listen on.') nargs='*', help='Port web server should listen on.')
parser.add_argument('--logging_level', default='ERROR', parser.add_argument(
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], '--logging_level', default='ERROR',
help='Set the logging level of detail.') choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
help='Set the logging level of detail.')
return parser return parser
# argparser is an ArgumentParser that contains command-line options expected # argparser is an ArgumentParser that contains command-line options expected
@@ -100,12 +101,14 @@ class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
query = self.path.split('?', 1)[-1] query = self.path.split('?', 1)[-1]
query = dict(urllib.parse.parse_qsl(query)) query = dict(urllib.parse.parse_qsl(query))
self.server.query_params = query self.server.query_params = query
self.wfile.write(b"<html><head><title>Authentication Status</title></head>") self.wfile.write(
self.wfile.write(b"<body><p>The authentication flow has completed.</p>") b"<html><head><title>Authentication Status</title></head>")
self.wfile.write(
b"<body><p>The authentication flow has completed.</p>")
self.wfile.write(b"</body></html>") self.wfile.write(b"</body></html>")
def log_message(self, format, *args): def log_message(self, format, *args):
"""Do not log messages to stdout while running as command line program.""" """Do not log messages to stdout while running as cmd. line program."""
@util.positional(3) @util.positional(3)
@@ -137,9 +140,9 @@ def run_flow(flow, storage, flags, http=None):
Run a local web server to handle redirects during OAuth Run a local web server to handle redirects during OAuth
authorization. authorization.
The tools module defines an ``ArgumentParser`` the already contains the flag The tools module defines an ``ArgumentParser`` the already contains the
definitions that ``run()`` requires. You can pass that ``ArgumentParser`` to flag definitions that ``run()`` requires. You can pass that
your ``ArgumentParser`` constructor:: ``ArgumentParser`` to your ``ArgumentParser`` constructor::
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description=__doc__, description=__doc__,
@@ -167,7 +170,7 @@ def run_flow(flow, storage, flags, http=None):
port_number = port port_number = port
try: try:
httpd = ClientRedirectServer((flags.auth_host_name, port), httpd = ClientRedirectServer((flags.auth_host_name, port),
ClientRedirectHandler) ClientRedirectHandler)
except socket.error: except socket.error:
pass pass
else: else:
@@ -175,13 +178,14 @@ def run_flow(flow, storage, flags, http=None):
break break
flags.noauth_local_webserver = not success flags.noauth_local_webserver = not success
if not success: if not success:
print('Failed to start a local webserver listening on either port 8080') print('Failed to start a local webserver listening '
print('or port 9090. Please check your firewall settings and locally') 'on either port 8080')
print('running programs that may be blocking or using those ports.') print('or port 9090. Please check your firewall settings and locally')
print() print('running programs that may be blocking or using those ports.')
print('Falling back to --noauth_local_webserver and continuing with') print()
print('authorization.') print('Falling back to --noauth_local_webserver and continuing with')
print() print('authorization.')
print()
if not flags.noauth_local_webserver: if not flags.noauth_local_webserver:
oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number) oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
@@ -197,7 +201,8 @@ def run_flow(flow, storage, flags, http=None):
print() print()
print(' ' + authorize_url) print(' ' + authorize_url)
print() print()
print('If your browser is on a different machine then exit and re-run this') print('If your browser is on a different machine then '
'exit and re-run this')
print('application with the command-line parameter ') print('application with the command-line parameter ')
print() print()
print(' --noauth_local_webserver') print(' --noauth_local_webserver')
@@ -216,7 +221,8 @@ def run_flow(flow, storage, flags, http=None):
if 'code' in httpd.query_params: if 'code' in httpd.query_params:
code = httpd.query_params['code'] code = httpd.query_params['code']
else: else:
print('Failed to find "code" in the query parameters of the redirect.') print('Failed to find "code" in the query parameters '
'of the redirect.')
sys.exit('Try running with --noauth_local_webserver.') sys.exit('Try running with --noauth_local_webserver.')
else: else:
code = input('Enter verification code: ').strip() code = input('Enter verification code: ').strip()
@@ -235,15 +241,15 @@ def run_flow(flow, storage, flags, http=None):
def message_if_missing(filename): def message_if_missing(filename):
"""Helpful message to display if the CLIENT_SECRETS file is missing.""" """Helpful message to display if the CLIENT_SECRETS file is missing."""
return _CLIENT_SECRETS_MESSAGE % filename return _CLIENT_SECRETS_MESSAGE % filename
try: try:
from oauth2client.old_run import run from oauth2client.old_run import run
from oauth2client.old_run import FLAGS from oauth2client.old_run import FLAGS
except ImportError: except ImportError:
def run(*args, **kwargs): def run(*args, **kwargs):
raise NotImplementedError( raise NotImplementedError(
'The gflags library must be installed to use tools.run(). ' 'The gflags library must be installed to use tools.run(). '
'Please install gflags or preferrably switch to using ' 'Please install gflags or preferrably switch to using '
'tools.run_flow().') 'tools.run_flow().')

View File

@@ -129,8 +129,10 @@ def positional(max_positional_args):
plural_s = '' plural_s = ''
if max_positional_args != 1: if max_positional_args != 1:
plural_s = 's' plural_s = 's'
message = '%s() takes at most %d positional argument%s (%d given)' % ( message = ('%s() takes at most %d positional '
wrapped.__name__, max_positional_args, plural_s, len(args)) 'argument%s (%d given)' % (
wrapped.__name__, max_positional_args,
plural_s, len(args)))
if positional_parameters_enforcement == POSITIONAL_EXCEPTION: if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
raise TypeError(message) raise TypeError(message)
elif positional_parameters_enforcement == POSITIONAL_WARNING: elif positional_parameters_enforcement == POSITIONAL_WARNING:

View File

@@ -104,7 +104,7 @@ def validate_token(key, token, user_id, action_id="", current_time=None):
# The given token should match the generated one with the same time. # The given token should match the generated one with the same time.
expected_token = generate_token(key, user_id, action_id=action_id, expected_token = generate_token(key, user_id, action_id=action_id,
when=token_time) when=token_time)
if len(token) != len(expected_token): if len(token) != len(expected_token):
return False return False