#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)
{
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 <ccusername/> ? */
+ }
+ }
+
+ /* 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
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
*/
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;
}
/* 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;
}
#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;
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;
/* 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;
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).
* /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)