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 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 @@ from google.appengine.ext.webapp.util import run_wsgi_app
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 @@ class StorageByKeyName(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 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 @@ class OAuth2Decorator(object):
# 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 @@ class OAuth2Decorator(object):
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 @@ class OAuth2Decorator(object):
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 @@ class OAuth2Decorator(object):
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 @@ class OAuth2Handler(webapp.RequestHandler):
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 @@ class OAuth2Handler(webapp.RequestHandler):
application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
def main():
run_wsgi_app(application)

View File

@@ -83,6 +83,7 @@ class Credentials(object):
"""
_abstract()
class Flow(object):
"""Base class for all Flow objects."""
pass
@@ -94,7 +95,6 @@ class Storage(object):
Store and retrieve a single credential.
"""
def get(self):
"""Retrieve credential.
@@ -187,6 +187,26 @@ class OAuth2Credentials(Credentials):
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 @@ class OAuth2Credentials(Credentials):
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 @@ class OAuth2Credentials(Credentials):
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 @@ class OAuth2Credentials(Credentials):
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 @@ class OAuth2Credentials(Credentials):
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 @@ class OAuth2Credentials(Credentials):
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 @@ class AccessTokenCredentials(OAuth2Credentials):
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 @@ class OAuth2WebServerFlow(Flow):
'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 @@ class OAuth2WebServerFlow(Flow):
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']

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 AccessTokenCredentialsError
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import AssertionCredentials
from oauth2client.client import FlowExchangeError
from oauth2client.client import OAuth2Credentials
from oauth2client.client import OAuth2WebServerFlow
@@ -115,6 +116,35 @@ class AccessTokenCredentialsTests(unittest.TestCase):
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):
def setUp(self):
@@ -137,7 +167,7 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
def test_exchange_failure(self):
http = HttpMockSequence([
({'status': '400'}, '{"error":"invalid_request"}')
({'status': '400'}, '{"error":"invalid_request"}'),
])
try:
@@ -159,7 +189,6 @@ class OAuth2WebServerFlowTest(unittest.TestCase):
self.assertNotEqual(credentials.token_expiry, None)
self.assertEqual(credentials.refresh_token, '8xLOxBtZp8')
def test_exchange_no_expires_in(self):
http = HttpMockSequence([
({'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)'
import base64
import httplib2
import unittest
import urlparse
@@ -31,20 +32,24 @@ try:
except ImportError:
from cgi import parse_qs
from apiclient.http import HttpMockSequence
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 FlowExchangeError
from oauth2client.appengine import AppAssertionCredentials
from oauth2client.appengine import OAuth2Decorator
from google.appengine.ext import webapp
from google.appengine.api import users
from oauth2client.appengine import OAuth2Handler
from google.appengine.ext import testbed
from webtest import TestApp
class UserMock(object):
"""Mock the app engine user service"""
def user_id(self):
return 'foo_user'
@@ -55,7 +60,7 @@ class Http2Mock(object):
content = {
'access_token': 'foo_access_token',
'refresh_token': 'foo_refresh_token',
'expires_in': 3600
'expires_in': 3600,
}
def request(self, token_uri, method, body, headers, *args, **kwargs):
@@ -64,6 +69,59 @@ class Http2Mock(object):
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):
def setUp(self):
@@ -79,14 +137,14 @@ class DecoratorTests(unittest.TestCase):
user_agent='foo_user_agent')
self.decorator = decorator
class TestRequiredHandler(webapp.RequestHandler):
@decorator.oauth_required
def get(self):
pass
class TestAwareHandler(webapp.RequestHandler):
@decorator.oauth_aware
def get(self):
self.response.out.write('Hello World!')
@@ -121,7 +179,7 @@ class DecoratorTests(unittest.TestCase):
# Now simulate the callback to /oauth2callback
response = self.app.get('/oauth2callback', {
'code': 'foo_access_code',
'state': 'foo_path'
'state': 'foo_path',
})
self.assertEqual('http://localhost/foo_path', response.headers['Location'])
self.assertEqual(None, self.decorator.credentials)
@@ -130,8 +188,10 @@ class DecoratorTests(unittest.TestCase):
response = self.app.get('/foo_path')
self.assertEqual('200 OK', response.status)
self.assertEqual(True, self.decorator.has_credentials())
self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
self.assertEqual('foo_refresh_token',
self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token',
self.decorator.credentials.access_token)
# Invalidate the stored Credentials
self.decorator.credentials._invalid = True
@@ -161,7 +221,7 @@ class DecoratorTests(unittest.TestCase):
url = self.decorator.authorize_url()
response = self.app.get('/oauth2callback', {
'code': 'foo_access_code',
'state': 'bar_path'
'state': 'bar_path',
})
self.assertEqual('http://localhost/bar_path', response.headers['Location'])
self.assertEqual(False, self.decorator.has_credentials())
@@ -171,8 +231,10 @@ class DecoratorTests(unittest.TestCase):
self.assertEqual('200 OK', response.status)
self.assertEqual('Hello World!', response.body)
self.assertEqual(True, self.decorator.has_credentials())
self.assertEqual('foo_refresh_token', self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token', self.decorator.credentials.access_token)
self.assertEqual('foo_refresh_token',
self.decorator.credentials.refresh_token)
self.assertEqual('foo_access_token',
self.decorator.credentials.access_token)
if __name__ == '__main__':
unittest.main()