Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cpanfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
requires 'Apache::Session::Counted';
requires 'Auth::GoogleAuth';
requires 'BSD::Resource';
requires 'CPAN::Checksums', '1.050';
requires 'CPAN::DistnameInfo';
Expand Down
2 changes: 2 additions & 0 deletions doc/authen_pause.schema.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ CREATE TABLE usertable (
`changed` int(11) DEFAULT NULL,
changedby char(10) DEFAULT NULL,
lastvisit datetime DEFAULT NULL,
mfa tinyint(1) DEFAULT 0,
mfa_secret32 varchar(16) DEFAULT NULL,
PRIMARY KEY (`user`),
KEY usertable_password (`password`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 PACK_KEYS=1;
Expand Down
2 changes: 2 additions & 0 deletions lib/pause_2017/PAUSE/Web.pm
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ sub startup {

# Load plugins to modify path/set stash values/provide helper methods
$app->plugin("WithCSRFProtection");
$app->plugin("PAUSE::Web::Plugin::WithMFAProtection");
$app->plugin("PAUSE::Web::Plugin::ConfigPerRequest");
$app->plugin("PAUSE::Web::Plugin::IsPauseClosed");
$app->plugin("PAUSE::Web::Plugin::GetActiveUserRecord");
Expand Down Expand Up @@ -81,6 +82,7 @@ sub startup {
for my $method (qw/get post/) {
my $route = $private->$method("/$name");
$route->with_csrf_protection if $method eq "post" and $action->{x_csrf_protection};
$route->with_mfa_protection if $method eq "post" and $action->{x_mfa_protection};
$route->to($action->{x_mojo_to});
}
}
Expand Down
17 changes: 17 additions & 0 deletions lib/pause_2017/PAUSE/Web/Config.pm
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ our %Actions = (
desc => "Edit your user name, your email addresses (both public and secret one), change the URL of your homepage.",
method => 'POST',
x_csrf_protection => 1,
x_mfa_protection => 1,
x_form => {
HIDDENNAME => {form_type => "hidden_field"},
pause99_edit_cred_fullname => {form_type => "text_field"},
Expand All @@ -456,6 +457,21 @@ our %Actions = (
pause99_edit_cred_sub => {form_type => "submit_button"},
},
},
mfa => {
x_mojo_to => "user-mfa#edit",
verb => "Multifactor Auth",
priv => "user",
cat => "User/06Account/03",
desc => "Multifactor Authentication.",
method => 'POST',
x_csrf_protection => 1,
x_form => {
HIDDENNAME => {form_type => "hidden_field"},
pause99_mfa_code => {form_type => "text_field"},
pause99_mfa_reset => {form_type => "hidden_field"},
pause99_mfa_sub => {form_type => "submit_button"},
},
},
pause_logout => {
x_mojo_to => "user#pause_logout",
verb => "About Logging Out",
Expand Down Expand Up @@ -572,6 +588,7 @@ our @AllowAdminTakeover = qw(
make_dist_comaint
remove_dist_comaint
giveup_dist_comaint
mfa
);

our @AllowMlreprTakeover = qw(
Expand Down
10 changes: 10 additions & 0 deletions lib/pause_2017/PAUSE/Web/Context.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use Email::MIME;
use Data::Dumper;
use PAUSE::Web::Config;
use PAUSE::Web::Exception;
use Auth::GoogleAuth;

our $VERSION = "1072";

Expand Down Expand Up @@ -40,6 +41,15 @@ sub version {
$version;
}

sub authenticator_for {
my ($self, $cpan_alias) = @_;
return Auth::GoogleAuth->new({
secret => $PAUSE::Config->{MFA_SECRET} || 'PAUSE MFA',
issuer => 'PAUSE',
key_id => $cpan_alias,
});
}

sub hostname {
my $self = shift;
$PAUSE::Config->{SERVER_NAME} || Sys::Hostname::hostname();
Expand Down
49 changes: 49 additions & 0 deletions lib/pause_2017/PAUSE/Web/Controller/User/Mfa.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package PAUSE::Web::Controller::User::Mfa;

use Mojo::Base "Mojolicious::Controller";
use Auth::GoogleAuth;

sub edit {
my $c = shift;
my $pause = $c->stash(".pause");
my $mgr = $c->app->pause;
my $req = $c->req;
my $u = $c->active_user_record;

my $cpan_alias = lc($u->{userid}) . '@cpan.org';
my $auth = $c->app->pause->authenticator_for($cpan_alias);
$pause->{mfa_qrcode} = $auth->qr_code;

if (uc $req->method eq 'POST' and $req->param("pause99_mfa_sub")) {
my $code = $req->param("pause99_mfa_code");
$req->param("pause99_mfa_code", undef);
if (!$auth->verify($code)) {
$pause->{error}{invalid_code} = 1;
return;
}
my ($mfa, $secret32);
if ($req->param("pause99_mfa_reset")) {
$mfa = 0;
$secret32 = undef;
$pause->{mfa_disabled} = 1;
} else {
$mfa = 1;
$secret32 = $auth->secret32;
$pause->{mfa_enabled} = 1;
}
my $dbh = $mgr->authen_connect;
my $tbl = $PAUSE::Config->{AUTHEN_USER_TABLE};
my $sql = "UPDATE $tbl SET mfa = ?, mfa_secret32 = ?, changed = ?, changedby = ? WHERE user = ?";
if ($dbh->do($sql, undef, $mfa, $secret32, time, $pause->{User}{userid}, $u->{userid})) {
my $mailblurb = $c->render_to_string("email/user/mfa/edit", format => "email");
my $header = {Subject => "User update for $u->{userid}"};
my @to = $u->{secretemail};
$mgr->send_mail_multi(\@to, $header, $mailblurb);
} else {
push @{$pause->{ERROR}}, sprintf(qq{Could not enter the data
into the database: <i>%s</i>.},$dbh->errstr);
}
}
}

1;
51 changes: 51 additions & 0 deletions lib/pause_2017/PAUSE/Web/Plugin/WithMFAProtection.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package PAUSE::Web::Plugin::WithMFAProtection;

use Mojo::Base 'Mojolicious::Plugin';

our $VERSION = '1.00';

sub register {
my ( $self, $app ) = @_;

my $routes = $app->routes;

$app->helper(
'reply.bad_mfa_code' => sub {
my ($c) = @_;
$c->res->code(403);
$c->render_maybe('bad_mfa_code')
or $c->render( text => 'Failed MFA check' );
return;
}
);

$routes->add_condition(
with_mfa_protection => sub {
my ( $route, $c ) = @_;

my $code = $c->param('mfa_code');
my $u = $c->active_user_record;
return 1 unless $u->{mfa};

my $cpan_alias = lc($u->{userid}) . '@cpan.org';

unless ( $code && $c->app->pause->authenticator_for($cpan_alias)->verify($code) ) {
$c->reply->bad_mfa_code;
return;
}

return 1;
}
);

$routes->add_shortcut(
with_mfa_protection => sub {
my ($route) = @_;
return $route->requires( with_mfa_protection => 1 );
}
);

return;
}

1;
19 changes: 19 additions & 0 deletions lib/pause_2017/templates/email/user/mfa/edit.email.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
% my $pause = stash(".pause") || {};
%
%#------------------------------------------------------------------
%
Record update in the PAUSE users database:

<%== sprintf "%11s: [%s]", "userid", $pause->{HiddenUser}{userid} %>

% if ($pause->{mfa_enabled}) {
Multifactor Authentication is enabled.
% } elsif ($pause->{mfa_disabled}) {
Multifactor Authentication is disabled.
% }

Data were entered by <%== $pause->{User}{userid} %> (<%== $pause->{User}{fullname} %>).
Please check if they are correct.

Thanks,
The PAUSE Team
5 changes: 5 additions & 0 deletions lib/pause_2017/templates/user/cred/edit.html.ep
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,9 @@ Account can be removed

</table>
%= csrf_field
% if ($pause->{HiddenUser}{mfa}) {
<p>MFA:
%= text_field 'mfa_code'
</p>
% }
<input type="submit" name="pause99_edit_cred_sub" value="Submit">
54 changes: 54 additions & 0 deletions lib/pause_2017/templates/user/mfa/edit.html.ep
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
% layout 'layout';
% my $pause = stash(".pause") || {};
% my $cpan_alias = lc($pause->{HiddenUser}{userid}) . '@cpan.org';

<input type="hidden" name="HIDDENNAME" value="<%= $pause->{HiddenUser}{userid} %>">


<h3><% if (!$pause->{HiddenUser}{mfa}) { %>Enable<% } else { %>Disable<% } %> Multifactor Authentication for <%= $pause->{HiddenUser}{userid} %>
% if (exists $pause->{UserGroups}{admin}) {
(lastvisit <%= $pause->{HiddenUser}{lastvisit} || "before 2005-12-02" %>)
% }
</h3>

% if (param("pause99_mfa_sub")) {
% if (my $error = $pause->{error}) {
<div class="messagebox error">
<b>ERROR</b>:
% if ($error->{invalid_code}) {
Verification Code is invalid.
% }
</div>
<hr>
% } elsif ($pause->{mfa_enabled}) {
<div class="messagebox info">
Multifactor Authentication is enabled.
</div>
<hr>
% } elsif ($pause->{mfa_disabled}) {
<div class="messagebox info">
Multifactor Authentication is disabled.
</div>
<hr>
% }
% }

% if (!$pause->{HiddenUser}{mfa}) {
<div>
<p>Submit 6-digit code to enable Multifactor Authentication.</p>
<img src="<%= $pause->{mfa_qrcode} %>">
</div>
% } else {
<p>Submit 6-digit code to disable Multifactor Authentication.</p>
<%= hidden_field "pause99_mfa_reset" => 1 %>
% }

<div>
<p>CODE: <%= text_field "pause99_mfa_code" => '',
size => 10,
maxlength => 10,
%>
<input type="submit" name="pause99_mfa_sub" value="Submit"></p>
</div>

%= csrf_field