-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add MFA protection to several pages of the PAUSE #455
base: master
Are you sure you want to change the base?
Changes from 1 commit
adc4cbf
78ffb03
d7ed0e7
356fed5
1c6756b
db61391
c28a487
e6f7b1a
34f0879
a3c6f7a
d53a7e4
684f2f0
ca5ea28
efdf931
abb0141
c2472f1
a93739e
0d09dbd
e1549a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package PAUSE::Web::Controller::User::Mfa; | ||
|
||
use Mojo::Base "Mojolicious::Controller"; | ||
use Auth::GoogleAuth; | ||
use PAUSE::Crypt; | ||
use Crypt::URandom qw(urandom); | ||
use Convert::Base32 qw(encode_base32); | ||
|
||
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 $auth = $c->app->pause->authenticator_for($u); | ||
$pause->{mfa_qrcode} = $auth->qr_code; | ||
if (!$u->{mfa_secret32}) { | ||
my $dbh = $mgr->authen_connect; | ||
my $tbl = $PAUSE::Config->{AUTHEN_USER_TABLE}; | ||
my $sql = "UPDATE $tbl SET mfa_secret32 = ?, changed = ?, changedby = ? WHERE user = ?"; | ||
$dbh->do($sql, undef, $auth->secret32, time, $pause->{User}{userid}, $u->{userid}) | ||
or push @{$pause->{ERROR}}, sprintf(qq{Could not enter the data into the database: <i>%s</i>.},$dbh->errstr); | ||
} | ||
|
||
if (uc $req->method eq 'POST' and $req->param("pause99_mfa_sub")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block of code says:
This means that you can supply an empty string in It would probably be better to invert the logic and start with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inverted the logic with 684f2f0 |
||
my $code = $req->param("pause99_mfa_code"); | ||
$req->param("pause99_mfa_code", undef); | ||
if ($code =~ /\A[0-9]{6}\z/ && !$auth->verify($code)) { | ||
$pause->{error}{invalid_code} = 1; | ||
return; | ||
} elsif ($code =~ /\A[a-z0-9]{5}\-[a-z0-9]{5}\z/ && $u->{mfa_recovery_codes} && $req->param("pause99_mfa_reset")) { | ||
my @recovery_codes = split / /, $u->{mfa_recovery_codes} // ''; | ||
if (!grep { PAUSE::Crypt::password_verify($code, $_) } @recovery_codes) { | ||
$pause->{error}{invalid_code} = 1; | ||
return; | ||
} | ||
} | ||
my ($mfa, $secret32, $recovery_codes); | ||
if ($req->param("pause99_mfa_reset")) { | ||
$mfa = 0; | ||
$secret32 = undef; | ||
$recovery_codes = undef; | ||
$c->flash(mfa_disabled => 1); | ||
} else { | ||
$mfa = 1; | ||
$secret32 = $auth->secret32; | ||
$c->flash(mfa_enabled => 1); | ||
my @codes = _generate_recovery_codes(); | ||
$c->flash(recovery_codes => \@codes); | ||
$recovery_codes = join " ", map { PAUSE::Crypt::hash_password($_) } @codes; | ||
} | ||
my $dbh = $mgr->authen_connect; | ||
my $tbl = $PAUSE::Config->{AUTHEN_USER_TABLE}; | ||
my $sql = "UPDATE $tbl SET mfa = ?, mfa_secret32 = ?, mfa_recovery_codes = ?, changed = ?, changedby = ? WHERE user = ?"; | ||
if ($dbh->do($sql, undef, $mfa, $secret32, $recovery_codes, 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); | ||
} | ||
$c->redirect_to('/authenquery?ACTION=mfa'); | ||
} | ||
} | ||
|
||
sub _generate_recovery_codes { | ||
my @codes; | ||
for (1 .. 8) { | ||
my $code = encode_base32(urandom(6)); | ||
$code =~ tr/lo/89/; | ||
$code =~ s/^(.{5})/$1-/; | ||
push @codes, $code; | ||
} | ||
@codes; | ||
} | ||
|
||
1; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
% layout 'layout'; | ||
% my $pause = stash(".pause") || {}; | ||
% my $cpan_alias = lc($pause->{HiddenUser}{userid}) . '@cpan.org'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed $cpan_alias with ca5ea28 |
||
|
||
<input type="hidden" name="HIDDENNAME" value="<%= $pause->{HiddenUser}{userid} %>"> | ||
|
||
% if (flash('mfa_enabled')) { | ||
<div class="messagebox info"> | ||
<p><b>Multifactor Authentication is enabled.</b></p> | ||
<p>Recovery codes:</p> | ||
<code> | ||
<ul> | ||
% for my $code (@{ flash('recovery_codes') }) { | ||
<li><%= $code %> | ||
% } | ||
</ul> | ||
</code> | ||
<p>Please write down these codes, as they will not show again.</p> | ||
</div> | ||
% } elsif (flash('mfa_disabled')) { | ||
<div class="messagebox info"> | ||
<p>Multifactor Authentication is disabled.</p> | ||
</div> | ||
% } | ||
|
||
<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 (my $error = $pause->{error}) { | ||
<div class="messagebox error"> | ||
<b>ERROR</b>: | ||
% if ($error->{invalid_code}) { | ||
Verification Code is invalid. | ||
% } | ||
</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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is 1 short for recovery codes which means you can't use them to disable your mfa like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that true?
So, 6 bytes b32 encoded is 10 bytes. Then There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the size with efdf931 |
||
maxlength => 10, | ||
%> | ||
<input type="submit" name="pause99_mfa_sub" value="Submit"></p> | ||
</div> | ||
|
||
%= csrf_field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow. I don't love this module. qr_code gives a link to quickchart.io which means the url containing the secrets to set up the 2fa are in someone else's web logs. I don't think we should do this.
I think we should construct our own images and inline them, something like:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
_generate_qrcode
with c2472f1There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and modified parameters with 0d09dbd