-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathMfa.pm
More file actions
95 lines (88 loc) · 3.39 KB
/
Mfa.pm
File metadata and controls
95 lines (88 loc) · 3.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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);
use Imager::QRCode qw(plot_qrcode);
use URI;
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} = _generate_qrcode($auth);
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")) {
my $code = $req->param("pause99_mfa_code");
$req->param("pause99_mfa_code", undef);
my $verified;
if ($code =~ /\A[0-9]{6}\z/ && $auth->verify($code)) {
$verified = 1;
} 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) {
$verified = 1;
}
}
unless ($verified) {
$pause->{error}{invalid_code} = 1;
return;
}
my ($secret32, $recovery_codes);
if ($req->param("pause99_mfa_reset")) {
$secret32 = undef;
$recovery_codes = undef;
$c->flash(mfa_disabled => 1);
} else {
$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_secret32 = ?, mfa_recovery_codes = ?, changed = ?, changedby = ? WHERE user = ?";
if ($dbh->do($sql, undef, $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;
}
# using $auth->qr_code directly is handy but insecure
sub _generate_qrcode {
my $auth = shift;
my $otpauth = $auth->qr_code(undef, undef, undef, 1);
my $img = plot_qrcode($otpauth, { casesensitive => 1, size => 4, margin => 4, version => 1, level => 'M' });
$img->write(data => \my $qr_png, type => 'png') or die "Failed to write image: " . $img->errstr;
my $data = URI->new("data:");
$data->data($qr_png);
$data->media_type('image/png');
$data;
}
1;