]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Test both tokeninfo- and HTML-based MFA challenges for Fortinet
authorDaniel Lenski <dlenski@gmail.com>
Thu, 21 Oct 2021 01:11:01 +0000 (18:11 -0700)
committerDaniel Lenski <dlenski@gmail.com>
Thu, 21 Oct 2021 15:37:36 +0000 (08:37 -0700)
Signed-off-by: Daniel Lenski <dlenski@gmail.com>
tests/fake-fortinet-server.py
tests/fortinet-auth-and-config

index 7d6eab87ede5a4f8861c7f8d4b5958c465b3e71c..ce0558f3d07d3d12a0c2242cd5dce6d8f009627f 100755 (executable)
@@ -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 /<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'))
@@ -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 ('''
+        <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():
index 2ce3518b0130844b86acadbc1095f17af2298678..f5fc016966c78e79d0043153515efa0314a4b99d 100755 (executable)
@@ -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"