package Net::OATH::Server::Lite::Endpoint::Login; use strict; use warnings; use overload q(&{}) => sub { shift->psgi_app }, fallback => 1; use Try::Tiny qw/try catch/; use Plack::Request; use Params::Validate; use JSON::XS qw/decode_json encode_json/; use Authen::OATH; use Net::OATH::Server::Lite::Error; my %DIGEST_MAP = ( SHA1 => q{Digest::SHA1}, MD5 => q{Digest::MD5}, # TODO: Support SHA256, SHA512 # SHA256 => q{Digest::SHA256}, # SHA512 => q{Digest::SHA512}, ); sub new { my $class = shift; my %args = Params::Validate::validate(@_, { data_handler => 1, }); my $self = bless { data_handler => $args{data_handler}, }, $class; return $self; } sub data_handler { my ($self, $handler) = @_; $self->{data_handler} = $handler if $handler; $self->{data_handler}; } sub psgi_app { my $self = shift; return $self->{psgi_app} ||= $self->compile_psgi_app; } sub compile_psgi_app { my $self = shift; my $app = sub { my $env = shift; my $req = Plack::Request->new($env); my $res; try { $res = $self->handle_request($req); } catch { # Internal Server Error warn $_; $res = $req->new_response(500); }; return $res->finalize; }; return $app; } sub handle_request { my ($self, $request) = @_; my $res = try { # DataHandler my $data_handler = $self->{data_handler}->new(request => $request); Net::OATH::Server::Lite::Error->throw( code => 500, error => q{server_error}, ) unless ($data_handler && $data_handler->isa(q{Net::OATH::Server::Lite::DataHandler})); # REQUEST_METHOD Net::OATH::Server::Lite::Error->throw() unless ($request->method eq q{POST}); my $params; eval { $params = decode_json($request->content); }; Net::OATH::Server::Lite::Error->throw() unless $params; # Params my $id = $params->{id} or Net::OATH::Server::Lite::Error->throw( description => q{missing id}, ); my $password = $params->{password} or Net::OATH::Server::Lite::Error->throw( description => q{missing password}, ); # obtain user model my $user = $data_handler->select_user($id) or Net::OATH::Server::Lite::Error->throw( code => 404, description => q{invalid id}, ); Net::OATH::Server::Lite::Error->throw( code => 500, error => q{server_error}, ) unless $user->isa(q{Net::OATH::Server::Lite::Model::User}); my $timestamp = ($params->{timestamp}) ? $params->{timestamp} : time(); my $counter = (defined $params->{counter}) ? $params->{counter} : $user->counter; my $is_valid = $self->is_valid_password($password, $user, $timestamp, $counter); if ($user->type eq q{hotp} and !defined $params->{counter}) { $user->counter($user->counter + 1); $data_handler->update_user($user); } if ($is_valid) { my $response_params = { id => $user->id, }; return $request->new_response(200, [ "Content-Type" => "application/json;charset=UTF-8", "Cache-Control" => "no-store", "Pragma" => "no-cache" ], [ encode_json($response_params) ]); } else { Net::OATH::Server::Lite::Error->throw( code => 400, description => q{invalid password}, ); } } catch { if ($_->isa("Net::OATH::Server::Lite::Error")) { my $error_params = { error => $_->error, }; $error_params->{error_description} = $_->description if $_->description; return $request->new_response($_->code, [ "Content-Type" => "application/json;charset=UTF-8", "Cache-Control" => "no-store", "Pragma" => "no-cache" ], [ encode_json($error_params) ]); } else { die $_; } }; } sub is_valid_password { my ($self, $password, $user, $timestamp, $counter) = @_; # generate password my $oath = Authen::OATH->new( digits => $user->digits, digest => _digest_for_oath($user->algorithm), timestep => $user->period, ); if ($user->type eq q{totp}) { # TOTP return ($password eq $oath->totp($user->secret, $timestamp)); } else { # HOTP return ($password eq $oath->hotp($user->secret, $counter)); } return 1; } sub _digest_for_oath { my $algorithm = shift; return ($DIGEST_MAP{$algorithm}) ? $DIGEST_MAP{$algorithm} : q{Digest::SHA1}; } 1;