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) | ||||
| @@ -220,8 +233,8 @@ class OAuth2Credentials(Credentials): | ||||
|       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
	 JacobMoshenko
					JacobMoshenko