Source code for oauth_dropins.disqus
"""Disqus OAuth drop-in.
Disqus API docs: https://disqus.com/api/docs/
This drop-in is even more similar to Instagram than Instagram is to
Facebook. Differences:
- urlopen must pass the api_key with each request (in addition to the
access_token)
- Response to access_token does not give much information about the user,
so we additionally fetch /user/details before saving
- Deny appears to be broken on Disqus's side (clicking "No Thanks" has
no effect), so we ignore that possibility for now.
TODO unify Disqus, Facebook, and Instagram
"""
import logging
import urllib.parse
from google.cloud import ndb
from webob import exc
from . import handlers, models
from .webutil import util
from .webutil.util import json_dumps, json_loads
DISQUS_CLIENT_ID = util.read('disqus_client_id')
DISQUS_CLIENT_SECRET = util.read('disqus_client_secret')
GET_AUTH_CODE_URL = (
'https://disqus.com/api/oauth/2.0/authorize/?' +
'&'.join((
'client_id=%(client_id)s',
'scope=%(scope)s',
'response_type=code',
'redirect_uri=%(redirect_uri)s',
)))
GET_ACCESS_TOKEN_URL = 'https://disqus.com/api/oauth/2.0/access_token/'
USER_DETAILS_URL = 'https://disqus.com/api/3.0/users/details.json?user=%d'
[docs]class StartHandler(handlers.StartHandler):
"""Starts Disqus auth. Requests an auth code and expects a redirect back.
"""
NAME = 'disqus'
LABEL = 'Disqus'
# Disqus scopes are comma separated: read, write, admin, email
# https://disqus.com/api/docs/requests/#data-availability
DEFAULT_SCOPE = 'read'
[docs] def redirect_url(self, state=None):
assert DISQUS_CLIENT_ID and DISQUS_CLIENT_SECRET, \
"Please fill in the disqus_client_id and disqus_client_secret files in your app's root directory."
return GET_AUTH_CODE_URL % {
'client_id': DISQUS_CLIENT_ID,
'scope': self.scope,
'redirect_uri': urllib.parse.quote_plus(self.to_url(state=state)),
}
[docs]class CallbackHandler(handlers.CallbackHandler):
"""The auth callback. Fetches an access token, stores it, and redirects home.
"""
def get(self):
if self.handle_error():
return
# https://disqus.com/api/docs/auth/
auth_code = util.get_required_param(self, 'code')
data = {
'grant_type': 'authorization_code',
'client_id': DISQUS_CLIENT_ID,
'client_secret': DISQUS_CLIENT_SECRET,
'redirect_uri': self.request_url_with_state(),
'code': auth_code,
}
resp = util.requests_post(GET_ACCESS_TOKEN_URL, data=data)
resp.raise_for_status()
try:
data = json_loads(resp.text)
except (ValueError, TypeError):
logging.error('Bad response:\n%s', resp, stack_info=True)
raise exc.HTTPBadRequest('Bad Disqus response to access token request')
access_token = data['access_token']
user_id = data['user_id']
# TODO is a username key preferred?
# username = data['username']
auth = DisqusAuth(id=str(user_id),
auth_code=auth_code,
access_token_str=access_token)
resp = auth.urlopen(USER_DETAILS_URL % user_id).read()
try:
user_data = json_loads(resp)['response']
except (ValueError, TypeError):
logging.error('Bad response:\n%s', resp, stack_info=True)
raise exc.HTTPBadRequest('Bad Disqus response to user details request')
auth.user_json = json_dumps(user_data)
logging.info('created disqus auth %s', auth)
auth.put()
self.finish(auth, state=self.request.get('state'))
[docs] def handle_error(handler):
"""Handles any error reported in the callback query parameters.
Args:
handler: CallbackHandler
Returns:
True if there was an error, False otherwise.
"""
error = handler.request.get('error')
if error:
if error == 'access_denied':
logging.info('User declined')
handler.finish(None, state=handler.request.get('state'))
return True
else:
raise exc.HTTPBadRequest(error)
return False