File Coverage

blib/lib/Mojolicious/Plugin/YubiVerify.pm
Criterion Covered Total %
statement 43 82 52.4
branch 0 18 0.0
condition 0 15 0.0
subroutine 14 20 70.0
pod 1 1 100.0
total 58 136 42.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::YubiVerify;
2              
3             #
4             # Copyright (c) by Kirill Miazine
5             #
6             # This software is distributed under an ISC-style license, please see
7             # for details.
8             #
9              
10 1     1   22459 use strict;
  1         2  
  1         26  
11 1     1   5 use warnings;
  1         2  
  1         32  
12 1     1   5 use base 'Mojolicious::Plugin';
  1         6  
  1         3902  
13              
14             our $VERSION = '0.06';
15              
16 1     1   17338 use Mojo::UserAgent;
  1         304025  
  1         9  
17 1     1   856 use URI::Escape qw(uri_escape);
  1         1511  
  1         63  
18 1     1   6 use MIME::Base64 qw(encode_base64 decode_base64);
  1         2  
  1         51  
19 1     1   691 use Digest::HMAC_SHA1 qw(hmac_sha1); # Mojo::Util's hmac_sha1_sum gives HEX
  1         1633  
  1         53  
20 1     1   810 use String::Random qw(random_string);
  1         4165  
  1         131  
21 1     1   8 use List::Util qw(shuffle);
  1         2  
  1         68  
22              
23 1     1   5 use constant API_ID => 1851; # API id and API key are "borrowed" from
  1         2  
  1         67  
24 1     1   4 use constant API_KEY => 'oBVbNt7IZehZGR99rvq8d6RZ1DM='; # http://demo.yubico.com/php-yubico/demo.php
  1         2  
  1         70  
25 1     1   5 use constant API_URLS => map { sprintf('http://api%s.yubico.com/wsapi/2.0/verify', $_) } ('', 2..5);
  1         1  
  1         3  
  5         68  
26 1     1   5 use constant PARALLEL => 2;
  1         1  
  1         76  
27 1         826 use constant STATUSMAP => (
28             OK => 'The OTP is valid.',
29             BAD_OTP => 'The OTP is invalid format.',
30             REPLAYED_OTP => 'The OTP has already been seen by the service.',
31             BAD_SIGNATURE => 'The HMAC signature verification failed.',
32             MISSING_PARAMETER => 'The request lacks a parameter.',
33             NO_SUCH_CLIENT => 'The request id does not exist.',
34             OPERATION_NOT_ALLOWED => 'The request id is not allowed to verify OTPs.',
35             BACKEND_ERROR => 'Unexpected error in our server. Please contact us if you see this error.',
36             NOT_ENOUGH_ANSWERS => 'Server could not get requested number of syncs during before timeout.',
37             REPLAYED_REQUEST => 'Server has seen the OTP/Nonce combination before.',
38 1     1   5 );
  1         2  
39              
40             sub register {
41 0     0 1   my ($plugin, $app, $conf) = @_;
42              
43 0   0       $conf->{'api_id'} ||= API_ID;
44 0   0       $conf->{'api_key'} ||= API_KEY;
45 0   0       $conf->{'parallel'} ||= PARALLEL;
46              
47             $app->helper(
48             yubi_verify => sub {
49 0     0     my $self = shift;
50 0 0         my $otp = shift or return;
51 0           my $ret_res = shift; # for testing
52              
53 0           my $ua = Mojo::UserAgent->new;
54 0           my $nonce = random_string('c' x 40);
55             my $query_string = _signedq(
56             $conf->{'api_key'},
57 0           id => $conf->{'api_id'},
58             otp => $otp,
59             nonce => $nonce,
60             timestamp => 1,
61             sl => 42,
62             timeout => undef,
63             );
64 0 0         my @res = grep { ref($_) eq 'HASH' and defined $_->{'status'} }
65 0           map { {_resp2p($_->res->body)} }
66 0 0         grep { $_->res->code and $_->res->code == 200 }
67 0           map { $ua->get("$_?$query_string") }
68 0           (shuffle(API_URLS))[0..($conf->{'parallel'}-1)];
69              
70 0           for my $res (@res) {
71 0 0         next if $res->{'status'} ne 'OK';
72 0 0 0       next if !defined $res->{'otp'} or $res->{'otp'} ne $otp;
73 0 0 0       next if !defined $res->{'nonce'} or $res->{'nonce'} ne $nonce;
74 0           my ($key_id) = ($res->{'otp'} =~ /^(.+)(.{32})$/);
75 0           my $h = delete $res->{'h'};
76             return ($key_id, ($ret_res ? \@res : ()))
77             if $res->{'status'} eq 'OK' and $h eq
78 0           _b64hmacsig(_sortedq(%{$res}),
79 0 0 0       decode_base64($conf->{'api_key'}));
    0          
80             }
81              
82 0 0         return (undef, ($ret_res ? \@res : ()))
83             }
84 0           );
85             }
86              
87             sub _sortedq {
88 0     0     my %p = @_; join('&', map { join('=', $_, uri_escape($p{$_}, '^A-Za-z0-9:._~-')) }
  0            
89 0           sort grep { defined $p{$_} } keys %p);
  0            
90             }
91              
92             sub _b64hmacsig {
93 0     0     encode_base64(hmac_sha1(@_), '');
94             }
95              
96             sub _signedq {
97 0     0     my $key = shift;
98 0           my $q = _sortedq(@_);
99             # avoid BAD_SIGNATURE,
100             # as in http://code.google.com/p/php-yubico/source/browse/trunk/Yubico.php
101 0           (my $h = _b64hmacsig($q, decode_base64($key))) =~ s/\+/%2B/g;
102 0           return "$q&h=$h";
103             }
104              
105             sub _resp2p {
106 0     0     my $p = {map { split /=/, $_, 2 } grep { /=/ } split /\r?\n/, $_[0]};
  0            
  0            
107 0           return map { $_ => $p->{$_} } qw(otp nonce h t status timestamp sessioncounter sessionuse sl);
  0            
108             }
109              
110             42;
111              
112             __END__