Add robot helpers and a sample.
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
1
samples/appengine_with_robots/apiclient
Symbolic link
1
samples/appengine_with_robots/apiclient
Symbolic link
@@ -0,0 +1 @@
|
||||
../appengine/apiclient
|
||||
9
samples/appengine_with_robots/app.yaml
Normal file
9
samples/appengine_with_robots/app.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
application: urlshortener-robot
|
||||
version: 2
|
||||
runtime: python
|
||||
api_version: 1
|
||||
|
||||
handlers:
|
||||
- url: .*
|
||||
script: main.py
|
||||
|
||||
1
samples/appengine_with_robots/gflags.py
Symbolic link
1
samples/appengine_with_robots/gflags.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../gflags.py
|
||||
1
samples/appengine_with_robots/gflags_validators.py
Symbolic link
1
samples/appengine_with_robots/gflags_validators.py
Symbolic link
@@ -0,0 +1 @@
|
||||
../../gflags_validators.py
|
||||
1
samples/appengine_with_robots/httplib2
Symbolic link
1
samples/appengine_with_robots/httplib2
Symbolic link
@@ -0,0 +1 @@
|
||||
../appengine/httplib2
|
||||
79
samples/appengine_with_robots/main.py
Normal file
79
samples/appengine_with_robots/main.py
Normal 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()
|
||||
1
samples/appengine_with_robots/oauth2
Symbolic link
1
samples/appengine_with_robots/oauth2
Symbolic link
@@ -0,0 +1 @@
|
||||
../appengine/oauth2
|
||||
1
samples/appengine_with_robots/oauth2client
Symbolic link
1
samples/appengine_with_robots/oauth2client
Symbolic link
@@ -0,0 +1 @@
|
||||
../../oauth2client/
|
||||
1
samples/appengine_with_robots/uritemplate
Symbolic link
1
samples/appengine_with_robots/uritemplate
Symbolic link
@@ -0,0 +1 @@
|
||||
../appengine/uritemplate
|
||||
18
samples/appengine_with_robots/welcome.html
Normal file
18
samples/appengine_with_robots/welcome.html
Normal 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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user