diff --git a/oauth2client/appengine.py b/oauth2client/appengine.py index b463100..3b7ca3d 100644 --- a/oauth2client/appengine.py +++ b/oauth2client/appengine.py @@ -36,7 +36,7 @@ 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.api import app_identity from google.appengine.ext import db from google.appengine.ext import webapp from google.appengine.ext.webapp.util import login_required @@ -56,75 +56,52 @@ class AppAssertionCredentials(AssertionCredentials): 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 + 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. - """ - def __init__(self, scope, - 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): + def __init__(self, scope, **kwargs): """Constructor for AppAssertionCredentials Args: - scope: string, scope of the credentials being requested. - 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. + scope: string or list of strings, scope(s) of the credentials being requested. """ + if type(scope) is list: + scope = ' '.join(scope) self.scope = scope - self.audience = audience - self.app_name = app_identity.get_service_account_name() super(AppAssertionCredentials, self).__init__( - assertion_type, None, - token_uri) + None, + None) @classmethod def from_json(cls, json): data = simplejson.loads(json) - retval = AccessTokenCredentials( - data['scope'], - data['audience'], - data['assertion_type'], - data['token_uri']) - return retval + return AppAssertionCredentials(data['scope']) - def _generate_assertion(self): - header = { - 'typ': 'JWT', - 'alg': 'RS256', - } + def _refresh(self, http_request): + """Refreshes the access_token. - now = int(time.time()) - claims = { - 'aud': self.audience, - 'scope': self.scope, - 'iat': now, - 'exp': now + 3600, - 'iss': self.app_name, - } + 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. - jwt_components = [base64.b64encode(simplejson.dumps(seg)) - for seg in [header, claims]] + Args: + http_request: callable, a callable that matches the method signature of + httplib2.Http.request, used to make the refresh request. - base_str = ".".join(jwt_components) - key_name, signature = app_identity.sign_blob(base_str) - jwt_components.append(base64.b64encode(signature)) - return ".".join(jwt_components) + 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): diff --git a/samples/appengine_with_robots/app.yaml b/samples/appengine_with_robots/app.yaml index e83ac03..c313482 100644 --- a/samples/appengine_with_robots/app.yaml +++ b/samples/appengine_with_robots/app.yaml @@ -1,4 +1,4 @@ -application: urlshortener-robot +application: robot-sample version: 2 runtime: python api_version: 1 diff --git a/samples/appengine_with_robots/main.py b/samples/appengine_with_robots/main.py index 5bfe13d..7462024 100644 --- a/samples/appengine_with_robots/main.py +++ b/samples/appengine_with_robots/main.py @@ -51,8 +51,10 @@ class MainHandler(webapp.RequestHandler): def get(self): path = os.path.join(os.path.dirname(__file__), 'welcome.html') shortened = service.url().list().execute() - short_and_long = [(item["id"], item["longUrl"]) for item in - shortened["items"]] + short_and_long = [] + if 'items' in shortened: + short_and_long = [(item["id"], item["longUrl"]) for item in + shortened["items"]] variables = { 'short_and_long': short_and_long, @@ -61,6 +63,7 @@ class MainHandler(webapp.RequestHandler): def post(self): long_url = self.request.get("longUrl") + credentials.refresh(http) shortened = service.url().insert(body={"longUrl": long_url}).execute() self.redirect("/") diff --git a/tests/test_oauth2client_appengine.py b/tests/test_oauth2client_appengine.py index 1f58814..a0f32b2 100644 --- a/tests/test_oauth2client_appengine.py +++ b/tests/test_oauth2client_appengine.py @@ -24,6 +24,7 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)' import base64 import httplib2 +import time import unittest import urlparse @@ -38,9 +39,12 @@ dev_appserver.fix_sys_path() from apiclient.http import HttpMockSequence from google.appengine.api import apiproxy_stub from google.appengine.api import apiproxy_stub_map +from google.appengine.api import app_identity from google.appengine.api import users +from google.appengine.api.memcache import memcache_stub from google.appengine.ext import testbed from google.appengine.ext import webapp +from google.appengine.runtime import apiproxy_errors from oauth2client.anyjson import simplejson from oauth2client.appengine import AppAssertionCredentials from oauth2client.appengine import OAuth2Decorator @@ -49,7 +53,6 @@ from oauth2client.client import AccessTokenRefreshError from oauth2client.client import FlowExchangeError from webtest import TestApp - class UserMock(object): """Mock the app engine user service""" @@ -76,51 +79,57 @@ class TestAppAssertionCredentials(unittest.TestCase): account_name = "service_account_name@appspot.com" signature = "signature" + class AppIdentityStubImpl(apiproxy_stub.APIProxyStub): def __init__(self): super(TestAppAssertionCredentials.AppIdentityStubImpl, self).__init__( 'app_identity_service') - def _Dynamic_GetServiceAccountName(self, request, response): - return response.set_service_account_name( - TestAppAssertionCredentials.account_name) + def _Dynamic_GetAccessToken(self, request, response): + response.set_access_token('a_token_123') + response.set_expiration_time(time.time() + 1800) - def _Dynamic_SignForApp(self, request, response): - return response.set_signature_bytes( - TestAppAssertionCredentials.signature) - def setUp(self): - app_identity_stub = self.AppIdentityStubImpl() + class ErroringAppIdentityStubImpl(apiproxy_stub.APIProxyStub): + + def __init__(self): + super(TestAppAssertionCredentials.ErroringAppIdentityStubImpl, self).__init__( + 'app_identity_service') + + def _Dynamic_GetAccessToken(self, request, response): + raise app_identity.BackendDeadlineExceeded() + + def test_raise_correct_type_of_exception(self): + app_identity_stub = self.ErroringAppIdentityStubImpl() + apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service", app_identity_stub) + apiproxy_stub_map.apiproxy.RegisterStub( + 'memcache', memcache_stub.MemcacheServiceStub()) - self.scope = "http://www.googleapis.com/scope" - self.credentials = AppAssertionCredentials(self.scope) + scope = "http://www.googleapis.com/scope" + try: + credentials = AppAssertionCredentials(scope) + http = httplib2.Http() + credentials.refresh(http) + self.fail('Should have raised an AccessTokenRefreshError') + except AccessTokenRefreshError: + pass - def test_assertion(self): - assertion = self.credentials._generate_assertion() + def test_get_access_token_on_refresh(self): + app_identity_stub = self.AppIdentityStubImpl() + apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap() + apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service", + app_identity_stub) + apiproxy_stub_map.apiproxy.RegisterStub( + 'memcache', memcache_stub.MemcacheServiceStub()) - parts = assertion.split(".") - self.assertTrue(len(parts) == 3) - - header, body, signature = [base64.b64decode(part) for part in parts] - - header_dict = simplejson.loads(header) - self.assertEqual(header_dict['typ'], 'JWT') - self.assertEqual(header_dict['alg'], 'RS256') - - body_dict = simplejson.loads(body) - self.assertEqual(body_dict['aud'], - 'https://accounts.google.com/o/oauth2/token') - self.assertEqual(body_dict['scope'], self.scope) - self.assertEqual(body_dict['iss'], self.account_name) - - issuedAt = body_dict['iat'] - self.assertTrue(issuedAt > 0) - self.assertEqual(body_dict['exp'], issuedAt + 3600) - - self.assertEqual(signature, self.signature) + scope = "http://www.googleapis.com/scope" + credentials = AppAssertionCredentials(scope) + http = httplib2.Http() + credentials.refresh(http) + self.assertEqual('a_token_123', credentials.access_token) class DecoratorTests(unittest.TestCase):