# values via a (cookie-based) session.
#
# In order to test with 2FA, the initial 'GET /' request should include
-# the query string '?want_2fa=1'. If >1, multiple rounds of 2FA token entry
-# will be required.
+# the query string '?want_2fa=1&type_2fa={tokeninfo,html}'. If want_2fa>1,
+# multiple rounds of 2FA token entry will be required. If type_2fa is not,
+# specified tokeninfo-type 2FA is the default.
########################################
import sys
# Respond to initial 'GET /' with a login form
# Respond to initial 'GET /<realm>' with a redirect to '/remote/login?realm=<realm>'
-# [Save want_2fa query parameter in the session for use later]
+# [Save want_2fa and type_2fa query parameters in the session for use later]
@app.route('/')
@app.route('/<realm>')
def realm(realm=None):
- session.update(step='GET-realm', want_2fa=int(request.args.get('want_2fa', 0)))
+ session.update(step='GET-realm', want_2fa=int(request.args.get('want_2fa', 0)), type_2fa=request.args.get('type_2fa', 'tokeninfo'))
# print(session)
if realm:
return redirect(url_for('login', realm=realm, lang='en'))
@app.route('/remote/logincheck', methods=['POST'])
def logincheck():
want_2fa = session.get('want_2fa')
-
- if want_2fa and request.form.get('username') and request.form.get('code'):
- if want_2fa == 1:
- return complete_2fa()
- else:
- session.update(want_2fa=want_2fa - 1)
- return send_2fa_challenge()
- elif (want_2fa and request.form.get('username') and request.form.get('credential')):
- return send_2fa_challenge()
+ type_2fa = session.get('type_2fa')
+
+ if want_2fa:
+ if ( (type_2fa == 'tokeninfo' and request.form.get('username') and request.form.get('code'))
+ or (type_2fa == 'html' and request.form.get('username') and request.form.get('magic'))):
+ # we've received (at least one round of) 2FA login
+ if want_2fa == 1:
+ return complete_2fa()
+ else:
+ session.update(want_2fa=want_2fa - 1)
+ return send_2fa_tokeninfo() if type_2fa == 'tokeninfo' else send_2fa_html()
+ elif request.form.get('username') and request.form.get('credential'):
+ # we've just received the initial non-2FA login
+ return send_2fa_tokeninfo() if type_2fa == 'tokeninfo' else send_2fa_html()
elif (request.form.get('username') and request.form.get('credential')):
return complete_non_2fa()
abort(405)
# 2FA completion: ensure that client has parroted back the same values
-# for username, reqid, polid, grp, portal, magic
+# for username, reqid, polid, grp/grpid, portal, magic
# [Save code in the session for potential use later]
-@check_form_against_session('username', 'reqid', 'polid', 'grp', 'portal', 'magic')
+@check_form_against_session('username', 'reqid', 'polid', 'grp', 'grpid', 'portal', 'magic')
def complete_2fa():
session.update(step='complete-2FA', code=request.form.get('code'))
# print(session)
return resp
-# 2FA initial login: ensure that client has sent the right realm value, and
-# reply with a token challenge containing all known fields.
+# Tokeninfo-based 2FA initial login: ensure that client has sent the right realm value, and
+# reply with a tokeninfo challenge containing all known fields.
# [Save username, credential, and challenge fields in the session for verification of client state later]
@check_form_against_session('realm')
-def send_2fa_challenge():
- session.update(step='send-2FA-challenge', username=request.form.get('username'), credential=request.form.get('credential'),
+def send_2fa_tokeninfo():
+ session.update(step='send-2FA-tokeninfo', username=request.form.get('username'), credential=request.form.get('credential'),
reqid=str(random.randint(10_000_000, 99_000_000)), polid='1-1-'+str(random.randint(10_000_000, 99_000_000)),
magic='1-'+str(random.randint(10_000_000, 99_000_000)), portal=random.choice('ABCD'), grp=random.choice('EFGH'))
# print(session)
return ('ret=2,reqid={reqid},polid={polid},grp={grp},portal={portal},magic={magic},'
- 'tokeninfo=,chal_msg=Please enter your token code ({want_2fa} remaining)'.format(**session),
+ 'tokeninfo=,chal_msg=Please enter your tokeninfo code ({want_2fa} remaining)'.format(**session),
{'content-type': 'text/plain'})
+# HTML-based 2FA initial login: ensure that client has sent the right realm value, and
+# reply with an HTML challenge containing all known fields.
+# [Save username, credential, and challenge fields in the session for verification of client state later]
+@check_form_against_session('realm')
+def send_2fa_html():
+ session.update(step='send-2FA-html', username=request.form.get('username'), credential=request.form.get('credential'),
+ reqid=str(random.randint(10_000_000, 99_000_000)), grpid='0,'+str(random.randint(1_000, 9_999))+',1',
+ magic='1-'+str(random.randint(10_000_000, 99_000_000)))
+ # print(session)
+
+ return ('''
+ <html><body><form action="{logincheck}" method="POST">
+ <b>Please enter your HTML 2FA code ({want_2fa} remaining)</b>
+ <input type="hidden" name="magic" value="{magic}">
+ <input type="hidden" name="username" value="{username}">
+ <input type="hidden" name="reqid" value="{reqid}">
+ <input type="hidden" name="grpid" value="{grpid}">
+ <input type="password" name="credential">
+ <input class="button" type="submit" value="OK">
+ </form></body></html>
+ '''.format(logincheck=url_for('logincheck'), **session),
+ 401,
+ {'content-type': 'text/html'})
+
+
# Non-2FA login: ensure that client has sent the right realm value
@check_form_against_session('realm')
def complete_non_2fa():