| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | # Perl interface for the Duo Auth API. | 
| 2 |  |  |  |  |  |  | # | 
| 3 |  |  |  |  |  |  | # This Perl module collection provides a Perl interface to the Auth API | 
| 4 |  |  |  |  |  |  | # integration for the Duo multifactor authentication service | 
| 5 |  |  |  |  |  |  | # (https://www.duosecurity.com/).  It differs from the Perl API sample code in | 
| 6 |  |  |  |  |  |  | # that it wraps all the returned data structures in objects with method calls, | 
| 7 |  |  |  |  |  |  | # abstracts some of the API details, and throws rich exceptions rather than | 
| 8 |  |  |  |  |  |  | # requiring the caller deal with JSON data structures directly. | 
| 9 |  |  |  |  |  |  |  | 
| 10 |  |  |  |  |  |  | package Net::Duo::Auth 1.01; | 
| 11 |  |  |  |  |  |  |  | 
| 12 | 3 |  |  | 3 |  | 3322 | use 5.014; | 
|  | 3 |  |  |  |  | 11 |  | 
| 13 | 3 |  |  | 3 |  | 18 | use strict; | 
|  | 3 |  |  |  |  | 4 |  | 
|  | 3 |  |  |  |  | 76 |  | 
| 14 | 3 |  |  | 3 |  | 15 | use warnings; | 
|  | 3 |  |  |  |  | 5 |  | 
|  | 3 |  |  |  |  | 103 |  | 
| 15 |  |  |  |  |  |  |  | 
| 16 | 3 |  |  | 3 |  | 2032 | use parent qw(Net::Duo); | 
|  | 3 |  |  |  |  | 945 |  | 
|  | 3 |  |  |  |  | 19 |  | 
| 17 |  |  |  |  |  |  |  | 
| 18 | 3 |  |  | 3 |  | 148 | use Carp qw(croak); | 
|  | 3 |  |  |  |  | 7 |  | 
|  | 3 |  |  |  |  | 136 |  | 
| 19 | 3 |  |  | 3 |  | 1896 | use Net::Duo::Auth::Async; | 
|  | 3 |  |  |  |  | 8 |  | 
|  | 3 |  |  |  |  | 86 |  | 
| 20 | 3 |  |  | 3 |  | 15 | use URI::Escape qw(uri_escape_utf8); | 
|  | 3 |  |  |  |  | 6 |  | 
|  | 3 |  |  |  |  | 2340 |  | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | # All dies are of constructed objects, which perlcritic misdiagnoses. | 
| 23 |  |  |  |  |  |  | ## no critic (ErrorHandling::RequireCarping) | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  | ############################################################################## | 
| 26 |  |  |  |  |  |  | # Auth API methods | 
| 27 |  |  |  |  |  |  | ############################################################################## | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | # Helper function to validate and canonicalize arguments to the auth and | 
| 30 |  |  |  |  |  |  | # auth_async functions.  Ensures that the arguments meet the calling contract | 
| 31 |  |  |  |  |  |  | # for the auth method (see below) and returns a reference to a new hash with | 
| 32 |  |  |  |  |  |  | # the canonicalized copy of data. | 
| 33 |  |  |  |  |  |  | # | 
| 34 |  |  |  |  |  |  | # $self     - The Net::Duo::Auth object | 
| 35 |  |  |  |  |  |  | # $args_ref - Reference to hash of arguments to an auth function | 
| 36 |  |  |  |  |  |  | # | 
| 37 |  |  |  |  |  |  | # Returns: Reference to hash of canonicalized arguments | 
| 38 |  |  |  |  |  |  | #  Throws: Text exception on internal call method error | 
| 39 |  |  |  |  |  |  | sub _canonicalize_auth_args { | 
| 40 | 3 |  |  | 3 |  | 6 | my ($self, $args_ref) = @_; | 
| 41 | 3 |  |  |  |  | 6 | my %args = %{$args_ref}; | 
|  | 3 |  |  |  |  | 12 |  | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | # Ensure we have either username or user_id, but not neither or both. | 
| 44 | 3 |  |  |  |  | 8 | my $user_count = grep { defined($args{$_}) } qw(username user_id); | 
|  | 6 |  |  |  |  | 16 |  | 
| 45 | 3 | 50 |  |  |  | 13 | if ($user_count < 1) { | 
|  |  | 50 |  |  |  |  |  | 
| 46 | 0 |  |  |  |  | 0 | croak('no username or user_id specified'); | 
| 47 |  |  |  |  |  |  | } elsif ($user_count > 1) { | 
| 48 | 0 |  |  |  |  | 0 | croak('username and user_id both given'); | 
| 49 |  |  |  |  |  |  | } | 
| 50 |  |  |  |  |  |  |  | 
| 51 |  |  |  |  |  |  | # Ensure factor is set. | 
| 52 | 3 | 50 |  |  |  | 10 | if (!defined($args{factor})) { | 
| 53 | 0 |  |  |  |  | 0 | croak('no factor specified'); | 
| 54 |  |  |  |  |  |  | } | 
| 55 |  |  |  |  |  |  |  | 
| 56 |  |  |  |  |  |  | # Set some defaults that we provide in our API guarantee. | 
| 57 | 3 |  |  |  |  | 5 | my $factor = $args{factor}; | 
| 58 | 3 | 100 | 66 |  |  | 23 | if ($factor eq 'push' || $factor eq 'phone' || $factor eq 'auto') { | 
|  |  |  | 100 |  |  |  |  | 
| 59 | 2 |  | 100 |  |  | 10 | $args{device} //= 'auto'; | 
| 60 |  |  |  |  |  |  | } | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | # Convert pushinfo to a URL-encoded string if it is present.  We use this | 
| 63 |  |  |  |  |  |  | # logic rather than _canonicalize_args so that we can preserve order. | 
| 64 | 3 | 100 |  |  |  | 9 | if ($args{pushinfo}) { | 
| 65 | 1 |  |  |  |  | 2 | my @pushinfo = @{ $args{pushinfo} }; | 
|  | 1 |  |  |  |  | 3 |  | 
| 66 | 1 |  |  |  |  | 2 | my @pairs; | 
| 67 | 1 |  |  |  |  | 4 | while (@pushinfo) { | 
| 68 | 2 |  |  |  |  | 8 | my $encoded_key   = uri_escape_utf8(shift(@pushinfo)); | 
| 69 | 2 |  |  |  |  | 35 | my $encoded_value = uri_escape_utf8(shift(@pushinfo)); | 
| 70 | 2 |  |  |  |  | 35 | my $pair          = $encoded_key . q{=} . $encoded_value; | 
| 71 | 2 |  |  |  |  | 6 | push(@pairs, $pair); | 
| 72 |  |  |  |  |  |  | } | 
| 73 | 1 |  |  |  |  | 5 | $args{pushinfo} = join(q{&}, @pairs); | 
| 74 |  |  |  |  |  |  | } | 
| 75 |  |  |  |  |  |  |  | 
| 76 |  |  |  |  |  |  | # Return the results.  Currently, we don't validate any of the other | 
| 77 |  |  |  |  |  |  | # arguments and just pass them straight to Duo.  We could do better about | 
| 78 |  |  |  |  |  |  | # this. | 
| 79 | 3 |  |  |  |  | 7 | return \%args; | 
| 80 |  |  |  |  |  |  | } | 
| 81 |  |  |  |  |  |  |  | 
| 82 |  |  |  |  |  |  | # Perform a synchronous user authentication.  The user will be authenticated | 
| 83 |  |  |  |  |  |  | # given the factor and additional information provided in the $args argument. | 
| 84 |  |  |  |  |  |  | # The call will not return until the user has authenticated or the call has | 
| 85 |  |  |  |  |  |  | # failed for some reason.  To do long-polling instead, see the auth_async | 
| 86 |  |  |  |  |  |  | # method. | 
| 87 |  |  |  |  |  |  | # | 
| 88 |  |  |  |  |  |  | # $self     - The Net::Duo::Auth object | 
| 89 |  |  |  |  |  |  | # $args_ref - Reference to hash of arguments, chosen from: | 
| 90 |  |  |  |  |  |  | #   user_id  - ID of user (either this or username is required) | 
| 91 |  |  |  |  |  |  | #   username - Username of user (either this or user_id is required) | 
| 92 |  |  |  |  |  |  | #   factor   - One of auto, push, passcode, or phone | 
| 93 |  |  |  |  |  |  | #   ipaddr   - IP address of user (optional) | 
| 94 |  |  |  |  |  |  | # For factor == push: | 
| 95 |  |  |  |  |  |  | #   device           - ID of the device (optional, default is "auto") | 
| 96 |  |  |  |  |  |  | #   type             - String to display before prompt (optional) | 
| 97 |  |  |  |  |  |  | #   display_username - String instead of username (optional) | 
| 98 |  |  |  |  |  |  | #   pushinfo         - Reference to array of pairs to show user (optional) | 
| 99 |  |  |  |  |  |  | # For factor == passcode: | 
| 100 |  |  |  |  |  |  | #   passcode - The passcode to validate | 
| 101 |  |  |  |  |  |  | # For factor == phone: | 
| 102 |  |  |  |  |  |  | #   device - The ID of the device to call (optional, default is "auto") | 
| 103 |  |  |  |  |  |  | # | 
| 104 |  |  |  |  |  |  | # Returns: Scalar context: true if user was authenticated, false otherwise | 
| 105 |  |  |  |  |  |  | #          List context: true/false for success, then hash of additional data | 
| 106 |  |  |  |  |  |  | #            status               - Status of authentication | 
| 107 |  |  |  |  |  |  | #            status_msg           - Detailed status message | 
| 108 |  |  |  |  |  |  | #            trusted_device_token - Token to use later for /preauth | 
| 109 |  |  |  |  |  |  | #  Throws: Net::Duo::Exception on failure | 
| 110 |  |  |  |  |  |  | sub auth { | 
| 111 | 2 |  |  | 2 | 1 | 13 | my ($self, $args_ref) = @_; | 
| 112 | 2 |  |  |  |  | 18 | my $args = $self->_canonicalize_auth_args($args_ref); | 
| 113 |  |  |  |  |  |  |  | 
| 114 |  |  |  |  |  |  | # Make the call to Duo. | 
| 115 | 2 |  |  |  |  | 12 | my $result = $self->call_json('POST', '/auth/v2/auth', $args); | 
| 116 |  |  |  |  |  |  |  | 
| 117 |  |  |  |  |  |  | # Ensure we got a valid result. | 
| 118 | 2 | 50 | 33 |  |  | 18 | if (!defined($result->{result})) { | 
|  |  | 50 |  |  |  |  |  | 
| 119 | 0 |  |  |  |  | 0 | my $error = 'no authentication result from Duo'; | 
| 120 | 0 |  |  |  |  | 0 | die Net::Duo::Exception->protocol($error, $result); | 
| 121 |  |  |  |  |  |  | } elsif ($result->{result} ne 'allow' && $result->{result} ne 'deny') { | 
| 122 | 0 |  |  |  |  | 0 | my $error = "invalid authentication result $result->{result}"; | 
| 123 | 0 |  |  |  |  | 0 | die Net::Duo::Exception->protocol($error, $result); | 
| 124 |  |  |  |  |  |  | } | 
| 125 |  |  |  |  |  |  |  | 
| 126 |  |  |  |  |  |  | # Determine whether the authentication succeeded, and return the correct | 
| 127 |  |  |  |  |  |  | # results. | 
| 128 | 2 |  |  |  |  | 3 | my $success = $result->{result} eq 'allow'; | 
| 129 | 2 |  |  |  |  | 5 | delete $result->{result}; | 
| 130 | 2 | 100 |  |  |  | 14 | return wantarray ? ($success, $result) : $success; | 
| 131 |  |  |  |  |  |  | } | 
| 132 |  |  |  |  |  |  |  | 
| 133 |  |  |  |  |  |  | # Perform an asynchronous authentication. | 
| 134 |  |  |  |  |  |  | # | 
| 135 |  |  |  |  |  |  | # Takes the same arguments as the auth method, but starts an asynchronous | 
| 136 |  |  |  |  |  |  | # authentication.  Returns a transaction ID, which can be passed to | 
| 137 |  |  |  |  |  |  | # auth_status() to long-poll the result of the authentication. | 
| 138 |  |  |  |  |  |  | # | 
| 139 |  |  |  |  |  |  | # $self     - The Net::Duo::Auth object | 
| 140 |  |  |  |  |  |  | # $args_ref - Reference to hash of arguments, chosen from: | 
| 141 |  |  |  |  |  |  | # | 
| 142 |  |  |  |  |  |  | # Returns: The transaction ID to poll with auth_status() | 
| 143 |  |  |  |  |  |  | #  Throws: Net::Duo::Exception on failure | 
| 144 |  |  |  |  |  |  | sub auth_async { | 
| 145 | 1 |  |  | 1 | 1 | 8 | my ($self, $args_ref) = @_; | 
| 146 | 1 |  |  |  |  | 4 | my $args = $self->_canonicalize_auth_args($args_ref); | 
| 147 | 1 |  |  |  |  | 3 | $args->{async} = 1; | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  | # Make the call to Duo. | 
| 150 | 1 |  |  |  |  | 4 | my $result = $self->call_json('POST', '/auth/v2/auth', $args); | 
| 151 |  |  |  |  |  |  |  | 
| 152 |  |  |  |  |  |  | # Return the transaction ID. | 
| 153 | 1 | 50 |  |  |  | 5 | if (!defined($result->{txid})) { | 
| 154 | 0 |  |  |  |  | 0 | my $error = 'no transaction ID in response to async auth call'; | 
| 155 | 0 |  |  |  |  | 0 | die Net::Duo::Exception->protocol($error, $result); | 
| 156 |  |  |  |  |  |  | } | 
| 157 | 1 |  |  |  |  | 10 | return Net::Duo::Auth::Async->new($self, $result->{txid}); | 
| 158 |  |  |  |  |  |  | } | 
| 159 |  |  |  |  |  |  |  | 
| 160 |  |  |  |  |  |  | # Confirm that authentication works properly. | 
| 161 |  |  |  |  |  |  | # | 
| 162 |  |  |  |  |  |  | # $self - The Net::Duo::Auth object | 
| 163 |  |  |  |  |  |  | # | 
| 164 |  |  |  |  |  |  | # Returns: Server time in seconds since UNIX epoch | 
| 165 |  |  |  |  |  |  | #  Throws: Net::Duo::Exception on failure | 
| 166 |  |  |  |  |  |  | sub check { | 
| 167 | 1 |  |  | 1 | 1 | 6 | my ($self) = @_; | 
| 168 | 1 |  |  |  |  | 19 | my $result = $self->call_json('GET', '/auth/v2/check'); | 
| 169 | 1 |  |  |  |  | 7 | return $result->{time}; | 
| 170 |  |  |  |  |  |  | } | 
| 171 |  |  |  |  |  |  |  | 
| 172 |  |  |  |  |  |  | # Send one or more passcodes (depending on Duo configuration) to a user via | 
| 173 |  |  |  |  |  |  | # SMS.  This should always succeed, so any error results in an exception. | 
| 174 |  |  |  |  |  |  | # | 
| 175 |  |  |  |  |  |  | # $self     - The Net::Duo::Auth object | 
| 176 |  |  |  |  |  |  | # $username - The username to send SMS passcodes to | 
| 177 |  |  |  |  |  |  | # $device   - ID of the device to which to send passcodes (optional) | 
| 178 |  |  |  |  |  |  | # | 
| 179 |  |  |  |  |  |  | # Returns: undef | 
| 180 |  |  |  |  |  |  | #  Throws: Net::Duo::Exception on failure | 
| 181 |  |  |  |  |  |  | sub send_sms_passcodes { | 
| 182 | 2 |  |  | 2 | 1 | 11 | my ($self, $username, $device) = @_; | 
| 183 | 2 |  | 100 |  |  | 14 | my $data = { | 
| 184 |  |  |  |  |  |  | username => $username, | 
| 185 |  |  |  |  |  |  | factor   => 'sms', | 
| 186 |  |  |  |  |  |  | device   => $device // 'auto', | 
| 187 |  |  |  |  |  |  | }; | 
| 188 | 2 |  |  |  |  | 16 | my $result = $self->call_json('POST', '/auth/v2/auth', $data); | 
| 189 | 2 | 50 |  |  |  | 10 | if ($result->{status} ne 'sent') { | 
| 190 | 0 |  |  |  |  | 0 | my $status  = $result->{status}; | 
| 191 | 0 |  |  |  |  | 0 | my $message = $result->{status_msg}; | 
| 192 | 0 |  |  |  |  | 0 | my $error   = "sending SMS passcodes returned $status: $message"; | 
| 193 | 0 |  |  |  |  | 0 | die Net::Duo::Exception->protocol($error, $result); | 
| 194 |  |  |  |  |  |  | } | 
| 195 | 2 |  |  |  |  | 14 | return; | 
| 196 |  |  |  |  |  |  | } | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | 1; | 
| 199 |  |  |  |  |  |  | __END__ |