From: Daniel Lenski Date: Thu, 21 Oct 2021 01:11:01 +0000 (-0700) Subject: Test both tokeninfo- and HTML-based MFA challenges for Fortinet X-Git-Tag: v8.20~53^2 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=169b686e6f9ab41f527eafd11543d586f1e78227;p=users%2Fdwmw2%2Fopenconnect.git Test both tokeninfo- and HTML-based MFA challenges for Fortinet Signed-off-by: Daniel Lenski --- diff --git a/tests/fake-fortinet-server.py b/tests/fake-fortinet-server.py index 7d6eab87..ce0558f3 100755 --- a/tests/fake-fortinet-server.py +++ b/tests/fake-fortinet-server.py @@ -34,8 +34,9 @@ # 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 @@ -86,11 +87,11 @@ def check_form_against_session(*fields): # Respond to initial 'GET /' with a login form # Respond to initial 'GET /' with a redirect to '/remote/login?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('/') 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')) @@ -111,24 +112,29 @@ def login(): @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) @@ -138,21 +144,46 @@ def complete_2fa(): 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 (''' +
+ Please enter your HTML 2FA code ({want_2fa} remaining) + + + + + + +
+ '''.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(): diff --git a/tests/fortinet-auth-and-config b/tests/fortinet-auth-and-config index 2ce3518b..f5fc0169 100755 --- a/tests/fortinet-auth-and-config +++ b/tests/fortinet-auth-and-config @@ -47,19 +47,25 @@ echo -n "Authenticating with username/password and NON-DEFAULT path... " echo ok -echo -n "Authenticating with username/password/token and DEFAULT path... " +echo -n "Authenticating with username/password/tokeninfo-2FA and DEFAULT path... " ( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=fortinet -q $ADDRESS:443/?want_2fa=1 -u test --token-mode=totp --token-secret=FAKE $FINGERPRINT --cookieonly >/dev/null 2>&1) || fail $PID "Could not receive cookie from fake Fortinet server" echo ok -echo -n "Authenticating with username/password/(2 round of token) and DEFAULT path... " +echo -n "Authenticating with username/password/HTML-2FA and DEFAULT path... " +( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=fortinet -q "$ADDRESS:443/?want_2fa=1&type_2fa=html" -u test --token-mode=totp --token-secret=FAKE $FINGERPRINT --cookieonly >/dev/null 2>&1) || + fail $PID "Could not receive cookie from fake Fortinet server" + +echo ok + +echo -n "Authenticating with username/password/(2 rounds of tokeninfo-2FA) and DEFAULT path... " ( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=fortinet -q $ADDRESS:443/?want_2fa=2 -u test --token-mode=totp --token-secret=FAKE $FINGERPRINT --cookieonly >/dev/null 2>&1) || fail $PID "Could not receive cookie from fake Fortinet server" echo ok -echo -n "Authenticating with username/password/token and NON-DEFAULT path... " +echo -n "Authenticating with username/password/tokeninfo-2FA and NON-DEFAULT path... " ( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=fortinet -q $ADDRESS:443/fake+Realm?want_2fa=1 -u test --token-mode=totp --token-secret=FAKE $FINGERPRINT --pfs --cookieonly >/dev/null 2>&1) || fail $PID "Could not receive cookie from fake Fortinet server"