]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Improve Fortinet auth
authorDaniel Lenski <dlenski@gmail.com>
Wed, 5 May 2021 17:18:55 +0000 (10:18 -0700)
committerDaniel Lenski <dlenski@gmail.com>
Wed, 5 May 2021 17:31:55 +0000 (10:31 -0700)
Use the same 'auth_id' values as GlobalProtect uses ('_login' for the normal
username/password form, and '_challenge' for the MFA challenge form). This
is a follow-on to 613fa87dd64853a042d982aca4a94279cd78ff58.

This also fixes:

- A potential double-free() issue if the challenge form is loaded repeatedly,
  adding a test of 2 rounds of challenge 2FA to verify it.
- Warnings about the unused 'invalid_cookie' label.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
fortinet.c
tests/fake-fortinet-server.py
tests/fortinet-auth-and-config

index f2db42582aa615d9cd62723bc6ab6d1da2fc51f6..d7ce369d5b6b55338dc2e8d8e3bb6d2e8084dc39 100644 (file)
@@ -144,7 +144,7 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
                ret = -ENOMEM;
                goto out;
        }
-       form->auth_id = strdup("fortinet_auth");
+       form->auth_id = strdup("_login");
        if (!form->auth_id)
                goto nomem;
        opt = form->opts = calloc(1, sizeof(*opt));
@@ -219,6 +219,7 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
                        opt->type = OC_FORM_OPT_HIDDEN;
                        free(opt2->label);
                        free(opt2->_value);
+                       opt2->label = opt2->_value = NULL;
 
                        /* Change 'credential' field to 'code'. */
                        opt2->_value = NULL;
@@ -229,6 +230,11 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
                        else
                                opt2->type = OC_FORM_OPT_PASSWORD;
 
+                       /* Change 'auth_id' to '_challenge'. */
+                       free(form->auth_id);
+                       if (!(form->auth_id = strdup("_challenge")))
+                               goto nomem;
+
                        /* Save a bunch of values to parrot back */
                        filter_opts(action_buf, resp_buf, "reqid,polid,grp,portal,peer,magic", 1);
                        if ((ret = buf_error(action_buf)))
@@ -564,7 +570,9 @@ static int fortinet_configure(struct openconnect_info *vpninfo)
                 * XX: See do_https_request() for why ret==0 can only happen
                 * if there was a successful-but-unfetched redirect.
                 */
+#if 0
        invalid_cookie:
+#endif
                ret = -EPERM;
                goto out;
        }
index 20c58399ab9af8104d6b0ed4fc859938e879a356..8dd636fbb69d336119868e8b47d0b47d4d99829b 100755 (executable)
@@ -34,7 +34,8 @@
 # values via a (cookie-based) session.
 #
 # In order to test with 2FA, the initial 'GET /' request should include
-# the query string '?want_2fa=1'.
+# the query string '?want_2fa=1'. If >1, multiple rounds of 2FA token entry
+# will be required.
 ########################################
 
 import sys
@@ -86,7 +87,7 @@ def check_form_against_session(*fields):
 @app.route('/')
 @app.route('/<realm>')
 def realm(realm=None):
-    session.update(step='GET-realm', want_2fa='want_2fa' in request.args)
+    session.update(step='GET-realm', want_2fa=int(request.args.get('want_2fa', 0)))
     # print(session)
     if realm:
         return redirect(url_for('login', realm=realm))
@@ -108,8 +109,12 @@ def login():
 def logincheck():
     want_2fa = session.get('want_2fa')
 
-    if (want_2fa and request.form.get('code')):
-        return complete_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()
     elif (request.form.get('username') and request.form.get('credential')):
@@ -141,7 +146,7 @@ def send_2fa_challenge():
     # print(session)
 
     return ('ret=2,reqid={reqid},polid={polid},grp={grp},portal={portal},magic={magic},'
-            'tokeninfo=,chal_msg=Please enter your token code'.format(**session),
+            'tokeninfo=,chal_msg=Please enter your token code ({want_2fa} remaining)'.format(**session),
             {'content-type': 'text/plain'})
 
 
index 1981171eadef531bd3314caf5c4dcc454ec28c7c..e569bebdac43a5f31e8a1a325930ae9c7cc761f0 100755 (executable)
@@ -51,6 +51,12 @@ echo -n "Authenticating with username/password/token 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"
 
+ok
+
+echo -n "Authenticating with username/password/(2 round of token) 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... "