diff --git a/acl/edit_user.cgi b/acl/edit_user.cgi
index 7ee2f0639..5aa2fc8c3 100755
--- a/acl/edit_user.cgi
+++ b/acl/edit_user.cgi
@@ -165,15 +165,6 @@ if ($access{'chcert'}) {
&ui_opt_textbox("cert", $user{'cert'}, 50, $text{'edit_none'}));
}
-# Two-factor details
-if ($user{'twofactor_provider'}) {
- ($prov) = grep { $_->[0] eq $user{'twofactor_provider'} }
- &webmin::list_twofactor_providers();
- print &ui_table_row($text{'edit_twofactor'},
- &text('edit_twofactorprov', "$prov->[1]",
- "$user{'twofactor_id'}"));
- }
-
if ($access{'lang'}) {
# Current language
print &ui_table_row($text{'edit_lang'},
@@ -304,6 +295,15 @@ if ($access{'times'}) {
&ui_textbox("hours_mto", $mt, 2)) ] ]));
}
+# Two-factor details
+if ($user{'twofactor_provider'}) {
+ ($prov) = grep { $_->[0] eq $user{'twofactor_provider'} }
+ &webmin::list_twofactor_providers();
+ print &ui_table_row($text{'edit_twofactor'},
+ &text('edit_twofactorprov', "$prov->[1]",
+ "$user{'twofactor_id'}"));
+ }
+
print &ui_hidden_table_end("security");
# Work out which modules can be selected
diff --git a/acl/twofactor.pl b/acl/twofactor.pl
index 087957094..4605f984a 100755
--- a/acl/twofactor.pl
+++ b/acl/twofactor.pl
@@ -1,7 +1,8 @@
#!/usr/local/bin/perl
# Validate the OTP for some user
-$no_acl_check++;
+$main::no_acl_check = 1;
+$main::no_referers_check = 1;
$ENV{'WEBMIN_CONFIG'} = "/etc/webmin";
$ENV{'WEBMIN_VAR'} = "/var/webmin";
if ($0 =~ /^(.*\/)[^\/]+$/) {
diff --git a/lang/en b/lang/en
index 01c6b883c..d77ad2755 100644
--- a/lang/en
+++ b/lang/en
@@ -133,9 +133,11 @@ session_mesg=You must enter a username and password to login to the Webmin serve
session_mesg2=You must enter a username and password to login.
session_user=Username
session_pass=Password
+session_twofactor=Two-factor token
session_login=Login
session_clear=Clear
session_failed=Login failed. Please try again.
+session_twofailed=Two-factor authentication failed : $1
session_logout=Logout successful. Use the form below to login again.
session_timed_out=Session timed out after $1 minutes of inactivity.
session_save=Remember login permanently?
diff --git a/miniserv.pl b/miniserv.pl
index 52ff18a20..51b182b52 100755
--- a/miniserv.pl
+++ b/miniserv.pl
@@ -1682,6 +1682,15 @@ if ($config{'userfile'}) {
local ($vu, $expired, $nonexist) =
&validate_user($in{'user'}, $in{'pass'}, $host,
$acptip, $port);
+ if ($vu && $twofactor{$vu}) {
+ # Check two-factor token ID
+ $err = &validate_twofactor(
+ $vu, $in{'twofactor'});
+ if ($err) {
+ $twofactor_msg = $err;
+ $vu = undef;
+ }
+ }
local $hrv = &handle_login(
$vu || $in{'user'}, $vu ? 1 : 0,
$expired, $nonexist, $in{'pass'},
@@ -1889,8 +1898,12 @@ if ($config{'userfile'}) {
$querystring = "page=".&urlize($rpage);
}
$method = "GET";
- $querystring .= "&failed=$failed_user" if ($failed_user);
- $querystring .= "&timed_out=$timed_out" if ($timed_out);
+ $querystring .= "&failed=$failed_user"
+ if ($failed_user);
+ $querystring .= "&twofactor_msg=".&urlize($twofactor_msg)
+ if ($twofactor_msg);
+ $querystring .= "&timed_out=$timed_out"
+ if ($timed_out);
$queryargs = "";
$page = $config{'session_login'};
$miniserv_internal = 1;
@@ -4413,6 +4426,9 @@ if (!$config{'webmincron_wrapper'}) {
$config{'webmincron_wrapper'} = $config{'root'}.
"/webmincron/webmincron.pl";
}
+if (!$config{'twofactor_wrapper'}) {
+ $config{'twofactor_wrapper'} = $config{'root'}."/acl/twofactor.pl";
+ }
}
# read_users_file()
@@ -4428,6 +4444,7 @@ undef(%allowhours);
undef(%lastchanges);
undef(%nochange);
undef(%temppass);
+undef(%twofactor);
if ($config{'userfile'}) {
open(USERS, $config{'userfile'});
while() {
@@ -4458,6 +4475,10 @@ if ($config{'userfile'}) {
$lastchanges{$user[0]} = $user[6];
$nochange{$user[0]} = $user[9];
$temppass{$user[0]} = $user[10];
+ if ($user[11] && $user[12]) {
+ $twofactor{$user[0]} = { 'provider' => $user[11],
+ 'id' => $user[12] };
+ }
}
close(USERS);
}
@@ -5671,80 +5692,8 @@ foreach my $cron (@webmincrons) {
"arg0=$cron->{'arg0'}\n";
$webmincron_last{$cron->{'id'}} = $now;
$changed = 1;
- my $pid = fork();
- if (!$pid) {
- # Run via a wrapper command, which we run like a CGI
- dbmclose(%sessiondb);
- open(STDOUT, ">&STDERR");
- &close_all_sockets();
- &close_all_pipes();
- close(LISTEN);
-
- # Setup CGI-like environment
- $envtz = $ENV{"TZ"};
- $envuser = $ENV{"USER"};
- $envpath = $ENV{"PATH"};
- $envlang = $ENV{"LANG"};
- $envroot = $ENV{"SystemRoot"};
- $envperllib = $ENV{'PERLLIB'};
- foreach my $k (keys %ENV) {
- delete($ENV{$k});
- }
- $ENV{"PATH"} = $envpath if ($envpath);
- $ENV{"TZ"} = $envtz if ($envtz);
- $ENV{"USER"} = $envuser if ($envuser);
- $ENV{"OLD_LANG"} = $envlang if ($envlang);
- $ENV{"SystemRoot"} = $envroot if ($envroot);
- $ENV{'PERLLIB'} = $envperllib if ($envperllib);
- $ENV{"HOME"} = $user_homedir;
- $ENV{"SERVER_SOFTWARE"} = $config{"server"};
- $ENV{"SERVER_ADMIN"} = $config{"email"};
- $root0 = $roots[0];
- $ENV{"SERVER_ROOT"} = $root0;
- $ENV{"SERVER_REALROOT"} = $root0;
- $ENV{"SERVER_PORT"} = $config{'port'};
- $ENV{"WEBMIN_CRON"} = 1;
- $ENV{"DOCUMENT_ROOT"} = $root0;
- $ENV{"DOCUMENT_REALROOT"} = $root0;
- $ENV{"MINISERV_CONFIG"} = $config_file;
- $ENV{"HTTPS"} = "ON" if ($use_ssl);
- $ENV{"MINISERV_PID"} = $miniserv_main_pid;
- $ENV{"SCRIPT_FILENAME"} = $config{'webmincron_wrapper'};
- if ($ENV{"SCRIPT_FILENAME"} =~ /^\Q$root0\E(\/.*)$/) {
- $ENV{"SCRIPT_NAME"} = $1;
- }
- $config{'webmincron_wrapper'} =~ /^(.*)\//;
- $ENV{"PWD"} = $1;
- foreach $k (keys %config) {
- if ($k =~ /^env_(\S+)$/) {
- $ENV{$1} = $config{$k};
- }
- }
- chdir($ENV{"PWD"});
- $SIG{'CHLD'} = 'DEFAULT';
- eval {
- # Have SOCK closed if the perl exec's something
- use Fcntl;
- fcntl(SOCK, F_SETFD, FD_CLOEXEC);
- };
-
- # Run the wrapper script by evaling it
- $pkg = "webmincron";
- $0 = $config{'webmincron_wrapper'};
- @ARGV = ( $cron );
- $main_process_id = $$;
- eval "
- \%pkg::ENV = \%ENV;
- package $pkg;
- do \$miniserv::config{'webmincron_wrapper'};
- die \$@ if (\$@);
- ";
- if ($@) {
- print STDERR "Perl cron failure : $@\n";
- }
-
- exit(0);
- }
+ my $pid = &execute_webmin_command($config{'webmincron_wrapper'},
+ [ $cron ]);
push(@childpids, $pid);
}
}
@@ -5965,3 +5914,114 @@ $tmp =~ s/\'/'/g;
$tmp =~ s/=/=/g;
return $tmp;
}
+
+# validate_twofactor(username, token)
+# Checks if a user's two-factor token is valid or not. Returns undef on success
+# or the error message on failure.
+sub validate_twofactor
+{
+my ($user, $token) = @_;
+my $tf = $twofactor{$user};
+$tf || return undef;
+pipe(TOKENr, TOKENw);
+my $pid = &execute_webmin_command($config{'twofactor_wrapper'},
+ [ $user, $tf->{'provider'}, $tf->{'id'}, $token ], TOKENw);
+close(TOKENw);
+waitpid($pid, 0);
+my $ex = $?;
+my $out = ;
+close(TOKENr);
+if ($ex) {
+ return $out || "Unknown two-factor authentication failure";
+ }
+return undef;
+}
+
+# execute_webmin_command(command, &argv, [stdout-fd])
+# Run some Webmin script in a sub-process, like webmincron.pl
+# Returns the PID of the new process.
+sub execute_webmin_command
+{
+my ($cmd, $argv, $fd) = @_;
+my $pid = fork();
+if (!$pid) {
+ # Run via a wrapper command, which we run like a CGI
+ dbmclose(%sessiondb);
+ if ($fd) {
+ open(STDOUT, ">&$fd");
+ }
+ else {
+ open(STDOUT, ">&STDERR");
+ }
+ &close_all_sockets();
+ &close_all_pipes();
+ close(LISTEN);
+
+ # Setup CGI-like environment
+ $envtz = $ENV{"TZ"};
+ $envuser = $ENV{"USER"};
+ $envpath = $ENV{"PATH"};
+ $envlang = $ENV{"LANG"};
+ $envroot = $ENV{"SystemRoot"};
+ $envperllib = $ENV{'PERLLIB'};
+ foreach my $k (keys %ENV) {
+ delete($ENV{$k});
+ }
+ $ENV{"PATH"} = $envpath if ($envpath);
+ $ENV{"TZ"} = $envtz if ($envtz);
+ $ENV{"USER"} = $envuser if ($envuser);
+ $ENV{"OLD_LANG"} = $envlang if ($envlang);
+ $ENV{"SystemRoot"} = $envroot if ($envroot);
+ $ENV{'PERLLIB'} = $envperllib if ($envperllib);
+ $ENV{"HOME"} = $user_homedir;
+ $ENV{"SERVER_SOFTWARE"} = $config{"server"};
+ $ENV{"SERVER_ADMIN"} = $config{"email"};
+ $root0 = $roots[0];
+ $ENV{"SERVER_ROOT"} = $root0;
+ $ENV{"SERVER_REALROOT"} = $root0;
+ $ENV{"SERVER_PORT"} = $config{'port'};
+ $ENV{"WEBMIN_CRON"} = 1;
+ $ENV{"DOCUMENT_ROOT"} = $root0;
+ $ENV{"DOCUMENT_REALROOT"} = $root0;
+ $ENV{"MINISERV_CONFIG"} = $config_file;
+ $ENV{"HTTPS"} = "ON" if ($use_ssl);
+ $ENV{"MINISERV_PID"} = $miniserv_main_pid;
+ $ENV{"SCRIPT_FILENAME"} = $cmd;
+ if ($ENV{"SCRIPT_FILENAME"} =~ /^\Q$root0\E(\/.*)$/) {
+ $ENV{"SCRIPT_NAME"} = $1;
+ }
+ $cmd =~ /^(.*)\//;
+ $ENV{"PWD"} = $1;
+ foreach $k (keys %config) {
+ if ($k =~ /^env_(\S+)$/) {
+ $ENV{$1} = $config{$k};
+ }
+ }
+ chdir($ENV{"PWD"});
+ $SIG{'CHLD'} = 'DEFAULT';
+ eval {
+ # Have SOCK closed if the perl exec's something
+ use Fcntl;
+ fcntl(SOCK, F_SETFD, FD_CLOEXEC);
+ };
+
+ # Run the wrapper script by evaling it
+ if ($cmd =~ /\/([^\/]+)\/([^\/]+)$/) {
+ $pkg = $1;
+ }
+ $0 = $cmd;
+ @ARGV = @$argv;
+ $main_process_id = $$;
+ eval "
+ \%pkg::ENV = \%ENV;
+ package $pkg;
+ do \"$cmd\";
+ die \$@ if (\$@);
+ ";
+ if ($@) {
+ print STDERR "Perl failure : $@\n";
+ }
+ exit(0);
+ }
+return $pid;
+}
diff --git a/session_login.cgi b/session_login.cgi
index e437d14fe..0b8a23c5c 100755
--- a/session_login.cgi
+++ b/session_login.cgi
@@ -46,7 +46,13 @@ if ($tconfig{'inframe'}) {
print "\n";
if (defined($in{'failed'})) {
- print "$text{'session_failed'}
\n";
+ if ($in{'twofactor_msg'}) {
+ print "",&text('session_twofailed',
+ &html_escape($in{'twofactor_msg'})),"
\n";
+ }
+ else {
+ print "$text{'session_failed'}
\n";
+ }
}
elsif ($in{'logout'}) {
print "$text{'session_logout'}
\n";
@@ -80,6 +86,14 @@ print &ui_table_row($text{'session_user'},
&ui_textbox("user", $in{'failed'}, 20, 0, undef, $tags));
print &ui_table_row($text{'session_pass'},
&ui_password("pass", undef, 20, 0, undef, $tags));
+
+# Two-factor token, for users that have it
+if ($miniserv{'twofactor_provider'}) {
+ print &ui_table_row($text{'session_twofactor'},
+ &ui_textbox("twofactor", undef, 20, 0, undef, $tags));
+ }
+
+# Remember session cookie?
if (!$gconfig{'noremember'}) {
print &ui_table_row(" ",
&ui_checkbox("save", 1, $text{'session_save'}, 0));