File Coverage

blib/lib/Auth/Yubikey_WebClient.pm
Criterion Covered Total %
statement 18 101 17.8
branch 0 28 0.0
condition 0 9 0.0
subroutine 6 10 60.0
pod 4 4 100.0
total 28 152 18.4


line stmt bran cond sub pod time code
1             package Auth::Yubikey_WebClient;
2              
3 1     1   14788 use warnings;
  1         2  
  1         33  
4 1     1   4 use strict;
  1         1  
  1         17  
5 1     1   502 use MIME::Base64;
  1         626  
  1         61  
6 1     1   401 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
  1         5802  
  1         82  
7 1     1   924 use LWP::UserAgent;
  1         35364  
  1         49  
8 1     1   11 use URI::Escape;
  1         1  
  1         967  
9              
10             =head1 NAME
11              
12             Auth::Yubikey_WebClient - Authenticating the Yubikey against the Yubico Web API
13              
14             =head1 VERSION
15              
16             Version 4.01
17              
18             =cut
19              
20             our $VERSION = '4.01';
21              
22             =head1 SYNOPSIS
23              
24             Authenticate against the Yubico server via the Web API in Perl
25              
26             Sample CGI script :-
27              
28             #!/usr/bin/perl
29              
30             use CGI;
31             use strict;
32              
33             my $cgi = new CGI;
34             my $otp = $cgi->param("otp");
35              
36             print $cgi->header();
37             print "\n";
38             print "
Yubikey :
\n";
39              
40             use Auth::Yubikey_WebClient;
41              
42             my $id = "";
43             my $api = "";
44             my $nonce = "";
45              
46             if($otp)
47             {
48             my $result = Auth::Yubikey_WebClient::yubikey_webclient($otp,$id,$api,$nonce);
49             # result can be either ERR or OK
50              
51             print "Authentication result : $result
";
52             }
53              
54             print "\n";
55              
56              
57             =head1 FUNCTIONS
58              
59             =head2 new
60              
61             Creates a new Yubikey Webclient connection
62              
63             use Auth::Yubikey_WebClient;
64              
65             my $yubi = Auth::Yubikey_WebClient->new({
66             id => ,
67             api => '' ,
68             nonce => '',
69             verify_hostname => 0 # optional - defaults to 1. Can be set to 0 if you do not want to check the validity of the SSL certificate when querying the Yubikey server
70             });
71              
72             You can overwrite the URL called if you want to call an alternate authentication server as well :-
73              
74             use Auth::Yubikey_WebClient;
75              
76             my $yubi = Auth::Yubikey_WebClient->new({
77             id => ,
78             api => '' ,
79             nonce => '',
80             url => 'http://www.otherserver.com/webapi.php'
81             });
82              
83             =cut
84              
85             sub new
86             {
87 0     0 1   my ($class,$options_ref) = @_;
88 0           my $self = {};
89              
90 0   0       bless $self, ref $class || $class;
91            
92 0 0         if(! defined $options_ref)
93             {
94 0           die "You did not pass any parameters to the Yubikey Web Client initialization";
95             }
96 0           my %options = %{$options_ref};
  0            
97              
98             # grab the variables from the initialization
99 0 0         if(defined $options{id})
100             {
101 0           $self->{id} = $options{id};
102             }
103             else
104             {
105 0           die "Can not start without a Yubikey ID";
106             }
107              
108 0 0         if(defined $options{api})
109             {
110 0           $self->{api} = $options{api};
111              
112 0 0         if(length($self->{api}) % 4 != 0)
113             {
114 0           die "Your API key must be in 4 byte lengths";
115             }
116             }
117             else
118             {
119 0           die "Can not start without a Yubikey API key";
120             }
121              
122 0 0         $self->{nonce} = defined $options{nonce} ? $options{nonce} : '';
123              
124 0 0         $self->{url} = defined $options{url} ? $options{url} : 'https://api2.yubico.com/wsapi/2.0/verify';
125              
126 0 0         $self->{verify_hostname} = defined $options{verify_hostname} ? $options{verify_hostname} : 1;
127              
128 0           return $self;
129             }
130              
131             =head2 debug
132              
133             Displays the debug info
134              
135             $yubi->debug();
136              
137             Prints out some debug information. Useful to be called after authentication to see what Yubico sent back. You can also call the variables yourself, for example if you'd like to see what the token ID is, call $yubi->{publicid}. The same goes for all the other variables printed in debug.
138              
139             =cut
140              
141             sub debug
142             {
143 0     0 1   my ($self) = @_;
144              
145 0           print "id = $self->{id}\n";
146 0           print "api = $self->{api}\n";
147 0           print "url = $self->{url}\n";
148 0           print "nonce = $self->{nonce}\n";
149 0           print "params = $self->{params}\n";
150 0           print "status = $self->{status}\n";
151 0           print "otp = $self->{otp}\n";
152 0           print "publicid = $self->{publicid}\n";
153 0           print "t = $self->{t}\n";
154 0           print "sl = $self->{sl}\n";
155 0           print "timestamp = $self->{timestamp}\n";
156 0           print "sessioncounter = $self->{sessioncounter}\n";
157 0           print "sessionuse = $self->{sessionuse}\n";
158              
159             # print "response = $self->{response}\n";
160            
161             }
162              
163             =head2 yubikey_webclient
164              
165             =cut
166              
167             sub yubikey_webclient
168             {
169 0     0 1   my ($otp,$id,$api,$nonce) = @_;
170              
171 0           my $yubi_tmp = new Auth::Yubikey_WebClient ( { id => $id, api => $api, nonce => $nonce } );
172              
173 0           return $yubi_tmp->otp($otp);
174             }
175              
176             =head2 otp
177              
178             Check a OTP for validity
179              
180             $result = $yubi->otp($otp);
181              
182             Call the otp procedure with the input from the yubikey. It will return the result.
183              
184             This function will also setup a few internal variables that was returned from Yubico.
185              
186             =cut
187              
188             sub otp
189             {
190 0     0 1   my ($self,$otp) = @_;
191              
192 0           chomp($otp);
193 0           $self->{otp} = $otp;
194              
195             # lets do a basic sanity check on the otp, before we blast it off to yubico...
196 0 0 0       if($self->{otp} !~ /[cbdefghijklnrtuv]/i || length($self->{otp}) < 32)
197             {
198 0           $self->{status} = "ERR_BAD_OTP";
199 0           return $self->{status};
200             }
201              
202             # Generate nonce unless passed
203 0 0         $self->{nonce} = hmac_sha1_hex(time, rand()) unless $self->{nonce};
204              
205             # Start generating the parameters
206 0           $self->{params} = "id=$self->{id}&nonce=$self->{nonce}&otp=" . uri_escape($self->{otp}) . "×tamp=1";
207 0           $self->{params} .= '&h=' . uri_escape(encode_base64(hmac_sha1($self->{params}, decode_base64($self->{api})), ''));
208              
209             # pass the request to yubico
210 0           my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => $self->{verify_hostname} });
211 0           my $req = HTTP::Request->new(GET => $self->{url} . "?$self->{params}");
212 0           my $res = $ua->request($req);
213 0 0         if($res->is_success)
214             {
215 0           $self->{response} = $res->content;
216             }
217             else
218             {
219 0           print $res->status_line . "\n";
220             }
221 0           chomp($self->{response});
222              
223 0 0         if($self->{response} !~ /status=ok/i)
224             {
225             # If the status is not ok, let's not even go through the rest...
226 0           $self->{response} =~ m/status=(.+)/;
227 0           $self->{status} = "ERR_$1";
228 0           $self->{status} =~ s/\s//g;
229 0           return $self->{status};
230             }
231              
232             #extract each of the lines, and store in a hash...
233              
234 0           my %result;
235 0           foreach (split(/\n/,$self->{response}))
236             {
237 0           chomp;
238 0 0         if($_ =~ /=/)
239             {
240 0           ($a,$b) = split(/=/,$_,2);
241 0           $b =~ s/\s//g;
242 0           $result{$a} = $b;
243 0           $self->{$a} = $b;
244             }
245             }
246              
247             # save the h parameter, that's what we'll be comparing to
248              
249 0           my $signatur=$result{h};
250 0           delete $result{h};
251 0           my $datastring='';
252              
253 0           my $key;
254 0           foreach $key (sort keys %result)
255             {
256 0           $result{$key} =~ s/\s//g;
257 0           $datastring .= "$key=$result{$key}&";
258             }
259 0           $datastring = substr($datastring,0,length($datastring)-1);
260              
261             # Check that nonce and OTP are the ones we asked for
262 0           $self->{status} = "ERR_MSG_AUTH";
263              
264 0 0 0       return "ERR_MSG_AUTH" unless ($self->{nonce} eq $result{nonce} and $self->{otp} eq $result{otp});
265              
266 0           my $hmac = encode_base64(hmac_sha1($datastring,decode_base64($self->{api})));
267              
268 0           chomp($hmac);
269              
270 0 0         if($hmac eq $signatur)
271             {
272 0           $self->{publicid} = substr(lc($self->{otp}),0,12);
273 0           $self->{status} = "OK";
274 0           return "OK";
275             }
276             else
277             {
278 0           $self->{status} = "ERR_HMAC";
279 0           return "ERR_HMAC";
280             }
281             }
282              
283             =head1 USAGE
284              
285             Before you can use this module, you need to register for an API key at Yubico. This is as simple as logging onto and entering your Yubikey's OTP and your email address. Once you have the API and ID, you need to provide those details to the module to work.
286              
287             =head1 AUTHOR
288              
289             Phil Massyn, C<< >>
290              
291             =head1 BUGS
292              
293             Please report any bugs or feature requests to C, or through
294             the web interface at L. I will be notified, and then you'll
295             automatically be notified of progress on your bug as I make changes.
296              
297              
298              
299              
300             =head1 SUPPORT
301              
302             You can find documentation for this module with the perldoc command.
303              
304             perldoc Auth::Yubikey_WebClient
305              
306              
307             You can also look for information at:
308              
309             =over 4
310              
311             =item * RT: CPAN's request tracker
312              
313             L
314              
315             =item * AnnoCPAN: Annotated CPAN documentation
316              
317             L
318              
319             =item * CPAN Ratings
320              
321             L
322              
323             =item * Search CPAN
324              
325             L
326              
327             =back
328              
329             =head1 Version history
330              
331             0.04 - Fixed bug L
332             1.00 - Added validation of the request to Yubico (Thanks to Kirill Miazine)
333             2.00 - Added nounce coding (Thanks to Ludvig af Klinteberg)
334             2.01 - Response turning into an array due to \r bug (Thanks to Peter Norin)
335             3.00 - Major update
336             4.01 - 13.10.2016 - Requested by Peter Norin - update to use LWP::UserAgent, and the option to overwrite a valid SSL certificate (verify_hostname). The API default server is changed to ssl.
337              
338             =head1 ACKNOWLEDGEMENTS
339              
340             =head1 COPYRIGHT & LICENSE
341              
342             Copyright 2016 Phil Massyn, all rights reserved.
343              
344             This program is free software; you can redistribute it and/or modify it
345             under the same terms as Perl itself.
346              
347              
348             =cut
349              
350             ; # End of Auth::Yubikey_WebClient