summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorByron Jones <glob@mozilla.com>2015-09-29 16:57:02 +0200
committerByron Jones <glob@mozilla.com>2015-09-29 16:57:02 +0200
commit05fed61671067cb6a750d41909ccb5692ba43808 (patch)
tree3ed5654e9383df29b48c009f16aed40d26782b1d
parent87c32cbdf12784dacbbcd9694753ac0e5e02afea (diff)
downloadbugzilla-05fed61671067cb6a750d41909ccb5692ba43808.tar.gz
bugzilla-05fed61671067cb6a750d41909ccb5692ba43808.tar.xz
Bug 1199090 - add printable recovery 2fa codes
-rw-r--r--Bugzilla/Auth.pm4
-rw-r--r--Bugzilla/MFA.pm50
-rw-r--r--Bugzilla/MFA/TOTP.pm4
-rw-r--r--js/account.js15
-rw-r--r--skins/standard/admin.css15
-rw-r--r--skins/standard/global.css11
-rw-r--r--template/en/default/account/prefs/mfa.html.tmpl88
-rw-r--r--template/en/default/mfa/recovery.html.tmpl35
-rw-r--r--template/en/default/mfa/totp/verify.html.tmpl2
-rwxr-xr-xtoken.cgi2
-rwxr-xr-xuserprefs.cgi28
11 files changed, 210 insertions, 44 deletions
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index b39bb827b..50e892914 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -133,8 +133,8 @@ sub mfa_verified {
my $params = Bugzilla->input_params;
$self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new();
- $params->{Bugzilla_restrictlogin} = $event->{restrictlogin};
- $params->{Bugzilla_remember} = $event->{remember};
+ $params->{Bugzilla_restrictlogin} = !!$event->{restrictlogin};
+ $params->{Bugzilla_remember} = !!$event->{remember};
$self->_handle_login_result({ user => $user }, $event->{type});
}
diff --git a/Bugzilla/MFA.pm b/Bugzilla/MFA.pm
index 21f42fbfb..4f0d8a547 100644
--- a/Bugzilla/MFA.pm
+++ b/Bugzilla/MFA.pm
@@ -8,7 +8,9 @@
package Bugzilla::MFA;
use strict;
+use Bugzilla::RNG qw( irand );
use Bugzilla::Token qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token );
+use Bugzilla::Util qw( trick_taint);
sub new {
my ($class, $user) = @_;
@@ -46,15 +48,15 @@ sub verify_prompt {
exit;
}
-sub verify_check {
+sub verify_token {
my ($self, $token) = @_;
# check token
my ($user_id) = Bugzilla::Token::GetTokenData($token);
my $user = Bugzilla::User->check({ id => $user_id, cache => 1 });
- # mfa verification
- $self->check(Bugzilla->input_params);
+ # verify mfa
+ $self->verify_check(Bugzilla->input_params);
# return event data
my $event = get_token_extra_data($token);
@@ -66,10 +68,49 @@ sub verify_check {
return $event;
}
+sub verify_check {
+ my ($self, $params) = @_;
+ $params->{code} //= '';
+
+ # recovery code verification
+ if (length($params->{code}) == 9) {
+ my $code = $params->{code};
+ foreach my $i (1..10) {
+ my $key = "recovery.$i";
+ if (($self->property_get($key) // '') eq $code) {
+ $self->property_delete($key);
+ return;
+ }
+ }
+ }
+
+ # mfa verification
+ $self->check($params);
+}
+
+# methods
+
+sub generate_recovery_codes {
+ my ($self) = @_;
+
+ my @codes;
+ foreach my $i (1..10) {
+ # generate 9 digit code
+ my $code;
+ $code .= irand(10) for 1..9;
+ push @codes, $code;
+ # store (replacing existing)
+ $self->property_set("recovery.$i", $code);
+ }
+
+ return \@codes;
+}
+
# helpers
sub property_get {
my ($self, $name) = @_;
+ trick_taint($name);
return scalar Bugzilla->dbh->selectrow_array(
"SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?",
undef, $self->{user}->id, $name);
@@ -77,6 +118,8 @@ sub property_get {
sub property_set {
my ($self, $name, $value) = @_;
+ trick_taint($name);
+ trick_taint($value);
Bugzilla->dbh->do(
"INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?",
undef, $self->{user}->id, $name, $value, $value);
@@ -84,6 +127,7 @@ sub property_set {
sub property_delete {
my ($self, $name) = @_;
+ trick_taint($name);
Bugzilla->dbh->do(
"DELETE FROM profile_mfa WHERE user_id = ? AND name = ?",
undef, $self->{user}->id, $name);
diff --git a/Bugzilla/MFA/TOTP.pm b/Bugzilla/MFA/TOTP.pm
index 859ca4b8d..64efcfc8d 100644
--- a/Bugzilla/MFA/TOTP.pm
+++ b/Bugzilla/MFA/TOTP.pm
@@ -58,10 +58,10 @@ sub prompt {
sub check {
my ($self, $params) = @_;
- my $code = $params->{code} // '';
+ my $code = $params->{code};
return if $self->_auth()->verify($code, 1);
- if (exists $params->{mfa_action}) {
+ if ($params->{mfa_action} && $params->{mfa_action} eq 'enable') {
ThrowUserError('mfa_totp_bad_enrolment_code');
}
else {
diff --git a/js/account.js b/js/account.js
index 12a6c7a10..84a7da5bf 100644
--- a/js/account.js
+++ b/js/account.js
@@ -93,15 +93,26 @@ $(function() {
$('#mfa-disable')
.click(function(event) {
event.preventDefault();
- $('#mfa-disable-container').show();
+ $('.mfa-api-blurb, #mfa-buttons').hide();
+ $('#mfa-disable-container, #mfa-auth-container').show();
$('#mfa-confirm').show();
- $('.mfa-api-blurb').hide();
$('#mfa-password').focus();
$('#update').attr('disabled', false);
$('.mfa-protected').hide();
$(this).hide();
});
+ $('#mfa-recovery')
+ .click(function(event) {
+ event.preventDefault();
+ $('.mfa-api-blurb, #mfa-buttons').hide();
+ $('#mfa-recovery-container, #mfa-auth-container').show();
+ $('#mfa-password').focus();
+ $('#update').attr('disabled', false).val('Generate Printable Recovery Codes');
+ $('#mfa-action').val('recovery');
+ $(this).hide();
+ });
+
var totp_popup;
$('#mfa-totp-apps, #mfa-totp-text')
.click(function(event) {
diff --git a/skins/standard/admin.css b/skins/standard/admin.css
index 160d51858..134dc2cee 100644
--- a/skins/standard/admin.css
+++ b/skins/standard/admin.css
@@ -270,11 +270,24 @@ input[disabled] {
height: 220px;
}
-#mfa-enroll-embedded {
+#mfa-enroll-embedded, #mfa-recovery-embedded {
background: none;
padding: 0;
}
+#mfa-recovery {
+ margin-top: 4px;
+}
+
+#mfa-recovery-frame {
+ display: block;
+ margin-top: 8px;
+ margin-left: 2em;
+ border: none;
+ width: 300px;
+ height: 200px;
+}
+
label.mfa-totp {
display: inline-block;
width: 155px;
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 3f0ec55a7..81736f052 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -517,7 +517,7 @@ div.user_match {
/* Rules specific for printing */
@media print {
- #header, #footer {
+ #header, #footer, #privacy-policy {
display: none;
}
@@ -529,6 +529,15 @@ div.user_match {
background-image: none;
background-color: #fff;
}
+
+ #prefnav, #prefcontent #update, #prefcontent #message {
+ display: none;
+ }
+
+ #prefcontent {
+ margin-left: 0 !important;
+ box-shadow: none !important;
+ }
}
/**************/
diff --git a/template/en/default/account/prefs/mfa.html.tmpl b/template/en/default/account/prefs/mfa.html.tmpl
index 5aed954f9..df272f7d3 100644
--- a/template/en/default/account/prefs/mfa.html.tmpl
+++ b/template/en/default/account/prefs/mfa.html.tmpl
@@ -23,7 +23,19 @@
[% END %]
<div id="mfa-container">
- [% IF user.mfa %]
+ [% IF mfa_recovery_token %]
+ <input type="hidden" name="mfa_action" id="mfa-action" value="">
+
+ <p>
+ Here are your recovery codes.
+ </p>
+
+ [% INCLUDE recovery_blurb %]
+ <iframe id="mfa-recovery-frame" tabindex="-1"
+ src="userprefs.cgi?tab=mfa&frame=recovery&t=[% mfa_recovery_token FILTER uri %]">
+ </iframe>
+
+ [% ELSIF user.mfa %]
<p>
Two-factor authentication is currently <b>enabled</b> using
<b>[% SWITCH user.mfa %]
@@ -32,30 +44,15 @@
</p>
<input type="hidden" name="mfa_action" id="mfa-action" value="disable">
- <button type="button" id="mfa-disable">Disable Two-factor Authentication</button>
- [% INCLUDE "mfa/protected.html.tmpl" %]
-
- <div id="mfa-disable-container" style="display:none">
-
- <p>
- Your current password and
- [% IF user.mfa == "TOTP" %]
- a TOTP verification code
- [% END %]
- is required to disable two-factor authentication.
- </p>
- <p>
- <label class="mfa-totp">Current Password:</label>
- <input type="password" name="password" id="mfa-password" required>
- </p>
-
- [% IF user.mfa == "TOTP" %]
- <label class="mfa-totp">Code:</label>
- <input type="text" name="code" id="mfa-totp-disable-code"
- placeholder="123456" maxlength="6" pattern="\d{6}" size="10"
- autocomplete="off" required autofocus>
- [% END %]
-
+ <div id="mfa-buttons">
+ <div>
+ <button type="button" id="mfa-disable">Disable Two-factor Authentication</button>
+ [% INCLUDE "mfa/protected.html.tmpl" %]
+ </div>
+ <div>
+ <button type="button" id="mfa-recovery">Generate Printable Recovery Codes</button>
+ [% INCLUDE "mfa/protected.html.tmpl" %]
+ </div>
</div>
<p class="mfa-api-blurb">
@@ -75,6 +72,38 @@
[% END %]
</p>
+ <div id="mfa-recovery-container" style="display:none">
+ <p>
+ Your current password and verification code is required to generate
+ recovery codes.
+ </p>
+ <ul>
+ <li><b>Generating recovery codes obsoletes previously generated ones</b></li>
+ </ul>
+ [% INCLUDE recovery_blurb %]
+ </div>
+
+ <div id="mfa-disable-container" style="display:none">
+ <p>
+ Your current password and verification code is required to disable
+ two-factor authentication.
+ </p>
+ </div>
+
+ <div id="mfa-auth-container" style="display:none">
+ <p>
+ <label class="mfa-totp">Current Password:</label>
+ <input type="password" name="password" id="mfa-password" required>
+ </p>
+
+ [% IF user.mfa == "TOTP" %]
+ <label class="mfa-totp">Code:</label>
+ <input type="text" name="code"
+ placeholder="123456" maxlength="9" pattern="\d{6,9}" size="10"
+ autocomplete="off" required autofocus>
+ [% END %]
+ </div>
+
[% ELSE %]
<p>
Two-factor authentication is currently <b>disabled</b>.
@@ -163,3 +192,12 @@
</div>
</div>
+
+[% BLOCK recovery_blurb %]
+ <ul>
+ <li>These codes can be used in case you lose your second factor</li>
+ <li>Please store them safely in a locked cabinet at home</li>
+ <li>If in doubt, generate and print new recovery codes</li>
+ <li><b>Do not store these codes electronically</b></li>
+ </ul>
+[% END %]
diff --git a/template/en/default/mfa/recovery.html.tmpl b/template/en/default/mfa/recovery.html.tmpl
new file mode 100644
index 000000000..b76d53ae9
--- /dev/null
+++ b/template/en/default/mfa/recovery.html.tmpl
@@ -0,0 +1,35 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% css = BLOCK %]
+
+#codes {
+ font-size: 100%;
+ font-family: monospace;
+}
+
+[% END %]
+
+[%
+ PROCESS global/header.html.tmpl
+ style_urls = ['skins/standard/admin.css']
+ no_body = 1
+ style = css
+%]
+<body id="mfa-recovery-embedded">
+ [% IF codes.size %]
+ <div id="codes">
+ [% FOREACH code IN codes %]
+ [% code FILTER html %]<br>
+ [% END %]
+ </div>
+ [% ELSE %]
+ <i>Codes already generated</i>
+ [% END %]
+</body>
+</html>
diff --git a/template/en/default/mfa/totp/verify.html.tmpl b/template/en/default/mfa/totp/verify.html.tmpl
index e61ee3866..ad75dc6bc 100644
--- a/template/en/default/mfa/totp/verify.html.tmpl
+++ b/template/en/default/mfa/totp/verify.html.tmpl
@@ -22,7 +22,7 @@
<input type="hidden" name="[% field FILTER html %]" value="[% postback.fields.item(field) FILTER html %]">
[% END %]
<input type="text" name="code" id="code"
- placeholder="123456" maxlength="6" pattern="\d{6}" size="10"
+ placeholder="123456" maxlength="9" pattern="\d{6,9}" size="10"
autocomplete="off" required autofocus><br>
<br>
<input type="submit" value="Submit">
diff --git a/token.cgi b/token.cgi
index 26b402159..fd3a38a8e 100755
--- a/token.cgi
+++ b/token.cgi
@@ -474,6 +474,6 @@ sub mfa_event_from_token {
}
# verify
- my $event = $user->mfa_provider->verify_check($token);
+ my $event = $user->mfa_provider->verify_token($token);
return ($user, $event);
}
diff --git a/userprefs.cgi b/userprefs.cgi
index dcb518b80..bf12259fb 100755
--- a/userprefs.cgi
+++ b/userprefs.cgi
@@ -187,7 +187,7 @@ sub MfaAccount {
my $dbh = Bugzilla->dbh;
return unless $user->mfa;
- my $event = $user->mfa_provider->verify_check($cgi->param('mfa_token'));
+ my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token'));
foreach my $action (@{ $event->{actions} }) {
if ($action->{type} eq 'set_login') {
@@ -308,7 +308,7 @@ sub MfaSettings {
my $user = Bugzilla->user;
return unless $user->mfa;
- my $event = $user->mfa_provider->verify_check($cgi->param('mfa_token'));
+ my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token'));
my $settings = $user->settings;
if ($event->{reset}) {
@@ -657,7 +657,7 @@ sub SaveMFA {
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
my $action = $cgi->param('mfa_action') // '';
- return unless $action eq 'enable' || $action eq 'disable';
+ return unless $action eq 'enable' || $action eq 'recovery' || $action eq 'disable';
my $crypt_password = $user->cryptpassword;
if (bz_crypt($cgi->param('password'), $crypt_password) ne $crypt_password) {
@@ -674,8 +674,17 @@ sub SaveMFA {
$settings->{api_key_only}->set('on');
clear_settings_cache(Bugzilla->user->id);
}
+
+ elsif ($action eq 'recovery') {
+ $user->mfa_provider->verify_check(Bugzilla->input_params);
+ my $codes = $user->mfa_provider->generate_recovery_codes();
+ my $token = issue_short_lived_session_token('mfa-recovery');
+ set_token_extra_data($token, $codes);
+ $vars->{mfa_recovery_token} = $token;
+ }
+
else {
- $user->mfa_provider->check(Bugzilla->input_params);
+ $user->mfa_provider->verify_check(Bugzilla->input_params);
$user->set_mfa('');
}
@@ -692,7 +701,14 @@ sub DoMFA {
-Expires => 'Thu, 01 Dec 1994 16:00:00 GMT',
-Pragma => 'no-cache',
);
- if ($provider =~ /^[a-z]+$/) {
+ if ($provider eq 'recovery') {
+ my $token = $cgi->param('t');
+ $vars->{codes} = get_token_extra_data($token);
+ delete_token($token);
+ $template->process("mfa/recovery.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ }
+ elsif ($provider =~ /^[a-z]+$/) {
trick_taint($provider);
$template->process("mfa/$provider/enroll.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
@@ -828,7 +844,7 @@ sub MfaApiKey {
my $dbh = Bugzilla->dbh;
return unless $user->mfa;
- my $event = $user->mfa_provider->verify_check($cgi->param('mfa_token'));
+ my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token'));
foreach my $action (@{ $event->{actions} }) {
if ($action->{type} eq 'create') {