From d69cebd8c703f0a1f6839944f1c949bce350b02e Mon Sep 17 00:00:00 2001 From: Byron Jones Date: Tue, 13 Oct 2015 00:49:00 +0800 Subject: Bug 1199089 - add support for duo-security --- Bugzilla/Config.pm | 6 + Bugzilla/Config/Auth.pm | 21 +++ Bugzilla/DuoWeb.pm | 193 +++++++++++++++++++++ Bugzilla/Install/Requirements.pm | 2 +- Bugzilla/MFA.pm | 28 ++- Bugzilla/MFA/Dummy.pm | 26 +++ Bugzilla/MFA/Duo.pm | 53 ++++++ Bugzilla/MFA/TOTP.pm | 8 +- Bugzilla/User.pm | 10 +- Bugzilla/WebService/User.pm | 2 +- images/duo.png | Bin 0 -> 5188 bytes js/account.js | 21 ++- js/duo-min.js | 32 ++++ skins/standard/admin.css | 14 +- t/Support/Files.pm | 8 + template/en/default/account/prefs/mfa.html.tmpl | 72 ++++++-- template/en/default/admin/params/auth.html.tmpl | 28 ++- template/en/default/admin/users/userdata.html.tmpl | 2 + template/en/default/global/user-error.html.tmpl | 2 +- template/en/default/mfa/dummy/verify.html.tmpl | 28 +++ template/en/default/mfa/duo/verify.html.tmpl | 95 ++++++++++ template/en/default/mfa/totp/enroll.html.tmpl | 2 - userprefs.cgi | 82 +++++++-- 23 files changed, 684 insertions(+), 51 deletions(-) create mode 100644 Bugzilla/DuoWeb.pm create mode 100644 Bugzilla/MFA/Dummy.pm create mode 100644 Bugzilla/MFA/Duo.pm create mode 100644 images/duo.png create mode 100644 js/duo-min.js create mode 100644 template/en/default/mfa/dummy/verify.html.tmpl create mode 100644 template/en/default/mfa/duo/verify.html.tmpl diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 3e9b793a6..7cc6c5dcb 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -216,6 +216,12 @@ sub update_params { } } + # Generate unique Duo integration secret key + if ($param->{duo_akey} eq '') { + require Bugzilla::Util; + $param->{duo_akey} = Bugzilla::Util::generate_random_password(40); + } + $param->{'utf8'} = 1 if $new_install; # --- REMOVE OLD PARAMS --- diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 217805bea..36287b107 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -148,6 +148,27 @@ sub get_param_list { type => 'b', default => 0, }, + + { + name => 'duo_host', + type => 't', + default => '', + }, + { + name => 'duo_akey', + type => 't', + default => '', + }, + { + name => 'duo_ikey', + type => 't', + default => '', + }, + { + name => 'duo_skey', + type => 't', + default => '', + }, ); return @param_list; } diff --git a/Bugzilla/DuoWeb.pm b/Bugzilla/DuoWeb.pm new file mode 100644 index 000000000..4fb28df9e --- /dev/null +++ b/Bugzilla/DuoWeb.pm @@ -0,0 +1,193 @@ +# https://github.com/duosecurity/duo_perl +# +# Copyright (c) 2012, Duo Security, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package Bugzilla::DuoWeb; + +use strict; +use warnings; + +use MIME::Base64; +use Digest::HMAC_SHA1 qw(hmac_sha1_hex); + +my $DUO_PREFIX = 'TX'; +my $APP_PREFIX = 'APP'; +my $AUTH_PREFIX = 'AUTH'; + +my $DUO_EXPIRE = 300; +my $APP_EXPIRE = 3600; + +my $IKEY_LEN = 20; +my $SKEY_LEN = 40; +my $AKEY_LEN = 40; + +our $ERR_USER = 'ERR|The username passed to sign_request() is invalid.'; +our $ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.'; +our $ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.'; +our $ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters."; +our $ERR_UNKNOWN = 'ERR|An unknown error has occurred.'; + + +sub _sign_vals { + my ($key, $vals, $prefix, $expire) = @_; + + my $exp = time + $expire; + + my $val = join '|', @{$vals}, $exp; + my $b64 = encode_base64($val, ''); + my $cookie = "$prefix|$b64"; + + my $sig = hmac_sha1_hex($cookie, $key); + + return "$cookie|$sig"; +} + + +sub _parse_vals { + my ($key, $val, $prefix, $ikey) = @_; + + my $ts = time; + + if (not defined $val) { + return ''; + } + + my @parts = split /\|/, $val; + if (scalar(@parts) != 3) { + return ''; + } + my ($u_prefix, $u_b64, $u_sig) = @parts; + + my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key); + + if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) { + return ''; + } + + if ($u_prefix ne $prefix) { + return ''; + } + + my @cookie_parts = split /\|/, decode_base64($u_b64); + if (scalar(@cookie_parts) != 3) { + return ''; + } + my ($user, $u_ikey, $exp) = @cookie_parts; + + if ($u_ikey ne $ikey) { + return ''; + } + + if ($ts >= $exp) { + return ''; + } + + return $user; +} + +=pod + Generate a signed request for Duo authentication. + The returned value should be passed into the Duo.init() call! + in the rendered web page used for Duo authentication. + + Arguments: + + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + username -- Primary-authenticated username +=cut + +sub sign_request { + my ($ikey, $skey, $akey, $username) = @_; + + if (not $username) { + return $ERR_USER; + } + + if (index($username, '|') != -1) { + return $ERR_USER; + } + + if (not $ikey or length $ikey != $IKEY_LEN) { + return $ERR_IKEY; + } + + if (not $skey or length $skey != $SKEY_LEN) { + return $ERR_SKEY; + } + + if (not $akey or length $akey < $AKEY_LEN) { + return $ERR_AKEY; + } + + my $vals = [ $username, $ikey ]; + + my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE); + my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE); + + if (not $duo_sig or not $app_sig) { + return $ERR_UNKNOWN; + } + + return "$duo_sig:$app_sig"; +} + +=pod + + Validate the signed response returned from Duo. + + Returns the username of the authenticated user, or '' (empty + string) if secondary authentication was denied. + + Arguments: + + ikey -- Duo integration key + skey -- Duo secret key + akey -- Application secret key + sig_response -- The signed response POST'ed to the server + +=cut + +sub verify_response { + my ($ikey, $skey, $akey, $sig_response) = @_; + + if (not defined $sig_response) { + return ''; + } + + my ($auth_sig, $app_sig) = split /:/, $sig_response; + my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey); + my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey); + + if ($auth_user ne $app_user) { + return ''; + } + + return $auth_user; +} +1; diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index e653e5f8c..bfd7e7bfa 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -448,7 +448,7 @@ use constant FEATURE_FILES => ( patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], updates => ['Bugzilla/Update.pm'], memcached => ['Bugzilla/Memcache.pm'], - mfa => ['Bugzilla/MFA/TOTP.pm'], + mfa => ['Bugzilla/MFA/*.pm'], ); # This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff diff --git a/Bugzilla/MFA.pm b/Bugzilla/MFA.pm index 4f0d8a547..868a75a7e 100644 --- a/Bugzilla/MFA.pm +++ b/Bugzilla/MFA.pm @@ -10,18 +10,38 @@ 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); +use Bugzilla::Util qw( trick_taint ); sub new { my ($class, $user) = @_; return bless({ user => $user }, $class); } +sub new_from { + my ($class, $user, $mfa) = @_; + $mfa //= ''; + if ($mfa eq 'TOTP') { + require Bugzilla::MFA::TOTP; + return Bugzilla::MFA::TOTP->new($user); + } + elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) { + require Bugzilla::MFA::Duo; + return Bugzilla::MFA::Duo->new($user); + } + else { + require Bugzilla::MFA::Dummy; + return Bugzilla::MFA::Dummy->new($user); + } +} + # abstract methods -# api call, returns required data to user-prefs enrollment page +# called during enrollment sub enroll {} +# api call, returns required data to user-prefs enrollment page +sub enroll_api {} + # called after the user has confirmed enrollment sub enrolled {} @@ -31,6 +51,10 @@ sub prompt {} # throws errors if code is invalid sub check {} +# if true verifcation can happen inline (during enrollment/pref changes) +# if false then the mfa provider requires an intermediate verification page +sub can_verify_inline { 0 } + # verification sub verify_prompt { diff --git a/Bugzilla/MFA/Dummy.pm b/Bugzilla/MFA/Dummy.pm new file mode 100644 index 000000000..d91f7ae42 --- /dev/null +++ b/Bugzilla/MFA/Dummy.pm @@ -0,0 +1,26 @@ +# 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. + +package Bugzilla::MFA::Dummy; +use strict; +use parent 'Bugzilla::MFA'; + +# if a user is configured to use a disabled or invalid mfa provider, we return +# this dummy provider. +# +# it provides no 2fa protection at all, but prevents crashing. + +sub prompt { + my ($self, $vars) = @_; + my $template = Bugzilla->template; + + print Bugzilla->cgi->header(); + $template->process('mfa/dummy/verify.html.tmpl', $vars) + || ThrowTemplateError($template->error()); +} + +1; diff --git a/Bugzilla/MFA/Duo.pm b/Bugzilla/MFA/Duo.pm new file mode 100644 index 000000000..4c9aa1184 --- /dev/null +++ b/Bugzilla/MFA/Duo.pm @@ -0,0 +1,53 @@ +# 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. + +package Bugzilla::MFA::Duo; +use strict; +use parent 'Bugzilla::MFA'; + +use Bugzilla::DuoWeb; +use Bugzilla::Error; + +sub can_verify_inline { + return 0; +} + +sub enroll { + my ($self, $params) = @_; + + $self->property_set('user', $params->{username}); +} + +sub prompt { + my ($self, $vars) = @_; + my $template = Bugzilla->template; + + $vars->{sig_request} = Bugzilla::DuoWeb::sign_request( + Bugzilla->params->{duo_ikey}, + Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_akey}, + $self->property_get('user'), + ); + + print Bugzilla->cgi->header(); + $template->process('mfa/duo/verify.html.tmpl', $vars) + || ThrowTemplateError($template->error()); +} + +sub check { + my ($self, $params) = @_; + + return if Bugzilla::DuoWeb::verify_response( + Bugzilla->params->{duo_ikey}, + Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_akey}, + $params->{sig_response} + ); + ThrowUserError('mfa_bad_code'); +} + +1; diff --git a/Bugzilla/MFA/TOTP.pm b/Bugzilla/MFA/TOTP.pm index 64efcfc8d..36791da15 100644 --- a/Bugzilla/MFA/TOTP.pm +++ b/Bugzilla/MFA/TOTP.pm @@ -16,6 +16,10 @@ use Bugzilla::Util qw( template_var generate_random_password ); use GD::Barcode::QRcode; use MIME::Base64 qw( encode_base64 ); +sub can_verify_inline { + return 1; +} + sub _auth { my ($self) = @_; return Auth::GoogleAuth->new({ @@ -25,7 +29,7 @@ sub _auth { }); } -sub enroll { +sub enroll_api { my ($self) = @_; # create a new secret for the user @@ -65,7 +69,7 @@ sub check { ThrowUserError('mfa_totp_bad_enrolment_code'); } else { - ThrowUserError('mfa_totp_bad_code'); + ThrowUserError('mfa_bad_code'); } } diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 6678e6171..d2de6b548 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -368,6 +368,7 @@ sub _check_mfa { my ($self, $provider) = @_; $provider = lc($provider // ''); return 'TOTP' if $provider eq 'totp'; + return 'Duo' if $provider eq 'duo'; return ''; } @@ -586,13 +587,8 @@ sub mfa_provider { my ($self) = @_; my $mfa = $self->{mfa} || return undef; return $self->{mfa_provider} if exists $self->{mfa_provider}; - if ($mfa eq 'TOTP') { - require Bugzilla::MFA::TOTP; - $self->{mfa_provider} = Bugzilla::MFA::TOTP->new($self); - } - else { - $self->{mfa_provider} = undef; - } + require Bugzilla::MFA; + $self->{mfa_provider} = Bugzilla::MFA->new_from($self, $mfa); return $self->{mfa_provider}; } diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm index 5812fbed2..a9dcdf4be 100644 --- a/Bugzilla/WebService/User.pm +++ b/Bugzilla/WebService/User.pm @@ -428,7 +428,7 @@ sub mfa_enroll { my $user = Bugzilla->login(LOGIN_REQUIRED); $user->set_mfa($provider_name); my $provider = $user->mfa_provider // die "Unknown MTA provider\n"; - return $provider->enroll(); + return $provider->enroll_api(); } sub whoami { diff --git a/images/duo.png b/images/duo.png new file mode 100644 index 000000000..4b81f82ef Binary files /dev/null and b/images/duo.png differ diff --git a/js/account.js b/js/account.js index 84a7da5bf..31c1a50e6 100644 --- a/js/account.js +++ b/js/account.js @@ -59,8 +59,10 @@ $(function() { $('#mfa-select').hide(); $('#update').attr('disabled', true); + $('#mfa-totp-enable-code').attr('required', true); $('#mfa-confirm').show(); $('.mfa-api-blurb').show(); + $('#mfa-enable-shared').show(); $('#mfa-enable-totp').show(); $('#mfa-totp-throbber').show(); $('#mfa-totp-issued').hide(); @@ -90,10 +92,25 @@ $(function() { }); }); + $('#mfa-select-duo') + .click(function(event) { + event.preventDefault(); + $('#mfa').val('Duo'); + + $('#mfa-select').hide(); + $('#update').attr('disabled', false); + $('#mfa-duo-user').attr('required', true); + $('#mfa-confirm').show(); + $('.mfa-api-blurb').show(); + $('#mfa-enable-shared').show(); + $('#mfa-enable-duo').show(); + $('#mfa-password').focus(); + }); + $('#mfa-disable') .click(function(event) { event.preventDefault(); - $('.mfa-api-blurb, #mfa-buttons').hide(); + $('.mfa-api-blurb, .mfa-buttons').hide(); $('#mfa-disable-container, #mfa-auth-container').show(); $('#mfa-confirm').show(); $('#mfa-password').focus(); @@ -105,7 +122,7 @@ $(function() { $('#mfa-recovery') .click(function(event) { event.preventDefault(); - $('.mfa-api-blurb, #mfa-buttons').hide(); + $('.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'); diff --git a/js/duo-min.js b/js/duo-min.js new file mode 100644 index 000000000..a7d8a24fc --- /dev/null +++ b/js/duo-min.js @@ -0,0 +1,32 @@ +// https://github.com/duosecurity/duo_perl +// +// Copyright (c) 2012, Duo Security, Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The name of the author may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +(function(a){var d,f,e=1,i,j=this,k,l=j.postMessage;a.postMessage=function(b,c,h){if(c){b=typeof b==="string"?b:a.param(b);h=h||parent;if(l)h.postMessage(b,c.replace(/([^:]+:\/\/[^\/]+).*/,"$1"));else if(c)h.location=c.replace(/#.*$/,"")+"#"+ +new Date+e++ +"&"+b}};a.receiveMessage=k=function(b,c,h){if(l){if(b){i&&k();i=function(g){if(typeof c==="string"&&g.origin!==c||a.isFunction(c)&&c(g.origin)===false)return false;b(g)}}if(j.addEventListener)j[b?"addEventListener":"removeEventListener"]("message", +i,false);else j[b?"attachEvent":"detachEvent"]("onmessage",i)}else{d&&clearInterval(d);d=null;if(b)d=setInterval(function(){var g=document.location.hash,m=/^#?\d+&/;if(g!==f&&m.test(g)){f=g;b({data:g.replace(m,"")})}},typeof c==="number"?c:typeof h==="number"?h:100)}}})(jQuery); +var D=jQuery,Duo={init:function(a){if(a)if(a.host){Duo._host=a.host;if(a.sig_request){Duo._sig_request=a.sig_request;if(Duo._sig_request.indexOf("ERR|")==0){a=Duo._sig_request.split("|");alert("Error: "+a[1])}else if(Duo._sig_request.indexOf(":")==-1)alert("Invalid sig_request value");else{var d=Duo._sig_request.split(":");if(d.length!=2)alert("Invalid sig_request value");else{Duo._duo_sig=d[0];Duo._app_sig=d[1];if(!a.post_action)a.post_action="";Duo._post_action=a.post_action;if(!a.post_argument)a.post_argument= +"sig_response";Duo._post_argument=a.post_argument}}}else alert("Error: missing 'sig_request' argument in Duo.init()")}else alert("Error: missing 'host' argument in Duo.init()");else alert("Error: missing arguments in Duo.init()")},ready:function(){var a=D("#duo_iframe");if(a.length){var d=D.param({tx:Duo._duo_sig,parent:document.location.href});a.attr("src","https://"+Duo._host+"/frame/web/v1/auth?"+d);D.receiveMessage(function(f){f=f.data+":"+Duo._app_sig;f=D('').attr("name", +Duo._post_argument).val(f);var e=D("#duo_form");if(!e.length){e=D("
");e.insertAfter(a)}e.attr("method","POST");e.attr("action",Duo._post_action);e.append(f);e.submit()},"https://"+Duo._host)}else alert("Error: missing IFRAME element with id 'duo_iframe'")}};D(document).ready(function(){Duo.ready()}); diff --git a/skins/standard/admin.css b/skins/standard/admin.css index 134dc2cee..6a91965e4 100644 --- a/skins/standard/admin.css +++ b/skins/standard/admin.css @@ -275,10 +275,15 @@ input[disabled] { padding: 0; } -#mfa-recovery { +.mfa-buttons button { margin-top: 4px; } +.mfa-buttons blockquote { + margin-top: 4px; + font-style: italic; +} + #mfa-recovery-frame { display: block; margin-top: 8px; @@ -288,9 +293,14 @@ input[disabled] { height: 200px; } -label.mfa-totp { +#mfa-container label { display: inline-block; width: 155px; text-align: right; font-weight: bold; } + +#mfa-container #duo-logo { + float: left; + margin-right: 1em; +} diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 2898fdd3f..49bfdb8e8 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -27,6 +27,10 @@ use Bugzilla; use File::Find; +use constant IGNORE => qw( + Bugzilla/DuoWeb.pm +); + @additional_files = (); @files = glob('*'); @@ -46,6 +50,10 @@ sub isTestingFile { my ($file) = @_; my $exclude; + foreach my $ignore (IGNORE) { + return undef if $ignore eq $file; + } + if ($file =~ /\.cgi$|\.pl$|\.pm$/) { return 1; } diff --git a/template/en/default/account/prefs/mfa.html.tmpl b/template/en/default/account/prefs/mfa.html.tmpl index df272f7d3..2d80520a1 100644 --- a/template/en/default/account/prefs/mfa.html.tmpl +++ b/template/en/default/account/prefs/mfa.html.tmpl @@ -40,11 +40,12 @@ Two-factor authentication is currently enabled using [% SWITCH user.mfa %] [% CASE "TOTP" %]TOTP + [% CASE "Duo" %]Duo Security [% END %].

-
+
[% INCLUDE "mfa/protected.html.tmpl" %] @@ -92,15 +93,26 @@ @@ -111,23 +123,39 @@ -
+

Select the two-factor system you want to use:

- -
- [%# TOTP %] - + + + [%# enable - TOTP %] + @@ -174,6 +202,26 @@
+ [%# enable - duo %] + + [% END %]