File Coverage

blib/lib/Net/Duo/Auth.pm
Criterion Covered Total %
statement 62 75 82.6
branch 13 20 65.0
condition 10 13 76.9
subroutine 12 12 100.0
pod 4 4 100.0
total 101 124 81.4


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__