From caaff31794ed5aa66ffbc8f2cf863e2219349b2b Mon Sep 17 00:00:00 2001 From: Jamie Cameron Date: Fri, 27 Sep 2013 17:02:30 -0700 Subject: [PATCH] Two-factor integration with miniserv --- acl/edit_user.cgi | 18 ++-- acl/twofactor.pl | 3 +- lang/en | 2 + miniserv.pl | 212 +++++++++++++++++++++++++++++----------------- session_login.cgi | 16 +++- 5 files changed, 164 insertions(+), 87 deletions(-) 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));