Stage 1 conversion to JSON for storing Credentials.
Reviewed in http://codereview.appspot.com/4972065/
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 64fd3ac..2811069 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -95,6 +95,16 @@
None,
token_uri)
+ @classmethod
+ def from_json(cls, json):
+ data = simplejson.loads(json)
+ retval = AccessTokenCredentials(
+ data['scope'],
+ data['audience'],
+ data['assertion_type'],
+ data['token_uri'])
+ return retval
+
def _generate_assertion(self):
header = {
'typ': 'JWT',
@@ -165,17 +175,28 @@
def get_value_for_datastore(self, model_instance):
cred = super(CredentialsProperty,
self).get_value_for_datastore(model_instance)
- return db.Blob(pickle.dumps(cred))
+ if cred is None:
+ cred = ''
+ else:
+ cred = cred.to_json()
+ return db.Blob(cred)
# For reading from datastore.
def make_value_from_datastore(self, value):
if value is None:
return None
- return pickle.loads(value)
+ if len(value) == 0:
+ return None
+ credentials = None
+ try:
+ credentials = Credentials.new_from_json(value)
+ except ValueError:
+ credentials = pickle.loads(value)
+ return credentials
def validate(self, value):
if value is not None and not isinstance(value, Credentials):
- raise BadValueError('Property %s must be convertible '
+ raise db.BadValueError('Property %s must be convertible '
'to an Credentials instance (%s)' %
(self.name, value))
return super(CredentialsProperty, self).validate(value)
@@ -215,15 +236,15 @@
oauth2client.Credentials
"""
if self._cache:
- credential = self._cache.get(self._key_name)
- if credential:
- return pickle.loads(credential)
+ json = self._cache.get(self._key_name)
+ if json:
+ return Credentials.new_from_json(json)
entity = self._model.get_or_insert(self._key_name)
credential = getattr(entity, self._property_name)
if credential and hasattr(credential, 'set_store'):
credential.set_store(self)
if self._cache:
- self._cache.set(self._key_name, pickle.dumps(credentials))
+ self._cache.set(self._key_name, credentials.to_json())
return credential
@@ -237,7 +258,7 @@
setattr(entity, self._property_name, credentials)
entity.put()
if self._cache:
- self._cache.set(self._key_name, pickle.dumps(credentials))
+ self._cache.set(self._key_name, credentials.to_json())
class CredentialsModel(db.Model):
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 52f6fb3..2b97d4d 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -43,6 +43,9 @@
logger = logging.getLogger(__name__)
+# Expiry is stored in RFC3339 UTC format
+EXPIRY_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
+
class Error(Exception):
"""Base error for this module."""
@@ -71,10 +74,15 @@
class Credentials(object):
"""Base class for all Credentials objects.
- Subclasses must define an authorize() method
- that applies the credentials to an HTTP transport.
+ Subclasses must define an authorize() method that applies the credentials to
+ an HTTP transport.
+
+ Subclasses must also specify a classmethod named 'from_json' that takes a JSON
+ string as input and returns an instaniated Crentials object.
"""
+ NON_SERIALIZED_MEMBERS = ['store']
+
def authorize(self, http):
"""Take an httplib2.Http instance (or equivalent) and
authorizes it for the set of credentials, usually by
@@ -84,6 +92,58 @@
"""
_abstract()
+ def _to_json(self, strip):
+ """Utility function for creating a JSON representation of an instance of Credentials.
+
+ Args:
+ strip: array, An array of names of members to not include in the JSON.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ t = type(self)
+ d = copy.copy(self.__dict__)
+ for member in strip:
+ del d[member]
+ if 'token_expiry' in d and isinstance(d['token_expiry'], datetime.datetime):
+ d['token_expiry'] = d['token_expiry'].strftime(EXPIRY_FORMAT)
+ # Add in information we will need later to reconsistitue this instance.
+ d['_class'] = t.__name__
+ d['_module'] = t.__module__
+ return simplejson.dumps(d)
+
+ def to_json(self):
+ """Creating a JSON representation of an instance of Credentials.
+
+ Returns:
+ string, a JSON representation of this instance, suitable to pass to
+ from_json().
+ """
+ return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
+
+ @classmethod
+ def new_from_json(cls, s):
+ """Utility class method to instantiate a Credentials subclass from a JSON
+ representation produced by to_json().
+
+ Args:
+ s: string, JSON from to_json().
+
+ Returns:
+ An instance of the subclass of Credentials that was serialized with
+ to_json().
+ """
+ data = simplejson.loads(s)
+ # Find and call the right classmethod from_json() to restore the object.
+ module = data['_module']
+ m = __import__(module)
+ for sub_module in module.split('.')[1:]:
+ m = getattr(m, sub_module)
+ kls = getattr(m, data['_class'])
+ from_json = getattr(kls, 'from_json')
+ return from_json(s)
+
class Flow(object):
"""Base class for all Flow objects."""
@@ -206,6 +266,36 @@
# refreshed.
self.invalid = False
+ def to_json(self):
+ return self._to_json(Credentials.NON_SERIALIZED_MEMBERS)
+
+ @classmethod
+ def from_json(cls, s):
+ """Instantiate a Credentials object from a JSON description of it. The JSON
+ should have been produced by calling .to_json() on the object.
+
+ Args:
+ data: dict, A deserialized JSON object.
+
+ Returns:
+ An instance of a Credentials subclass.
+ """
+ data = simplejson.loads(s)
+ if 'token_expiry' in data and not isinstance(data['token_expiry'],
+ datetime.datetime):
+ data['token_expiry'] = datetime.datetime.strptime(
+ data['token_expiry'], EXPIRY_FORMAT)
+ retval = OAuth2Credentials(
+ data['access_token'],
+ data['client_id'],
+ data['client_secret'],
+ data['refresh_token'],
+ data['token_expiry'],
+ data['token_uri'],
+ data['user_agent'])
+ retval.invalid = data['invalid']
+ return retval
+
@property
def access_token_expired(self):
"""True if the credential is expired or invalid.
@@ -218,7 +308,7 @@
if not self.token_expiry:
return False
- now = datetime.datetime.now()
+ now = datetime.datetime.utcnow()
if now >= self.token_expiry:
logger.info('access_token is expired. Now: %s, token_expiry: %s',
now, self.token_expiry)
@@ -318,7 +408,7 @@
self.refresh_token = d.get('refresh_token', self.refresh_token)
if 'expires_in' in d:
self.token_expiry = datetime.timedelta(
- seconds=int(d['expires_in'])) + datetime.datetime.now()
+ seconds=int(d['expires_in'])) + datetime.datetime.utcnow()
else:
self.token_expiry = None
if self.store:
@@ -446,6 +536,15 @@
None,
user_agent)
+
+ @classmethod
+ def from_json(cls, s):
+ data = simplejson.loads(s)
+ retval = AccessTokenCredentials(
+ data['access_token'],
+ data['user_agent'])
+ return retval
+
def _refresh(self, http_request):
raise AccessTokenCredentialsError(
"The access_token is expired or invalid and can't be refreshed.")
@@ -601,7 +700,7 @@
refresh_token = d.get('refresh_token', None)
token_expiry = None
if 'expires_in' in d:
- token_expiry = datetime.datetime.now() + datetime.timedelta(
+ token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
seconds=int(d['expires_in']))
logger.info('Successfully retrieved access token: %s' % content)
diff --git a/oauth2client/file.py b/oauth2client/file.py
index b7f9c7d..89140b8 100644
--- a/oauth2client/file.py
+++ b/oauth2client/file.py
@@ -23,7 +23,20 @@
import pickle
import threading
+
+try: # pragma: no cover
+ import simplejson
+except ImportError: # pragma: no cover
+ try:
+ # Try to import from django, should work on App Engine
+ from django.utils import simplejson
+ except ImportError:
+ # Should work for Python2.6 and higher.
+ import json as simplejson
+
+
from client import Storage as BaseStorage
+from client import Credentials
class Storage(BaseStorage):
@@ -40,25 +53,40 @@
oauth2client.client.Credentials
"""
self._lock.acquire()
+ credentials = None
try:
f = open(self._filename, 'r')
- credentials = pickle.loads(f.read())
+ content = f.read()
f.close()
+ except IOError:
+ self._lock.release()
+ return credentials
+
+ # First try reading as JSON, and if that fails fall back to pickle.
+ try:
+ credentials = Credentials.new_from_json(content)
credentials.set_store(self)
- except:
- credentials = None
- self._lock.release()
+ except ValueError:
+ # TODO(jcgregorio) On a future release remove this path to finally remove
+ # all pickle support.
+ try:
+ credentials = pickle.loads(content)
+ credentials.set_store(self)
+ except:
+ pass
+ finally:
+ self._lock.release()
return credentials
def put(self, credentials):
- """Write a pickled Credentials to file.
+ """Write Credentials to file.
Args:
credentials: Credentials, the credentials to store.
"""
self._lock.acquire()
f = open(self._filename, 'w')
- f.write(pickle.dumps(credentials))
+ f.write(credentials.to_json())
f.close()
self._lock.release()
diff --git a/oauth2client/multistore_file.py b/oauth2client/multistore_file.py
index 8841194..e3e3f6d 100644
--- a/oauth2client/multistore_file.py
+++ b/oauth2client/multistore_file.py
@@ -21,7 +21,9 @@
'userAgent': '<user agent>',
'scope': '<scope>'
},
- 'credential': '<base64 encoding of pickeled Credential object>'
+ 'credential': {
+ # JSON serialized Credentials.
+ }
}
]
}
@@ -47,6 +49,7 @@
import json as simplejson
from client import Storage as BaseStorage
+from client import Credentials
logger = logging.getLogger(__name__)
@@ -295,7 +298,8 @@
user_agent = raw_key['userAgent']
scope = raw_key['scope']
key = (client_id, user_agent, scope)
- credential = pickle.loads(base64.b64decode(cred_entry['credential']))
+ credential = None
+ credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential']))
return (key, credential)
def _write(self):
@@ -312,7 +316,7 @@
'userAgent': cred_key[1],
'scope': cred_key[2]
}
- raw_cred = base64.b64encode(pickle.dumps(cred))
+ raw_cred = simplejson.loads(cred.to_json())
raw_creds.append({'key': raw_key, 'credential': raw_cred})
self._locked_json_write(raw_data)
@@ -330,6 +334,7 @@
The credential specified or None if not present
"""
key = (client_id, user_agent, scope)
+
return self._data.get(key, None)
def _update_credential(self, cred, scope):
diff --git a/oauth2client/tools.py b/oauth2client/tools.py
index dc779b4..574a747 100644
--- a/oauth2client/tools.py
+++ b/oauth2client/tools.py
@@ -129,12 +129,16 @@
print '--noauth_local_webserver.'
print
+ code = None
if FLAGS.auth_local_webserver:
httpd.handle_request()
if 'error' in httpd.query_params:
sys.exit('Authentication request was rejected.')
if 'code' in httpd.query_params:
code = httpd.query_params['code']
+ else:
+ print 'Failed to find "code" in the query parameters of the redirect.'
+ sys.exit('Try running with --noauth_local_webserver.')
else:
code = raw_input('Enter verification code: ').strip()