Add robot helpers and a sample.
diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py
index 71aa8f0..a9766c7 100644
--- a/oauth2client/appengine.py
+++ b/oauth2client/appengine.py
@@ -21,14 +21,29 @@
import httplib2
import pickle
+import time
+import base64
+import logging
+
+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 AccessTokenRefreshError
+from client import AssertionCredentials
from client import Credentials
from client import Flow
from client import OAuth2WebServerFlow
from client import Storage
from google.appengine.api import memcache
from google.appengine.api import users
+from google.appengine.api.app_identity import app_identity
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import login_required
@@ -36,6 +51,76 @@
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
+
+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. The algorithm used for generating the assertion is
+ the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
+ the following link:
+
+ http://self-issued.info/docs/draft-jones-json-web-token.html
+
+ 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.
+
+ AssertionFlowCredentials objects may be safely pickled and unpickled.
+ """
+
+ def __init__(self, scope, user_agent,
+ audience='https://accounts.google.com/o/oauth2/token',
+ assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
+ token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
+ """Constructor for AppAssertionCredentials
+
+ Args:
+ scope: string, scope of the credentials being requested.
+ user_agent: string, The HTTP User-Agent to provide for this application.
+ audience: string, The audience, or verifier of the assertion. For
+ convenience defaults to Google's audience.
+ assertion_type: string, Type name that will identify the format of the
+ assertion string. For convience, defaults to the JSON Web Token (JWT)
+ assertion type string.
+ token_uri: string, URI for token endpoint. For convenience
+ defaults to Google's endpoints but any OAuth 2.0 provider can be used.
+ """
+ self.scope = scope
+ self.audience = audience
+ self.app_name = app_identity.get_service_account_name()
+
+ super(AppAssertionCredentials, self).__init__(
+ assertion_type,
+ user_agent,
+ token_uri)
+
+ def _generate_assertion(self):
+ header = {
+ 'typ': 'JWT',
+ 'alg': 'RS256',
+ }
+
+ now = int(time.time())
+ claims = {
+ 'aud': self.audience,
+ 'scope': self.scope,
+ 'iat': now,
+ 'exp': now + 3600,
+ 'iss': self.app_name,
+ }
+
+ jwt_components = [base64.b64encode(simplejson.dumps(seg))
+ for seg in [header, claims]]
+
+ base_str = ".".join(jwt_components)
+ key_name, signature = app_identity.sign_blob(base_str)
+ jwt_components.append(base64.b64encode(signature))
+ return ".".join(jwt_components)
+
+
class FlowProperty(db.Property):
"""App Engine datastore Property for Flow.
@@ -117,7 +202,7 @@
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 an CredentialsProperty
+ 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
@@ -189,6 +274,7 @@
# in API calls
"""
+
def __init__(self, client_id, client_secret, scope, user_agent,
auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token'):
@@ -205,8 +291,8 @@
token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
"""
- self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
- auth_uri, token_uri)
+ self.flow = OAuth2WebServerFlow(client_id, client_secret, scope,
+ user_agent, auth_uri, token_uri)
self.credentials = None
self._request_handler = None
@@ -220,6 +306,7 @@
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
+
def check_oauth(request_handler, *args):
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request.
@@ -255,6 +342,7 @@
method: callable, to be decorated method of a webapp.RequestHandler
instance.
"""
+
def setup_oauth(request_handler, *args):
user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request.
@@ -308,10 +396,12 @@
error = self.request.get('error')
if error:
errormsg = self.request.get('error_description', error)
- self.response.out.write('The authorization request failed: %s' % errormsg)
+ self.response.out.write(
+ 'The authorization request failed: %s' % errormsg)
else:
user = users.get_current_user()
- flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
+ flow = pickle.loads(memcache.get(user.user_id(),
+ namespace=OAUTH2CLIENT_NAMESPACE))
# This code should be ammended with application specific error
# handling. The following cases should be considered:
# 1. What if the flow doesn't exist in memcache? Or is corrupt?
@@ -328,6 +418,7 @@
application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
+
def main():
run_wsgi_app(application)
diff --git a/oauth2client/client.py b/oauth2client/client.py
index 3c59980..523a185 100644
--- a/oauth2client/client.py
+++ b/oauth2client/client.py
@@ -83,6 +83,7 @@
"""
_abstract()
+
class Flow(object):
"""Base class for all Flow objects."""
pass
@@ -94,7 +95,6 @@
Store and retrieve a single credential.
"""
-
def get(self):
"""Retrieve credential.
@@ -187,6 +187,26 @@
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 = {
+ 'user-agent': self.user_agent,
+ 'content-type': 'application/x-www-form-urlencoded',
+ }
+ return headers
+
def _refresh(self, http_request):
"""Refresh the access_token using the refresh_token.
@@ -194,16 +214,9 @@
http: An instance of httplib2.Http.request
or something that acts like it.
"""
- body = urllib.urlencode({
- 'grant_type': 'refresh_token',
- 'client_id': self.client_id,
- 'client_secret': self.client_secret,
- 'refresh_token' : self.refresh_token
- })
- headers = {
- 'user-agent': self.user_agent,
- 'content-type': 'application/x-www-form-urlencoded'
- }
+ body = self._generate_refresh_request_body()
+ headers = self._generate_refresh_request_headers()
+
logging.info("Refresing access_token")
resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers)
@@ -214,14 +227,14 @@
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.now()
else:
self.token_expiry = None
if self.store is not None:
self.store(self)
else:
- # An {'error':...} response body means the token is expired or revoked, so
- # we flag the credentials as such.
+ # An {'error':...} response body means the token is expired or revoked,
+ # so we flag the credentials as such.
logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status']
try:
@@ -232,7 +245,8 @@
if self.store is not None:
self.store(self)
else:
- logging.warning("Unable to store refreshed credentials, no Storage provided.")
+ logging.warning(
+ "Unable to store refreshed credentials, no Storage provided.")
except:
pass
raise AccessTokenRefreshError(error_msg)
@@ -266,6 +280,10 @@
def new_request(uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None):
+ if not self.access_token:
+ logging.info("Attempting refresh to obtain initial access_token")
+ self._refresh(request_orig)
+
"""Modify the request headers to add the appropriate
Authorization header."""
if headers == None:
@@ -275,8 +293,10 @@
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)
+
if resp.status == 401:
logging.info("Refreshing because we got a 401")
self._refresh(request_orig)
@@ -341,6 +361,57 @@
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.
+ """
+
+ def __init__(self, assertion_type, user_agent,
+ token_uri='https://accounts.google.com/o/oauth2/token', **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()
+
+
class OAuth2WebServerFlow(Flow):
"""Does the Web Server Flow for OAuth 2.0.
@@ -420,15 +491,16 @@
'client_secret': self.client_secret,
'code': code,
'redirect_uri': self.redirect_uri,
- 'scope': self.scope
+ 'scope': self.scope,
})
headers = {
'user-agent': self.user_agent,
- 'content-type': 'application/x-www-form-urlencoded'
+ 'content-type': 'application/x-www-form-urlencoded',
}
if http is None:
http = httplib2.Http()
- resp, content = http.request(self.token_uri, method='POST', body=body, headers=headers)
+ resp, content = http.request(self.token_uri, method='POST', body=body,
+ headers=headers)
if resp.status == 200:
# TODO(jcgregorio) Raise an error if simplejson.loads fails?
d = simplejson.loads(content)
@@ -436,12 +508,13 @@
refresh_token = d.get('refresh_token', None)
token_expiry = None
if 'expires_in' in d:
- token_expiry = datetime.datetime.now() + datetime.timedelta(seconds = int(d['expires_in']))
+ token_expiry = datetime.datetime.now() + datetime.timedelta(
+ seconds=int(d['expires_in']))
logging.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)
+ return OAuth2Credentials(access_token, self.client_id,
+ self.client_secret, refresh_token, token_expiry,
+ self.token_uri, self.user_agent)
else:
logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status']