From: Daniel Lenski Date: Sat, 8 Sep 2018 05:22:40 +0000 (-0700) Subject: GlobalProtect: query and parse prelogin.esp and use it to build auth forms, including... X-Git-Tag: v8.00~78 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=3e91f7bf7dc0e26307403b47320862050fd0f9a9;p=users%2Fdwmw2%2Fopenconnect.git GlobalProtect: query and parse prelogin.esp and use it to build auth forms, including preliminary SAML support Until recently, I've believed the prelogin.esp to be useless, because the initial GlobalProtect login form always contains the same two fields: username and password. However, the prelogin response is also important for signalling when SAML login is required. When the VPN uses SAML login, the official GP clients redirect the user to a web-based authentication flow (e.g. Okta, https://github.com/dlenski/openconnect/issues/116). That auth flow eventually sends the official client back to the GP VPN, armed with a special cookie field, `portal-userauthcookie` or `prelogin-cookie`, that needs to be submitted in place of the password (already supported by openconnect as of 8b2bc5f22dda). This preliminary SAML support simply includes the SAML method and URL in the form banner, and fails with an error message if the cookie field name was not specified (since it cannot be autodetected). Signed-off-by: Daniel Lenski --- diff --git a/auth-globalprotect.c b/auth-globalprotect.c index bd15a856..a745c062 100644 --- a/auth-globalprotect.c +++ b/auth-globalprotect.c @@ -24,6 +24,12 @@ #include "openconnect-internal.h" +struct login_context { + char *username; /* Username that has already succeeded in some form */ + char *alt_secret; /* Alternative secret (DO NOT FREE) */ + struct oc_auth_form *form; +}; + void gpst_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf) { @@ -35,45 +41,98 @@ void gpst_common_headers(struct openconnect_info *vpninfo, vpninfo->useragent = orig_ua; } -/* The GlobalProtect auth form always has two visible fields: +/* Parse pre-login response ({POST,GET} /{global-protect,ssl-vpn}/pre-login.esp) + * + * Extracts the relevant arguments from the XML (username-label, password-label) + * and uses them to build an auth form, which always has two visible fields: + * * 1) username * 2) one secret value: * - normal account password * - "challenge" (2FA) password, along with form name in auth_id - * - cookie from external authentication flow (INSTEAD OF password) + * - cookie from external authentication flow ("alternative secret" INSTEAD OF password) * - * This function steals the value of username for - * use in the auth form; pw_or_cookie_field is NOT stolen. */ -static struct oc_auth_form *new_auth_form(struct openconnect_info *vpninfo, - char *prompt, char *auth_id, char *username, char *pw_or_cookie_field) +static int parse_prelogin_xml(struct openconnect_info *vpninfo, xmlNode *xml_node, void *cb_data) { - struct oc_auth_form *form; + struct login_context *ctx = cb_data; + struct oc_auth_form *form = ctx->form; struct oc_form_opt *opt, *opt2; + char *prompt = NULL, *username_label = NULL, *password_label = NULL; + char *saml_method = NULL, *saml_path = NULL; + int result = 0; - form = calloc(1, sizeof(*form)); - if (!form) - return NULL; - form->message = strdup(prompt ? : _("Please enter your username and password")); - form->auth_id = strdup(auth_id ? : "_login"); + if (!xmlnode_is_named(xml_node, "prelogin-response")) + goto out; - opt = form->opts = calloc(1, sizeof(*opt)); - if (!opt) { + for (xml_node = xml_node->children; xml_node; xml_node = xml_node->next) { + char *s = NULL; + if (!xmlnode_get_val(xml_node, "saml-request", &s)) { + int len; + saml_path = openconnect_base64_decode(&len, s); + if (len < 0) { + vpn_progress(vpninfo, PRG_ERR, "Could not decode SAML request as base64: %s\n", s); + free(s); + result = -EINVAL; + goto out; + } + free(s); + saml_path = realloc(saml_path, len+1); + saml_path[len] = '\0'; + } else { + xmlnode_get_val(xml_node, "saml-auth-method", &saml_method); + xmlnode_get_val(xml_node, "authentication-message", &prompt); + xmlnode_get_val(xml_node, "username-label", &username_label); + xmlnode_get_val(xml_node, "password-label", &password_label); + /* XX: should we save the certificate username from ? */ + } + } + + /* XX: Alt-secret form field must be specified for SAML, because we can't autodetect it */ + if ((saml_method || saml_path) && !ctx->alt_secret) { + vpn_progress(vpninfo, PRG_ERR, "SAML authentication via %s to %s is required.\n" + "Must specify destination form field by appending :field_name to login URL.\n", + saml_method, saml_path); + result = -EINVAL; + } + + /* Replace old form */ + free_auth_form(ctx->form); + form = ctx->form = calloc(1, sizeof(*form)); + if (!form) { nomem: free_auth_form(form); - return NULL; + result = -ENOMEM; + goto out; } + if (saml_path && asprintf(&form->banner, _("SAML login is required via %s to this URL:\n\t%s"), saml_method, saml_path) == 0) + goto nomem; + form->message = prompt ? : strdup(_("Please enter your username and password")); + prompt = NULL; + form->auth_id = strdup("_login"); + + /* First field (username) */ + opt = form->opts = calloc(1, sizeof(*opt)); + if (!opt) + goto nomem; opt->name = strdup("user"); - opt->label = strdup(_("Username: ")); - opt->type = username ? OC_FORM_OPT_HIDDEN : OC_FORM_OPT_TEXT; - opt->_value = username; + if (asprintf(&opt->label, "%s: ", username_label ? : _("Username")) == 0) + goto nomem; + if (!ctx->username) + opt->type = OC_FORM_OPT_TEXT; + else { + opt->type = OC_FORM_OPT_HIDDEN; + opt->_value = ctx->username; + ctx->username = NULL; + } + /* Second field (secret) */ opt2 = opt->next = calloc(1, sizeof(*opt)); if (!opt2) goto nomem; - opt2->name = strdup(pw_or_cookie_field ? : "passwd"); - if (asprintf(&opt2->label, "%s: ", auth_id ? _("Challenge") : (pw_or_cookie_field ? : _("Password"))) == 0) - return NULL; + opt2->name = strdup(ctx->alt_secret ? : "passwd"); + if (asprintf(&opt2->label, "%s: ", ctx->alt_secret ? : password_label ? : _("Password")) == 0) + goto nomem; /* XX: Some VPNs use a password in the first form, followed by a * a token in the second ("challenge") form. Others use only a @@ -83,7 +142,19 @@ static struct oc_auth_form *new_auth_form(struct openconnect_info *vpninfo, else opt2->type = OC_FORM_OPT_PASSWORD; - return form; + vpn_progress(vpninfo, PRG_TRACE, "%s%s: \"%s\" %s(%s)=%s, \"%s\" %s(%s)\n", + form->auth_id[0] == '_' ? "Login form" : "Challenge form ", + form->auth_id[0] != '_' ? form->auth_id : "", + opt->label, opt->name, opt->type == OC_FORM_OPT_TEXT ? "TEXT" : "HIDDEN", opt->_value, + opt2->label, opt2->name, opt2->type == OC_FORM_OPT_PASSWORD ? "PASSWORD" : "TOKEN"); + +out: + free(prompt); + free(username_label); + free(password_label); + free(saml_method); + free(saml_path); + return result; } /* Callback function to create a new form from a challenge @@ -91,18 +162,31 @@ static struct oc_auth_form *new_auth_form(struct openconnect_info *vpninfo, */ static int challenge_cb(struct openconnect_info *vpninfo, char *prompt, char *inputStr, void *cb_data) { - struct oc_auth_form **out_form = cb_data; - struct oc_auth_form *form = *out_form; - char *username; + struct login_context *ctx = cb_data; + struct oc_auth_form *form = ctx->form; + struct oc_form_opt *opt = form->opts, *opt2 = form->opts->next; - /* Steal and reuse username from existing form */ - username = form->opts ? form->opts->_value : NULL; - form->opts->_value = NULL; - free_auth_form(form); - form = *out_form = new_auth_form(vpninfo, prompt, inputStr, username, NULL); - if (!form) + /* Replace prompt, inputStr, and password prompt; + * clear password field, and make user field hidden. + */ + free(form->message); + free(form->auth_id); + free(opt2->label); + free(opt2->_value); + opt2->_value = NULL; + opt->type = OC_FORM_OPT_HIDDEN; + + if ( !(form->message = strdup(prompt)) + || !(form->auth_id = strdup(inputStr)) + || !(opt2->label = strdup(_("Challenge: "))) ) return -ENOMEM; + vpn_progress(vpninfo, PRG_TRACE, "%s%s: \"%s\" %s(%s)=%s, \"%s\" %s(%s)\n", + form->auth_id[0] == '_' ? "Login form" : "Challenge form ", + form->auth_id[0] != '_' ? form->auth_id : "", + opt->label, opt->name, opt->type == OC_FORM_OPT_TEXT ? "TEXT" : "HIDDEN", opt->_value, + opt2->label, opt2->name, opt2->type == OC_FORM_OPT_PASSWORD ? "PASSWORD" : "TOKEN"); + return -EAGAIN; } @@ -349,14 +433,12 @@ out: /* Main login entry point * * portal: 0 for gateway login, 1 for portal login - * pw_or_cookie_field: "alternate secret" field (see new_auth_form) + * alt_secret: "alternate secret" field (see new_auth_form) * */ -static int gpst_login(struct openconnect_info *vpninfo, int portal, char *pw_or_cookie_field) +static int gpst_login(struct openconnect_info *vpninfo, int portal, struct login_context *ctx) { - int result; - - struct oc_auth_form *form = NULL; + int result, blind_retry = 0; struct oc_text_buf *request_body = buf_alloc(); const char *request_body_type = "application/x-www-form-urlencoded"; char *xml_buf = NULL, *orig_path; @@ -370,21 +452,29 @@ static int gpst_login(struct openconnect_info *vpninfo, int portal, char *pw_or_ } #endif - /* Ask the user to fill in the auth form; repeat as necessary */ for (;;) { - if (!form) - form = new_auth_form(vpninfo, NULL, NULL, NULL, pw_or_cookie_field); - if (!form) - return -ENOMEM; + /* submit prelogin request to get form */ + orig_path = vpninfo->urlpath; + vpninfo->urlpath = strdup(portal ? "global-protect/prelogin.esp?tmp=tmp&clientVer=4100&clientos=Windows" : "ssl-vpn/prelogin.esp"); + result = do_https_request(vpninfo, "POST", NULL, NULL, &xml_buf, 0); + free(vpninfo->urlpath); + vpninfo->urlpath = orig_path; + + if (result >= 0) + result = gpst_xml_or_error(vpninfo, xml_buf, parse_prelogin_xml, NULL, ctx); + if (result) + goto out; + got_form: /* process auth form */ - result = process_auth_form(vpninfo, form); + result = process_auth_form(vpninfo, ctx->form); if (result) goto out; + replay_form: /* generate token code if specified */ - result = do_gen_tokencode(vpninfo, form); + result = do_gen_tokencode(vpninfo, ctx->form); if (result) { vpn_progress(vpninfo, PRG_ERR, _("Failed to generate OTP tokencode; disabling token\n")); vpninfo->token_bypassed = 1; @@ -399,9 +489,9 @@ static int gpst_login(struct openconnect_info *vpninfo, int portal, char *pw_or_ append_opt(request_body, "computer", vpninfo->localname); if (vpninfo->ip_info.addr) append_opt(request_body, "preferred-ip", vpninfo->ip_info.addr); - if (form->auth_id && form->auth_id[0]!='_') - append_opt(request_body, "inputStr", form->auth_id); - append_form_opts(vpninfo, form, request_body); + if (ctx->form->auth_id && ctx->form->auth_id[0]!='_') + append_opt(request_body, "inputStr", ctx->form->auth_id); + append_form_opts(vpninfo, ctx->form, request_body); if ((result = buf_error(request_body))) goto out; @@ -415,24 +505,38 @@ static int gpst_login(struct openconnect_info *vpninfo, int portal, char *pw_or_ /* Result could be either a JavaScript challenge or XML */ if (result >= 0) result = gpst_xml_or_error(vpninfo, xml_buf, portal ? parse_portal_xml : parse_login_xml, - challenge_cb, &form); - if (result == -EAGAIN) { - /* New form is already populated from the challenge */ - continue; - } else if (result == -EACCES) { - /* Invalid username/password; reuse same form, but blank */ - nuke_opt_values(form->opts); - } else if (portal && result == 0) { - /* Portal login succeeded; now login to gateway */ - portal = 0; - free_auth_form(form); - form = NULL; - } else - break; + challenge_cb, ctx); + if (result == -EACCES) { + /* Invalid username/password; reuse same form, but blank, + * unless we just did a blind retry. + */ + nuke_opt_values(ctx->form->opts); + if (!blind_retry) + goto got_form; + else + blind_retry = 0; + } else { + /* Save successful username */ + if (!ctx->username) + ctx->username = strdup(ctx->form->opts->_value); + if (result == -EAGAIN) { + /* New form is already populated from the challenge */ + goto got_form; + } else if (portal && result == 0) { + /* Portal login succeeded; blindly retry same credentials on gateway, + * unless it was a challenge auth form or alt-secret form. + */ + portal = 0; + if (ctx->form->auth_id[0] == '_' && ctx->alt_secret) { + blind_retry = 1; + goto replay_form; + } + } else + break; + } } out: - free_auth_form(form); buf_free(request_body); free(xml_buf); return result; @@ -440,7 +544,7 @@ out: int gpst_obtain_cookie(struct openconnect_info *vpninfo) { - char *pw_or_cookie_field = NULL; + struct login_context ctx = { .username=NULL, .alt_secret=NULL, .form=NULL }; int result; /* An alternate password/secret field may be specified in the "URL path" (or --usergroup). @@ -449,29 +553,30 @@ int gpst_obtain_cookie(struct openconnect_info *vpninfo) * /gateway:prelogin-cookie */ if (vpninfo->urlpath - && (pw_or_cookie_field = strrchr(vpninfo->urlpath, ':'))!=NULL) { - *pw_or_cookie_field = '\0'; - pw_or_cookie_field++; + && (ctx.alt_secret = strrchr(vpninfo->urlpath, ':')) != NULL) { + *(ctx.alt_secret) = '\0'; + ctx.alt_secret = strdup(ctx.alt_secret+1); } if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "portal") || !strncmp(vpninfo->urlpath, "global-protect", 14))) { /* assume the server is a portal */ - return gpst_login(vpninfo, 1, pw_or_cookie_field); + result = gpst_login(vpninfo, 1, &ctx); } else if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "gateway") || !strncmp(vpninfo->urlpath, "ssl-vpn", 7))) { /* assume the server is a gateway */ - return gpst_login(vpninfo, 0, pw_or_cookie_field); + result = gpst_login(vpninfo, 0, &ctx); } else { /* first try handling it as a gateway, then a portal */ - result = gpst_login(vpninfo, 0, pw_or_cookie_field); + result = gpst_login(vpninfo, 0, &ctx); if (result == -EEXIST) { - /* XX: Don't we want to start by trying the same username/password the user just - entered for the 'gateway' attempt? */ - result = gpst_login(vpninfo, 1, pw_or_cookie_field); + result = gpst_login(vpninfo, 1, &ctx); if (result == -EEXIST) vpn_progress(vpninfo, PRG_ERR, _("Server is neither a GlobalProtect portal nor a gateway.\n")); } - return result; } + free(ctx.username); + free(ctx.alt_secret); + free_auth_form(ctx.form); + return result; } int gpst_bye(struct openconnect_info *vpninfo, const char *reason) diff --git a/gpst.c b/gpst.c index 380e51bc..d0fb0b3b 100644 --- a/gpst.c +++ b/gpst.c @@ -248,6 +248,24 @@ int gpst_xml_or_error(struct openconnect_info *vpninfo, char *response, goto bad_xml; } + /* Is it Error.. ? */ + if (xmlnode_is_named(xml_node, "prelogin-response")) { + char *s = NULL; + int has_err = 0; + xmlNode *x; + for (x=xml_node->children; x; x=x->next) { + if (!xmlnode_get_val(x, "status", &s)) + has_err = strcmp(s, "Success"); + else + xmlnode_get_val(x, "msg", &err); + } + free(s); + if (has_err) + goto out; + free(err); + err = NULL; + } + /* is it user.name...... */ if (xmlnode_is_named(xml_node, "challenge")) { for (xml_node=xml_node->children; xml_node; xml_node=xml_node->next) {