diff --git a/src/config.c b/src/config.c index d423e9db..59dc75cb 100644 --- a/src/config.c +++ b/src/config.c @@ -50,6 +50,7 @@ const struct vpn_config invalid_cfg = { .otp = {'\0'}, .otp_prompt = NULL, .otp_delay = -1, + .token_script = NULL, .no_ftm_push = -1, .pinentry = NULL, .realm = {'\0'}, @@ -266,6 +267,9 @@ int load_config(struct vpn_config *cfg, const char *filename) } else if (strcmp(key, "otp-prompt") == 0) { free(cfg->otp_prompt); cfg->otp_prompt = strdup(val); + } else if (strcmp(key, "2fa-token-script") == 0) { + free(cfg->token_script); + cfg->token_script = strdup(val); } else if (strcmp(key, "otp-delay") == 0) { long otp_delay = strtol(val, NULL, 0); @@ -495,6 +499,7 @@ int load_config(struct vpn_config *cfg, const char *filename) void destroy_vpn_config(struct vpn_config *cfg) { free(cfg->otp_prompt); + free(cfg->token_script); free(cfg->pinentry); free(cfg->cookie); #if HAVE_USR_SBIN_PPPD @@ -538,6 +543,8 @@ void merge_config(struct vpn_config *dst, struct vpn_config *src) strcpy(dst->otp, src->otp); if (src->otp_delay != invalid_cfg.otp_delay) dst->otp_delay = src->otp_delay; + if (src->token_script != invalid_cfg.token_script) + dst->token_script = src->token_script; if (src->no_ftm_push != invalid_cfg.no_ftm_push) dst->no_ftm_push = src->no_ftm_push; if (src->cookie != invalid_cfg.cookie) { diff --git a/src/config.h b/src/config.h index fdf11220..5874198a 100644 --- a/src/config.h +++ b/src/config.h @@ -77,6 +77,7 @@ struct vpn_config { uint16_t saml_port; char saml_session_id[MAX_SAML_SESSION_ID_LENGTH + 1]; char *otp_prompt; + char *token_script; unsigned int otp_delay; int no_ftm_push; char *pinentry; diff --git a/src/http.c b/src/http.c index a2e342f9..7acafccb 100644 --- a/src/http.c +++ b/src/http.c @@ -795,22 +795,69 @@ int auth_log_in(struct tunnel *tunnel) snprintf(tokenparams, sizeof(tokenparams), "ftmpush=1"); } else { if (cfg->otp[0] == '\0') { - // Interactively ask user for 2FA token - char hint[USERNAME_SIZE + 1 + REALM_SIZE + 1 + GATEWAY_HOST_SIZE + 5]; - - sprintf(hint, "%s_%s_%s_2fa", - cfg->username, cfg->realm, cfg->gateway_host); - read_password(cfg->pinentry, hint, - "Two-factor authentication token: ", - cfg->otp, OTP_SIZE); - - if (cfg->otp[0] == '\0') { - log_error("No token specified\n"); - return 0; + if (cfg->token_script != NULL && cfg->token_script[0] != '\0') { + // Use the script to retrieve the 2fa token + FILE *fp_script; + + if (access(cfg->token_script, F_OK) != 0) { + log_error("2FA token script '%s' not found.\n", cfg->token_script); + return 0; + } + + if (access(cfg->token_script, X_OK) != 0) { + log_error("2FA token script '%s' does not have execution permission.\n", cfg->token_script); + return 0; + } + + int status; + + log_info("Executing 2FA token script: %s\n", cfg->token_script); + fp_script = popen(cfg->token_script, "r"); + if (fp_script == NULL) { + log_error("Unable to execute the script.\n"); + return 0; + } + + char *fgets_ret = fgets(cfg->otp, OTP_SIZE, fp_script); + + status = pclose(fp_script); + + if (status == -1) { + log_error("Error while closing script process.\n"); + return 0; + } + + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + log_error("2FA token script failed with exit code %d.\n", WEXITSTATUS(status)); + return 0; + } + + if (fgets_ret != NULL) { + cfg->otp[strcspn(cfg->otp, "\r\n ")] = 0; + log_info("2FA token successfully retrieved from script.\n"); + } else { + log_error("The script terminated successfully but didn't output any token.\n"); + return 0; + } + } else { + // Interactively ask user for 2FA token + char hint[USERNAME_SIZE + 1 + REALM_SIZE + 1 + GATEWAY_HOST_SIZE + 5]; + + sprintf(hint, "%s_%s_%s_2fa", + cfg->username, cfg->realm, cfg->gateway_host); + read_password(cfg->pinentry, hint, + "Two-factor authentication token: ", + cfg->otp, OTP_SIZE); + + if (cfg->otp[0] == '\0') { + log_error("No token specified\n"); + return 0; + } } } url_encode(tokenresponse, cfg->otp); + memset(cfg->otp, '\0', OTP_SIZE + 1); // clear OTP for next run snprintf(tokenparams, sizeof(tokenparams), "code=%s&code2=&magic=%s", tokenresponse, magic); diff --git a/src/main.c b/src/main.c index c374c52d..464e73c1 100644 --- a/src/main.c +++ b/src/main.c @@ -80,6 +80,7 @@ "Usage: openfortivpn [[:]] [-u ] [-p ]\n" \ " [--cookie=] [--cookie-on-stdin] [--saml-login]\n" \ " [--otp=] [--otp-delay=] [--otp-prompt=]\n" \ +" [--2fa-token-script=]\n" \ " [--pinentry=] [--realm=]\n" \ " [--ifname=] [--set-routes=<0|1>]\n" \ " [--half-internet-routes=<0|1>] [--set-dns=<0|1>]\n" \ @@ -122,6 +123,10 @@ PPPD_USAGE \ " -o , --otp= One-Time-Password.\n" \ " --otp-prompt= Search for the OTP prompt starting with this string.\n" \ " --otp-delay= Wait seconds before sending the OTP.\n" \ +" --2fa-token-script= Path to an external script to retrieve the 2FA token.\n" \ +" The script must return 0 exit code and print the token to stdout.\n" \ +" Any output other than the token (e.g. logs) must be printed to stderr.\n" \ +" In case of error it must return a non 0 exit code.\n" \ " --no-ftm-push Do not use FTM push if the server provides the option.\n" \ " --pinentry= Use the program to supply a secret instead of asking for it.\n" \ " --realm= Use specified authentication realm.\n" \ @@ -294,6 +299,7 @@ int main(int argc, char *argv[]) {"otp", required_argument, NULL, 'o'}, {"otp-prompt", required_argument, NULL, 0}, {"otp-delay", required_argument, NULL, 0}, + {"2fa-token-script", required_argument, NULL, 0}, {"no-ftm-push", no_argument, &cli_cfg.no_ftm_push, 1}, {"ifname", required_argument, NULL, 0}, {"set-routes", required_argument, NULL, 0}, @@ -508,6 +514,12 @@ int main(int argc, char *argv[]) cli_cfg.otp_prompt = strdup(optarg); break; } + if (strcmp(long_options[option_index].name, + "2fa-token-script") == 0) { + free(cli_cfg.token_script); + cli_cfg.token_script = strdup(optarg); + break; + } if (strcmp(long_options[option_index].name, "ifname") == 0) { strncpy(cli_cfg.iface_name, optarg, IF_NAMESIZE - 1);