Implement login fix for browsers without window.opener support #11

This commit is contained in:
RunasSudo 2018-01-28 22:28:10 +11:00
parent baeba08366
commit ebe6530557
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 218 additions and 74 deletions

View File

@ -16,6 +16,7 @@
import click
import flask
import flask_session
import timeago
from eos.core.objects import *
@ -62,6 +63,16 @@ if 'EOSWEB_SETTINGS' in os.environ:
# Connect to database
db_connect(app.config['DB_NAME'], app.config['DB_URI'], app.config['DB_TYPE'])
# Configure sessions
if app.config['DB_TYPE'] == 'mongodb':
app.config['SESSION_TYPE'] = 'mongodb'
app.config['SESSION_MONGODB'] = dbinfo.provider.client
app.config['SESSION_MONGODB_DB'] = dbinfo.provider.db_name
elif app.config['DB_TYPE'] == 'postgresql':
app.config['SESSION_TYPE'] = 'sqlalchemy'
app.config['SQLALCHEMY_DATABASE_URI'] = dbinfo.provider.conn.dsn
flask_session.Session(app)
# Set configs
User.admins = app.config['ADMINS']
@ -298,6 +309,12 @@ def election_admin_schedule_task(election):
return flask.redirect(flask.url_for('election_admin_summary', election_id=election._id))
@app.route('/election/<election_id>/stage_ballot', methods=['POST'])
@using_election
def election_api_stage_ballot(election):
flask.session['staged_ballot'] = json.loads(flask.request.data)
return 'OK'
@app.route('/election/<election_id>/cast_ballot', methods=['POST'])
@using_election
def election_api_cast_vote(election):
@ -305,7 +322,7 @@ def election_api_cast_vote(election):
# Voting is not yet open or has closed
return flask.Response('Voting is not yet open or has closed', 409)
data = json.loads(flask.request.data)
data = flask.session['staged_ballot']
if 'user' not in flask.session:
# User is not authenticated
@ -336,6 +353,8 @@ def election_api_cast_vote(election):
vote.save()
del flask.session['staged_ballot']
return flask.Response(json.dumps({
'voter': EosObject.serialise_and_wrap(voter, None, SerialiseOptions(should_protect=True)),
'vote': EosObject.serialise_and_wrap(vote, None, SerialiseOptions(should_protect=True))
@ -365,13 +384,28 @@ def debug():
@app.route('/auth/login')
def login():
flask.session['login_next'] = flask.request.referrer
return flask.render_template('auth/login.html')
@app.route('/auth/stage_next', methods=['POST'])
def auth_stage_next():
flask.session['login_next'] = flask.request.data
return 'OK'
@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
if flask.request.referrer:
return flask.redirect(flask.request.referrer)
else:
return flask.redirect('/')
@app.route('/auth/login_callback')
def login_callback():
print(flask.session)
if 'login_next' in flask.session and flask.session['login_next']:
return flask.redirect(flask.session['login_next'])
else:
return flask.redirect('/')
@app.route('/auth/login_complete')

View File

@ -59,8 +59,13 @@
{% endblock %}
{% block buttons %}
<button class="ui left floated button" onclick="prevTemplate(2);">Back</button>
{% if is_cast %}
<a class="ui left floated button" href="{{ election_base_url }}booth">Reset</a>
<button class="ui right floated primary button" id="cast_button" onclick="castBallot();"{% if not username %} style="display: none;"{% endif %}>Cast ballot</button>
{% else %}
<button class="ui left floated button" onclick="prevTemplate(2);">Back</button>
<button class="ui right floated primary button" id="cast_button" onclick="stageBallot(castBallot);"{% if not username %} style="display: none;"{% endif %}>Cast ballot</button>
{% endif %}
{% endblock %}
{% block after %}
@ -75,30 +80,90 @@
});
function login(el) {
// Stage the vote for casting in case we change page
stageBallot(function(data) {
// Stage the next page in case we change page
stageNext("{{ election_base_url }}booth?cast", function(data) {
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
});
});
}
function callback_complete(username) {
$("#cast_button").show();
$("#booth_logged_in_as").text("You are currently logged in as " + username + ".");
// Ballot was staged when we clicked the login button
castBallot();
return true;
}
function stageBallot(callback) {
// Prepare ballot
var deauditedBallot = booth.ballot.deaudit();
$.ajax({
url: "{{ election_base_url }}stage_ballot",
type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(deauditedBallot, null),
"fingerprint": booth.fingerprint || null
}),
contentType: "application/json",
dataType: "text",
async: false // so window.open happens in main thread
})
.done(function(data) {
callback(data);
})
.fail(function(xhr, status, err) {
if (xhr.responseText && xhr.responseText.length < 100) {
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
} else {
$("#error_unknown_tech").text("Technical details: " + err);
}
$("#error_unknown").removeClass("hidden");
$("#error_invalid_id").addClass("hidden");
console.error(xhr);
throw err;
});
}
function stageNext(url, callback) {
$.ajax({
url: "/auth/stage_next",
type: "POST",
data: url,
contentType: "text/plain",
dataType: "text",
async: false // so window.open happens in main thread
})
.done(function(data) {
callback(data);
})
.fail(function(xhr, status, err) {
if (xhr.responseText && xhr.responseText.length < 100) {
$("#error_unknown_tech").text("Technical details: " + err + " – " + xhr.responseText);
} else {
$("#error_unknown_tech").text("Technical details: " + err);
}
$("#error_unknown").removeClass("hidden");
$("#error_invalid_id").addClass("hidden");
console.error(xhr);
throw err;
});
}
function castBallot() {
$("#cast_prompt").hide();
$("#casting").show();
// Prepare ballot
booth.ballot = booth.ballot.deaudit();
$.ajax({
url: "{{ election_base_url }}cast_ballot",
type: "POST",
data: eosjs.eos.core.objects.__all__.EosObject.to_json({
"ballot": eosjs.eos.core.objects.__all__.EosObject.serialise_and_wrap(booth.ballot, null),
"fingerprint": booth.fingerprint || null
}),
contentType: "application/json",
dataType: "text"
})
.done(function(data) {
@ -134,6 +199,10 @@
throw err;
});
}
{% if is_cast %}
castBallot();
{% endif %}
</script>
{% endblock %}

View File

@ -31,6 +31,12 @@
<li><a href="/auth/{{ auth_method[0] }}/login" target="_blank" onclick="login(this);return false;">{{ auth_method[1] }}</a></li>
{% endfor %}
</ul>
<div class="ui hidden error message">
<div class="header">Error logging in</div>
<p>Your log in details appear to be correct, however there was an unknown error while logging you in.</p>
<p>Please try again. If the problem persists, contact your election administrator.</p>
</div>
{% endblock %}
{% block basecontent %}
@ -40,8 +46,8 @@
window.open(el.getAttribute("href"), "eos_login_window", "width=400,height=600");
}
function callback_complete(name) {
window.location = "/";
function callback_complete() {
window.location = "{{ url_for('login_callback') }}";
return true;
}
</script>

View File

@ -23,33 +23,46 @@
{% block basecontent %}
<div class="ui middle aligned center aligned grid" style="height: 100%;">
<div class="column" style="max-width: 400px;">
<div class="ui hidden success message">
<div class="ui hidden success message" id="success-popup">
<div class="header">Log in successful</div>
<p>You have successfully logged in to your account.</p>
<p>You may now close this window and return to your previous page.</p>
</div>
<div class="ui hidden success message" id="success-redirect">
<div class="header">Log in successful</div>
<p>You have successfully logged in to your account.</p>
<p>Please wait, you should be returned to your previous page momentarily.</p>
</div>
<div class="ui hidden error message">
<div class="header">Error logging in</div>
<p>Your log in details appear to be correct, however there was an unknown error while logging you in.</p>
<p>Please close this window try again. If the problem persists, contact your election administrator.</p>
<p>Please close this window and try again. If the problem persists, contact your election administrator.</p>
</div>
</div>
</div>
<script>
if (window.opener && window.opener.callback_complete) {
// Normal popup window
// Redirect happens in the opener
try {
var result = window.opener.callback_complete("{{ session.user.name }}");
if (result) {
$(".success.message").removeClass("hidden");
window.setTimeout(function() {
window.close();
}, 1000);
$("#success-popup").removeClass("hidden");
setTimeout(window.close, 1000);
} else {
$(".error.message").removeClass("hidden");
}
} catch (ex) {
$(".error.message").removeClass("hidden");
console.error(ex);
throw ex;
}
} else {
// This browser does not support popups
// Manually redirect in this window
$("#success-redirect").removeClass("hidden");
window.location = "{{ url_for('login_callback') }}";
}
</script>
{% endblock %}

View File

@ -192,12 +192,15 @@
// === BOOTH TASKS ===
// TODO: Make modular
templates['booth/base.html'] = null;
if (location.search.indexOf('?cast') < 0) {
// Normal booth
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/welcome.html');
}
});
templates['booth/base.html'] = null;
templates['booth/welcome.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
@ -252,7 +255,25 @@
templates['booth/audit.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot});
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: false});
}
});
templates['booth/cast.html'] = null;
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/complete.html', {voter: booth.voter, vote: booth.vote});
}
});
templates['booth/complete.html'] = null;
}
} else {
// Cast immediately
{% if session.staged_ballot %}
booth.ballot = eosjs.eos.core.objects.__all__.EosObject.deserialise_and_unwrap(eosjs.eos.core.objects.__all__.EosObject.from_json('{{ eos.core.objects.EosObject.to_json(session.staged_ballot.ballot)|safe }}'), null);
{% endif %}
boothTasks.append({
activate: function(fromLeft) {
showTemplate('booth/cast.html', {ballot: booth.ballot, is_cast: true});
}
});
templates['booth/cast.html'] = null;

View File

@ -2,6 +2,7 @@ coverage==4.4.1
Flask==0.12.2
Flask-Mail==0.9.1
Flask-OAuthlib==0.9.4
Flask-Session==0.3.1
gunicorn==19.7.1
libsass==0.13.4
premailer==3.1.1