diff options
author | Adam Goodman <akgood@duosecurity.com> | 2012-01-25 11:41:40 -0500 |
---|---|---|
committer | Adam Goodman <akgood@duosecurity.com> | 2012-01-25 11:41:40 -0500 |
commit | 9847008070022cf19a2fb5db33e36cc11c8aec45 (patch) | |
tree | b9f995308c8216d7b4a370dde98128715fb06e25 | |
parent | 01d525f4282227092cfb2cfa5488ae1734301a55 (diff) | |
download | openvpn-duo-plugin-9847008070022cf19a2fb5db33e36cc11c8aec45.tar.gz openvpn-duo-plugin-9847008070022cf19a2fb5db33e36cc11c8aec45.zip |
add perl version of duo_openvpn.py
-rw-r--r-- | duo_openvpn.pl | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/duo_openvpn.pl b/duo_openvpn.pl new file mode 100644 index 0000000..9998fcd --- /dev/null +++ b/duo_openvpn.pl @@ -0,0 +1,223 @@ +#/usr/bin/env perl + +use strict; +use warnings; +use LWP::UserAgent; +use Sys::Syslog qw(:standard); +use URI::Escape; +use MIME::Base64; +use JSON::XS; +use Digest::HMAC_SHA1 qw(hmac_sha1_hex); +use Data::Dumper; +$Data::Dumper::Indent = 0; +$Data::Dumper::Terse = 1; + +my $API_RESULT_AUTH = qr/^auth$/; +my $API_RESULT_ALLOW = qr/^allow$/; +my $API_RESULT_DENY = qr/^deny$/; +my $API_RESULT_ENROLL = qr/^enroll$/; +my $control = $ENV{'control'}; + +openlog 'duo_openvpn.pl', 'pid', 'LOG_AUTH'; + +if (not $control) { + logger('required control configuration'); + exit 1; +} + +my $ikey = $ENV{'ikey'}; +my $skey = $ENV{'skey'}; +my $host = $ENV{'host'}; + +if (not $ikey or not $skey or not $host) { + logger('required ikey/skey/host configuration'); + failure(); + exit 1; +} + +my $username = $ENV{'username'}; +my $password = $ENV{'password'}; +my $ipaddr = $ENV{'ipaddr'} || '0.0.0.0'; + +if (not $control or not $username or not $host) { + logger('required environmental variables not found'); + failure(); +} + +preauth(); +auth(); +failure(); + + +sub canonicalize { + my $host = shift; + my $uri = shift; + my $params = shift; + + my @canon = ('POST', lc $host, $uri); + my @args = (); + + foreach my $key (keys %{$params}) { + push @args, (uri_escape($key) . '=' . uri_escape($params->{$key})); + } + + return join '&', @canon, @args; +} + + +sub sign { + my ($ikey, $skey, $host, $path, $args) = @_; + + my $sig = hmac_sha1_hex(canonicalize($host, $path, $args), $skey); + my $auth = "$ikey:$sig"; + return 'Basic ' . encode_base64($auth); +} + + +sub call { + my ($ikey, $skey, $host, $path, $kwargs) = @_; + + my $ua = LWP::UserAgent->new(); + + $ua->default_header( + 'Authorization' => sign($ikey, $skey, $host, $path, $kwargs), + ); + + my $response = $ua->post('https://' . $host . $path, $kwargs); + my $data = '{}'; + + if ($response->is_success) { + $data = $response->content; + } + + return ($response->code, $response->message, $data); +} + + +sub api { + my ($ikey, $skey, $host, $path, $args) = @_; + + my ($status, $reason, $json) = call($ikey, $skey, $host, $path, $args); + + if ($status != 200) { + logger("Received $status $reason: $json"); + failure(); + } + + my $data = decode_json $json; + + if (defined $data->{stat} and $data->{stat} !~ /^OK$/o) { + logger("Received error response: $json"); + failure(); + } + + if (not defined $data->{response}) { + logger("Received bad response: $json"); + failure(); + } + + if (not defined $data->{'response'}{'result'}) { + logger("invalid API response: $json"); + failure(); + } + + if (not defined $data->{'response'}{'status'}) { + logger("invalid API response: $json"); + failure(); + } + + return $data->{response}; +} + + +sub auth { + logger("authentication for $username"); + + my $args = { + 'user' => $username, + 'factor' => 'auto', + 'auto' => $password, + 'ipaddr' => $ipaddr, + }; + + my $response = api($ikey, $skey, $host, '/rest/v1/auth', $args); + + my $result = $response->{'result'}; + my $status = $response->{'status'}; + + if ($result =~ $API_RESULT_ALLOW) { + logger("auth success for $username: $status"); + success(); + } + elsif ($result =~ $API_RESULT_DENY) { + logger("auth failure for $username: $status"); + failure(); + } + else { + logger("unknown auth result: $result"); + failure(); + } +} + + +sub preauth { + logger("pre-authentication for $username"); + + my $args = { + user => $username, + }; + + my $response = api($ikey, $skey, $host, '/rest/v1/preauth', $args); + + my $result = $response->{'result'}; + my $status = $response->{'status'}; + + if ($result =~ $API_RESULT_AUTH) { + return; + } + + if ($result =~ $API_RESULT_ENROLL) { + logger("user $username is not enrolled: $status"); + failure(); + } + elsif ($result =~ $API_RESULT_DENY) { + logger("preauth failure for $username: $status"); + failure(); + } + elsif ($result =~ $API_RESULT_ALLOW) { + logger("preauth success for $username: $status"); + success(); + } + else { + logger("unknown preauth result: $result"); + failure(); + } +} + + +sub logger { + my $msg = shift; + syslog('info', "Duo OpenVPN: $msg"); +} + + +sub success { + logger("writing success code to $control"); + + open CONTROL, '>', $control or die "Error [$control]: $!"; + print CONTROL '1'; + close CONTROL; + + exit 0; +} + + +sub failure { + logger("writing failure code to $control"); + + open CONTROL, '>', $control or die "Error [$control]: $!"; + print CONTROL '0'; + close CONTROL; + + exit 1; +} |