summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Bugzilla/DuoAPI.pm161
-rw-r--r--Bugzilla/MFA/Duo.pm18
-rw-r--r--t/Support/Files.pm1
-rw-r--r--template/en/default/account/prefs/mfa.html.tmpl32
-rw-r--r--template/en/default/mfa/duo/not_enrolled.html.tmpl60
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 %]