From c70dabcbe11faacb6d482de5c60751edda5dbd79 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 25 Nov 2017 22:37:54 +1100 Subject: [PATCH] Basic Reddit authentication flow --- eosweb/core/main.py | 53 +++++++++++++++ eosweb/core/templates/auth/login.html | 47 +++++++++++++ .../core/templates/auth/login_complete.html | 40 +++++++++++ eosweb/core/templates/base.html | 11 ++- eosweb/redditauth/__init__.py | 0 eosweb/redditauth/main.py | 68 +++++++++++++++++++ eosweb/redditauth/settings.py | 20 ++++++ 7 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 eosweb/core/templates/auth/login.html create mode 100644 eosweb/core/templates/auth/login_complete.html create mode 100644 eosweb/redditauth/__init__.py create mode 100644 eosweb/redditauth/main.py create mode 100644 eosweb/redditauth/settings.py diff --git a/eosweb/core/main.py b/eosweb/core/main.py index 0b1dafd..3445211 100644 --- a/eosweb/core/main.py +++ b/eosweb/core/main.py @@ -50,6 +50,28 @@ if 'EOSWEB_SETTINGS' in os.environ: # Connect to database db_connect(app.config['DB_NAME'], app.config['MONGO_URI']) +# Make Flask's serialisation, e.g. for sessions, EosObject aware +class EosObjectJSONEncoder(flask.json.JSONEncoder): + def default(self, obj): + if isinstance(obj, EosObject): + return EosObject.serialise_and_wrap(obj) + return super().default(obj) +class EosObjectJSONDecoder(flask.json.JSONDecoder): + def __init__(self, *args, **kwargs): + self.super_object_hook = kwargs.get('object_hook', None) + kwargs['object_hook'] = self.my_object_hook + super().__init__(*args, **kwargs) + + def my_object_hook(self, val): + if 'type' in val: + if val['type'] in EosObject.objects: + return EosObject.deserialise_and_unwrap(val) + if self.super_object_hook: + return self.super_object_hook(val) + return val +app.json_encoder = EosObjectJSONEncoder +app.json_decoder = EosObjectJSONDecoder + @app.cli.command('test') @click.option('--prefix', default=None) @click.option('--lang', default=None) @@ -123,6 +145,8 @@ def count_test_election(): def inject_globals(): return {'eos': eos, 'eosweb': eosweb, 'SHA256': eos.core.hashing.SHA256} +# === Views === + @app.route('/') def index(): return flask.render_template('index.html') @@ -197,6 +221,35 @@ def election_api_cast_vote(election): 'vote': EosObject.serialise_and_wrap(vote) }), mimetype='application/json') +@app.route('/debug') +def debug(): + assert False + +@app.route('/auth/login') +def login(): + return flask.render_template('auth/login.html') + +@app.route('/auth/logout') +def logout(): + flask.session['user'] = None + #return flask.redirect(flask.request.args['next'] if 'next' in flask.request.args else '/') + # I feel like there's some kind of exploit here, so we'll leave this for now + return flask.redirect('/') + +@app.route('/auth/login_complete') +def login_complete(): + return flask.render_template('auth/login_complete.html') + +@app.route('/auth/login_cancelled') +def login_cancelled(): + return flask.render_template('auth/login_cancelled.html') + +# === Apps === + +for app_name in app.config['APPS']: + app_main = importlib.import_module(app_name + '.main') + app_main.main(app) + # === Model-Views === model_view_map = {} diff --git a/eosweb/core/templates/auth/login.html b/eosweb/core/templates/auth/login.html new file mode 100644 index 0000000..0bece56 --- /dev/null +++ b/eosweb/core/templates/auth/login.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} + +{# + Eos - Verifiable elections + Copyright © 2017 RunasSudo (Yingtong Li) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +#} + +{% block title %}Login{% endblock %} + +{% block content %} + {% if session.user %} +

You are currently logged in as {{ session.user.username }}. Please select an option from the list below to switch accounts.

+ {% else %} +

You are not currently logged in. Please select an option from the list below to log in.

+ {% endif %} + +{% endblock %} + +{% block basecontent %} + {{ super() }} + +{% endblock %} diff --git a/eosweb/core/templates/auth/login_complete.html b/eosweb/core/templates/auth/login_complete.html new file mode 100644 index 0000000..749ddf5 --- /dev/null +++ b/eosweb/core/templates/auth/login_complete.html @@ -0,0 +1,40 @@ +{% extends 'semantic_base.html' %} + +{# + Eos - Verifiable elections + Copyright © 2017 RunasSudo (Yingtong Li) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +#} + +{% block title %}Login{% endblock %} + +{% block basecontent %} +
+
+
+
Log in successful
+

You have successfully logged in to your account.

+

You may now close this window and return to your previous page.

+
+
+
+ + +{% endblock %} diff --git a/eosweb/core/templates/base.html b/eosweb/core/templates/base.html index 8c31769..e4ef317 100644 --- a/eosweb/core/templates/base.html +++ b/eosweb/core/templates/base.html @@ -26,7 +26,16 @@
diff --git a/eosweb/redditauth/__init__.py b/eosweb/redditauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eosweb/redditauth/main.py b/eosweb/redditauth/main.py new file mode 100644 index 0000000..d93e836 --- /dev/null +++ b/eosweb/redditauth/main.py @@ -0,0 +1,68 @@ +# Eos - Verifiable elections +# Copyright © 2017 RunasSudo (Yingtong Li) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from flask_oauthlib.client import OAuth + +import flask + +from eos.core.objects import * + +import base64 +import uuid + +class RedditUser(DocumentObject): + oauth_token = StringField(is_protected=True) + username = StringField() + +def main(app): + oauth = OAuth() + reddit = oauth.remote_app('Reddit', + request_token_url=None, + authorize_url='https://www.reddit.com/api/v1/authorize.compact', + request_token_params={'duration': 'temporary', 'scope': 'identity'}, + access_token_url='https://www.reddit.com/api/v1/access_token', + access_token_method='POST', + access_token_headers={ + 'Authorization': 'Basic ' + base64.b64encode('{}:{}'.format(app.config['REDDIT_OAUTH_CLIENT_ID'], app.config['REDDIT_OAUTH_CLIENT_SECRET']).encode('ascii')).decode('ascii'), + 'User-Agent': app.config['REDDIT_USER_AGENT'] + }, + consumer_key=app.config['REDDIT_OAUTH_CLIENT_ID'], + consumer_secret=app.config['REDDIT_OAUTH_CLIENT_SECRET'] + ) + + @app.route('/auth/reddit/login') + def reddit_login(): + return reddit.authorize(callback=app.config['BASE_URI'] + flask.url_for('reddit_oauth_authorized'), state=uuid.uuid4()) + + @reddit.tokengetter + def get_reddit_oauth_token(): + return (flask.session.get('user').oauth_token, '') + + @app.route('/auth/reddit/oauth_callback') + def reddit_oauth_authorized(): + resp = reddit.authorized_response() + if resp is None: + # Request denied + return flask.redirect(flask.url_for('login_cancelled')) + + user = RedditUser() + user.oauth_token = resp['access_token'] + flask.session['user'] = user + + me = reddit.get('https://oauth.reddit.com/api/v1/me') + user.username = me.data['name'] + + return flask.redirect(flask.url_for('login_complete')) diff --git a/eosweb/redditauth/settings.py b/eosweb/redditauth/settings.py new file mode 100644 index 0000000..a7fe7f5 --- /dev/null +++ b/eosweb/redditauth/settings.py @@ -0,0 +1,20 @@ +# Eos - Verifiable elections +# Copyright © 2017 RunasSudo (Yingtong Li) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +REDDIT_OAUTH_CLIENT_ID = 'xxxxxxxxxxxxxx' +REDDIT_OAUTH_CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx' + +REDDIT_USER_AGENT = 'FIXME'