diff options
Diffstat (limited to 'Bugzilla')
-rw-r--r-- | Bugzilla/DuoAPI.pm | 161 | ||||
-rw-r--r-- | Bugzilla/MFA/Duo.pm | 18 |
2 files changed, 179 insertions, 0 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}); } |