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 @@ static int parse_portal_xml(struct openconnect_info *vpninfo, xmlNode *xml_node, /* 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 @@ static int gpst_login(struct openconnect_info *vpninfo, int portal, char *pw_or_ 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) {