]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1225366 - allow duo authentication for users already enrolled with duo
authorByron Jones <glob@mozilla.com>
Mon, 23 Nov 2015 06:46:59 +0000 (14:46 +0800)
committerByron Jones <glob@mozilla.com>
Mon, 23 Nov 2015 06:46:59 +0000 (14:46 +0800)
Bugzilla/DuoAPI.pm [new file with mode: 0644]
Bugzilla/MFA/Duo.pm
t/Support/Files.pm
template/en/default/account/prefs/mfa.html.tmpl
template/en/default/mfa/duo/not_enrolled.html.tmpl [new file with mode: 0644]

diff --git a/Bugzilla/DuoAPI.pm b/Bugzilla/DuoAPI.pm
new file mode 100644 (file)
index 0000000..ab50a61
--- /dev/null
@@ -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;
index 4c9aa1184cd5840035e93464f64cefd100636301..91096689f6ce775bbd3d51bff351a92238e48fda 100644 (file)
@@ -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});
 }
 
index 49bfdb8e8610e67dbd2a707315a80876227021bf..00e0efd3435d37ba7703b083c19df94e8464970a 100644 (file)
@@ -28,6 +28,7 @@ use Bugzilla;
 use File::Find;
 
 use constant IGNORE => qw(
+    Bugzilla/DuoAPI.pm
     Bugzilla/DuoWeb.pm
 );
 
index 2d80520a103885560d7cc7e7316025971474d357..2fbe45a6097cf7ed036d25d153ab1f8692f98d41 100644 (file)
       [% 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 %]
 
     </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 (file)
index 0000000..f6a594d
--- /dev/null
@@ -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 %]