Milimetric has uploaded a new change for review.
https://gerrit.wikimedia.org/r/179962
Change subject: [WIP] Use the wonderful mwoauth
......................................................................
[WIP] Use the wonderful mwoauth
Instead of re-implementing the thorough and somewhat complicated
Mediawiki OAuth hand shake, we rely on the wonderful mwoauth package and
remove all the old unnecessary code. We also remove no longer needed
dependencies like the JSON Web Token library.
Change-Id: Ia899f0a71ea770e3f402eac0645be4fdd924379a
TODO: something seems broken with an mwoauth dependency
---
M requirements.txt
M scripts/install
M wikimetrics/configurables.py
M wikimetrics/controllers/authentication.py
4 files changed, 37 insertions(+), 159 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/analytics/wikimetrics
refs/changes/62/179962/1
diff --git a/requirements.txt b/requirements.txt
index f13f0c5..6b47232 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@
celery>=3.0
celery-with-redis>=3.0
PyYAML==3.10
-PyJWT==0.1.6
python-dateutil==2.2
alembic==0.6.4
mock==1.0.0
+mwoauth==0.2.2
diff --git a/scripts/install b/scripts/install
index cab3028..d3cea65 100755
--- a/scripts/install
+++ b/scripts/install
@@ -10,21 +10,8 @@
$pip install --upgrade --no-use-wheel setuptools
-($pip freeze | grep -P httplib2==[0-9.]+) && sudo $pip uninstall -y httplib2
&& echo "*** uninstalled httplib2"
-
# is it wikimetrics_dir=${$1:-$(pwd)}?
wikimetrics_dir=${1:-$(pwd)}
-
-good_httplib=$(pip freeze | grep 'httplib2.*pywikibot')
-if [ -z "${good_httplib}" ]
- then
- tmpdir=$(mktemp -d /tmp/wikimetrics_httplib2.XXXXXXXXX)
- cd "${tmpdir}"
- git clone --depth=1
https://github.com/wikimedia/pywikibot-externals-httplib2
- cd pywikibot-externals-httplib2
- sudo python setup.py install
- echo "*** installed pywikibot's httplib2"
-fi
cd $wikimetrics_dir
sudo $pip install -e .
diff --git a/wikimetrics/configurables.py b/wikimetrics/configurables.py
index 4c0ae52..5f2e549 100644
--- a/wikimetrics/configurables.py
+++ b/wikimetrics/configurables.py
@@ -3,6 +3,7 @@
import yaml
import subprocess
from celery.signals import task_postrun
+from mwoauth import ConsumerToken
def compose_connection_string(user, password, host, dbName):
@@ -135,76 +136,11 @@
consumer_secret=app.config['GOOGLE_CLIENT_SECRET'],
)
- def better_parse_response(resp, content, strict=False):
- ct, options = parse_options_header(resp['content-type'])
- if ct in ('application/json', 'text/javascript'):
- try:
- return json.loads(content)
- # handle json decode errors from parse_response
- # this is useful in the identify call because the response is
- # 'application/json' but the content is encoded
- except:
- return content
- elif ct in ('application/xml', 'text/xml'):
- # technically, text/xml is ascii based but because many
- # implementations get that wrong and utf-8 is a superset
- # of utf-8 anyways, so there is not much harm in assuming
- # utf-8 here
- charset = options.get('charset', 'utf-8')
- return get_etree().fromstring(content.decode(charset))
- elif ct != 'application/x-www-form-urlencoded':
- if strict:
- return content
- charset = options.get('charset', 'utf-8')
- return url_decode(content, charset=charset).to_dict()
-
- # TODO: Even worse, definitely patch upstream or consider switching to
rauth
- nasty_patch_to_oauth.parse_response = better_parse_response
-
- # TODO: patch upstream
- # NOTE: a million thanks to Merlijn_van_Deen, author of
- #
https://wikitech.wikimedia.org/wiki/Setting_up_Flask_cgi_app_as_a_tool/OAuth
- class MediaWikiOAuthRemoteApp(OAuthRemoteApp):
- def handle_oauth1_response(self):
- """
- Handles an oauth1 authorization response. The return value of
- this method is forwarded as the first argument to the handling
- view function.
- """
- client = self.make_client()
- resp, content = client.request(
- '{0}&oauth_verifier={1}'.format(
- self.expand_url(self.access_token_url),
- request.args['oauth_verifier'],
- ),
- self.access_token_method
- )
- data = nasty_patch_to_oauth.parse_response(resp, content)
- if not self.status_okay(resp):
- raise OAuthException(
- 'Invalid response from ' + self.name,
- type='invalid_response',
- data=data
- )
- return data
-
- global meta_mw
- meta_mw_base_url = app.config['META_MW_BASE_URL']
- meta_mw = MediaWikiOAuthRemoteApp(
- oauth,
- 'meta_mw',
- base_url=meta_mw_base_url,
- request_token_url=meta_mw_base_url + app.config['META_MW_BASE_INDEX'],
- request_token_params={
- 'title': 'Special:MWOAuth/initiate',
- 'oauth_callback': 'oob'
- },
- access_token_url=meta_mw_base_url + app.config['META_MW_TOKEN_URI'],
- authorize_url=meta_mw_base_url + app.config['META_MW_AUTH_URI'],
- consumer_key=app.config['META_MW_CONSUMER_KEY'],
- consumer_secret=app.config['META_MW_CLIENT_SECRET'],
+ global mw_oauth_token
+ mw_oauth_token = ConsumerToken(
+ app.config['META_MW_CONSUMER_KEY'],
+ app.config['META_MW_CLIENT_SECRET'],
)
- oauth.remote_apps['meta_mw'] = meta_mw
# TODO: look into making a single config object that has empty sections if
diff --git a/wikimetrics/controllers/authentication.py
b/wikimetrics/controllers/authentication.py
index df8508f..196eccb 100644
--- a/wikimetrics/controllers/authentication.py
+++ b/wikimetrics/controllers/authentication.py
@@ -11,10 +11,11 @@
session,
flash,
)
+from mwoauth import Handshaker
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
from flask.ext.login import login_user, logout_user, current_user
-from wikimetrics.configurables import app, db, login_manager, google, meta_mw
+from wikimetrics.configurables import app, db, login_manager, google,
mw_oauth_token
from wikimetrics.models import UserStore
from wikimetrics.enums import UserRole
from wikimetrics.utils import json_error
@@ -87,61 +88,26 @@
return redirect(url_for('home_index'))
+def make_handshaker_mw():
+ return Handshaker(
+ app.config['META_MW_BASE_URL'] + app.config['META_MW_BASE_INDEX'],
+ mw_oauth_token,
+ )
+
+
@app.route('/login/meta_mw')
@is_public
def login_meta_mw():
"""
Make a request to meta.wikimedia.org for Authentication.
"""
- session['access_token'] = None
- redirector = meta_mw.authorize()
-
- # MW's authorize requires an oauth_consumer_key
- redirector.headers['Location'] += '&oauth_consumer_key=' +
meta_mw.consumer_key
- return redirector
-
-
-def process_mw_jwt(identify_token_encoded):
- try:
- identify_token = jwt.decode(
- identify_token_encoded,
- app.config['META_MW_CLIENT_SECRET']
- )
-
- # Verify the issuer is who we expect (server sends $wgCanonicalServer)
- iss = urllib2.urlparse.urlparse(identify_token['iss']).netloc
- mw_domain =
urllib2.urlparse.urlparse(app.config['META_MW_BASE_URL']).netloc
- if iss != mw_domain:
- raise Exception('JSON Web Token Validation Problem, iss')
-
- # Verify we are the intended audience
- if identify_token['aud'] != app.config['META_MW_CONSUMER_KEY']:
- raise Exception('JSON Web Token Validation Problem, aud')
-
- # Verify we are within the time limits of the token.
- # Issued at (iat) should be in the past
- now = int(time.time())
- if int(identify_token['iat']) > now:
- raise Exception('JSON Web Token Validation Problem, iat')
-
- # Expiration (exp) should be in the future
- if int(identify_token['exp']) < now:
- raise Exception('JSON Web Token Validation Problem, exp')
-
- # Verify we haven't seen this nonce before,
- # which would indicate a replay attack
- # TODO: implement nonce but this is not high priority
- #if identify_token['nonce'] != <<original request nonce>>
- #raise Exception('JSON Web Token Validation Problem, nonce')
-
- return identify_token
- except Exception, e:
- flash(e.message)
- raise e
+ handshaker = make_handshaker_mw()
+ redirect_url, request_token = handshaker.initiate()
+ session['request_token'] = request_token
+ return redirect(redirect_url)
@app.route('/auth/meta_mw')
-@meta_mw.authorized_handler
@is_public
def auth_meta_mw(resp):
"""
@@ -153,26 +119,21 @@
if resp is None:
flash('You need to grant the app permissions in order to login.',
'error')
return redirect(url_for('login'))
-
- session['access_token'] = (
- resp['oauth_token'],
- resp['oauth_token_secret']
- )
-
+
try:
- identify_token_encoded = meta_mw.post(
- app.config['META_MW_BASE_URL'] +
app.config['META_MW_IDENTIFY_URI'],
- ).data
- identify_token = process_mw_jwt(identify_token_encoded)
-
- username = identify_token['username']
- userid = identify_token['sub']
-
+ handshaker = make_handshaker_mw()
+ access_token = handshaker.complete(session['request_token'],
request.query_string)
+ session['acces_token'] = access_token
+
+ identity = handshaker.identify(access_token)
+ username = identity['username']
+ userid = identity['sub']
+
db_session = db.get_session()
user = None
try:
user =
db_session.query(UserStore).filter_by(meta_mw_id=userid).one()
-
+
except NoResultFound:
try:
user = UserStore(
@@ -185,29 +146,23 @@
except:
db_session.rollback()
raise
-
+
except MultipleResultsFound:
- return 'Multiple users found with your id!!! Contact Administrator'
-
+ flash('Multiple users found with your id!!! Contact
Administrator', 'error')
+ return redirect(url_for('login'))
+
user.login(db_session)
if login_user(user):
user.detach_from(db_session)
- redirect_to = session.get('next') or url_for('home_index')
- redirect_to = urllib2.unquote(redirect_to)
- return redirect(redirect_to)
-
+ del session['request_token']
+
except Exception, e:
flash('Access to this application was revoked. Please re-login!')
app.logger.exception(str(e))
return redirect(url_for('login'))
-
- next_url = request.args.get('next') or url_for('index')
- return redirect(next_url)
-
-@meta_mw.tokengetter
-def get_meta_wiki_token(token=None):
- return session.get('access_token')
+ redirect_to = session.get('next') or url_for('home_index')
+ return redirect(urllib2.unquote(redirect_to))
@app.route('/login/google')
--
To view, visit https://gerrit.wikimedia.org/r/179962
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ia899f0a71ea770e3f402eac0645be4fdd924379a
Gerrit-PatchSet: 1
Gerrit-Project: analytics/wikimetrics
Gerrit-Branch: master
Gerrit-Owner: Milimetric <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits