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
try:
  from oauth2client.appengine import CredentialsModel, OAuth2Decorator
except ImportError:
  from oauth2client.contrib.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


# global
json_service = None

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 prompt=consent! 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', prompt='consent', # 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. """ @staticmethod def to(to_path): StartHandler.to_path = to_path global oauth_decorator assert oauth_decorator return oauth_decorator.callback_handler()