diff options
author | Byron Jones <glob@mozilla.com> | 2015-09-29 16:57:02 +0200 |
---|---|---|
committer | Byron Jones <glob@mozilla.com> | 2015-09-29 16:57:02 +0200 |
commit | 05fed61671067cb6a750d41909ccb5692ba43808 (patch) | |
tree | 3ed5654e9383df29b48c009f16aed40d26782b1d | |
parent | 87c32cbdf12784dacbbcd9694753ac0e5e02afea (diff) | |
download | bugzilla-05fed61671067cb6a750d41909ccb5692ba43808.tar.gz bugzilla-05fed61671067cb6a750d41909ccb5692ba43808.tar.xz |
Bug 1199090 - add printable recovery 2fa codes
-rw-r--r-- | Bugzilla/Auth.pm | 4 | ||||
-rw-r--r-- | Bugzilla/MFA.pm | 50 | ||||
-rw-r--r-- | Bugzilla/MFA/TOTP.pm | 4 | ||||
-rw-r--r-- | js/account.js | 15 | ||||
-rw-r--r-- | skins/standard/admin.css | 15 | ||||
-rw-r--r-- | skins/standard/global.css | 11 | ||||
-rw-r--r-- | template/en/default/account/prefs/mfa.html.tmpl | 88 | ||||
-rw-r--r-- | template/en/default/mfa/recovery.html.tmpl | 35 | ||||
-rw-r--r-- | template/en/default/mfa/totp/verify.html.tmpl | 2 | ||||
-rwxr-xr-x | token.cgi | 2 | ||||
-rwxr-xr-x | userprefs.cgi | 28 |
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"> @@ -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') { |