File Coverage

blib/lib/Net/OATH/Server/Lite/Endpoint/Login.pm
Criterion Covered Total %
statement 22 25 88.0
branch n/a
condition n/a
subroutine 8 9 88.8
pod n/a
total 30 34 88.2


line stmt bran cond sub pod time code
1             package Net::OATH::Server::Lite::Endpoint::Login;
2 4     4   36098 use strict;
  4         9  
  4         124  
3 4     4   18 use warnings;
  4         17  
  4         167  
4             use overload
5 0     0   0 q(&{}) => sub { shift->psgi_app },
6 4     4   1965 fallback => 1;
  4         1634  
  4         28  
7              
8 4     4   668 use Try::Tiny qw/try catch/;
  4         1835  
  4         218  
9 4     4   1873 use Plack::Request;
  4         203924  
  4         117  
10 4     4   506 use Params::Validate;
  4         5520  
  4         240  
11 4     4   2033 use JSON::XS qw/decode_json encode_json/;
  4         12790  
  4         238  
12              
13 4     4   3463 use Authen::OATH;
  0            
  0            
14             use Net::OATH::Server::Lite::Error;
15              
16             my %DIGEST_MAP = (
17             SHA1 => q{Digest::SHA1},
18             MD5 => q{Digest::MD5},
19             # TODO: Support SHA256, SHA512
20             # SHA256 => q{Digest::SHA256},
21             # SHA512 => q{Digest::SHA512},
22             );
23              
24             sub new {
25             my $class = shift;
26             my %args = Params::Validate::validate(@_, {
27             data_handler => 1,
28             });
29             my $self = bless {
30             data_handler => $args{data_handler},
31             }, $class;
32             return $self;
33             }
34              
35             sub data_handler {
36             my ($self, $handler) = @_;
37             $self->{data_handler} = $handler if $handler;
38             $self->{data_handler};
39             }
40              
41             sub psgi_app {
42             my $self = shift;
43             return $self->{psgi_app}
44             ||= $self->compile_psgi_app;
45             }
46              
47             sub compile_psgi_app {
48             my $self = shift;
49              
50             my $app = sub {
51             my $env = shift;
52             my $req = Plack::Request->new($env);
53             my $res; try {
54             $res = $self->handle_request($req);
55             } catch {
56             # Internal Server Error
57             warn $_;
58             $res = $req->new_response(500);
59             };
60             return $res->finalize;
61             };
62              
63             return $app;
64             }
65              
66             sub handle_request {
67             my ($self, $request) = @_;
68              
69             my $res = try {
70              
71             # DataHandler
72             my $data_handler = $self->{data_handler}->new(request => $request);
73             Net::OATH::Server::Lite::Error->throw(
74             code => 500,
75             error => q{server_error},
76             ) unless ($data_handler && $data_handler->isa(q{Net::OATH::Server::Lite::DataHandler}));
77              
78             # REQUEST_METHOD
79             Net::OATH::Server::Lite::Error->throw()
80             unless ($request->method eq q{POST});
81              
82             my $params;
83             eval {
84             $params = decode_json($request->content);
85             };
86             Net::OATH::Server::Lite::Error->throw() unless $params;
87              
88             # Params
89             my $id = $params->{id} or
90             Net::OATH::Server::Lite::Error->throw(
91             description => q{missing id},
92             );
93              
94             my $password = $params->{password} or
95             Net::OATH::Server::Lite::Error->throw(
96             description => q{missing password},
97             );
98              
99             # obtain user model
100             my $user = $data_handler->select_user($id) or
101             Net::OATH::Server::Lite::Error->throw(
102             code => 404,
103             description => q{invalid id},
104             );
105             Net::OATH::Server::Lite::Error->throw(
106             code => 500,
107             error => q{server_error},
108             ) unless $user->isa(q{Net::OATH::Server::Lite::Model::User});
109              
110             my $timestamp = ($params->{timestamp}) ? $params->{timestamp} : time();
111             my $counter = (defined $params->{counter}) ? $params->{counter} : $user->counter;
112             my $is_valid = $self->is_valid_password($password, $user, $timestamp, $counter);
113             if ($user->type eq q{hotp} and !defined $params->{counter}) {
114             $user->counter($user->counter + 1);
115             $data_handler->update_user($user);
116             }
117              
118             if ($is_valid) {
119             my $response_params = {
120             id => $user->id,
121             };
122             return $request->new_response(200,
123             [ "Content-Type" => "application/json;charset=UTF-8",
124             "Cache-Control" => "no-store",
125             "Pragma" => "no-cache" ],
126             [ encode_json($response_params) ]);
127             } else {
128             Net::OATH::Server::Lite::Error->throw(
129             code => 400,
130             description => q{invalid password},
131             );
132             }
133              
134             } catch {
135             if ($_->isa("Net::OATH::Server::Lite::Error")) {
136             my $error_params = {
137             error => $_->error,
138             };
139             $error_params->{error_description} = $_->description if $_->description;
140              
141             return $request->new_response($_->code,
142             [ "Content-Type" => "application/json;charset=UTF-8",
143             "Cache-Control" => "no-store",
144             "Pragma" => "no-cache" ],
145             [ encode_json($error_params) ]);
146             } else {
147             die $_;
148             }
149             };
150             }
151              
152             sub is_valid_password {
153             my ($self, $password, $user, $timestamp, $counter) = @_;
154              
155             # generate password
156             my $oath =
157             Authen::OATH->new(
158             digits => $user->digits,
159             digest => _digest_for_oath($user->algorithm),
160             timestep => $user->period,
161             );
162              
163             if ($user->type eq q{totp}) {
164             # TOTP
165             return ($password eq $oath->totp($user->secret, $timestamp));
166             } else {
167             # HOTP
168             return ($password eq $oath->hotp($user->secret, $counter));
169             }
170              
171             return 1;
172             }
173              
174             sub _digest_for_oath {
175             my $algorithm = shift;
176             return ($DIGEST_MAP{$algorithm}) ? $DIGEST_MAP{$algorithm} : q{Digest::SHA1};
177             }
178              
179             1;