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__ |