]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
attempt to implement Fortinet challenge-based 2FA (ping #225)
authorDaniel Lenski <dlenski@gmail.com>
Thu, 18 Feb 2021 03:27:10 +0000 (19:27 -0800)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:13:31 +0000 (20:13 -0700)
2FA involves reading a bunch of values from the initial response and
parroting them back, along with changing the 'credential' field to 'code'.

This is based on Openfortivpn's implementation
(https://github.com/adrienverge/openfortivpn/blob/master/src/http.c#L711-L754),
and has been tested for basic correctness against a toy HTTPS server that
gives responses with the right format.

Still to do:

- Another one-time password mode: https://github.com/adrienverge/openfortivpn/blob/master/src/http.c#L672-L679, https://github.com/adrienverge/openfortivpn/commit/61dd4fcbbfe1dd89b95081e7d5f5c3e933d7897a
- There's a mobile-app-based "push" mode: https://github.com/adrienverge/openfortivpn/commit/2f16d964560d9b00acbf7bfc7131a0ac40c688f2

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
fortinet.c

index 9b7130b24434339b50df8485a1b1728f2295a929..90f2d5779a5a62d93e59a586c10239cc7c9841d0 100644 (file)
@@ -59,6 +59,34 @@ void fortinet_common_headers(struct openconnect_info *vpninfo,
        */
 }
 
+/* XX: consolidate with gpst.c version (differs only in '&' vs ',' as separator for input) */
+static int filter_opts(struct oc_text_buf *buf, const char *query, const char *incexc, int include)
+{
+       const char query_sep = ',';
+       const char *f, *endf, *eq;
+       const char *found, *comma;
+
+       for (f = query; *f; f=(*endf) ? endf+1 : endf) {
+               endf = strchr(f, query_sep) ? : f+strlen(f);
+               eq = strchr(f, '=');
+               if (!eq || eq > endf)
+                       eq = endf;
+
+               for (found = incexc; *found; found=(*comma) ? comma+1 : comma) {
+                       comma = strchr(found, ',') ? : found+strlen(found);
+                       if (!strncmp(found, f, MAX(comma-found, eq-f)))
+                               break;
+               }
+
+               if ((include && *found) || (!include && !*found)) {
+                       if (buf->pos && buf->data[buf->pos-1] != '?' && buf->data[buf->pos-1] != '&')
+                               buf_append(buf, "&");
+                       buf_append_bytes(buf, f, (int)(endf-f));
+               }
+       }
+       return buf_error(buf);
+}
+
 int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
 {
        int ret;
@@ -118,7 +146,16 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
                buf_truncate(resp_buf);
                append_form_opts(vpninfo, form, resp_buf);
                append_opt(resp_buf, "realm", vpninfo->authgroup ?: "");
-               buf_append(resp_buf, "&ajax=1&just_logged_in=1");
+
+               if (!form->action) {
+                       /* "normal" form (fields 'username', 'credential') */
+                       buf_append(resp_buf, "&ajax=1&just_logged_in=1");
+               } else {
+                       /* 2FA form (fields 'username', 'code', and a bunch of values
+                        * from the previous response which we mindlessly parrot back)
+                        */
+                       buf_append(resp_buf, "&code2=&%s", form->action);
+               }
 
                if ((ret = buf_error(resp_buf)))
                        goto out;
@@ -142,13 +179,42 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
 
                        /* XX: We didn't get SVPNCOOKIE. 2FA? */
                        if (!strncmp(form_buf, "ret=", 4) && strstr(form_buf, ",tokeninfo=")) {
-                               vpn_progress(vpninfo, PRG_ERR,
-                                            _("Got 2FA form response. Not yet implemented.\n"));
-                               ret = -EINVAL;
-                               goto out;
+                               const char *prompt;
+                               struct oc_text_buf *action_buf = buf_alloc();
+
+                               /* Hide 'username' field */
+                               opt->type = OC_FORM_OPT_HIDDEN;
+                               free(opt2->label);
+                               free(opt2->_value);
+
+                               /* Change 'credential' field to 'code'. */
+                               opt2->_value = NULL;
+                               opt2->name = strdup("code");
+                               opt2->label = strdup("Code: ");
+                               if (!can_gen_tokencode(vpninfo, form, opt2))
+                                       opt2->type = OC_FORM_OPT_TOKEN;
+                               else
+                                       opt2->type = OC_FORM_OPT_PASSWORD;
+
+                               /* Save a bunch of values to parrot back */
+                               filter_opts(action_buf, form_buf, "reqid,polid,grp,portal,peer,magic", 1);
+                               if ((ret = buf_error(action_buf)))
+                                       goto out;
+                               free(form->action);
+                               form->action = action_buf->data;
+                               action_buf->data = NULL;
+                               buf_free(action_buf);
+
+                               if ((prompt = strstr(form_buf, ",chal_msg="))) {
+                                       char *end = strchr(prompt, ',');
+                                       if (end)
+                                               *end = '\0';
+                                       prompt += 10;
+                                       free(form->message);
+                                       form->message = strdup(prompt);
+                               }
                        }
                }
-               free(form_buf);
        }
 
  out: