Skip to content
This repository has been archived by the owner on Feb 8, 2018. It is now read-only.

Commit

Permalink
Barely logged in with Twitter. Barely (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
chadwhitacre committed Sep 13, 2012
1 parent 3448df6 commit a9f40c8
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 39 deletions.
7 changes: 6 additions & 1 deletion configure-aspen.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
website.github_client_secret = os.environ['GITHUB_CLIENT_SECRET'].decode('ASCII')
website.github_callback = os.environ['GITHUB_CALLBACK'].decode('ASCII')

website.twitter_consumer_key = os.environ['TWITTER_CONSUMER_KEY'].decode('ASCII')
website.twitter_consumer_secret = os.environ['TWITTER_CONSUMER_SECRET'].decode('ASCII')
website.twitter_callback = os.environ['TWITTER_CALLBACK'].decode('ASCII')

website.hooks.inbound_early.register(gittip.canonize)
website.hooks.inbound_early.register(gittip.configure_payments)
website.hooks.inbound_early.register(gittip.csrf.inbound)
Expand All @@ -23,9 +27,10 @@


def add_stuff(request):
from gittip.networks import github
from gittip.networks import github, twitter
request.context['__version__'] = gittip.__version__
request.context['username'] = None
request.context['github'] = github
request.context['twitter'] = twitter

website.hooks.inbound_early.register(add_stuff)
50 changes: 12 additions & 38 deletions gittip/networks/twitter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import requests
from aspen import json, log, Response
from aspen.website import Website
from aspen.utils import typecheck
from gittip import db, networks


Expand All @@ -13,34 +11,10 @@ def upsert(user_info):
)


def oauth_url(website, action, then=u""):
"""Given a website object and a string, return a URL string.
`action' is one of 'opt-in', 'lock' and 'unlock'
`then' is either a twitter username or an URL starting with '/'. It's
where we'll send the user after we get the redirect back from
GitHub.
"""
typecheck(website, Website, action, unicode, then, unicode)
assert action in [u'opt-in', u'lock', u'unlock']
url = u"https://twitter.com/login/oauth/authorize?consumer_key=%s&redirect_uri=%s"
url %= (website.twitter_consumer_key, website.twitter_callback)

# Pack action,then into data and base64-encode. Querystring isn't
# available because it's consumed by the initial GitHub request.

data = u'%s,%s' % (action, then)
data = data.encode('UTF-8').encode('base64').decode('US-ASCII')
url += u'?data=%s' % data
return url


def oauth_dance(website, qs):
"""Given a querystring, return a dict of user_info.
The querystring should be the querystring that we get from GitHub when
The querystring should be the querystring that we get from Twitter when
we send the user to the return value of oauth_url above.
See also:
Expand All @@ -49,16 +23,16 @@ def oauth_dance(website, qs):
"""

log("Doing an OAuth dance with Github.")
log("Doing an OAuth dance with Twitter.")

if 'error' in qs:
raise Response(500, str(qs['error']))
if 'denied' in qs:
raise Response(500, str(qs['denied']))

data = { 'code': qs['code'].encode('US-ASCII')
, 'client_id': website.twitter_client_id
, 'client_secret': website.twitter_client_secret
, 'client_id': website.twitter_customer_key
, 'client_secret': website.twitter_customer_secret
}
r = requests.post("https://twitter.com/login/oauth/access_token", data=data)
r = requests.post("https://api.twitter.com/oauth/access_token", data=data)
assert r.status_code == 200, (r.status_code, r.text)

back = dict([pair.split('=') for pair in r.text.split('&')]) # XXX
Expand All @@ -72,24 +46,24 @@ def oauth_dance(website, qs):
)
assert r.status_code == 200, (r.status_code, r.text)
user_info = json.loads(r.text)
log("Done with OAuth dance with Github for %s (%s)."
log("Done with OAuth dance with Twitter for %s (%s)."
% (user_info['login'], user_info['id']))

return user_info


def resolve(screen_name):
def resolve(user_id):
"""Given str, return a participant_id.
"""
FETCH = """\
SELECT participant_id
FROM social_network_users
WHERE network='twitter'
AND user_info -> 'screen_namec' = %s
AND user_info -> 'user_id' = %s
""" # XXX Uniqueness constraint on screen_name?
rec = db.fetchone(FETCH, (screen_name,))
rec = db.fetchone(FETCH, (user_id,))
if rec is None:
raise Exception("Twitter user %s has no participant." % (screen_name))
raise Exception("Twitter user %s has no participant." % (user_id))
return rec['participant_id']
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
./vendor/mock-0.8.0.tar.gz
./vendor/balanced-0.8.18.tar.gz

./vendor/requests-oauth-0.4.1.tar.gz

./
Binary file added vendor/requests-oauth-0.4.1.tar.gz
Binary file not shown.
16 changes: 16 additions & 0 deletions www/on/twitter/%screen_name/lock-fail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
username = path['login']
^L
{% extends templates/base.html %}

{% block body %}

<div id="their-voice">
<h2 class="first">Are you really {{ username }}?</h2>

<p>Your attempt to lock or unlock this account failed because you&rsquo;re
logged into GitHub as someone else. Please <a href="https://github.com/logout"
target="_blank">log out of GitHub</a> and <a
href="./">try again</a>.</p>
</div>

{% end %}
97 changes: 97 additions & 0 deletions www/on/twitter/associate
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Associate a Twitter account with a Gittip account.

First we do the OAuth dance with Twitter. Once we've authenticated the user
against Twitter, we record them in our social_network_users table. This table
contains information for Twitter users whether or not they are explicit
participants in the Gittip community.

"""
import requests
from oauth_hook import OAuthHook
from aspen import log, Response, json
from gittip import db
from gittip.networks import twitter, set_as_claimed
from gittip.authentication import User
from urlparse import parse_qs

# ========================== ^L

if 'denied' in qs:
request.redirect('/')


token = qs['oauth_token']
secret, action, then = website.oauth_cache.pop(token)


oauth_hook = OAuthHook(token, secret, header_auth=True)
response = requests.post( "https://api.twitter.com/oauth/access_token"
, data={"oauth_verifier": qs['oauth_verifier']}
, hooks={'pre_request': oauth_hook}
)
assert response.status_code == 200

qs = parse_qs(response.text)
token = qs['oauth_token'][0]
secret = qs['oauth_token_secret'][0]
user_id = qs['user_id'][0]


oauth_hook = OAuthHook(token, secret, header_auth=True)
response = requests.get( "https://api.twitter.com/1/users/show.json?user_id=%s" % user_id
, hooks={'pre_request': oauth_hook}
)
user_info = json.loads(response.text)
assert response.status_code == 200


# Load Twitter user info.

if action not in [u'opt-in', u'lock', u'unlock']:
raise Response(400)

# Make sure we have a Twitter screen_name.
screen_name = user_info.get('screen_name')
if screen_name is None:
log(u"We got a user_info from Twitter with no screen_name [%s, %s]"
% (action, then))
raise Response(400)

# Do something.
log(u"%s wants to %s" % (screen_name, action))
if action == 'opt-in': # opt in
participant_id, is_claimed, is_locked, balance = twitter.upsert(user_info)
set_as_claimed(participant_id)
user = User.from_id(participant_id) # give them a session
else: # lock or unlock
if then != screen_name:

# The user could spoof `then' to match their screen_name, but the most
# they can do is lock/unlock their own Twitter account in a convoluted
# way.

then = u'/on/twitter/%s/lock-fail.html' % then

else:

# Associate the Twitter screen_name with a randomly-named, unclaimed
# Gittip participant.

participant_id, is_claimed, is_locked, balance \
= twitter.upsert(user_info)
assert participant_id != screen_name, screen_name # sanity check

db.execute( "UPDATE social_network_users "
"SET is_locked=%s "
"WHERE participant_id=%s"
, (action == 'lock', participant_id)
)

if then == u'':
then = u'/%s/' % participant_id
if not then.startswith(u'/'):
# Interpret it as a Twitter screen_name.
then = u'/on/twitter/%s/' % then
request.redirect(then)

# ========================== ^L text/plain
32 changes: 32 additions & 0 deletions www/on/twitter/redirect
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from urlparse import parse_qs

import requests
from oauth_hook import OAuthHook

OAuthHook.consumer_key = website.twitter_consumer_key
OAuthHook.consumer_secret = website.twitter_consumer_secret

website.oauth_cache = {}

# ========================== ^L

oauth_hook = OAuthHook(header_auth=True)
response = requests.post( "https://api.twitter.com/oauth/request_token"
, hooks={'pre_request': oauth_hook}
)

assert response.status_code == 200, response.status_code # safety check

qs = parse_qs(response.text)

token = qs['oauth_token'][0]
secret = qs['oauth_token_secret'][0]
assert qs['oauth_callback_confirmed'][0] == "true" # sanity check

action = qs.get('action', 'opt-in')
then = qs.get('then', '')
website.oauth_cache[token] = (secret, action, then)

url = "https://api.twitter.com/oauth/authenticate?oauth_token=%s"
request.redirect(url % token)
# ========================== ^L text/plain

0 comments on commit a9f40c8

Please sign in to comment.