File Coverage

blib/lib/WWW/Suffit/Plugin/ConfigAuth.pm
Criterion Covered Total %
statement 33 162 20.3
branch 0 64 0.0
condition 0 80 0.0
subroutine 11 19 57.8
pod 1 1 100.0
total 45 326 13.8


line stmt bran cond sub pod time code
1             package WWW::Suffit::Plugin::ConfigAuth;
2 1     1   165761 use strict;
  1         8  
  1         46  
3 1     1   6 use warnings;
  1         3  
  1         82  
4 1     1   805 use utf8;
  1         612  
  1         26  
5              
6             =encoding utf8
7              
8             =head1 NAME
9              
10             WWW::Suffit::Plugin::ConfigAuth - The Suffit plugin for authentication and authorization providing via configuration
11              
12             =head1 SYNOPSIS
13              
14             sub startup {
15             my $self = shift->SUPER::startup();
16             $self->plugin('ConfigAuth', {
17             configsection => 'auth',
18             });
19              
20             # . . .
21             }
22              
23             ... configuration:
24              
25             # ConfigAuth configuration
26            
27             User test:5f4dcc3b5aa765d61d8327deb882cf99:role=Regular%20user
28             User admin:5f4dcc3b5aa765d61d8327deb882cf99
29             User foo:bar:status=1&uid=2&name=user
30            
31              
32             =head1 DESCRIPTION
33              
34             The Suffit plugin for authentication and authorization providing via configuration
35              
36             =head1 OPTIONS
37              
38             This plugin supports the following options
39              
40             =head2 configsection
41              
42             configsection => 'auth'
43              
44             This option sets a section name of the config file for define
45             namespace of configuration directives for this plugin
46              
47             Default: none (without section)
48              
49             =head1 HELPERS
50              
51             This plugin provides the following helpers
52              
53             =head2 configauth.init
54              
55             my $init = $self->configauth->init;
56              
57             This method returns the init object (L)
58             that contains data of initialization:
59              
60             {
61             error => '...', # Error message
62             status => 500, # HTTP status code
63             code => 'E7000', # The Suffit error code
64             }
65              
66             For example (in your controller):
67              
68             # Check init status
69             my $init = $self->configauth->init;
70             if (my $err = $init->get('/error')) {
71             $self->reply->error($init->get('/status'),
72             $init->get('/code'), $err);
73             return;
74             }
75              
76             =head2 configauth.authenticate
77              
78             my $auth = $self->configauth->authenticate({
79             username => $username,
80             password => $password,
81             loginpage => 'login', # -- To login-page!!
82             expiration => $remember ? SESSION_EXPIRE_MAX : SESSION_EXPIRATION,
83             realm => "Test zone",
84             });
85             if (my $err = $auth->get('/error')) {
86             if (my $location = $auth->get('/location')) { # Redirect
87             $self->flash(message => $err);
88             $self->redirect_to($location); # 'login' -- To login-page!!
89             } elsif ($auth->get('/status') >= 500) { # Fatal server errors
90             $self->reply->error($auth->get('/status'), $auth->get('/code'), $err);
91             } else { # User errors (show on login page)
92             $self->stash(error => $err);
93             return $self->render;
94             }
95             return;
96             }
97              
98             This helper performs authentication backend subprocess and returns
99             result object (L) that contains data structure:
100              
101             {
102             error => '', # Error message
103             status => 200, # HTTP status code
104             code => 'E0000', # The Suffit error code
105             username => $username, # User name
106             referer => $referer, # Referer
107             loginpage => $loginpage, # Login page for redirects (location)
108             location => undef, # Location URL for redirects
109             }
110              
111             =head2 configauth.authorize
112              
113             my $auth = $self->configauth->authorize({
114             referer => $referer,
115             username => $username,
116             loginpage => 'login', # -- To login-page!!
117             });
118             if (my $err = $auth->get('/error')) {
119             if (my $location = $auth->get('/location')) {
120             $self->flash(message => $err);
121             $self->redirect_to($location); # 'login' -- To login-page!!
122             } else {
123             $self->reply->error($auth->get('/status'), $auth->get('/code'), $err);
124             }
125             return;
126             }
127              
128             This helper performs authorization backend subprocess and returns
129             result object (L) that contains data structure:
130              
131             {
132             error => '', # Error message
133             status => 200, # HTTP status code
134             code => 'E0000', # The Suffit error code
135             username => $username, # User name
136             referer => $referer, # Referer
137             loginpage => $loginpage, # Login page for redirects (location)
138             location => undef, # Location URL for redirects
139             user => { # User data
140             address => "127.0.0.1", # User (client) IP address
141             base => "http://localhost:8080", # Base URL of request
142             comment => "No comments", # Comment
143             email => 'test@example.com', # Email address
144             email_md5 => "a84450...366", # MD5 hash of email address
145             method => "ANY", # Current method of request
146             name => "Bob Smith", # Full user name
147             path => "/", # Current query-path of request
148             role => "Regular user", # User role
149             status => true, # User status in JSON::PP::Boolean notation
150             uid => 1, # User ID
151             username => $username, # User name
152             },
153             }
154              
155             =head1 METHODS
156              
157             Internal methods
158              
159             =head2 register
160              
161             This method register the plugin and helpers in L application
162              
163             =head1 SEE ALSO
164              
165             L, L
166              
167             =head1 AUTHOR
168              
169             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
170              
171             =head1 COPYRIGHT
172              
173             Copyright (C) 1998-2026 D&D Corporation
174              
175             =head1 LICENSE
176              
177             This program is distributed under the terms of the Artistic License Version 2.0
178              
179             See the C file or L for details
180              
181             =cut
182              
183 1     1   690 use Mojo::Base 'Mojolicious::Plugin';
  1         16139  
  1         9  
184              
185             our $VERSION = '1.01';
186              
187 1     1   2950 use Digest::SHA qw/sha224_hex sha256_hex sha384_hex sha512_hex/;
  1         5153  
  1         170  
188 1     1   750 use Mojo::File qw/path/;
  1         315871  
  1         107  
189 1     1   14 use Mojo::Util qw/encode md5_sum sha1_sum hmac_sha1_sum secure_compare/;
  1         2  
  1         79  
190 1     1   874 use Mojo::JSON::Pointer;
  1         1226  
  1         10  
191 1     1   797 use Mojo::Parameters;
  1         3643  
  1         8  
192 1     1   945 use WWW::Suffit::Const qw/ :session /;
  1         3642  
  1         297  
193 1     1   782 use WWW::Suffit::Util qw/json_load json_save/;
  1         72192  
  1         3745  
194              
195             sub register {
196 0     0 1   my ($plugin, $app, $opts) = @_; # $self = $plugin
197 0   0       $opts //= {};
198 0           my $configsection = $opts->{configsection};
199 0           my %payload = ( # Ok by default
200             error => '', # Error message
201             status => 200, # HTTP status code
202             code => 'E0000', # The Suffit error code
203             );
204              
205             # List of users from config
206             $app->helper('configauth.users' => sub {
207 0     0     my $c = shift;
208 0 0         state $users = $configsection
209             ? $c->conf->list("/$configsection/user")
210             : $c->conf->list("/user");
211 0           });
212              
213             # Auth helpers (methods)
214 0           $app->helper('configauth.authenticate'=> \&_authenticate);
215 0           $app->helper('configauth.authorize' => \&_authorize);
216              
217             # Check users
218 0           my $users = $app->configauth->users;
219 0 0         unless (scalar @$users) {
220 0 0         my $err = $configsection
221             ? sprintf("No any users found in %s section of configuration", $configsection)
222             : "No any users found in configuration";
223 0           $app->log->error(sprintf("[E7010] %s", $err));
224 0           $payload{error} = $err;
225 0           $payload{status} = 500;
226 0           $payload{code} = 'E7010';
227 0     0     return $app->helper('configauth.init' => sub { Mojo::JSON::Pointer->new({%payload}) });
  0            
228             }
229             #$app->log->error(Mojo::Util::dumper($users));
230              
231             # Ok
232 0     0     return $app->helper('configauth.init' => sub { Mojo::JSON::Pointer->new({%payload}) });
  0            
233             }
234             sub _authenticate {
235 0     0     my $self = shift;
236 0 0         my %args = scalar(@_) ? scalar(@_) % 2 ? ref($_[0]) eq 'HASH' ? (%{$_[0]}) : () : (@_) : ();
  0 0          
    0          
237 0           my $cache = $self->app->cache;
238 0           my $now = time();
239 0   0       my $username = $args{username} || '';
240 0   0       my $password = $args{password} // '';
241 0 0         $password = encode('UTF-8', $password) if length $password; # chars to bytes
242 0   0       my $referer = $args{referer} // $self->req->headers->header("Referer") // '';
      0        
243 0   0       my $loginpage = $args{loginpage} // '';
244 0   0       my $expiration = $args{expiration} || 0;
245 0           my %payload = ( # Ok by default
246             error => '', # Error message
247             status => 200, # HTTP status code
248             code => 'E0000', # The Suffit error code
249             username => $username, # User name
250             referer => $referer, # Referer
251             loginpage => $loginpage, # Login page for redirects (location)
252             location => undef, # Location URL for redirects
253             );
254 0           my $json_file = path($self->app->datadir, sprintf("u.%s.json", $username));
255 0           my $file = $json_file->to_string;
256              
257             # Check username
258 0 0         unless (length $username) {
259 0           $self->log->error("[E7001] Incorrect username");
260 0           $payload{error} = "Incorrect username";
261 0           $payload{status} = 400;
262 0           $payload{code} = 'E7001';
263 0           return Mojo::JSON::Pointer->new({%payload});
264             }
265              
266             # Get user key and file
267 0           my $ustat_key = sprintf("auth.ustat.%s", hmac_sha1_sum(sprintf("%s:%s", encode('UTF-8', $username), $password), $self->app->mysecret));
268 0   0       my $ustat_tm = $cache->get($ustat_key) || 0;
269 0 0 0       if ($expiration && (-e $file) && ($ustat_tm + $expiration) > $now) { # Ok!
      0        
270 0           $self->log->debug(sprintf("$$: User data is still valid. Expired at %s", scalar(localtime($ustat_tm + $expiration))));
271 0           return Mojo::JSON::Pointer->new({%payload});
272             }
273              
274             # Get password database from cache
275 0           my $pwdb = $cache->get('auth.pwdb');
276 0 0         unless ($pwdb) {
277 0   0       my $users = $self->configauth->users || [];
278 0           $pwdb = { (_parse_pwdb_lines(@$users)) };
279 0           $cache->set('auth.pwdb' => $pwdb); # store whole password database to cache
280             #$self->log->error(Mojo::Util::dumper( $pwdb ));
281             }
282              
283             # Authentication: Check by password database
284 0   0       my $pw = encode('UTF-8', $pwdb->{$username}->{pwd} // '');
285 0   0       my $ar = Mojo::Parameters->new($pwdb->{$username}->{arg} // '')->charset('UTF-8');
286 0 0         unless (_check_pw($password, $pw)) { # Oops. Incorrect username/password
287 0           $self->log->error(sprintf("[%s] %s: %s", 401, 'E7005', 'Incorrect username/password'));
288 0           $payload{error} = 'Incorrect username/password';
289 0           $payload{status} = 401;
290 0           $payload{code} = 'E7005';
291 0           return Mojo::JSON::Pointer->new({%payload});
292             }
293             #$self->log->error(Mojo::Util::dumper( $ar ));
294              
295             # User data with required fields!
296 0   0       my $data = $ar->to_hash || {};
297 0           $data->{address} = $self->remote_ip($self->app->trustedproxies);
298 0   0       $data->{base} = $args{base_url} || $self->base_url;
299 0   0       $data->{method} = $args{method} || $self->req->method || "ANY";
300 0   0       $data->{path} = $self->req->url->path->to_string || "/";
301 0           $data->{referer} = $referer;
302             # required fields:
303 0 0         $data->{status} = $data->{status} ? \1 : \0;
304 0   0       $data->{uid} ||= 0;
305 0   0       $data->{username} //= $username;
306 0   0       $data->{name} //= $username;
307 0   0       $data->{role} //= '';
308 0   0       $data->{email} //= '';
309             $data->{email_md5} //= $data->{email} ? md5_sum($data->{email}) : '',
310 0 0 0       $data->{comment} //= '';
      0        
311              
312             # Save json file with user data
313 0           json_save($file, $data);
314 0 0         unless (-e $file) {
315 0           $self->log->error(sprintf("[E7007] Can't save file %s", $file));
316 0           $payload{error} = sprintf("Can't save file DATADIR/u.%s.json", $username);
317 0           $payload{status} = 500;
318 0           $payload{code} = 'E7007';
319 0           return Mojo::JSON::Pointer->new({%payload});
320             }
321              
322             # Fixed to cache
323 0           $cache->set($ustat_key, $now);
324              
325             # Ok
326 0           return Mojo::JSON::Pointer->new({%payload});
327             }
328             sub _authorize {
329 0     0     my $self = shift;
330 0 0         my %args = scalar(@_) ? scalar(@_) % 2 ? ref($_[0]) eq 'HASH' ? (%{$_[0]}) : () : (@_) : ();
  0 0          
    0          
331 0   0       my $username = $args{username} || '';
332 0   0       my $referer = $args{referer} // $self->req->headers->header("Referer") // '';
      0        
333 0   0       my $loginpage = $args{loginpage} // '';
334 0           my %payload = ( # Ok by default
335             error => '', # Error message
336             status => 200, # HTTP status code
337             code => 'E0000', # The Suffit error code
338             username => $username, # User name
339             referer => $referer, # Referer
340             loginpage => $loginpage, # Login page for redirects (location)
341             location => undef, # Location URL for redirects
342             user => { # User data with required fields (defaults)
343             status => \0, # User status
344             uid => 0, # User ID
345             username => $username, # User name
346             name => $username, # Full name
347             role => "", # User role
348             email => "", # Email address
349             email_md5 => "", # MD5 of email address
350             comment => "", # Comment
351             },
352             );
353              
354             # Check username
355 0 0         unless (length $username) {
356 0           $self->log->error("[E7009] Incorrect username");
357 0           $payload{error} = "Incorrect username";
358 0           $payload{status} = 400;
359 0           $payload{code} = 'E7009';
360 0           return Mojo::JSON::Pointer->new({%payload});
361             }
362              
363             # Get user file name
364 0           my $file = path($self->app->datadir, sprintf("u.%s.json", $username))->to_string;
365              
366             # Load user file with user data
367 0 0         my $user = -e $file ? json_load($file) : {};
368              
369             # Check user data
370 0 0         unless ($user->{username}) {
371 0           $self->log->error(sprintf("[E7008] File %s not found or incorrect", $file));
372 0           $payload{error} = sprintf("File DATADIR/u.%s.json not found or incorrect", $username);
373 0           $payload{status} = 500;
374 0           $payload{code} = 'E7008';
375 0           return Mojo::JSON::Pointer->new({%payload});
376             }
377              
378             # Ok
379 0           $payload{user} = {%{$user}}; # Set user data to pyload hash
  0            
380 0           return Mojo::JSON::Pointer->new({%payload});
381             }
382              
383             sub _parse_pwdb_lines {
384 0     0     my @lines = @_;
385 0           my %r = ();
386 0           for (@lines) {
387 0 0         next unless $_;
388 0           my @line = split ':', $_;
389 0   0       my ($usr, $pwd, $arg) = ($line[0] // '', $line[1] // '', $line[2] // '');
      0        
      0        
390 0 0 0       next unless length($usr) && length($pwd);
391 0 0         if (@line == 3) { # username:password:params
    0          
392 0           $r{$usr} = {
393             pwd => $pwd,
394             arg => $arg,
395             };
396             } elsif (@line == 2) { # username:password
397 0           $r{$usr} = {
398             pwd => $pwd
399             };
400             }
401             }
402 0           return %r;
403             }
404             sub _check_pw {
405 0   0 0     my $pwd = shift // '';
406 0   0       my $sum = shift // '';
407 0 0 0       return 0 unless length($pwd) && length($sum);
408 0 0         if ($sum =~ /^[0-9a-f]+$/i) {
409 0 0         if (length($sum) == 32) { # md5: acbd18db4cc2f85cedef654fccc4a4d8
    0          
    0          
    0          
    0          
    0          
410 0           return secure_compare(md5_sum($pwd), lc($sum));
411             } elsif(length($sum) == 40) { # sha1: 0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
412 0           return secure_compare(sha1_sum($pwd), lc($sum));
413             } elsif(length($sum) == 56) { # sha224: d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01
414 0           return secure_compare(sha224_hex($pwd), lc($sum));
415             } elsif(length($sum) == 64) { # sha224: 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
416 0           return secure_compare(sha256_hex($pwd), lc($sum));
417             } elsif(length($sum) == 96) { # sha384: a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7
418 0           return secure_compare(sha384_hex($pwd), lc($sum));
419             } elsif(length($sum) == 128) { # sha512: b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86
420 0           return secure_compare(sha512_hex($pwd), lc($sum));
421             } else { # Plain text (unsafe)
422 0           return secure_compare($pwd, $sum);
423             }
424             } else { # Plain text (unsafe)
425 0           return secure_compare($pwd, $sum);
426             }
427 0           return 0;
428             }
429              
430             1;
431              
432             __END__