diff options
-rw-r--r-- | Bugzilla/DuoAPI.pm | 161 | ||||
-rw-r--r-- | Bugzilla/MFA/Duo.pm | 18 | ||||
-rw-r--r-- | t/Support/Files.pm | 1 | ||||
-rw-r--r-- | template/en/default/account/prefs/mfa.html.tmpl | 32 | ||||
-rw-r--r-- | template/en/default/mfa/duo/not_enrolled.html.tmpl | 60 |
5 files changed, 256 insertions, 16 deletions
diff --git a/Bugzilla/DuoAPI.pm b/Bugzilla/DuoAPI.pm new file mode 100644 index 000000000..ab50a61e2 --- /dev/null +++ b/Bugzilla/DuoAPI.pm @@ -0,0 +1,161 @@ +package Bugzilla::DuoAPI; +use strict; +use warnings; + +our $VERSION = '1.0'; + +=head1 NAME + +Duo::API - Reference client to call Duo Security's API methods. + +=head1 SYNOPSIS + + use Duo::API; + my $client = Duo::API->new('INTEGRATION KEY', 'SECRET KEY', 'HOSTNAME'); + my $res = $client->json_api_call('GET', '/auth/v2/check', {}); + +=head1 SEE ALSO + +Duo for Developers: L<https://www.duosecurity.com/api> + +=head1 COPYRIGHT + +Copyright (c) 2013 Duo Security + +This program is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=head1 DESCRIPTION + +Duo::API objects have the following methods: + +=over 4 + +=item new($integration_key, $integration_secret_key, $api_hostname) + +Returns a handle to sign and send requests. These parameters are +obtained when creating an API integration. + +=item json_api_call($method, $path, \%params) + +Make a request to an API endpoint with the given HTTPS method and +parameters. Returns the parsed result if successful or dies with the +error message from the Duo Security service. + +=item api_call($method, $path, \%params) + +Make a request without parsing the response. + +=item canonicalize_params(\%params) + +Serialize a parameter hash reference to a string to sign or send. + +=item sign($method, $path, $canon_params, $date) + +Return the Authorization header for a request. C<$canon_params> is the +string returned by L<canonicalize_params>. + +=back + +=cut + +use CGI qw(); +use Carp qw(croak); +use Digest::HMAC_SHA1 qw(hmac_sha1_hex); +use JSON qw(decode_json); +use LWP::UserAgent; +use MIME::Base64 qw(encode_base64); +use POSIX qw(strftime); + +sub new { + my($proto, $ikey, $skey, $host) = @_; + my $class = ref($proto) || $proto; + my $self = { + 'ikey' => $ikey, + 'skey' => $skey, + 'host' => $host, + }; + bless($self, $class); + return $self; +} + +sub canonicalize_params { + my ($self, $params) = @_; + + my @ret; + while (my ($k, $v) = each(%{$params})) { + push(@ret, join('=', CGI::escape($k), CGI::escape($v))); + } + return join('&', sort(@ret)); +} + +sub sign { + my ($self, $method, $path, $canon_params, $date) = @_; + my $canon = join("\n", + $date, + uc($method), + lc($self->{'host'}), + $path, + $canon_params); + my $sig = hmac_sha1_hex($canon, $self->{'skey'}); + my $auth = join(':', + $self->{'ikey'}, + $sig); + $auth = 'Basic ' . encode_base64($auth, ''); + return $auth; +} + +sub api_call { + my ($self, $method, $path, $params) = @_; + $params ||= {}; + + my $canon_params = $self->canonicalize_params($params); + my $date = strftime('%a, %d %b %Y %H:%M:%S -0000', + gmtime(time())); + my $auth = $self->sign($method, $path, $canon_params, $date); + + my $ua = LWP::UserAgent->new(); + my $req = HTTP::Request->new(); + $req->method($method); + $req->protocol('HTTP/1.1'); + $req->header('If-SSL-Cert-Subject' => qr{CN=[^=]+\.duosecurity.com$}); + $req->header('Authorization' => $auth); + $req->header('Date' => $date); + $req->header('Host' => $self->{'host'}); + + if (grep(/^$method$/, qw(POST PUT))) { + $req->header('Content-type' => 'application/x-www-form-urlencoded'); + $req->content($canon_params); + } + else { + $path .= '?' . $canon_params; + } + + $req->uri('https://' . $self->{'host'} . $path); + if ($ENV{'DEBUG'}) { + print STDERR $req->as_string(); + } + my $res = $ua->request($req); + return $res; +} + +sub json_api_call { + my $self = shift; + my $res = $self->api_call(@_); + my $json = $res->content(); + if ($json !~ /^{/) { + croak($json); + } + my $ret = decode_json($json); + if (($ret->{'stat'} || '') ne 'OK') { + my $msg = join('', + 'Error ', $ret->{'code'}, ': ', $ret->{'message'}); + if (defined($ret->{'message_detail'})) { + $msg .= ' (' . $ret->{'message_detail'} . ')'; + } + croak($msg); + } + return $ret->{'response'}; +} + +1; diff --git a/Bugzilla/MFA/Duo.pm b/Bugzilla/MFA/Duo.pm index 4c9aa1184..91096689f 100644 --- a/Bugzilla/MFA/Duo.pm +++ b/Bugzilla/MFA/Duo.pm @@ -9,6 +9,7 @@ package Bugzilla::MFA::Duo; use strict; use parent 'Bugzilla::MFA'; +use Bugzilla::DuoAPI; use Bugzilla::DuoWeb; use Bugzilla::Error; @@ -19,6 +20,23 @@ sub can_verify_inline { sub enroll { my ($self, $params) = @_; + # verify that the user is enrolled with duo + my $client = Bugzilla::DuoAPI->new( + Bugzilla->params->{duo_ikey}, + Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_host} + ); + my $response = $client->json_api_call('POST', '/auth/v2/preauth', { username => $params->{username} }); + + # not enrolled - show a nice error page instead of just throwing + unless ($response->{result} eq 'auth' || $response->{result} eq 'allow') { + print Bugzilla->cgi->header(); + my $template = Bugzilla->template; + $template->process('mfa/duo/not_enrolled.html.tmpl', { email => $params->{username} }) + || ThrowTemplateError($template->error()); + exit; + } + $self->property_set('user', $params->{username}); } diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 49bfdb8e8..00e0efd34 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -28,6 +28,7 @@ use Bugzilla; use File::Find; use constant IGNORE => qw( + Bugzilla/DuoAPI.pm Bugzilla/DuoWeb.pm ); diff --git a/template/en/default/account/prefs/mfa.html.tmpl b/template/en/default/account/prefs/mfa.html.tmpl index 2d80520a1..2fbe45a60 100644 --- a/template/en/default/account/prefs/mfa.html.tmpl +++ b/template/en/default/account/prefs/mfa.html.tmpl @@ -138,7 +138,7 @@ [% IF Param("duo_host") && user.in_group("mozilla-employee-confidential") %] <button type="button" id="mfa-select-duo">Duo Security</button><br> <blockquote> - Requires a smartphone and a <a href="https://www.duosecurity.com/" target="_blank">Duo Security</a> + Requires a <a href="https://mana.mozilla.org/wiki/display/SD/DuoSecurity" target="_blank">Duo Security</a> account (recommended for Mozilla employees). </blockquote> [% END %] @@ -202,25 +202,25 @@ </div> - [%# enable - duo %] - <div id="mfa-enable-duo" style="display:none"> + [% IF Param("duo_host") && user.in_group("mozilla-employee-confidential") %] + [%# enable - duo %] + <div id="mfa-enable-duo" style="display:none"> - <p> - <label>Duo Username:</label> - <input type="text" name="username" id="mfa-duo-user"> - </p> + <p> + <label>Duo Username:</label> + <input type="text" name="username" id="mfa-duo-user"> + </p> - <p> - <img src="images/duo.png" id="duo-logo" width="32" height="32"> - Verification with Duo Security will be performed before your account is updated.<br> + <p> + <img src="images/duo.png" id="duo-logo" width="32" height="32"> + Verification with Duo Security will be performed before your account is updated.<br> - [% IF user.in_group("mozilla-employee-confidential") %] - You must <a href="https://login.mozilla.com/duo_enrollments/" target="_blank"> - sign up for Duo Security via login.mozilla.com</a> before you can use Duo 2FA. - [% END %] - </p> + You must be <a href="https://mana.mozilla.org/wiki/display/SD/DuoSecurity" target="_blank"> + enrolled with Duo Security via login.mozilla.com</a> before you can use Duo 2FA. + </p> - </div> + </div> + [% END %] [% END %] diff --git a/template/en/default/mfa/duo/not_enrolled.html.tmpl b/template/en/default/mfa/duo/not_enrolled.html.tmpl new file mode 100644 index 000000000..f6a594dc2 --- /dev/null +++ b/template/en/default/mfa/duo/not_enrolled.html.tmpl @@ -0,0 +1,60 @@ +[%# 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. + #%] + +[% js = BLOCK %] + + $(function() { + $('#return') + .click(function(event) { + event.preventDefault(); + window.history.back(); + }); + }); + +[% END %] + +[% css = BLOCK %] + + #duo_container { + background: #fff; + padding: 10px; + margin-bottom: 1em; + } + +[% END %] + +[% + INCLUDE global/header.html.tmpl + title = "Duo Security Not Available" + style = css + javascript = js +%] + +<h1>You have not enrolled in Duo Security</h1> + +<div id="duo_container"> + <p> + The email address <b>[% email FILTER html %]</b> is not enrolled in Duo Security on + <a href="https://login.mozilla.com/" target="_blank">login.mozilla.com</a>. + </p> + + <p> + Please ensure you are using your Mozilla LDAP username, and that you have + completed the <a href="https://mana.mozilla.org/wiki/display/SD/DuoSecurity" target="_blank"> + Duo Security enrollment process</a>. + </p> + + <p> + Duo Security MFA may not yet be available for your Mozilla account.<br> + Contact End User Services / ServiceDesk for more information. + </p> +</div> + +<button type="button" id="return">Return</button> + +[% INCLUDE global/footer.html.tmpl %] |