Source code for oauth_dropins.googleplus
"""Google+ OAuth drop-in.
Google+ API docs: https://developers.google.com/+/api/latest/
Python API client docs: https://developers.google.com/api-client-library/python/
TODO: check that overriding CallbackHandler.finish() actually works.
"""
import json
import httplib2
import logging
import appengine_config
import handlers
import models
from apiclient import discovery
from apiclient.errors import HttpError
from oauth2client.appengine import CredentialsModel, OAuth2Decorator
from oauth2client.client import OAuth2Credentials
from google.appengine.ext import db
from google.appengine.ext import ndb
from webutil import handlers as webutil_handlers
from webutil import util
# suppress "execute() takes at most 1 positional argument (2 given)"
# log warnings from google-api-python-client/oauth2client/util.py:124
import oauth2client
oauth2client.util.positional_parameters_enforcement = \
oauth2client.util.POSITIONAL_IGNORE
# global
json_service = None
[docs]def init_json_service():
global json_service
if json_service is None:
# service names and versions:
# https://developers.google.com/api-client-library/python/apis/
json_service = discovery.build('plus', 'v1')
# global. initialized in StartHandler.to_path().
oauth_decorator = None
[docs]class GooglePlusAuth(models.BaseAuth):
"""An authenticated Google+ user or page.
Provides methods that return information about this user (or page) and make
OAuth-signed requests to the Google+ API. Stores OAuth credentials in the
datastore. See models.BaseAuth for usage details.
Google+-specific details: implements http() and api() but not urlopen(). api()
returns a apiclient.discovery.Resource. The datastore entity key name is the
Google+ user id. Uses credentials from the stored CredentialsModel since
google-api-python-client stores refresh tokens there.
"""
user_json = ndb.TextProperty()
creds_model = ndb.KeyProperty(kind='CredentialsModel')
# deprecated. TODO: remove
creds_json = ndb.TextProperty()
[docs] def site_name(self):
return 'Google+'
[docs] def user_display_name(self):
"""Returns the user's name.
"""
return json.loads(self.user_json)['displayName']
[docs] def creds(self):
"""Returns an oauth2client.OAuth2Credentials.
"""
if self.creds_model:
return db.get(self.creds_model.to_old_key()).credentials
else:
# TODO: remove creds_json
return OAuth2Credentials.from_json(self.creds_json)
[docs] def access_token(self):
"""Returns the OAuth access token string.
"""
return self.creds().access_token
[docs] def http(self, **kwargs):
"""Returns an httplib2.Http that adds OAuth credentials to requests.
"""
http = httplib2.Http(**kwargs)
self.creds().authorize(http)
return http
[docs] def api(self):
"""Returns an apiclient.discovery.Resource for the Google+ JSON API.
To use it, first choose a resource type (e.g. People), then make a call,
then execute that call with an authorized Http instance. For example:
gpa = GooglePlusAuth.get_by_id('123')
results_json = gpa.people().search(query='ryan').execute(gpa.http())
More details: https://developers.google.com/api-client-library/python/
"""
init_json_service()
return json_service
[docs]def handle_exception(self, e, debug):
"""Exception handler that passes back HttpErrors as real HTTP errors.
"""
if isinstance(e, HttpError):
logging.exception(e)
self.response.set_status(e.resp.status)
self.response.write(str(e))
else:
return webutil_handlers.handle_exception(self, e, debug)
[docs]class StartHandler(handlers.StartHandler, handlers.CallbackHandler):
"""Starts and finishes the OAuth flow. The decorator handles the redirects.
"""
handle_exception = handle_exception
# G+ scopes: https://developers.google.com/+/api/oauth#oauth-scopes
DEFAULT_SCOPE = 'https://www.googleapis.com/auth/plus.me'
[docs] @classmethod
def to(cls, to_path, scopes=None):
"""Override this since we need to_path to instantiate the oauth decorator.
"""
global oauth_decorator
if oauth_decorator is None:
oauth_decorator = OAuth2Decorator(
client_id=appengine_config.GOOGLE_CLIENT_ID,
client_secret=appengine_config.GOOGLE_CLIENT_SECRET,
scope=cls.make_scope_str(scopes, separator=' '),
callback_path=to_path,
# make sure we ask for a refresh token so we can use it to get an access
# token offline. requires approval_prompt=force! more:
# ~/etc/google+_oauth_credentials_debugging_for_plusstreamfeed_bridgy
# http://googleappsdeveloper.blogspot.com.au/2011/10/upcoming-changes-to-oauth-20-endpoint.html
access_type='offline',
approval_prompt='force',
# https://developers.google.com/accounts/docs/OAuth2WebServer#incrementalAuth
include_granted_scopes='true')
class Handler(cls):
@oauth_decorator.oauth_required
def get(self):
assert (appengine_config.GOOGLE_CLIENT_ID and
appengine_config.GOOGLE_CLIENT_SECRET), (
"Please fill in the google_client_id and google_client_secret files in "
"your app's root directory.")
# get the current user
init_json_service()
try:
user = json_service.people().get(userId='me')\
.execute(oauth_decorator.http())
except BaseException, e:
util.interpret_http_exception(e)
raise
logging.debug('Got one person: %r', user)
store = oauth_decorator.credentials.store
creds_model_key = ndb.Key(store._model.kind(), store._key_name)
auth = GooglePlusAuth(id=user['id'],
creds_model=creds_model_key,
user_json=json.dumps(user))
auth.put()
self.finish(auth, state=self.request.get('state'))
@oauth_decorator.oauth_required
def post(self):
return self.get()
return Handler
[docs]class CallbackHandler(object):
"""OAuth callback handler factory.
"""
[docs] @staticmethod
def to(to_path):
StartHandler.to_path = to_path
global oauth_decorator
assert oauth_decorator
return oauth_decorator.callback_handler()