File Coverage

blib/lib/AnyEvent/Yubico.pm
Criterion Covered Total %
statement 85 95 89.4
branch 19 32 59.3
condition 2 3 66.6
subroutine 14 14 100.0
pod 0 6 0.0
total 120 150 80.0


line stmt bran cond sub pod time code
1             package AnyEvent::Yubico;
2              
3 1     1   37966 use strict;
  1         2  
  1         30  
4 1     1   1106 use AnyEvent::HTTP;
  1         52546  
  1         113  
5 1     1   4162 use UUID::Tiny;
  1         76955  
  1         214  
6 1     1   12 use MIME::Base64;
  1         2  
  1         55  
7 1     1   1293 use Digest::HMAC_SHA1 qw(hmac_sha1);
  1         2596  
  1         98  
8 1     1   1306 use URI::Escape;
  1         2803  
  1         1670  
9              
10             our $VERSION = '0.9.3';
11              
12             # Creates a new Yubico instance to be used for validation of OTPs.
13             sub new {
14 2     2 0 2701 my $class = shift;
15 2         16 my $self = {
16             sign_request => 1,
17             local_timeout => 30.0,
18             urls => [
19             "https://api.yubico.com/wsapi/2.0/verify",
20             "https://api2.yubico.com/wsapi/2.0/verify",
21             "https://api3.yubico.com/wsapi/2.0/verify",
22             "https://api4.yubico.com/wsapi/2.0/verify",
23             "https://api5.yubico.com/wsapi/2.0/verify"
24             ]
25             };
26              
27 2         5 my $options = shift;
28              
29 2         11 $self = { %$self, %$options };
30              
31 2         11 return bless $self, $class;
32             };
33              
34             # Verifies the given OTP and returns a true value if the OTP could be
35             # verified, false otherwise.
36             sub verify {
37 1     1 0 7 return verify_async(@_)->recv->{status} eq 'OK';
38             }
39              
40             # Verifies the given OTP and returns a hash containing the server response.
41             sub verify_sync {
42 3     3 0 8734 return verify_async(@_)->recv
43             }
44              
45             # Non-blocking version of verify_sync, which returns a condition variable
46             # (see AnyEvent->condvar for details).
47             sub verify_async {
48 5     5 0 14 my($self, $otp, $callback) = @_;
49              
50 5         31 my $nonce = create_UUID_as_string(UUID_V4);
51 5         1636 $nonce =~ s/-//g;
52              
53 5         33 my $params = {
54             id => $self->{client_id},
55             nonce => $nonce,
56             otp => $otp
57             };
58              
59 5 50       31 if(exists $self->{timeout}) {
60 0         0 $params->{timeout} = $self->{timeout};
61             }
62 5 50       16 if(exists $self->{sl}) {
63 0         0 $params->{sl} = $self->{sl};
64             }
65 5 50       22 if($self->{timestamp}) {
66 0         0 $params->{timestamp} = 1;
67             }
68              
69 5 100 66     47 if($self->{sign_request} and !$self->{api_key} eq '') {
70 4         15 $params->{h} = $self->sign($params);
71             }
72              
73 5         11 my $query = "";
74 5         17 for my $key (keys %$params) {
75 19         303 $query = "$query&$key=".uri_escape($params->{$key});
76             }
77 5         62 $query = "?".substr($query, 1);
78            
79 5         9 my $last_response;
80 5         11 my @requests = ();
81 5         172 my $result_var = AnyEvent->condvar(cb => $callback);
82             my $inner_var = AnyEvent->condvar(cb => sub {
83 5     5   107 my $result = shift->recv;
84              
85 5         55 foreach my $req (@requests) {
86 21         11224 undef $req;
87             }
88              
89 5 100       76 if(exists $result->{status}) {
    50          
90 3         21 $result_var->send($result);
91             } elsif(exists $last_response->{status}) {
92             #All responses returned replayed request.
93 2         14 $result_var->send($last_response);
94             } else {
95             #Didn't get any valid responses.
96 0         0 $result_var->croak("No valid response!");
97             }
98 5         6493 });
99              
100 5         37 foreach my $url (@{$self->{urls}}) {
  5         20  
101 21         21789 $inner_var->begin();
102             push(@requests, http_get("$url$query",
103             timeout => $self->{local_timeout},
104             tls_ctx => 'high',
105             sub {
106 9     9   3015554 my($body, $hdr) = @_;
107              
108 9 100       161 if(not $hdr->{Status} =~ /^2/) {
109             #Error, store message if none exists.
110 6 100       29 if(not exists $last_response->{status}) {
111 2         8 $last_response->{status} = $hdr->{Reason};
112             }
113 6         27 $inner_var->end();
114 6         63 return;
115             }
116              
117 3         84 my $response = parse_response($body);
118              
119 3 50       16 if(! exists $response->{status}) {
120             #Response does not look valid, discard.
121 0         0 $inner_var->end();
122 0         0 return;
123             }
124              
125 3 100       95 if(! $self->{api_key} eq '') {
126 2         8 my $signature = $response->{h};
127 2         5 delete $response->{h};
128 2 50       11 if(! $signature eq $self->sign($response)) {
129 0         0 $response->{status} = "BAD_RESPONSE_SIGNATURE";
130             }
131             }
132              
133 3         6 $last_response = $response;
134              
135 3 50       14 if($response->{status} eq "REPLAYED_REQUEST") {
136             #Replayed request, wait for next.
137 0         0 $inner_var->end();
138             } else {
139             #Definitive response, return it.
140 3 50       12 if($response->{status} eq "OK") {
141 0 0       0 $inner_var->croak("Response nonce does not match!") if(! $nonce eq $response->{nonce});
142 0 0       0 $inner_var->croak("Response OTP does not match!") if(! $otp eq $response->{otp});
143             }
144              
145 3         25 $inner_var->send($response);
146             }
147 21         448 }));
148             }
149              
150 5         21053 return $result_var;
151             };
152              
153             # Signs a parameter hash using the client API key.
154             sub sign {
155 8     8 0 7642 my ($self, $params) = @_;
156 8         18 my $content = "";
157              
158 8         135 foreach my $key (sort keys %$params) {
159 23         73 $content = $content."&$key=$params->{$key}";
160             }
161 8         26 $content = substr($content, 1);
162              
163 8         55 my $key = decode_base64($self->{api_key});
164 8         50 my $signature = encode_base64(hmac_sha1($content, $key), '');
165              
166 8         302 return $signature;
167             }
168              
169             # Parses a response body into a hash.
170             sub parse_response {
171 3     3 0 34 my $body = shift;
172 3         9 my $response = {};
173              
174 3 50       18 if($body) {
175 3         20 my @lines = split(' ', $body);
176 3         11 foreach my $line (@lines) {
177 11         20 my $index = index($line, '=');
178 11         103 $response->{substr($line, 0, $index)} = substr($line, $index+1);
179             }
180             }
181              
182 3         10 return $response;
183             }
184              
185             1;
186             __END__