Add robot helpers and a sample.

This commit is contained in:
JacobMoshenko
2011-06-20 09:53:10 -04:00
parent d7bba20323
commit 8e90510215
14 changed files with 411 additions and 43 deletions

View File

@@ -21,14 +21,29 @@ __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import httplib2 import httplib2
import pickle 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 AccessTokenRefreshError
from client import AssertionCredentials
from client import Credentials from client import Credentials
from client import Flow from client import Flow
from client import OAuth2WebServerFlow from client import OAuth2WebServerFlow
from client import Storage from client import Storage
from google.appengine.api import memcache from google.appengine.api import memcache
from google.appengine.api import users 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 db
from google.appengine.ext import webapp from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import login_required from google.appengine.ext.webapp.util import login_required
@@ -36,6 +51,76 @@ from google.appengine.ext.webapp.util import run_wsgi_app
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 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): class FlowProperty(db.Property):
"""App Engine datastore Property for Flow. """App Engine datastore Property for Flow.
@@ -117,7 +202,7 @@ class StorageByKeyName(Storage):
Args: Args:
model: db.Model, model class model: db.Model, model class
key_name: string, key name for the entity that has the credentials key_name: string, key name for the entity that has the credentials
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 cache: memcache, a write-through cache to put in front of the datastore
""" """
self._model = model self._model = model
@@ -189,6 +274,7 @@ class OAuth2Decorator(object):
# in API calls # in API calls
""" """
def __init__(self, client_id, client_secret, scope, user_agent, def __init__(self, client_id, client_secret, scope, user_agent,
auth_uri='https://accounts.google.com/o/oauth2/auth', auth_uri='https://accounts.google.com/o/oauth2/auth',
token_uri='https://accounts.google.com/o/oauth2/token'): token_uri='https://accounts.google.com/o/oauth2/token'):
@@ -205,8 +291,8 @@ class OAuth2Decorator(object):
token_uri: string, URI for token endpoint. For convenience token_uri: string, URI for token endpoint. For convenience
defaults to Google's endpoints but any OAuth 2.0 provider can be used. defaults to Google's endpoints but any OAuth 2.0 provider can be used.
""" """
self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent, self.flow = OAuth2WebServerFlow(client_id, client_secret, scope,
auth_uri, token_uri) user_agent, auth_uri, token_uri)
self.credentials = None self.credentials = None
self._request_handler = None self._request_handler = None
@@ -220,6 +306,7 @@ class OAuth2Decorator(object):
method: callable, to be decorated method of a webapp.RequestHandler method: callable, to be decorated method of a webapp.RequestHandler
instance. instance.
""" """
def check_oauth(request_handler, *args): def check_oauth(request_handler, *args):
user = users.get_current_user() user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request. # Don't use @login_decorator as this could be used in a POST request.
@@ -255,6 +342,7 @@ class OAuth2Decorator(object):
method: callable, to be decorated method of a webapp.RequestHandler method: callable, to be decorated method of a webapp.RequestHandler
instance. instance.
""" """
def setup_oauth(request_handler, *args): def setup_oauth(request_handler, *args):
user = users.get_current_user() user = users.get_current_user()
# Don't use @login_decorator as this could be used in a POST request. # Don't use @login_decorator as this could be used in a POST request.
@@ -308,10 +396,12 @@ class OAuth2Handler(webapp.RequestHandler):
error = self.request.get('error') error = self.request.get('error')
if error: if error:
errormsg = self.request.get('error_description', 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: else:
user = users.get_current_user() 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 # This code should be ammended with application specific error
# handling. The following cases should be considered: # handling. The following cases should be considered:
# 1. What if the flow doesn't exist in memcache? Or is corrupt? # 1. What if the flow doesn't exist in memcache? Or is corrupt?
@@ -328,6 +418,7 @@ class OAuth2Handler(webapp.RequestHandler):
application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)]) application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
def main(): def main():
run_wsgi_app(application) run_wsgi_app(application)

View File

@@ -83,6 +83,7 @@ class Credentials(object):
""" """
_abstract() _abstract()
class Flow(object): class Flow(object):
"""Base class for all Flow objects.""" """Base class for all Flow objects."""
pass pass
@@ -94,7 +95,6 @@ class Storage(object):
Store and retrieve a single credential. Store and retrieve a single credential.
""" """
def get(self): def get(self):
"""Retrieve credential. """Retrieve credential.
@@ -187,6 +187,26 @@ class OAuth2Credentials(Credentials):
self.__dict__.update(state) self.__dict__.update(state)
self.store = None 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): def _refresh(self, http_request):
"""Refresh the access_token using the refresh_token. """Refresh the access_token using the refresh_token.
@@ -194,16 +214,9 @@ class OAuth2Credentials(Credentials):
http: An instance of httplib2.Http.request http: An instance of httplib2.Http.request
or something that acts like it. or something that acts like it.
""" """
body = urllib.urlencode({ body = self._generate_refresh_request_body()
'grant_type': 'refresh_token', headers = self._generate_refresh_request_headers()
'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'
}
logging.info("Refresing access_token") logging.info("Refresing access_token")
resp, content = http_request( resp, content = http_request(
self.token_uri, method='POST', body=body, headers=headers) self.token_uri, method='POST', body=body, headers=headers)
@@ -214,14 +227,14 @@ class OAuth2Credentials(Credentials):
self.refresh_token = d.get('refresh_token', self.refresh_token) self.refresh_token = d.get('refresh_token', self.refresh_token)
if 'expires_in' in d: if 'expires_in' in d:
self.token_expiry = datetime.timedelta( self.token_expiry = datetime.timedelta(
seconds = int(d['expires_in'])) + datetime.datetime.now() seconds=int(d['expires_in'])) + datetime.datetime.now()
else: else:
self.token_expiry = None self.token_expiry = None
if self.store is not None: if self.store is not None:
self.store(self) self.store(self)
else: else:
# An {'error':...} response body means the token is expired or revoked, so # An {'error':...} response body means the token is expired or revoked,
# we flag the credentials as such. # so we flag the credentials as such.
logging.error('Failed to retrieve access token: %s' % content) logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status'] error_msg = 'Invalid response %s.' % resp['status']
try: try:
@@ -232,7 +245,8 @@ class OAuth2Credentials(Credentials):
if self.store is not None: if self.store is not None:
self.store(self) self.store(self)
else: else:
logging.warning("Unable to store refreshed credentials, no Storage provided.") logging.warning(
"Unable to store refreshed credentials, no Storage provided.")
except: except:
pass pass
raise AccessTokenRefreshError(error_msg) raise AccessTokenRefreshError(error_msg)
@@ -266,6 +280,10 @@ class OAuth2Credentials(Credentials):
def new_request(uri, method='GET', body=None, headers=None, def new_request(uri, method='GET', body=None, headers=None,
redirections=httplib2.DEFAULT_MAX_REDIRECTS, redirections=httplib2.DEFAULT_MAX_REDIRECTS,
connection_type=None): connection_type=None):
if not self.access_token:
logging.info("Attempting refresh to obtain initial access_token")
self._refresh(request_orig)
"""Modify the request headers to add the appropriate """Modify the request headers to add the appropriate
Authorization header.""" Authorization header."""
if headers == None: if headers == None:
@@ -275,8 +293,10 @@ class OAuth2Credentials(Credentials):
headers['user-agent'] = self.user_agent + ' ' + headers['user-agent'] headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
else: else:
headers['user-agent'] = self.user_agent headers['user-agent'] = self.user_agent
resp, content = request_orig(uri, method, body, headers, resp, content = request_orig(uri, method, body, headers,
redirections, connection_type) redirections, connection_type)
if resp.status == 401: if resp.status == 401:
logging.info("Refreshing because we got a 401") logging.info("Refreshing because we got a 401")
self._refresh(request_orig) self._refresh(request_orig)
@@ -341,6 +361,57 @@ class AccessTokenCredentials(OAuth2Credentials):
raise AccessTokenCredentialsError( raise AccessTokenCredentialsError(
"The access_token is expired or invalid and can't be refreshed.") "The access_token is expired or invalid and can't be refreshed.")
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): class OAuth2WebServerFlow(Flow):
"""Does the Web Server Flow for OAuth 2.0. """Does the Web Server Flow for OAuth 2.0.
@@ -420,15 +491,16 @@ class OAuth2WebServerFlow(Flow):
'client_secret': self.client_secret, 'client_secret': self.client_secret,
'code': code, 'code': code,
'redirect_uri': self.redirect_uri, 'redirect_uri': self.redirect_uri,
'scope': self.scope 'scope': self.scope,
}) })
headers = { headers = {
'user-agent': self.user_agent, 'user-agent': self.user_agent,
'content-type': 'application/x-www-form-urlencoded' 'content-type': 'application/x-www-form-urlencoded',
} }
if http is None: if http is None:
http = httplib2.Http() 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: if resp.status == 200:
# TODO(jcgregorio) Raise an error if simplejson.loads fails? # TODO(jcgregorio) Raise an error if simplejson.loads fails?
d = simplejson.loads(content) d = simplejson.loads(content)
@@ -436,12 +508,13 @@ class OAuth2WebServerFlow(Flow):
refresh_token = d.get('refresh_token', None) refresh_token = d.get('refresh_token', None)
token_expiry = None token_expiry = None
if 'expires_in' in d: 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) logging.info('Successfully retrieved access token: %s' % content)
return OAuth2Credentials(access_token, self.client_id, self.client_secret, return OAuth2Credentials(access_token, self.client_id,
refresh_token, token_expiry, self.token_uri, self.client_secret, refresh_token, token_expiry,
self.user_agent) self.token_uri, self.user_agent)
else: else:
logging.error('Failed to retrieve access token: %s' % content) logging.error('Failed to retrieve access token: %s' % content)
error_msg = 'Invalid response %s.' % resp['status'] error_msg = 'Invalid response %s.' % resp['status']

View File

@@ -0,0 +1 @@
../appengine/apiclient

View File

@@ -0,0 +1,9 @@
application: urlshortener-robot
version: 2
runtime: python
api_version: 1
handlers:
- url: .*
script: main.py

View File

@@ -0,0 +1 @@
../../gflags.py

View File

@@ -0,0 +1 @@
../../gflags_validators.py

View File

@@ -0,0 +1 @@
../appengine/httplib2

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env python
#
# Copyright 2007 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.
#
"""Starting template for Google App Engine applications.
Use this project as a starting point if you are just beginning to build a
Google App Engine project which will access and manage data held under a role
account for the App Engine app. More information about using Google App Engine
apps to call Google APIs can be found in Scenario 1 of the following document:
<https://sites.google.com/site/oauthgoog/Home/google-oauth2-assertion-flow>
"""
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
import httplib2
import logging
import os
import pickle
from apiclient.discovery import build
from google.appengine.api import memcache
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import run_wsgi_app
from oauth2client.appengine import AppAssertionCredentials
credentials = AppAssertionCredentials(
scope='https://www.googleapis.com/auth/urlshortener',
user_agent='my-sample-app/1.0')
http = credentials.authorize(httplib2.Http(memcache))
service = build("urlshortener", "v1", http=http)
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"]]
variables = {
'short_and_long': short_and_long,
}
self.response.out.write(template.render(path, variables))
def post(self):
long_url = self.request.get("longUrl")
shortened = service.url().insert(body={"longUrl": long_url}).execute()
self.redirect("/")
def main():
application = webapp.WSGIApplication(
[
('/', MainHandler),
],
debug=True)
run_wsgi_app(application)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1 @@
../appengine/oauth2

View File

@@ -0,0 +1 @@
../../oauth2client/

View File

@@ -0,0 +1 @@
../appengine/uritemplate

View File

@@ -0,0 +1,18 @@
<html>
<head>
<title>Welcome</title>
</head>
<body>
<form action="." method="post">
Long Url: <input name="longUrl" type="text" />
<input type="submit" />
</form>
<table>
<tr><th>Shortened</th><th>Original</th></tr>
{% for item in short_and_long %}
<tr><td><a href="{{ item.0 }}">{{ item.0 }}</a></td><td><a href="{{ item.1 }}">{{ item.1 }}</a></td></tr>
{% endfor %}
</table>
</body>
</html>

View File

@@ -35,6 +35,7 @@ from apiclient.http import HttpMockSequence
from oauth2client.client import AccessTokenCredentials from oauth2client.client import AccessTokenCredentials
from oauth2client.client import AccessTokenCredentialsError from oauth2client.client import AccessTokenCredentialsError
from oauth2client.client import AccessTokenRefreshError from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
from oauth2client.client import FlowExchangeError from oauth2client.client import FlowExchangeError
from oauth2client.client import OAuth2Credentials from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow from oauth2client.client import OAuth2WebServerFlow
@@ -115,6 +116,35 @@ class AccessTokenCredentialsTests(unittest.TestCase):
self.assertEqual(400, resp.status) self.assertEqual(400, resp.status)
class TestAssertionCredentials(unittest.TestCase):
assertion_text = "This is the assertion"
assertion_type = "http://www.google.com/assertionType"
class AssertionCredentialsTestImpl(AssertionCredentials):
def _generate_assertion(self):
return TestAssertionCredentials.assertion_text
def setUp(self):
user_agent = "fun/2.0"
self.credentials = self.AssertionCredentialsTestImpl(self.assertion_type,
user_agent)
def test_assertion_body(self):
body = urlparse.parse_qs(self.credentials._generate_refresh_request_body())
self.assertEqual(body['assertion'][0], self.assertion_text)
self.assertEqual(body['assertion_type'][0], self.assertion_type)
def test_assertion_refresh(self):
http = HttpMockSequence([
({'status': '200'}, '{"access_token":"1/3w"}'),
({'status': '200'}, 'echo_request_headers'),
])
http = self.credentials.authorize(http)
resp, content = http.request("http://example.com")
self.assertEqual(content['authorization'], 'OAuth 1/3w')
class OAuth2WebServerFlowTest(unittest.TestCase): class OAuth2WebServerFlowTest(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -137,7 +167,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_exchange_failure(self): def test_exchange_failure(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '400'}, '{"error":"invalid_request"}') ({'status': '400'}, '{"error":"invalid_request"}'),
]) ])
try: try:
@@ -159,7 +189,6 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
self.assertNotEqual(credentials.token_expiry, None) self.assertNotEqual(credentials.token_expiry, None)
self.assertEqual(credentials.refresh_token, '8xLOxBtZp8') self.assertEqual(credentials.refresh_token, '8xLOxBtZp8')
def test_exchange_no_expires_in(self): def test_exchange_no_expires_in(self):
http = HttpMockSequence([ http = HttpMockSequence([
({'status': '200'}, """{ "access_token":"SlAV32hkKG", ({'status': '200'}, """{ "access_token":"SlAV32hkKG",

View File

@@ -22,6 +22,7 @@ Unit tests for objects created from discovery documents.
__author__ = 'jcgregorio@google.com (Joe Gregorio)' __author__ = 'jcgregorio@google.com (Joe Gregorio)'
import base64
import httplib2 import httplib2
import unittest import unittest
import urlparse import urlparse
@@ -31,20 +32,24 @@ try:
except ImportError: except ImportError:
from cgi import parse_qs from cgi import parse_qs
from apiclient.http import HttpMockSequence
from apiclient.anyjson import simplejson from apiclient.anyjson import simplejson
from webtest import TestApp 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 users
from google.appengine.ext import testbed
from google.appengine.ext import webapp
from oauth2client.client import AccessTokenRefreshError from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import FlowExchangeError from oauth2client.client import FlowExchangeError
from oauth2client.appengine import AppAssertionCredentials
from oauth2client.appengine import OAuth2Decorator from oauth2client.appengine import OAuth2Decorator
from google.appengine.ext import webapp
from google.appengine.api import users
from oauth2client.appengine import OAuth2Handler from oauth2client.appengine import OAuth2Handler
from google.appengine.ext import testbed from webtest import TestApp
class UserMock(object): class UserMock(object):
"""Mock the app engine user service""" """Mock the app engine user service"""
def user_id(self): def user_id(self):
return 'foo_user' return 'foo_user'
@@ -55,7 +60,7 @@ class Http2Mock(object):
content = { content = {
'access_token': 'foo_access_token', 'access_token': 'foo_access_token',
'refresh_token': 'foo_refresh_token', 'refresh_token': 'foo_refresh_token',
'expires_in': 3600 'expires_in': 3600,
} }
def request(self, token_uri, method, body, headers, *args, **kwargs): def request(self, token_uri, method, body, headers, *args, **kwargs):
@@ -64,6 +69,59 @@ class Http2Mock(object):
return (self, simplejson.dumps(self.content)) return (self, simplejson.dumps(self.content))
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_SignForApp(self, request, response):
return response.set_signature_bytes(
TestAppAssertionCredentials.signature)
def setUp(self):
app_identity_stub = self.AppIdentityStubImpl()
apiproxy_stub_map.apiproxy.RegisterStub("app_identity_service",
app_identity_stub)
self.scope = "http://www.googleapis.com/scope"
user_agent = "hal/3.0"
self.credentials = AppAssertionCredentials(self.scope, user_agent)
def test_assertion(self):
assertion = self.credentials._generate_assertion()
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)
class DecoratorTests(unittest.TestCase): class DecoratorTests(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -79,14 +137,14 @@ class DecoratorTests(unittest.TestCase):
user_agent='foo_user_agent') user_agent='foo_user_agent')
self.decorator = decorator self.decorator = decorator
class TestRequiredHandler(webapp.RequestHandler): class TestRequiredHandler(webapp.RequestHandler):
@decorator.oauth_required @decorator.oauth_required
def get(self): def get(self):
pass pass
class TestAwareHandler(webapp.RequestHandler): class TestAwareHandler(webapp.RequestHandler):
@decorator.oauth_aware @decorator.oauth_aware
def get(self): def get(self):
self.response.out.write('Hello World!') self.response.out.write('Hello World!')
@@ -121,7 +179,7 @@ class DecoratorTests(unittest.TestCase):
# Now simulate the callback to /oauth2callback # Now simulate the callback to /oauth2callback
response = self.app.get('/oauth2callback', { response = self.app.get('/oauth2callback', {
'code': 'foo_access_code', 'code': 'foo_access_code',
'state': 'foo_path' 'state': 'foo_path',
}) })
self.assertEqual('http://localhost/foo_path', response.headers['Location']) self.assertEqual('http://localhost/foo_path', response.headers['Location'])
self.assertEqual(None, self.decorator.credentials) self.assertEqual(None, self.decorator.credentials)
@@ -130,8 +188,10 @@ class DecoratorTests(unittest.TestCase):
response = self.app.get('/foo_path') response = self.app.get('/foo_path')
self.assertEqual('200 OK', response.status) self.assertEqual('200 OK', response.status)
self.assertEqual(True, self.decorator.has_credentials()) self.assertEqual(True, self.decorator.has_credentials())
self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token) self.assertEqual('foo_refresh_token',
self.assertEqual('foo_access_token', self.decorator.credentials.access_token) self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token',
self.decorator.credentials.access_token)
# Invalidate the stored Credentials # Invalidate the stored Credentials
self.decorator.credentials._invalid = True self.decorator.credentials._invalid = True
@@ -161,7 +221,7 @@ class DecoratorTests(unittest.TestCase):
url = self.decorator.authorize_url() url = self.decorator.authorize_url()
response = self.app.get('/oauth2callback', { response = self.app.get('/oauth2callback', {
'code': 'foo_access_code', 'code': 'foo_access_code',
'state': 'bar_path' 'state': 'bar_path',
}) })
self.assertEqual('http://localhost/bar_path', response.headers['Location']) self.assertEqual('http://localhost/bar_path', response.headers['Location'])
self.assertEqual(False, self.decorator.has_credentials()) self.assertEqual(False, self.decorator.has_credentials())
@@ -171,8 +231,10 @@ class DecoratorTests(unittest.TestCase):
self.assertEqual('200 OK', response.status) self.assertEqual('200 OK', response.status)
self.assertEqual('Hello World!', response.body) self.assertEqual('Hello World!', response.body)
self.assertEqual(True, self.decorator.has_credentials()) self.assertEqual(True, self.decorator.has_credentials())
self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token) self.assertEqual('foo_refresh_token',
self.assertEqual('foo_access_token', self.decorator.credentials.access_token) self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token',
self.decorator.credentials.access_token)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()