File Coverage

blib/lib/Auth/Yubikey_WebClient.pm
Criterion Covered Total %
statement 18 102 17.6
branch 0 28 0.0
condition 0 9 0.0
subroutine 6 10 60.0
pod 4 4 100.0
total 28 153 18.3


line stmt bran cond sub pod time code
1             package Auth::Yubikey_WebClient;
2              
3 1     1   69340 use warnings;
  1         3  
  1         33  
4 1     1   5 use strict;
  1         2  
  1         19  
5 1     1   505 use MIME::Base64;
  1         683  
  1         62  
6 1     1   454 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
  1         5215  
  1         56  
7 1     1   707 use LWP::UserAgent;
  1         50356  
  1         36  
8 1     1   9 use URI::Escape;
  1         2  
  1         1212  
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.02
17              
18             =cut
19              
20             our $VERSION = '4.02';
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 0           die "You did not pass any parameters to the Yubikey Web Client initialization";
94             }
95 0           my %options = %{$options_ref};
  0            
96              
97             # grab the variables from the initialization
98 0 0         if(defined $options{id}) {
99 0           $self->{id} = $options{id};
100             } else {
101 0           die "Can not start without a Yubikey ID";
102             }
103              
104 0 0         if(defined $options{api}) {
105 0           $self->{api} = $options{api};
106              
107 0 0         if(length($self->{api}) % 4 != 0) {
108 0           die "Your API key must be in 4 byte lengths";
109             }
110             } else {
111 0           die "Can not start without a Yubikey API key";
112             }
113              
114 0 0         $self->{nonce} = defined $options{nonce} ? $options{nonce} : '';
115              
116 0 0         $self->{url} = defined $options{url} ? $options{url} : 'https://api2.yubico.com/wsapi/2.0/verify';
117              
118 0 0         $self->{verify_hostname} = defined $options{verify_hostname} ? $options{verify_hostname} : 1;
119              
120 0           return $self;
121             }
122              
123             =head2 debug
124              
125             Displays the debug info
126              
127             $yubi->debug();
128              
129             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.
130              
131             =cut
132              
133             sub debug
134             {
135 0     0 1   my ($self) = @_;
136              
137 0           print "id = $self->{id}\n";
138 0           print "api = $self->{api}\n";
139 0           print "url = $self->{url}\n";
140 0           print "nonce = $self->{nonce}\n";
141 0           print "params = $self->{params}\n";
142 0           print "status = $self->{status}\n";
143 0           print "otp = $self->{otp}\n";
144 0           print "publicid = $self->{publicid}\n";
145 0           print "t = $self->{t}\n";
146 0           print "sl = $self->{sl}\n";
147 0           print "timestamp = $self->{timestamp}\n";
148 0           print "sessioncounter = $self->{sessioncounter}\n";
149 0           print "sessionuse = $self->{sessionuse}\n";
150              
151             # print "response = $self->{response}\n";
152              
153             }
154              
155             =head2 yubikey_webclient
156              
157             =cut
158              
159             sub yubikey_webclient
160             {
161 0     0 1   my ($otp,$id,$api,$nonce) = @_;
162              
163 0           my $yubi_tmp = new Auth::Yubikey_WebClient ( { id => $id, api => $api, nonce => $nonce } );
164              
165 0           return $yubi_tmp->otp($otp);
166             }
167              
168             =head2 otp
169              
170             Check a OTP for validity
171              
172             $result = $yubi->otp($otp);
173              
174             Call the otp procedure with the input from the yubikey. It will return the result.
175              
176             This function will also setup a few internal variables that was returned from Yubico.
177              
178             =cut
179              
180             sub otp
181             {
182 0     0 1   my ($self,$otp) = @_;
183              
184 0           chomp($otp);
185 0           $self->{otp} = $otp;
186              
187             # lets do a basic sanity check on the otp, before we blast it off to yubico...
188 0 0 0       if($self->{otp} !~ /[cbdefghijklnrtuv]/i || length($self->{otp}) < 32) {
189 0           $self->{status} = "ERR_BAD_OTP";
190 0           return $self->{status};
191             }
192              
193             # Generate nonce unless passed
194 0 0         $self->{nonce} = hmac_sha1_hex(time, rand()) unless $self->{nonce};
195              
196             # Start generating the parameters
197 0           $self->{params} = "id=$self->{id}&nonce=$self->{nonce}&otp=" . uri_escape($self->{otp}) . "×tamp=1";
198 0           $self->{params} .= '&h=' . uri_escape(encode_base64(hmac_sha1($self->{params}, decode_base64($self->{api})), ''));
199              
200             # pass the request to yubico
201 0           my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => $self->{verify_hostname} });
202 0           $ua->env_proxy(); # 4.02
203 0           my $req = HTTP::Request->new(GET => $self->{url} . "?$self->{params}");
204 0           my $res = $ua->request($req);
205 0 0         if($res->is_success) {
206 0           $self->{response} = $res->content;
207             } else {
208 0           print $res->status_line . "\n";
209             }
210 0           chomp($self->{response});
211              
212 0 0         if($self->{response} !~ /status=ok/i) {
213             # If the status is not ok, let's not even go through the rest...
214 0           $self->{response} =~ m/status=(.+)/;
215 0           $self->{status} = "ERR_$1";
216 0           $self->{status} =~ s/\s//g;
217 0           return $self->{status};
218             }
219              
220             #extract each of the lines, and store in a hash...
221              
222 0           my %result;
223 0           foreach (split(/\n/,$self->{response})) {
224 0           chomp;
225 0 0         if($_ =~ /=/)
226             {
227 0           ($a,$b) = split(/=/,$_,2);
228 0           $b =~ s/\s//g;
229 0           $result{$a} = $b;
230 0           $self->{$a} = $b;
231             }
232             }
233              
234             # save the h parameter, that's what we'll be comparing to
235              
236 0           my $signatur=$result{h};
237 0           delete $result{h};
238 0           my $datastring='';
239              
240 0           my $key;
241 0           foreach $key (sort keys %result) {
242 0           $result{$key} =~ s/\s//g;
243 0           $datastring .= "$key=$result{$key}&";
244             }
245 0           $datastring = substr($datastring,0,length($datastring)-1);
246              
247             # Check that nonce and OTP are the ones we asked for
248 0           $self->{status} = "ERR_MSG_AUTH";
249              
250 0 0 0       return "ERR_MSG_AUTH" unless ($self->{nonce} eq $result{nonce} and $self->{otp} eq $result{otp});
251              
252 0           my $hmac = encode_base64(hmac_sha1($datastring,decode_base64($self->{api})));
253 0           chomp($hmac);
254 0 0         if($hmac eq $signatur) {
255 0           $self->{publicid} = substr(lc($self->{otp}),0,12);
256 0           $self->{status} = "OK";
257 0           return "OK";
258             } else {
259 0           $self->{status} = "ERR_HMAC";
260 0           return "ERR_HMAC";
261             }
262             }
263              
264             =head1 USAGE
265              
266             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.
267              
268             =head1 AUTHOR
269              
270             Phil Massyn, C<< >>
271              
272             =head1 BUGS
273              
274             Please report any bugs or feature requests to C, or through
275             the web interface at L. I will be notified, and then you'll
276             automatically be notified of progress on your bug as I make changes.
277              
278              
279              
280              
281             =head1 SUPPORT
282              
283             You can find documentation for this module with the perldoc command.
284              
285             perldoc Auth::Yubikey_WebClient
286              
287              
288             You can also look for information at:
289              
290             =over 4
291              
292             =item * RT: CPAN's request tracker
293              
294             L
295              
296             =item * AnnoCPAN: Annotated CPAN documentation
297              
298             L
299              
300             =item * CPAN Ratings
301              
302             L
303              
304             =item * Search CPAN
305              
306             L
307              
308             =back
309              
310             =head1 Version history
311              
312             0.04 - Fixed bug L
313             1.00 - Added validation of the request to Yubico (Thanks to Kirill Miazine)
314             2.00 - Added nounce coding (Thanks to Ludvig af Klinteberg)
315             2.01 - Response turning into an array due to \r bug (Thanks to Peter Norin)
316             3.00 - Major update
317             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.
318             4.02 - 2019.04.04 - Request by Alexandre Linte - Support for proxy servers
319              
320             =head1 ACKNOWLEDGEMENTS
321              
322             =head1 COPYRIGHT & LICENSE
323              
324             Copyright 2016 Phil Massyn, all rights reserved.
325              
326             This program is free software; you can redistribute it and/or modify it
327             under the same terms as Perl itself.
328              
329              
330             =cut
331              
332             ; # End of Auth::Yubikey_WebClient