Add robot helpers and a sample.
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
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 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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user