Permalink
Browse files

Vendored OAuth2 library + migration.

  • Loading branch information...
1 parent 4892e14 commit 16b69dd68dea2d021967c4126d5907556bde3693 @samuelclay committed Aug 11, 2012
View
75 apps/feed_import/migrations/0006_reader_credentials.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'OAuthToken.credential'
+ db.add_column('feed_import_oauthtoken', 'credential',
+ self.gf('django.db.models.fields.TextField')(null=True, blank=True),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'OAuthToken.credential'
+ db.delete_column('feed_import_oauthtoken', 'credential')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'feed_import.oauthtoken': {
+ 'Meta': {'object_name': 'OAuthToken'},
+ 'access_token': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'access_token_secret': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'credential': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'remote_ip': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'request_token': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'request_token_secret': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'session_id': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['feed_import']
View
1 vendor/oauth2client/__init__.py
@@ -0,0 +1 @@
+__version__ = "1.0c2"
View
32 vendor/oauth2client/anyjson.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utility module to import a JSON module
+
+Hides all the messy details of exactly where
+we get a simplejson module from.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+try: # pragma: no cover
+ # Should work for Python2.6 and higher.
+ import json as simplejson
+except ImportError: # pragma: no cover
+ try:
+ import simplejson
+ except ImportError:
+ # Try to import from django, should work on App Engine
+ from django.utils import simplejson
View
599 vendor/oauth2client/appengine.py
@@ -0,0 +1,599 @@
+# Copyright (C) 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for Google App Engine
+
+Utilities for making it easier to use OAuth 2.0 on Google App Engine.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import base64
+import httplib2
+import logging
+import pickle
+import time
+
+import clientsecrets
+
+from google.appengine.api import app_identity
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp.util import login_required
+from google.appengine.ext.webapp.util import run_wsgi_app
+from oauth2client import util
+from oauth2client.anyjson import simplejson
+from oauth2client.client import AccessTokenRefreshError
+from oauth2client.client import AssertionCredentials
+from oauth2client.client import Credentials
+from oauth2client.client import Flow
+from oauth2client.client import OAuth2WebServerFlow
+from oauth2client.client import Storage
+
+logger = logging.getLogger(__name__)
+
+OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
+
+
+class InvalidClientSecretsError(Exception):
+ """The client_secrets.json file is malformed or missing required fields."""
+ pass
+
+
+class AppAssertionCredentials(AssertionCredentials):
+ """Credentials object for App Engine Assertion Grants
+
+ This object will allow an App Engine application to identify itself to Google
+ and other OAuth 2.0 servers that can verify assertions. It can be used for
+ the purpose of accessing data stored under an account assigned to the App
+ Engine application itself.
+
+ This credential does not require a flow to instantiate because it represents
+ a two legged flow, and therefore has all of the required information to
+ generate and refresh its own access tokens.
+ """
+
+ @util.positional(2)
+ def __init__(self, scope, **kwargs):
+ """Constructor for AppAssertionCredentials
+
+ Args:
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ """
+ if type(scope) is list:
+ scope = ' '.join(scope)
+ self.scope = scope
+
+ super(AppAssertionCredentials, self).__init__(
+ 'ignored' # assertion_type is ignore in this subclass.
+ )
+
+ @classmethod
+ def from_json(cls, json):
+ data = simplejson.loads(json)
+ return AppAssertionCredentials(data['scope'])
+
+ def _refresh(self, http_request):
+ """Refreshes the access_token.
+
+ Since the underlying App Engine app_identity implementation does its own
+ caching we can skip all the storage hoops and just to a refresh using the
+ API.
+
+ Args:
+ http_request: callable, a callable that matches the method signature of
+ httplib2.Http.request, used to make the refresh request.
+
+ Raises:
+ AccessTokenRefreshError: When the refresh fails.
+ """
+ try:
+ (token, _) = app_identity.get_access_token(self.scope)
+ except app_identity.Error, e:
+ raise AccessTokenRefreshError(str(e))
+ self.access_token = token
+
+
+class FlowProperty(db.Property):
+ """App Engine datastore Property for Flow.
+
+ Utility property that allows easy storage and retreival of an
+ oauth2client.Flow"""
+
+ # Tell what the user type is.
+ data_type = Flow
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ flow = super(FlowProperty,
+ self).get_value_for_datastore(model_instance)
+ return db.Blob(pickle.dumps(flow))
+
+ # For reading from datastore.
+ def make_value_from_datastore(self, value):
+ if value is None:
+ return None
+ return pickle.loads(value)
+
+ def validate(self, value):
+ if value is not None and not isinstance(value, Flow):
+ raise db.BadValueError('Property %s must be convertible '
+ 'to a FlowThreeLegged instance (%s)' %
+ (self.name, value))
+ return super(FlowProperty, self).validate(value)
+
+ def empty(self, value):
+ return not value
+
+
+class CredentialsProperty(db.Property):
+ """App Engine datastore Property for Credentials.
+
+ Utility property that allows easy storage and retrieval of
+ oath2client.Credentials
+ """
+
+ # Tell what the user type is.
+ data_type = Credentials
+
+ # For writing to datastore.
+ def get_value_for_datastore(self, model_instance):
+ logger.info("get: Got type " + str(type(model_instance)))
+ cred = super(CredentialsProperty,
+ self).get_value_for_datastore(model_instance)
+ 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):
+ logger.info("make: Got type " + str(type(value)))
+ if value is None:
+ return None
+ if len(value) == 0:
+ return None
+ try:
+ credentials = Credentials.new_from_json(value)
+ except ValueError:
+ credentials = None
+ return credentials
+
+ def validate(self, value):
+ value = super(CredentialsProperty, self).validate(value)
+ logger.info("validate: Got type " + str(type(value)))
+ if value is not None and not isinstance(value, Credentials):
+ raise db.BadValueError('Property %s must be convertible '
+ 'to a Credentials instance (%s)' %
+ (self.name, value))
+ #if value is not None and not isinstance(value, Credentials):
+ # return None
+ return value
+
+
+class StorageByKeyName(Storage):
+ """Store and retrieve a single credential to and from
+ the App Engine datastore.
+
+ This Storage helper presumes the Credentials
+ have been stored as a CredenialsProperty
+ on a datastore model class, and that entities
+ are stored by key_name.
+ """
+
+ @util.positional(4)
+ def __init__(self, model, key_name, property_name, cache=None):
+ """Constructor for Storage.
+
+ Args:
+ model: db.Model, model class
+ key_name: string, key name for the entity that has the credentials
+ property_name: string, name of the property that is a CredentialsProperty
+ cache: memcache, a write-through cache to put in front of the datastore
+ """
+ self._model = model
+ self._key_name = key_name
+ self._property_name = property_name
+ self._cache = cache
+
+ def locked_get(self):
+ """Retrieve Credential from datastore.
+
+ Returns:
+ oauth2client.Credentials
+ """
+ if self._cache:
+ json = self._cache.get(self._key_name)
+ if json:
+ return Credentials.new_from_json(json)
+
+ credential = None
+ entity = self._model.get_by_key_name(self._key_name)
+ if entity is not None:
+ 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, credential.to_json())
+
+ return credential
+
+ def locked_put(self, credentials):
+ """Write a Credentials to the datastore.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ entity = self._model.get_or_insert(self._key_name)
+ setattr(entity, self._property_name, credentials)
+ entity.put()
+ if self._cache:
+ self._cache.set(self._key_name, credentials.to_json())
+
+ def locked_delete(self):
+ """Delete Credential from datastore."""
+
+ if self._cache:
+ self._cache.delete(self._key_name)
+
+ entity = self._model.get_by_key_name(self._key_name)
+ if entity is not None:
+ entity.delete()
+
+
+class CredentialsModel(db.Model):
+ """Storage for OAuth 2.0 Credentials
+
+ Storage of the model is keyed by the user.user_id().
+ """
+ credentials = CredentialsProperty()
+
+
+class OAuth2Decorator(object):
+ """Utility for making OAuth 2.0 easier.
+
+ Instantiate and then use with oauth_required or oauth_aware
+ as decorators on webapp.RequestHandler methods.
+
+ Example:
+
+ decorator = OAuth2Decorator(
+ client_id='837...ent.com',
+ client_secret='Qh...wwI',
+ scope='https://www.googleapis.com/auth/plus')
+
+
+ class MainHandler(webapp.RequestHandler):
+
+ @decorator.oauth_required
+ def get(self):
+ http = decorator.http()
+ # http is authorized with the user's Credentials and can be used
+ # in API calls
+
+ """
+
+ @util.positional(4)
+ def __init__(self, client_id, client_secret, scope,
+ auth_uri='https://accounts.google.com/o/oauth2/auth',
+ token_uri='https://accounts.google.com/o/oauth2/token',
+ user_agent=None,
+ message=None,
+ callback_path='/oauth2callback',
+ **kwargs):
+
+ """Constructor for OAuth2Decorator
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string client secret.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ user_agent: string, User agent of your application, default to None.
+ message: Message to display if there are problems with the OAuth 2.0
+ configuration. The message may contain HTML and will be presented on the
+ web interface for any method that uses the decorator.
+ callback_path: string, The absolute path to use as the callback URI. Note
+ that this must match up with the URI given when registering the
+ application in the APIs Console.
+ **kwargs: dict, Keyword arguments are be passed along as kwargs to the
+ OAuth2WebServerFlow constructor.
+ """
+ self.flow = None
+ self.credentials = None
+ self._client_id = client_id
+ self._client_secret = client_secret
+ self._scope = scope
+ self._auth_uri = auth_uri
+ self._token_uri = token_uri
+ self._user_agent = user_agent
+ self._kwargs = kwargs
+ self._message = message
+ self._in_error = False
+ self._callback_path = callback_path
+
+ def _display_error_message(self, request_handler):
+ request_handler.response.out.write('<html><body>')
+ request_handler.response.out.write(self._message)
+ request_handler.response.out.write('</body></html>')
+
+ def oauth_required(self, method):
+ """Decorator that starts the OAuth 2.0 dance.
+
+ Starts the OAuth dance for the logged in user if they haven't already
+ granted access for this application.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+
+ def check_oauth(request_handler, *args, **kwargs):
+ if self._in_error:
+ self._display_error_message(request_handler)
+ return
+
+ user = users.get_current_user()
+ # Don't use @login_decorator as this could be used in a POST request.
+ if not user:
+ request_handler.redirect(users.create_login_url(
+ request_handler.request.uri))
+ return
+
+ self._create_flow(request_handler)
+
+ # Store the request URI in 'state' so we can use it later
+ self.flow.params['state'] = request_handler.request.url
+ self.credentials = StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').get()
+
+ if not self.has_credentials():
+ return request_handler.redirect(self.authorize_url())
+ try:
+ method(request_handler, *args, **kwargs)
+ except AccessTokenRefreshError:
+ return request_handler.redirect(self.authorize_url())
+
+ return check_oauth
+
+ def _create_flow(self, request_handler):
+ """Create the Flow object.
+
+ The Flow is calculated lazily since we don't know where this app is
+ running until it receives a request, at which point redirect_uri can be
+ calculated and then the Flow object can be constructed.
+
+ Args:
+ request_handler: webapp.RequestHandler, the request handler.
+ """
+ if self.flow is None:
+ redirect_uri = request_handler.request.relative_url(
+ self._callback_path) # Usually /oauth2callback
+ self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
+ self._scope, redirect_uri=redirect_uri,
+ user_agent=self._user_agent,
+ auth_uri=self._auth_uri,
+ token_uri=self._token_uri, **self._kwargs)
+
+
+ def oauth_aware(self, method):
+ """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
+
+ Does all the setup for the OAuth dance, but doesn't initiate it.
+ This decorator is useful if you want to create a page that knows
+ whether or not the user has granted access to this application.
+ From within a method decorated with @oauth_aware the has_credentials()
+ and authorize_url() methods can be called.
+
+ Args:
+ method: callable, to be decorated method of a webapp.RequestHandler
+ instance.
+ """
+
+ def setup_oauth(request_handler, *args, **kwargs):
+ if self._in_error:
+ self._display_error_message(request_handler)
+ return
+
+ user = users.get_current_user()
+ # Don't use @login_decorator as this could be used in a POST request.
+ if not user:
+ request_handler.redirect(users.create_login_url(
+ request_handler.request.uri))
+ return
+
+ self._create_flow(request_handler)
+
+ self.flow.params['state'] = request_handler.request.url
+ self.credentials = StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').get()
+ method(request_handler, *args, **kwargs)
+ return setup_oauth
+
+ def has_credentials(self):
+ """True if for the logged in user there are valid access Credentials.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ return self.credentials is not None and not self.credentials.invalid
+
+ def authorize_url(self):
+ """Returns the URL to start the OAuth dance.
+
+ Must only be called from with a webapp.RequestHandler subclassed method
+ that had been decorated with either @oauth_required or @oauth_aware.
+ """
+ url = self.flow.step1_get_authorize_url()
+ return str(url)
+
+ def http(self):
+ """Returns an authorized http instance.
+
+ Must only be called from within an @oauth_required decorated method, or
+ from within an @oauth_aware decorated method where has_credentials()
+ returns True.
+ """
+ return self.credentials.authorize(httplib2.Http())
+
+ @property
+ def callback_path(self):
+ """The absolute path where the callback will occur.
+
+ Note this is the absolute path, not the absolute URI, that will be
+ calculated by the decorator at runtime. See callback_handler() for how this
+ should be used.
+
+ Returns:
+ The callback path as a string.
+ """
+ return self._callback_path
+
+
+ def callback_handler(self):
+ """RequestHandler for the OAuth 2.0 redirect callback.
+
+ Usage:
+ app = webapp.WSGIApplication([
+ ('/index', MyIndexHandler),
+ ...,
+ (decorator.callback_path, decorator.callback_handler())
+ ])
+
+ Returns:
+ A webapp.RequestHandler that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ decorator = self
+
+ class OAuth2Handler(webapp.RequestHandler):
+ """Handler for the redirect_uri of the OAuth 2.0 dance."""
+
+ @login_required
+ def get(self):
+ error = self.request.get('error')
+ if error:
+ errormsg = self.request.get('error_description', error)
+ self.response.out.write(
+ 'The authorization request failed: %s' % errormsg)
+ else:
+ user = users.get_current_user()
+ decorator._create_flow(self)
+ credentials = decorator.flow.step2_exchange(self.request.params)
+ StorageByKeyName(
+ CredentialsModel, user.user_id(), 'credentials').put(credentials)
+ self.redirect(str(self.request.get('state')))
+
+ return OAuth2Handler
+
+ def callback_application(self):
+ """WSGI application for handling the OAuth 2.0 redirect callback.
+
+ If you need finer grained control use `callback_handler` which returns just
+ the webapp.RequestHandler.
+
+ Returns:
+ A webapp.WSGIApplication that handles the redirect back from the
+ server during the OAuth 2.0 dance.
+ """
+ return webapp.WSGIApplication([
+ (self.callback_path, self.callback_handler())
+ ])
+
+
+class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
+ """An OAuth2Decorator that builds from a clientsecrets file.
+
+ Uses a clientsecrets file as the source for all the information when
+ constructing an OAuth2Decorator.
+
+ Example:
+
+ decorator = OAuth2DecoratorFromClientSecrets(
+ os.path.join(os.path.dirname(__file__), 'client_secrets.json')
+ scope='https://www.googleapis.com/auth/plus')
+
+
+ class MainHandler(webapp.RequestHandler):
+
+ @decorator.oauth_required
+ def get(self):
+ http = decorator.http()
+ # http is authorized with the user's Credentials and can be used
+ # in API calls
+ """
+
+ @util.positional(3)
+ def __init__(self, filename, scope, message=None, cache=None):
+ """Constructor
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. The message may contain HTML and
+ will be presented on the web interface for any method that uses the
+ decorator.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+ """
+ try:
+ client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
+ if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
+ raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
+ super(OAuth2DecoratorFromClientSecrets,
+ self).__init__(
+ client_info['client_id'],
+ client_info['client_secret'],
+ scope,
+ auth_uri=client_info['auth_uri'],
+ token_uri=client_info['token_uri'],
+ message=message)
+ except clientsecrets.InvalidClientSecretsError:
+ self._in_error = True
+ if message is not None:
+ self._message = message
+ else:
+ self._message = "Please configure your application for OAuth 2.0"
+
+
+@util.positional(2)
+def oauth2decorator_from_clientsecrets(filename, scope,
+ message=None, cache=None):
+ """Creates an OAuth2Decorator populated from a clientsecrets file.
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. The message may contain HTML and
+ will be presented on the web interface for any method that uses the
+ decorator.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+
+ Returns: An OAuth2Decorator
+
+ """
+ return OAuth2DecoratorFromClientSecrets(filename, scope,
+ message=message, cache=cache)
View
1,206 vendor/oauth2client/client.py
@@ -0,0 +1,1206 @@
+# Copyright (C) 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""An OAuth 2.0 client.
+
+Tools for interacting with OAuth 2.0 protected resources.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import base64
+import clientsecrets
+import copy
+import datetime
+import httplib2
+import logging
+import os
+import sys
+import time
+import urllib
+import urlparse
+
+from oauth2client import util
+from oauth2client.anyjson import simplejson
+
+HAS_OPENSSL = False
+try:
+ from oauth2client.crypt import Signer
+ from oauth2client.crypt import make_signed_jwt
+ from oauth2client.crypt import verify_signed_jwt_with_certs
+ HAS_OPENSSL = True
+except ImportError:
+ pass
+
+try:
+ from urlparse import parse_qsl
+except ImportError:
+ from cgi import parse_qsl
+
+logger = logging.getLogger(__name__)
+
+# Expiry is stored in RFC3339 UTC format
+EXPIRY_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
+
+# Which certs to use to validate id_tokens received.
+ID_TOKEN_VERIFICATON_CERTS = 'https://www.googleapis.com/oauth2/v1/certs'
+
+# Constant to use for the out of band OAuth 2.0 flow.
+OOB_CALLBACK_URN = 'urn:ietf:wg:oauth:2.0:oob'
+
+
+class Error(Exception):
+ """Base error for this module."""
+ pass
+
+
+class FlowExchangeError(Error):
+ """Error trying to exchange an authorization grant for an access token."""
+ pass
+
+
+class AccessTokenRefreshError(Error):
+ """Error trying to refresh an expired access token."""
+ pass
+
+class UnknownClientSecretsFlowError(Error):
+ """The client secrets file called for an unknown type of OAuth 2.0 flow. """
+ pass
+
+
+class AccessTokenCredentialsError(Error):
+ """Having only the access_token means no refresh is possible."""
+ pass
+
+
+class VerifyJwtTokenError(Error):
+ """Could on retrieve certificates for validation."""
+ pass
+
+
+def _abstract():
+ raise NotImplementedError('You need to override this function')
+
+
+class MemoryCache(object):
+ """httplib2 Cache implementation which only caches locally."""
+
+ def __init__(self):
+ self.cache = {}
+
+ def get(self, key):
+ return self.cache.get(key)
+
+ def set(self, key, value):
+ self.cache[key] = value
+
+ def delete(self, key):
+ self.cache.pop(key, None)
+
+
+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 also specify a classmethod named 'from_json' that takes a JSON
+ string as input and returns an instaniated Credentials 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
+ replacing http.request() with a method that adds in
+ the appropriate headers and then delegates to the original
+ Http.request() method.
+ """
+ _abstract()
+
+ def refresh(self, http):
+ """Forces a refresh of the access_token.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+ """
+ _abstract()
+
+ def apply(self, headers):
+ """Add the authorization to the headers.
+
+ Args:
+ headers: dict, the headers to add the Authorization header to.
+ """
+ _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:
+ if member in d:
+ 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']
+ try:
+ m = __import__(module)
+ except ImportError:
+ # In case there's an object from the old package structure, update it
+ module = module.replace('.apiclient', '')
+ m = __import__(module)
+
+ m = __import__(module, fromlist=module.split('.')[:-1])
+ kls = getattr(m, data['_class'])
+ from_json = getattr(kls, 'from_json')
+ return from_json(s)
+
+ @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.
+ """
+ return Credentials()
+
+
+class Flow(object):
+ """Base class for all Flow objects."""
+ pass
+
+
+class Storage(object):
+ """Base class for all Storage objects.
+
+ Store and retrieve a single credential. This class supports locking
+ such that multiple processes and threads can operate on a single
+ store.
+ """
+
+ def acquire_lock(self):
+ """Acquires any lock necessary to access this Storage.
+
+ This lock is not reentrant.
+ """
+ pass
+
+ def release_lock(self):
+ """Release the Storage lock.
+
+ Trying to release a lock that isn't held will result in a
+ RuntimeError.
+ """
+ pass
+
+ def locked_get(self):
+ """Retrieve credential.
+
+ The Storage lock must be held when this is called.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ _abstract()
+
+ def locked_put(self, credentials):
+ """Write a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ _abstract()
+
+ def locked_delete(self):
+ """Delete a credential.
+
+ The Storage lock must be held when this is called.
+ """
+ _abstract()
+
+ def get(self):
+ """Retrieve credential.
+
+ The Storage lock must *not* be held when this is called.
+
+ Returns:
+ oauth2client.client.Credentials
+ """
+ self.acquire_lock()
+ try:
+ return self.locked_get()
+ finally:
+ self.release_lock()
+
+ def put(self, credentials):
+ """Write a credential.
+
+ The Storage lock must be held when this is called.
+
+ Args:
+ credentials: Credentials, the credentials to store.
+ """
+ self.acquire_lock()
+ try:
+ self.locked_put(credentials)
+ finally:
+ self.release_lock()
+
+ def delete(self):
+ """Delete credential.
+
+ Frees any resources associated with storing the credential.
+ The Storage lock must *not* be held when this is called.
+
+ Returns:
+ None
+ """
+ self.acquire_lock()
+ try:
+ return self.locked_delete()
+ finally:
+ self.release_lock()
+
+
+class OAuth2Credentials(Credentials):
+ """Credentials object for OAuth 2.0.
+
+ Credentials can be applied to an httplib2.Http object using the authorize()
+ method, which then adds the OAuth 2.0 access token to each request.
+
+ OAuth2Credentials objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(8)
+ def __init__(self, access_token, client_id, client_secret, refresh_token,
+ token_expiry, token_uri, user_agent, id_token=None):
+ """Create an instance of OAuth2Credentials.
+
+ This constructor is not usually called by the user, instead
+ OAuth2Credentials objects are instantiated by the OAuth2WebServerFlow.
+
+ Args:
+ access_token: string, access token.
+ client_id: string, client identifier.
+ client_secret: string, client secret.
+ refresh_token: string, refresh token.
+ token_expiry: datetime, when the access_token expires.
+ token_uri: string, URI of token endpoint.
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ id_token: object, The identity of the resource owner.
+
+ Notes:
+ store: callable, A callable that when passed a Credential
+ will store the credential back to where it came from.
+ This is needed to store the latest access_token if it
+ has expired and been refreshed.
+ """
+ self.access_token = access_token
+ self.client_id = client_id
+ self.client_secret = client_secret
+ self.refresh_token = refresh_token
+ self.store = None
+ self.token_expiry = token_expiry
+ self.token_uri = token_uri
+ self.user_agent = user_agent
+ self.id_token = id_token
+
+ # True if the credentials have been revoked or expired and can't be
+ # refreshed.
+ self.invalid = False
+
+ def authorize(self, http):
+ """Authorize an httplib2.Http instance with these credentials.
+
+ The modified http.request method will add authentication headers to 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,
+ http.request.credentials, which is the Credentials object that authorized
+ it.
+
+ Args:
+ http: An instance of httplib2.Http
+ or something that acts like it.
+
+ Returns:
+ A modified instance of http that was passed in.
+
+ Example:
+
+ h = httplib2.Http()
+ h = credentials.authorize(h)
+
+ You can't create a new OAuth subclass of httplib2.Authenication
+ because it never gets passed the absolute URI, which is needed for
+ signing. So instead we have to overload 'request' with a closure
+ that adds in the Authorization header and then calls the original
+ version of 'request()'.
+ """
+ request_orig = http.request
+
+ # The closure that will replace 'httplib2.Http.request'.
+ @util.positional(1)
+ def new_request(uri, method='GET', body=None, headers=None,
+ redirections=httplib2.DEFAULT_MAX_REDIRECTS,
+ connection_type=None):
+ if not self.access_token:
+ logger.info('Attempting refresh to obtain initial access_token')
+ self._refresh(request_orig)
+
+ # Modify the request headers to add the appropriate
+ # Authorization header.
+ if headers is None:
+ headers = {}
+ self.apply(headers)
+
+ if self.user_agent is not None:
+ if 'user-agent' in headers:
+ headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
+ else:
+ headers['user-agent'] = self.user_agent
+
+ resp, content = request_orig(uri, method, body, headers,
+ redirections, connection_type)
+
+ # Older API (GData) respond with 403
+ if resp.status in [401, 403]:
+ logger.info('Refreshing due to a %s' % str(resp.status))
+ self._refresh(request_orig)
+ self.apply(headers)
+ return request_orig(uri, method, body, headers,
+ redirections, connection_type)
+ else:
+ return (resp, content)
+
+ # Replace the request method with our own closure.
+ http.request = new_request
+
+ # Set credentials as a property of the request method.
+ setattr(http.request, 'credentials', self)
+
+ return http
+
+ def refresh(self, http):
+ """Forces a refresh of the access_token.
+
+ Args:
+ http: httplib2.Http, an http object to be used to make the refresh
+ request.
+ """
+ self._refresh(http.request)
+
+ def apply(self, headers):
+ """Add the authorization to the headers.
+
+ Args:
+ headers: dict, the headers to add the Authorization header to.
+ """
+ headers['Authorization'] = 'Bearer ' + self.access_token
+
+ 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):
+ try:
+ data['token_expiry'] = datetime.datetime.strptime(
+ data['token_expiry'], EXPIRY_FORMAT)
+ except:
+ data['token_expiry'] = None
+ retval = OAuth2Credentials(
+ data['access_token'],
+ data['client_id'],
+ data['client_secret'],
+ data['refresh_token'],
+ data['token_expiry'],
+ data['token_uri'],
+ data['user_agent'],
+ id_token=data.get('id_token', None))
+ retval.invalid = data['invalid']
+ return retval
+
+ @property
+ def access_token_expired(self):
+ """True if the credential is expired or invalid.
+
+ If the token_expiry isn't set, we assume the token doesn't expire.
+ """
+ if self.invalid:
+ return True
+
+ if not self.token_expiry:
+ return False
+
+ now = datetime.datetime.utcnow()
+ if now >= self.token_expiry:
+ logger.info('access_token is expired. Now: %s, token_expiry: %s',
+ now, self.token_expiry)
+ return True
+ return False
+
+ def set_store(self, store):
+ """Set the Storage for the credential.
+
+ Args:
+ store: Storage, an implementation of Stroage object.
+ This is needed to store the latest access_token if it
+ has expired and been refreshed. This implementation uses
+ locking to check for updates before updating the
+ access_token.
+ """
+ self.store = store
+
+ def _updateFromCredential(self, other):
+ """Update this Credential from another instance."""
+ self.__dict__.update(other.__getstate__())
+
+ def __getstate__(self):
+ """Trim the state down to something that can be pickled."""
+ d = copy.copy(self.__dict__)
+ del d['store']
+ return d
+
+ def __setstate__(self, state):
+ """Reconstitute the state of the object from being pickled."""
+ self.__dict__.update(state)
+ self.store = None
+
+ def _generate_refresh_request_body(self):
+ """Generate the body that will be used in the refresh request."""
+ body = urllib.urlencode({
+ 'grant_type': 'refresh_token',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'refresh_token': self.refresh_token,
+ })
+ return body
+
+ def _generate_refresh_request_headers(self):
+ """Generate the headers that will be used in the refresh request."""
+ headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+
+ if self.user_agent is not None:
+ headers['user-agent'] = self.user_agent
+
+ return headers
+
+ def _refresh(self, http_request):
+ """Refreshes the access_token.
+
+ This method first checks by reading the Storage object if available.
+ If a refresh is still needed, it holds the Storage lock until the
+ refresh is completed.
+
+ Args:
+ http_request: callable, a callable that matches the method signature of
+ httplib2.Http.request, used to make the refresh request.
+
+ Raises:
+ AccessTokenRefreshError: When the refresh fails.
+ """
+ if not self.store:
+ self._do_refresh_request(http_request)
+ else:
+ self.store.acquire_lock()
+ try:
+ new_cred = self.store.locked_get()
+ if (new_cred and not new_cred.invalid and
+ new_cred.access_token != self.access_token):
+ logger.info('Updated access_token read from Storage')
+ self._updateFromCredential(new_cred)
+ else:
+ self._do_refresh_request(http_request)
+ finally:
+ self.store.release_lock()
+
+ def _do_refresh_request(self, http_request):
+ """Refresh the access_token using the refresh_token.
+
+ Args:
+ http_request: callable, a callable that matches the method signature of
+ httplib2.Http.request, used to make the refresh request.
+
+ Raises:
+ AccessTokenRefreshError: When the refresh fails.
+ """
+ body = self._generate_refresh_request_body()
+ headers = self._generate_refresh_request_headers()
+
+ logger.info('Refreshing access_token')
+ resp, content = http_request(
+ self.token_uri, method='POST', body=body, headers=headers)
+ if resp.status == 200:
+ # TODO(jcgregorio) Raise an error if loads fails?
+ d = simplejson.loads(content)
+ self.access_token = d['access_token']
+ 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.utcnow()
+ else:
+ self.token_expiry = None
+ if self.store:
+ self.store.locked_put(self)
+ else:
+ # An {'error':...} response body means the token is expired or revoked,
+ # so we flag the credentials as such.
+ logger.info('Failed to retrieve access token: %s' % content)
+ error_msg = 'Invalid response %s.' % resp['status']
+ try:
+ d = simplejson.loads(content)
+ if 'error' in d:
+ error_msg = d['error']
+ self.invalid = True
+ if self.store:
+ self.store.locked_put(self)
+ except StandardError:
+ pass
+ raise AccessTokenRefreshError(error_msg)
+
+
+class AccessTokenCredentials(OAuth2Credentials):
+ """Credentials object for OAuth 2.0.
+
+ Credentials can be applied to an httplib2.Http object using the
+ authorize() method, which then signs each request from that object
+ with the OAuth 2.0 access token. This set of credentials is for the
+ use case where you have acquired an OAuth 2.0 access_token from
+ another place such as a JavaScript client or another web
+ application, and wish to use it from Python. Because only the
+ access_token is present it can not be refreshed and will in time
+ expire.
+
+ AccessTokenCredentials objects may be safely pickled and unpickled.
+
+ Usage:
+ credentials = AccessTokenCredentials('<an access token>',
+ 'my-user-agent/1.0')
+ http = httplib2.Http()
+ http = credentials.authorize(http)
+
+ Exceptions:
+ AccessTokenCredentialsExpired: raised when the access_token expires or is
+ revoked.
+ """
+
+ def __init__(self, access_token, user_agent):
+ """Create an instance of OAuth2Credentials
+
+ This is one of the few types if Credentials that you should contrust,
+ Credentials objects are usually instantiated by a Flow.
+
+ Args:
+ access_token: string, access token.
+ user_agent: string, The HTTP User-Agent to provide for this application.
+
+ Notes:
+ store: callable, a callable that when passed a Credential
+ will store the credential back to where it came from.
+ """
+ super(AccessTokenCredentials, self).__init__(
+ access_token,
+ None,
+ None,
+ None,
+ None,
+ 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.")
+
+
+class AssertionCredentials(OAuth2Credentials):
+ """Abstract Credentials object used for OAuth 2.0 assertion grants.
+
+ This credential does not require a flow to instantiate because it
+ represents a two legged flow, and therefore has all of the required
+ information to generate and refresh its own access tokens. It must
+ be subclassed to generate the appropriate assertion string.
+
+ AssertionCredentials objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(2)
+ def __init__(self, assertion_type, user_agent=None,
+ token_uri='https://accounts.google.com/o/oauth2/token',
+ **unused_kwargs):
+ """Constructor for AssertionFlowCredentials.
+
+ Args:
+ assertion_type: string, assertion type that will be declared to the auth
+ server
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ """
+ super(AssertionCredentials, self).__init__(
+ None,
+ None,
+ None,
+ None,
+ None,
+ token_uri,
+ user_agent)
+ self.assertion_type = assertion_type
+
+ def _generate_refresh_request_body(self):
+ assertion = self._generate_assertion()
+
+ body = urllib.urlencode({
+ 'assertion_type': self.assertion_type,
+ 'assertion': assertion,
+ 'grant_type': 'assertion',
+ })
+
+ return body
+
+ def _generate_assertion(self):
+ """Generate the assertion string that will be used in the access token
+ request.
+ """
+ _abstract()
+
+if HAS_OPENSSL:
+ # PyOpenSSL is not a prerequisite for oauth2client, so if it is missing then
+ # don't create the SignedJwtAssertionCredentials or the verify_id_token()
+ # method.
+
+ class SignedJwtAssertionCredentials(AssertionCredentials):
+ """Credentials object used for OAuth 2.0 Signed JWT assertion grants.
+
+ This credential does not require a flow to instantiate because it represents
+ a two legged flow, and therefore has all of the required information to
+ generate and refresh its own access tokens.
+
+ SignedJwtAssertionCredentials requires PyOpenSSL and because of that it does
+ not work on App Engine. For App Engine you may consider using
+ AppAssertionCredentials.
+ """
+
+ MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
+
+ @util.positional(4)
+ def __init__(self,
+ service_account_name,
+ private_key,
+ scope,
+ private_key_password='notasecret',
+ user_agent=None,
+ token_uri='https://accounts.google.com/o/oauth2/token',
+ **kwargs):
+ """Constructor for SignedJwtAssertionCredentials.
+
+ Args:
+ service_account_name: string, id for account, usually an email address.
+ private_key: string, private key in P12 format.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ private_key_password: string, password for private_key.
+ user_agent: string, HTTP User-Agent to provide for this application.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ kwargs: kwargs, Additional parameters to add to the JWT token, for
+ example prn=joe@xample.org."""
+
+ super(SignedJwtAssertionCredentials, self).__init__(
+ 'http://oauth.net/grant_type/jwt/1.0/bearer',
+ user_agent=user_agent,
+ token_uri=token_uri,
+ )
+
+ if type(scope) is list:
+ scope = ' '.join(scope)
+ self.scope = scope
+
+ # Keep base64 encoded so it can be stored in JSON.
+ self.private_key = base64.b64encode(private_key)
+
+ self.private_key_password = private_key_password
+ self.service_account_name = service_account_name
+ self.kwargs = kwargs
+
+ @classmethod
+ def from_json(cls, s):
+ data = simplejson.loads(s)
+ retval = SignedJwtAssertionCredentials(
+ data['service_account_name'],
+ base64.b64decode(data['private_key']),
+ data['scope'],
+ private_key_password=data['private_key_password'],
+ user_agent=data['user_agent'],
+ token_uri=data['token_uri'],
+ **data['kwargs']
+ )
+ retval.invalid = data['invalid']
+ retval.access_token = data['access_token']
+ return retval
+
+ def _generate_assertion(self):
+ """Generate the assertion that will be used in the request."""
+ now = long(time.time())
+ payload = {
+ 'aud': self.token_uri,
+ 'scope': self.scope,
+ 'iat': now,
+ 'exp': now + SignedJwtAssertionCredentials.MAX_TOKEN_LIFETIME_SECS,
+ 'iss': self.service_account_name
+ }
+ payload.update(self.kwargs)
+ logger.debug(str(payload))
+
+ private_key = base64.b64decode(self.private_key)
+ return make_signed_jwt(
+ Signer.from_string(private_key, self.private_key_password), payload)
+
+ # Only used in verify_id_token(), which is always calling to the same URI
+ # for the certs.
+ _cached_http = httplib2.Http(MemoryCache())
+
+ @util.positional(2)
+ def verify_id_token(id_token, audience, http=None,
+ cert_uri=ID_TOKEN_VERIFICATON_CERTS):
+ """Verifies a signed JWT id_token.
+
+ This function requires PyOpenSSL and because of that it does not work on
+ App Engine. For App Engine you may consider using AppAssertionCredentials.
+
+ Args:
+ id_token: string, A Signed JWT.
+ audience: string, The audience 'aud' that the token should be for.
+ http: httplib2.Http, instance to use to make the HTTP request. Callers
+ should supply an instance that has caching enabled.
+ cert_uri: string, URI of the certificates in JSON format to
+ verify the JWT against.
+
+ Returns:
+ The deserialized JSON in the JWT.
+
+ Raises:
+ oauth2client.crypt.AppIdentityError if the JWT fails to verify.
+ """
+ if http is None:
+ http = _cached_http
+
+ resp, content = http.request(cert_uri)
+
+ if resp.status == 200:
+ certs = simplejson.loads(content)
+ return verify_signed_jwt_with_certs(id_token, certs, audience)
+ else:
+ raise VerifyJwtTokenError('Status code: %d' % resp.status)
+
+
+def _urlsafe_b64decode(b64string):
+ # Guard against unicode strings, which base64 can't handle.
+ b64string = b64string.encode('ascii')
+ padded = b64string + '=' * (4 - len(b64string) % 4)
+ return base64.urlsafe_b64decode(padded)
+
+
+def _extract_id_token(id_token):
+ """Extract the JSON payload from a JWT.
+
+ Does the extraction w/o checking the signature.
+
+ Args:
+ id_token: string, OAuth 2.0 id_token.
+
+ Returns:
+ object, The deserialized JSON payload.
+ """
+ segments = id_token.split('.')
+
+ if (len(segments) != 3):
+ raise VerifyJwtTokenError(
+ 'Wrong number of segments in token: %s' % id_token)
+
+ return simplejson.loads(_urlsafe_b64decode(segments[1]))
+
+
+def _parse_exchange_token_response(content):
+ """Parses response of an exchange token request.
+
+ Most providers return JSON but some (e.g. Facebook) return a
+ url-encoded string.
+
+ Args:
+ content: The body of a response
+
+ Returns:
+ Content as a dictionary object. Note that the dict could be empty,
+ i.e. {}. That basically indicates a failure.
+ """
+ resp = {}
+ try:
+ resp = simplejson.loads(content)
+ except StandardError:
+ # different JSON libs raise different exceptions,
+ # so we just do a catch-all here
+ resp = dict(parse_qsl(content))
+
+ # some providers respond with 'expires', others with 'expires_in'
+ if resp and 'expires' in resp:
+ resp['expires_in'] = resp.pop('expires')
+
+ return resp
+
+
+@util.positional(4)
+def credentials_from_code(client_id, client_secret, scope, code,
+ redirect_uri='postmessage', http=None, user_agent=None,
+ token_uri='https://accounts.google.com/o/oauth2/token'):
+ """Exchanges an authorization code for an OAuth2Credentials object.
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string, client secret.
+ scope: string or list of strings, scope(s) to request.
+ code: string, An authroization code, most likely passed down from
+ the client
+ redirect_uri: string, this is generally set to 'postmessage' to match the
+ redirect_uri that the client specified
+ http: httplib2.Http, optional http instance to use to do the fetch
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ Returns:
+ An OAuth2Credentials object.
+
+ Raises:
+ FlowExchangeError if the authorization code cannot be exchanged for an
+ access token
+ """
+ flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+ redirect_uri=redirect_uri, user_agent=user_agent,
+ auth_uri='https://accounts.google.com/o/oauth2/auth',
+ token_uri=token_uri)
+
+ credentials = flow.step2_exchange(code, http=http)
+ return credentials
+
+
+@util.positional(3)
+def credentials_from_clientsecrets_and_code(filename, scope, code,
+ message = None,
+ redirect_uri='postmessage',
+ http=None,
+ cache=None):
+ """Returns OAuth2Credentials from a clientsecrets file and an auth code.
+
+ Will create the right kind of Flow based on the contents of the clientsecrets
+ file or will raise InvalidClientSecretsError for unknown types of Flows.
+
+ Args:
+ filename: string, File name of clientsecrets.
+ scope: string or list of strings, scope(s) to request.
+ code: string, An authorization code, most likely passed down from
+ the client
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. If message is provided then
+ sys.exit will be called in the case of an error. If message in not
+ provided then clientsecrets.InvalidClientSecretsError will be raised.
+ redirect_uri: string, this is generally set to 'postmessage' to match the
+ redirect_uri that the client specified
+ http: httplib2.Http, optional http instance to use to do the fetch
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+
+ Returns:
+ An OAuth2Credentials object.
+
+ Raises:
+ FlowExchangeError if the authorization code cannot be exchanged for an
+ access token
+ UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
+ clientsecrets.InvalidClientSecretsError if the clientsecrets file is
+ invalid.
+ """
+ flow = flow_from_clientsecrets(filename, scope, message=message, cache=cache,
+ redirect_uri=redirect_uri)
+ credentials = flow.step2_exchange(code, http=http)
+ return credentials
+
+
+class OAuth2WebServerFlow(Flow):
+ """Does the Web Server Flow for OAuth 2.0.
+
+ OAuth2WebServerFlow objects may be safely pickled and unpickled.
+ """
+
+ @util.positional(4)
+ def __init__(self, client_id, client_secret, scope,
+ redirect_uri=None,
+ user_agent=None,
+ auth_uri='https://accounts.google.com/o/oauth2/auth',
+ token_uri='https://accounts.google.com/o/oauth2/token',
+ **kwargs):
+ """Constructor for OAuth2WebServerFlow.
+
+ Args:
+ client_id: string, client identifier.
+ client_secret: string client secret.
+ scope: string or list of strings, scope(s) of the credentials being
+ requested.
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+ a non-web-based application, or a URI that handles the callback from
+ the authorization server.
+ user_agent: string, HTTP User-Agent to provide for this application.
+ auth_uri: string, URI for authorization endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ **kwargs: dict, The keyword arguments are all optional and required
+ parameters for the OAuth calls.
+ """
+ self.client_id = client_id
+ self.client_secret = client_secret
+ if type(scope) is list:
+ scope = ' '.join(scope)
+ self.scope = scope
+ self.redirect_uri = redirect_uri
+ self.user_agent = user_agent
+ self.auth_uri = auth_uri
+ self.token_uri = token_uri
+ self.params = {
+ 'access_type': 'offline',
+ }
+ self.params.update(kwargs)
+
+ @util.positional(1)
+ def step1_get_authorize_url(self, redirect_uri=None):
+ """Returns a URI to redirect to the provider.
+
+ Args:
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+ a non-web-based application, or a URI that handles the callback from
+ the authorization server. This parameter is deprecated, please move to
+ passing the redirect_uri in via the constructor.
+
+ Returns:
+ A URI as a string to redirect the user to begin the authorization flow.
+ """
+ if redirect_uri is not None:
+ logger.warning(('The redirect_uri parameter for'
+ 'OAuth2WebServerFlow.step1_get_authorize_url is deprecated. Please'
+ 'move to passing the redirect_uri in via the constructor.'))
+ self.redirect_uri = redirect_uri
+
+ if self.redirect_uri is None:
+ raise ValueError('The value of redirect_uri must not be None.')
+
+ query = {
+ 'response_type': 'code',
+ 'client_id': self.client_id,
+ 'redirect_uri': self.redirect_uri,
+ 'scope': self.scope,
+ }
+ query.update(self.params)
+ parts = list(urlparse.urlparse(self.auth_uri))
+ query.update(dict(parse_qsl(parts[4]))) # 4 is the index of the query part
+ parts[4] = urllib.urlencode(query)
+ return urlparse.urlunparse(parts)
+
+ @util.positional(2)
+ def step2_exchange(self, code, http=None):
+ """Exhanges a code for OAuth2Credentials.
+
+ Args:
+ code: string or dict, either the code as a string, or a dictionary
+ of the query parameters to the redirect_uri, which contains
+ the code.
+ http: httplib2.Http, optional http instance to use to do the fetch
+
+ Returns:
+ An OAuth2Credentials object that can be used to authorize requests.
+
+ Raises:
+ FlowExchangeError if a problem occured exchanging the code for a
+ refresh_token.
+ """
+
+ if not (isinstance(code, str) or isinstance(code, unicode)):
+ if 'code' not in code:
+ if 'error' in code:
+ error_msg = code['error']
+ else:
+ error_msg = 'No code was supplied in the query parameters.'
+ raise FlowExchangeError(error_msg)
+ else:
+ code = code['code']
+
+ body = urllib.urlencode({
+ 'grant_type': 'authorization_code',
+ 'client_id': self.client_id,
+ 'client_secret': self.client_secret,
+ 'code': code,
+ 'redirect_uri': self.redirect_uri,
+ 'scope': self.scope,
+ })
+ headers = {
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+
+ if self.user_agent is not None:
+ headers['user-agent'] = self.user_agent
+
+ if http is None:
+ http = httplib2.Http()
+
+ resp, content = http.request(self.token_uri, method='POST', body=body,
+ headers=headers)
+ d = _parse_exchange_token_response(content)
+ if resp.status == 200 and 'access_token' in d:
+ access_token = d['access_token']
+ refresh_token = d.get('refresh_token', None)
+ token_expiry = None
+ if 'expires_in' in d:
+ token_expiry = datetime.datetime.utcnow() + datetime.timedelta(
+ seconds=int(d['expires_in']))
+
+ if 'id_token' in d:
+ d['id_token'] = _extract_id_token(d['id_token'])
+
+ logger.info('Successfully retrieved access token: %s' % content)
+ return OAuth2Credentials(access_token, self.client_id,
+ self.client_secret, refresh_token, token_expiry,
+ self.token_uri, self.user_agent,
+ id_token=d.get('id_token', None))
+ else:
+ logger.info('Failed to retrieve access token: %s' % content)
+ if 'error' in d:
+ # you never know what those providers got to say
+ error_msg = unicode(d['error'])
+ else:
+ error_msg = 'Invalid response: %s.' % str(resp.status)
+ raise FlowExchangeError(error_msg)
+
+
+@util.positional(2)
+def flow_from_clientsecrets(filename, scope, redirect_uri=None, message=None, cache=None):
+ """Create a Flow from a clientsecrets file.
+
+ Will create the right kind of Flow based on the contents of the clientsecrets
+ file or will raise InvalidClientSecretsError for unknown types of Flows.
+
+ Args:
+ filename: string, File name of client secrets.
+ scope: string or list of strings, scope(s) to request.
+ redirect_uri: string, Either the string 'urn:ietf:wg:oauth:2.0:oob' for
+ a non-web-based application, or a URI that handles the callback from
+ the authorization server.
+ message: string, A friendly string to display to the user if the
+ clientsecrets file is missing or invalid. If message is provided then
+ sys.exit will be called in the case of an error. If message in not
+ provided then clientsecrets.InvalidClientSecretsError will be raised.
+ cache: An optional cache service client that implements get() and set()
+ methods. See clientsecrets.loadfile() for details.
+
+ Returns:
+ A Flow object.
+
+ Raises:
+ UnknownClientSecretsFlowError if the file describes an unknown kind of Flow.
+ clientsecrets.InvalidClientSecretsError if the clientsecrets file is
+ invalid.
+ """
+ try:
+ client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
+ if client_type in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
+ return OAuth2WebServerFlow(
+ client_info['client_id'],
+ client_info['client_secret'],
+ scope,
+ redirect_uri=redirect_uri,
+ user_agent=None,
+ auth_uri=client_info['auth_uri'],
+ token_uri=client_info['token_uri'])
+
+ except clientsecrets.InvalidClientSecretsError:
+ if message:
+ sys.exit(message)
+ else:
+ raise
+ else:
+ raise UnknownClientSecretsFlowError(
+ 'This OAuth 2.0 flow is unsupported: "%s"' * client_type)
View
150 vendor/oauth2client/clientsecrets.py
@@ -0,0 +1,150 @@
+# Copyright (C) 2011 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for reading OAuth 2.0 client secret files.
+
+A client_secrets.json file contains all the information needed to interact with
+an OAuth 2.0 protected service.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+
+from anyjson import simplejson
+
+# Properties that make a client_secrets.json file valid.
+TYPE_WEB = 'web'
+TYPE_INSTALLED = 'installed'
+
+VALID_CLIENT = {
+ TYPE_WEB: {
+ 'required': [
+ 'client_id',
+ 'client_secret',
+ 'redirect_uris',
+ 'auth_uri',
+ 'token_uri'],
+ 'string': [
+ 'client_id',
+ 'client_secret'
+ ]
+ },
+ TYPE_INSTALLED: {
+ 'required': [
+ 'client_id',
+ 'client_secret',
+ 'redirect_uris',
+ 'auth_uri',
+ 'token_uri'],
+ 'string': [
+ 'client_id',
+ 'client_secret'
+ ]
+ }
+ }
+
+class Error(Exception):
+ """Base error for this module."""
+ pass
+
+
+class InvalidClientSecretsError(Error):
+ """Format of ClientSecrets file is invalid."""
+ pass
+
+
+def _validate_clientsecrets(obj):
+ if obj is None or len(obj) != 1:
+ raise InvalidClientSecretsError('Invalid file format.')
+ client_type = obj.keys()[0]
+ if client_type not in VALID_CLIENT.keys():
+ raise InvalidClientSecretsError('Unknown client type: %s.' % client_type)
+ client_info = obj[client_type]
+ for prop_name in VALID_CLIENT[client_type]['required']:
+ if prop_name not in client_info:
+ raise InvalidClientSecretsError(
+ 'Missing property "%s" in a client type of "%s".' % (prop_name,
+ client_type))
+ for prop_name in VALID_CLIENT[client_type]['string']:
+ if client_info[prop_name].startswith('[['):
+ raise InvalidClientSecretsError(
+ 'Property "%s" is not configured.' % prop_name)
+ return client_type, client_info
+
+
+def load(fp):
+ obj = simplejson.load(fp)
+ return _validate_clientsecrets(obj)
+
+
+def loads(s):
+ obj = simplejson.loads(s)
+ return _validate_clientsecrets(obj)
+
+
+def _loadfile(filename):
+ try:
+ fp = file(filename, 'r')
+ try:
+ obj = simplejson.load(fp)
+ finally:
+ fp.close()
+ except IOError:
+ raise InvalidClientSecretsError('File not found: "%s"' % filename)
+ return _validate_clientsecrets(obj)
+
+
+def loadfile(filename, cache=None):
+ """Loading of client_secrets JSON file, optionally backed by a cache.
+
+ Typical cache storage would be App Engine memcache service,
+ but you can pass in any other cache client that implements
+ these methods:
+ - get(key, namespace=ns)
+ - set(key, value, namespace=ns)
+
+ Usage:
+ # without caching
+ client_type, client_info = loadfile('secrets.json')
+ # using App Engine memcache service
+ from google.appengine.api import memcache
+ client_type, client_info = loadfile('secrets.json', cache=memcache)
+
+ Args:
+ filename: string, Path to a client_secrets.json file on a filesystem.
+ cache: An optional cache service client that implements get() and set()
+ methods. If not specified, the file is always being loaded from
+ a filesystem.
+
+ Raises:
+ InvalidClientSecretsError: In case of a validation error or some
+ I/O failure. Can happen only on cache miss.
+
+ Returns:
+ (client_type, client_info) tuple, as _loadfile() normally would.
+ JSON contents is validated only during first load. Cache hits are not
+ validated.
+ """
+ _SECRET_NAMESPACE = 'oauth2client:secrets#ns'
+
+ if not cache:
+ return _loadfile(filename)
+
+ obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
+ if obj is None:
+ client_type, client_info = _loadfile(filename)
+ obj = { client_type: client_info }
+ cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
+
+ return obj.iteritems().next()
View
246 vendor/oauth2client/crypt.py
@@ -0,0 +1,246 @@
+#!/usr/bin/python2.4
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import hashlib
+import logging
+import time
+
+from OpenSSL import crypto
+from anyjson import simplejson
+
+
+logger = logging.getLogger(__name__)
+
+CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
+AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
+MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
+
+
+class AppIdentityError(Exception):
+ pass
+
+
+class Verifier(object):
+ """Verifies the signature on a message."""
+
+ def __init__(self, pubkey):
+ """Constructor.
+
+ Args:
+ pubkey, OpenSSL.crypto.PKey, The public key to verify with.
+ """
+ self._pubkey = pubkey
+
+ def verify(self, message, signature):
+ """Verifies a message against a signature.
+
+ Args:
+ message: string, The message to verify.
+ signature: string, The signature on the message.
+
+ Returns:
+ True if message was singed by the private key associated with the public
+ key that this object was constructed with.
+ """
+ try:
+ crypto.verify(self._pubkey, signature, message, 'sha256')
+ return True
+ except:
+ return False
+
+ @staticmethod
+ def from_string(key_pem, is_x509_cert):
+ """Construct a Verified instance from a string.
+
+ Args:
+ key_pem: string, public key in PEM format.
+ is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
+ expected to be an RSA key in PEM format.
+
+ Returns:
+ Verifier instance.
+
+ Raises:
+ OpenSSL.crypto.Error if the key_pem can't be parsed.
+ """
+ if is_x509_cert:
+ pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
+ else:
+ pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
+ return Verifier(pubkey)
+
+
+class Signer(object):
+ """Signs messages with a private key."""
+
+ def __init__(self, pkey):
+ """Constructor.
+
+ Args:
+ pkey, OpenSSL.crypto.PKey, The private key to sign with.
+ """
+ self._key = pkey
+
+ def sign(self, message):
+ """Signs a message.
+
+ Args:
+ message: string, Message to be signed.
+
+ Returns:
+ string, The signature of the message for the given key.
+ """
+ return crypto.sign(self._key, message, 'sha256')
+
+ @staticmethod
+ def from_string(key, password='notasecret'):
+ """Construct a Signer instance from a string.
+
+ Args:
+ key: string, private key in P12 format.
+ password: string, password for the private key file.
+
+ Returns:
+ Signer instance.
+
+ Raises:
+ OpenSSL.crypto.Error if the key can't be parsed.
+ """
+ pkey = crypto.load_pkcs12(key, password).get_privatekey()
+ return Signer(pkey)
+
+
+def _urlsafe_b64encode(raw_bytes):
+ return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
+
+
+def _urlsafe_b64decode(b64string):
+ # Guard against unicode strings, which base64 can't handle.
+ b64string = b64string.encode('ascii')
+ padded = b64string + '=' * (4 - len(b64string) % 4)
+ return base64.urlsafe_b64decode(padded)
+
+
+def _json_encode(data):
+ return simplejson.dumps(data, separators = (',', ':'))
+
+
+def make_signed_jwt(signer, payload):
+ """Make a signed JWT.
+
+ See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+ Args:
+ signer: crypt.Signer, Cryptographic signer.
+ payload: dict, Dictionary of data to convert to JSON and then sign.
+
+ Returns:
+ string, The JWT for the payload.
+ """
+ header = {'typ': 'JWT', 'alg': 'RS256'}
+
+ segments = [
+ _urlsafe_b64encode(_json_encode(header)),
+ _urlsafe_b64encode(_json_encode(payload)),
+ ]
+ signing_input = '.'.join(segments)
+
+ signature = signer.sign(signing_input)
+ segments.append(_urlsafe_b64encode(signature))
+
+ logger.debug(str(segments))
+
+ return '.'.join(segments)
+
+
+def verify_signed_jwt_with_certs(jwt, certs, audience):
+ """Verify a JWT against public certs.
+
+ See http://self-issued.info/docs/draft-jones-json-web-token.html.
+
+ Args:
+ jwt: string, A JWT.
+ certs: dict, Dictionary where values of public keys in PEM format.
+ audience: string, The audience, 'aud', that this JWT should contain. If
+ None then the JWT's 'aud' parameter is not verified.
+
+ Returns:
+ dict, The deserialized JSON payload in the JWT.
+
+ Raises:
+ AppIdentityError if any checks are failed.
+ """
+ segments = jwt.split('.')
+
+ if (len(segments) != 3):
+ raise AppIdentityError(
+ 'Wrong number of segments in token: %s' % jwt)
+ signed = '%s.%s' % (segments[0], segments[1])
+
+ signature = _urlsafe_b64decode(segments[2])
+
+ # Parse token.
+ json_body = _urlsafe_b64decode(segments[1])
+ try:
+ parsed = simplejson.loads(json_body)
+ except:
+ raise AppIdentityError('Can\'t parse token: %s' % json_body)
+
+ # Check signature.
+ verified = False
+ for (keyname, pem) in certs.items():
+ verifier = Verifier.from_string(pem, True)
+ if (verifier.verify(signed, signature)):
+ verified = True
+ break
+ if not verified:
+ raise AppIdentityError('Invalid token signature: %s' % jwt)
+
+ # Check creation timestamp.
+ iat = parsed.get('iat')
+ if iat is None:
+ raise AppIdentityError('No iat field in token: %s' % json_body)
+ earliest = iat - CLOCK_SKEW_SECS
+
+ # Check expiration timestamp.
+ now = long(time.time())
+ exp = parsed.get('exp')
+ if exp is None:
+ raise AppIdentityError('No exp field in token: %s' % json_body)
+ if exp >= now + MAX_TOKEN_LIFETIME_SECS:
+ raise AppIdentityError(
+ 'exp field too far in future: %s' % json_body)
+ latest = exp + CLOCK_SKEW_SECS
+
+ if now < earliest:
+ raise AppIdentityError('Token used too early, %d < %d: %s' %
+ (now, earliest, json_body))
+ if now > latest:
+ raise AppIdentityError('Token used too late, %d > %d: %s' %
+ (now, latest, json_body))
+
+ # Check audience.
+ if audience is not None:
+ aud = parsed.get('aud')
+ if aud is None:
+ raise AppIdentityError('No aud field in token: %s' % json_body)
+ if aud != audience:
+ raise AppIdentityError('Wrong recipient, %s != %s: %s' %
+ (aud, audience, json_body))
+
+ return parsed
View
134 vendor/oauth2client/django_orm.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2010 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""OAuth 2.0 utilities for Django.
+
+Utilities for using OAuth 2.0 in conjunction with
+the Django datastore.
+"""
+
+__author__ = 'jcgregorio@google.com (Joe Gregorio)'
+
+import oauth2client
+import base64
+import pickle
+
+from django.db import models
+from oauth2client.client import Storage as BaseStorage
+
+class CredentialsField(models.Field):
+
+ __metaclass__ = models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ if 'null' not in kwargs:
+ kwargs['null'] = True
+ super(CredentialsField, self).__init__(*args, **kwargs)
+
+ def get_internal_type(self):
+ return "TextField"
+
+ def to_python(self, value):
+ if value is None:
+ return None
+ if isinstance(value, oauth2client.client.Credentials):
+ return value
+ return pickle.loads(base64.b64decode(value))
+
+ def get_db_prep_value(self, value, connection, prepared=False):
+ if value is None:
+ return None
+ return base64.b64encode(pickle.dumps(value))
+
+
+class FlowField(models.Field):
+
+ __metaclass__ = models.SubfieldBase
+
+ def __init__(self, *args, **kwargs):
+ if 'null' not in kwargs:
+ kwargs['null'] = True
+ super(FlowField, self).__init__(*args, **kwargs)
+
+ def get_internal_type(self):
+ return "TextField"
+
+ def to_python(self, value):
+ if value is None:
+ return None
+ if isinstance(value, oauth2client.client.Flow):
+ return value
+ return pickle.loads(base64.b64decode(value))
+
+ def get_db_prep_value(self, value, connection, prepared=False):
+ if value is None:
+ return None
+ return base64.b64encode(pickle.dumps(value))
+
+
+class Storage(BaseStorage):
+ """Store and retrieve a single credential to and from
+ the datastore.
+
+ This Storage helper presumes the Credentials
+ have been stored as a CredenialsField
+ on a db model class.
+ """
+
+ def __init__(self, model_class, key_name, key_value, property_name):
+ """Constructor for Storage.
+