diff --git a/acl/index.cgi b/acl/index.cgi index 232aa198e..f11584a95 100755 --- a/acl/index.cgi +++ b/acl/index.cgi @@ -188,11 +188,9 @@ if (uc($ENV{'HTTPS'}) eq "ON" && $miniserv{'ca'}) { push(@links, "cert_form.cgi"); push(@titles, $text{'index_cert'}); } -if ($miniserv{'twofactor_provider'}) { - push(@icons, "images/twofactor.gif"); - push(@links, "twofactor_form.cgi"); - push(@titles, $text{'index_twofactor'}); - } +push(@icons, "images/twofactor.gif"); +push(@links, "twofactor_form.cgi"); +push(@titles, $text{'index_twofactor'}); if ($access{'rbacenable'}) { push(@icons, "images/rbac.gif"); push(@links, "edit_rbac.cgi"); diff --git a/acl/lang/en b/acl/lang/en index 37174e3ba..a248e8b38 100644 --- a/acl/lang/en +++ b/acl/lang/en @@ -466,6 +466,7 @@ twofactor_enable=Enroll For Two-Factor Authentication twofactor_header=Two-factor authentication enrollment details twofactor_enrolling=Enrolling for two-factor authentication with provider $1 .. twofactor_failed=.. enrollment failed : $1 -twofactor_done=.. complete. Your ID with this provider is $1. +twofactor_done=.. complete. Your ID with this provider is $1. +twofactor_setup=Two-factor authentication has not been enabled on this system yet, but can be turned on using the Webmin Configuration module. __norefs=1 diff --git a/acl/save_twofactor.cgi b/acl/save_twofactor.cgi index c72ba9fd8..f02f0f024 100644 --- a/acl/save_twofactor.cgi +++ b/acl/save_twofactor.cgi @@ -13,9 +13,12 @@ $user || &error($twxt{'twofactor_euser'}); if ($in{'enable'}) { # Validate enrollment inputs - $vfunc = "webmin::parse_twofactor_form_".$miniserv{'twofactor_provider'}; - $details = &$vfunc(\%in, $user); - &error($details) if (!ref($details)); + $vfunc = "webmin::parse_twofactor_form_". + $miniserv{'twofactor_provider'}; + if (defined(&$vfunc)) { + $details = &$vfunc(\%in, $user); + &error($details) if (!ref($details)); + } &ui_print_header(undef, $text{'twofactor_title'}, ""); ($prov) = grep { $_->[0] eq $miniserv{'twofactor_provider'} } @@ -32,6 +35,12 @@ if ($in{'enable'}) { else { print &text('twofactor_done', $user->{'twofactor_id'}),"

\n"; + # Print provider-specific message + $mfunc = "webmin::message_twofactor_".$miniserv{'twofactor_provider'}; + if (defined(&$mfunc)) { + print &$mfunc($user); + } + # Save user $user->{'twofactor_provider'} = $miniserv{'twofactor_provider'}; &modify_user($user->{'name'}, $user); diff --git a/acl/twofactor_form.cgi b/acl/twofactor_form.cgi index 05c009083..f7f337e54 100644 --- a/acl/twofactor_form.cgi +++ b/acl/twofactor_form.cgi @@ -6,6 +6,12 @@ require './acl-lib.pl'; &error_setup($text{'twofactor_err'}); &get_miniserv_config(\%miniserv); +if (!$miniserv{'twofactor_provider'}) { + &ui_print_endpage(&text('twofactor_setup', + '../webmin/edit_twofactor.cgi')); + return; + } + # Get the user ($user) = grep { $_->{'name'} eq $base_remote_user } &list_users(); $user || &error($twxt{'twofactor_euser'}); @@ -24,10 +30,12 @@ else { ($prov) = grep { $_->[0] eq $miniserv{'twofactor_provider'} } &webmin::list_twofactor_providers(); print &text('twofactor_desc', $prov->[1], $prov->[2]),"

\n"; - print &ui_table_start($text{'twofactor_header'}, undef, 2); $ffunc = "webmin::show_twofactor_form_".$miniserv{'twofactor_provider'}; - print &$ffunc($user); - print &ui_table_end(); + if (defined(&$ffunc)) { + print &ui_table_start($text{'twofactor_header'}, undef, 2); + print &$ffunc($user); + print &ui_table_end(); + } @buts = ( [ "enable", $text{'twofactor_enable'} ] ); } print &ui_form_end(\@buts); diff --git a/webmin/change_twofactor.cgi b/webmin/change_twofactor.cgi index 2925d8a75..578b061a1 100644 --- a/webmin/change_twofactor.cgi +++ b/webmin/change_twofactor.cgi @@ -26,7 +26,11 @@ $msg = $text{'restart_done'}."

\n"; if ($in{'twofactor_provider'}) { $msg .= &text('twofactor_enrolllink', "../acl/twofactor_form.cgi")."

\n"; - if ($prov->[2]) { + $mfunc = "message_twofactor_apikey_".$in{'twofactor_provider'}; + if (defined(&$mfunc)) { + $msg .= &$mfunc(\%miniserv)."

\n"; + } + elsif ($prov->[2]) { $msg .= &text('twofactor_url', $prov->[1], $prov->[2])."

\n"; } } diff --git a/webmin/lang/en b/webmin/lang/en index 774e23874..76d1664a0 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -1047,5 +1047,11 @@ twofactor_eauthytoken=Authy token must be a number twofactor_eauthyotp=Authy token is invalid twofactor_enrolllink=You can now enroll for two-factor authentication in the Webmin Users module. twofactor_url=To learn more about $1, see it's website at $2. +twofactor_etoptmodule=The Perl module $1 needed for two-factor authentication is not installed. Use the Perl Modules page in Webmin to install it. +twofactor_qrcode=Enter the secret code $1 in the Google Authenticator app, or scan the QR code below. +twofactor_etoptid=Invalid TOPT base32-encoded secret +twofactor_etotptoken=Google Authenticator token must be a number +twofactor_etoptmodule2=Missing Perl module $1 +twofactor_etoptmatch=Incorrect OTP code __norefs=1 diff --git a/webmin/webmin-lib.pl b/webmin/webmin-lib.pl index 26e38c14c..33e370834 100755 --- a/webmin/webmin-lib.pl +++ b/webmin/webmin-lib.pl @@ -2186,8 +2186,8 @@ return $rv; } # validate_twofactor_apikey_authy(&in, &miniserv) -# Validates inputs from show_twofactor_apikey_authy, and stores them. Returns undef -# if OK, or an error message on failure +# Validates inputs from show_twofactor_apikey_authy, and stores them. Returns +# undef if OK, or an error message on failure sub validate_twofactor_apikey_authy { my ($in, $miniserv) = @_; @@ -2320,16 +2320,67 @@ else { } # validate_twofactor_apikey_topt() -# Checks that the needed Perl module for TOPT is installed +# Checks that the needed Perl module for TOPT is installed. sub validate_twofactor_apikey_topt { -# XXX +my ($miniserv, $in) = @_; +eval "use Authen::OATH"; +if ($@) { + return &text('twofactor_etoptmodule', 'Authen::OATH', + "../cpan/download.cgi?source=3&cpan=Authen::OATH&mode=2&". + "return=/$module_name/&returndesc=".&urlize($text{'index_return'})) + } +return undef; } -# show_twofactor_form_topt() -# XXX is this even needed? -sub show_twofactor_form_topt +# enroll_twofactor_topt(&in, &user) +# Generate a secret for this user, based-32 encoded +sub enroll_twofactor_topt { +my ($in, $user) = @_; +&seed_random(); +my $secret = ""; +while(length($secret) < 10) { + $secret .= chr(rand()*256); + } +$user->{'twofactor_id'} = &encode_base32($secret); +return undef; } +# message_twofactor_topt(&user) +# Returns HTML to display after a user enrolls +sub message_twofactor_topt +{ +my ($user) = @_; +my $url = "https://chart.googleapis.com/chart". + "?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/". + $user->{'name'}."%3Fsecret%3D".$user->{'twofactor_id'}; +my $rv; +$rv .= &text('twofactor_qrcode', "$user->{'twofactor_id'}")."

\n"; +$rv .= "

\n"; +return $rv; +} + +# validate_twofactor_totp(id, token, apikey) +# Checks the validity of some token with google authenticator +sub validate_twofactor_totp +{ +my ($id, $token, $apikey) = @_; +$id =~ /^[A-Z0-9=]+$/ || return $text{'twofactor_etoptid'}; +$token =~ /^\d+$/ || return $text{'twofactor_etotptoken'}; +eval "use Authen::OATH"; +if ($@) { + return &text('twofactor_etoptmodule2', 'Authen::OATH'); + } +my $secret = &decode_base32($id); +my $oauth = Authen::OATH->new(); +my $now = time(); +foreach my $t ($now - 30, $now, $now + 30) { + my $expected = $oauth->totp($secret, $t); + return undef if ($expected eq $token); + } +return $text{'twofactor_etoptmatch'}; +} + + 1;