File Coverage

blib/lib/Mojolicious/Plugin/Hakkefuin.pm
Criterion Covered Total %
statement 226 251 90.0
branch 47 90 52.2
condition 40 82 48.7
subroutine 41 50 82.0
pod 1 1 100.0
total 355 474 74.8


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Hakkefuin;
2 4     4   4001118 use Mojo::Base 'Mojolicious::Plugin';
  4         11  
  4         31  
3              
4 4     4   3211 use CellBIS::Random;
  4         64545  
  4         171  
5 4     4   3941 use Mojo::Hakkefuin;
  4         20  
  4         26  
6 4     4   2755 use Mojo::Hakkefuin::Utils;
  4         13  
  4         36  
7 4     4   2667 use Mojo::Hakkefuin::Sessions;
  4         14  
  4         34  
8 4     4   222 use Mojo::Util qw(secure_compare);
  4         12  
  4         16260  
9              
10             # ABSTRACT: The Minimalistic Mojolicious Authentication
11             our $VERSION = '0.1.3';
12             our $VERSION_STATE = 'Beta';
13             our $CODENAME = 'YataMirror';
14              
15             has mojo_hf => 'Mojo::Hakkefuin';
16             has utils => sub {
17             state $utils = Mojo::Hakkefuin::Utils->new(random => 'String::Random');
18             };
19             has cookies => sub {
20             state $cookies = Mojolicious::Plugin::Hakkefuin::_cookies->new(
21             utils => shift->utils,
22             random => 'String::Random'
23             );
24             };
25             has cookies_lock => sub {
26             state $cookie = Mojolicious::Plugin::Hakkefuin::_cookiesLock->new(
27             utils => shift->utils,
28             random => 'String::Random'
29             );
30             };
31             has random => 'String::Random';
32             has crand => sub { state $crand = CellBIS::Random->new };
33             has session_manager => sub {undef};
34              
35             sub register {
36 4     4 1 243 my ($self, $app, $conf) = @_;
37              
38             # Helper for version
39             $app->helper(
40             mojo_version => sub {
41             return {
42 0     0   0 'num' => $VERSION,
43             'state' => $VERSION_STATE,
44             'codename' => $CODENAME
45             };
46             }
47 4         52 );
48              
49             # Home Dir
50 4         601 my $home = $app->home->detect;
51              
52             # Check Config
53 4   50     94 $conf //= {};
54 4   50     34 $conf->{'helper.prefix'} //= 'mhf';
55 4   50     41 $conf->{'stash.prefix'} //= 'mhf';
56 4   100     25 $conf->{'via'} //= 'sqlite';
57 4   50     20 $conf->{'dir'} //= 'migrations';
58 4   50     25 $conf->{'csrf.name'} //= 'mhf_csrf_token';
59 4   50     39 $conf->{'lock'} //= 1;
60 4   50     22 $conf->{'s.time'} //= '1w';
61 4   50     27 $conf->{'c.time'} //= '1w';
62 4   50     71 $conf->{'cl.time'} //= '60m';
63             $conf->{'callback'} //= {
64       0     'has_auth' => sub { },
65       0     'sign_in' => sub { },
66       0     'sign_out' => sub { },
67       3     'lock' => sub { },
68       3     'unlock' => sub { }
69 4   50     61 };
70              
71             my $time_cookies = {
72             session => $self->utils->time_convert($conf->{'s.time'}),
73             cookies => $self->utils->time_convert($conf->{'c.time'}),
74 4         19 lock => $self->utils->time_convert($conf->{'cl.time'}),
75             };
76 4   50     40 $conf->{'cookies'}
77             //= {name => 'clg', path => '/', httponly => 1, secure => 0};
78 4   50     33 $conf->{'cookies_lock'}
79             //= {name => 'clglc', path => '/', httponly => 1, secure => 0};
80 4   50     31 $conf->{'session'}
81             //= {cookie_name => '_mhf', cookie_path => '/', secure => 0};
82 4   33     30 $conf->{'session'}->{default_expiration} //= $time_cookies->{session};
83 4         35 $conf->{dir} = $home . '/' . $conf->{'dir'};
84              
85             # Build Mojo::Hakkefuin Params
86             my @mhf_params
87             = $conf->{table_config} && $conf->{migration}
88             ? (table_config => $conf->{table_config}, migration => $conf->{migration})
89 4 50 33     68 : (via => $conf->{via}, dir => $conf->{dir});
90 4         11 push @mhf_params, dir => $conf->{dir};
91 4         10 push @mhf_params, via => $conf->{via};
92 4 50       11 push @mhf_params, dsn => $conf->{dsn} if $conf->{dsn};
93 4         14 my $mhf = $self->mojo_hf->new(@mhf_params);
94              
95             # Build shared sessions manager once
96 4         24 my $sessions = $self->session_manager;
97 4 50       19 unless ($sessions) {
98 4         7 $sessions = Mojo::Hakkefuin::Sessions->new(%{$conf->{session}});
  4         115  
99 4 50       88 $sessions->max_age(1) if $sessions->can('max_age');
100 4         44 $self->session_manager($sessions);
101             }
102              
103             # Check Database Migration
104 4         49 $mhf->check_file_migration();
105 4         20 $mhf->check_migration();
106              
107             # Helper Prefix
108 4         22 my $pre = $conf->{'helper.prefix'};
109              
110             $app->hook(
111             after_build_tx => sub {
112 44     44   427623 my ($tx, $c) = @_;
113              
114             # Reuse shared sessions object to avoid per-request allocation
115 44 100 66     222 $c->sessions($sessions) unless $c->sessions && $c->sessions == $sessions;
116             }
117 4         97 );
118              
119 4     3   176 $app->helper($pre . '_lock' => sub { $self->_lock($conf, $mhf, @_) });
  3         9732  
120 4     3   761 $app->helper($pre . '_unlock' => sub { $self->_unlock($conf, $mhf, @_) });
  3         9325  
121 4     5   399 $app->helper($pre . '_signin' => sub { $self->_sign_in($conf, $mhf, @_) });
  5         30318  
122 4     4   344 $app->helper($pre . '_signout' => sub { $self->_sign_out($conf, $mhf, @_) });
  4         125  
123 4     24   412 $app->helper($pre . '_has_auth' => sub { $self->_has_auth($conf, $mhf, @_) });
  24         61115  
124             $app->helper(
125 4     2   408 $pre . '_auth_update' => sub { $self->_auth_update($conf, $mhf, @_) });
  2         113  
126              
127 4     0   379 $app->helper($pre . '_csrf' => sub { $self->_csrf($conf, @_) });
  0         0  
128             $app->helper(
129 4     9   362 $pre . '_csrf_regen' => sub { $self->_csrfreset($conf, $mhf, @_) });
  9         641  
130 4     31   350 $app->helper($pre . '_csrf_get' => sub { $self->_csrf_get($conf, @_) });
  31         494  
131 4     0   383 $app->helper($pre . '_csrf_val' => sub { $self->_csrf_val($conf, @_) });
  0         0  
132 4     3   405 $app->helper(mhf_backend => sub { $mhf->backend });
  3         73  
133             }
134              
135             sub _normalize_override {
136 7     7   21 my ($self, $opts) = @_;
137 7 100 66     68 return {} unless $opts && ref $opts eq 'HASH';
138 2         6 return $opts;
139             }
140              
141             sub _timeframe {
142 10     10   31 my ($self, $conf, $opts) = @_;
143              
144 10   100     50 $opts //= {};
145             return {
146             session => $self->utils->time_convert($opts->{s_time} // $conf->{'s.time'}),
147             cookies => $self->utils->time_convert($opts->{c_time} // $conf->{'c.time'}),
148 10   66     53 lock => $self->utils->time_convert($opts->{cl_time} // $conf->{'cl.time'}),
      66        
      33        
149             };
150             }
151              
152             sub _session_expiration {
153 7     7   21 my ($self, $c, $seconds) = @_;
154 7 50       24 return unless defined $seconds;
155 7         40 $c->session(expiration => $seconds);
156             }
157              
158             sub _lock {
159 3     3   12 my ($self, $conf, $mhf, $c, $identify) = @_;
160              
161             return {result => 0, code => 400, data => 'lock disabled'}
162 3 50       19 unless $conf->{lock};
163              
164 3         34 my $check_auth = $self->_has_auth($conf, $mhf, $c);
165             return $check_auth
166 3 50 33     28 if $check_auth->{result} == 0 || $check_auth->{result} == 3;
167             return {result => 2, code => 423, data => $check_auth->{data}}
168 3 50       47 if $check_auth->{result} == 2; # already locked
169              
170 3         16 my $backend_id = $c->stash($conf->{'stash.prefix'} . '.backend-id');
171 3 50       81 return {result => 0, code => 404, data => 'missing backend id'}
172             unless $backend_id;
173              
174 3         19 my $times = $self->_timeframe($conf);
175 3         30 my $seed = $check_auth->{data}->{cookie};
176 3         17 my $lock_val = $self->cookies_lock->create($conf, $c, $seed, $times->{lock});
177              
178 3         15 my $upd_coolock = $mhf->backend->upd_coolock($backend_id, $lock_val);
179 3         250 my $upd_state = $mhf->backend->upd_lckstate($backend_id, 1);
180              
181 3 50 33     233 unless ($upd_coolock->{code} == 200 && $upd_state->{code} == 200) {
182 0         0 $self->cookies_lock->delete($conf, $c);
183             $mhf->backend->upd_coolock($backend_id, $check_auth->{data}->{cookie_lock})
184 0 0       0 if $check_auth->{data}->{cookie_lock};
185 0         0 return {result => 0, code => 500, data => 'failed to lock'};
186             }
187              
188 3         17 my $result = {result => 1, code => 200, data => {lock => 1}};
189             $conf->{callback}->{lock}->($c, $result)
190 3 50       41 if ref $conf->{callback}->{lock} eq 'CODE';
191 3         32 return $result;
192             }
193              
194             sub _unlock {
195 3     3   15 my ($self, $conf, $mhf, $c, $identify) = @_;
196              
197             return {result => 0, code => 400, data => 'lock disabled'}
198 3 50       17 unless $conf->{lock};
199              
200 3         16 my $check_auth = $self->_has_auth($conf, $mhf, $c);
201             return $check_auth
202 3 50 33     32 if $check_auth->{result} == 0 || $check_auth->{result} == 3;
203              
204 3         16 my $backend_id = $c->stash($conf->{'stash.prefix'} . '.backend-id');
205 3 50       37 return {result => 0, code => 404, data => 'missing backend id'}
206             unless $backend_id;
207              
208 3         16 my $lock_cookie = $self->cookies_lock->check($c, $conf);
209 3         105 my $stored_lock = $check_auth->{data}->{cookie_lock};
210              
211 3 50       13 if ($check_auth->{result} == 1) {
212 0         0 $self->cookies_lock->delete($conf, $c);
213 0         0 return {result => 1, code => 200, data => {lock => 0}};
214             }
215 3 50 33     24 return {result => 0, code => 401, data => 'lock cookie missing'}
216             unless $lock_cookie && $stored_lock;
217 3 50       13 return {result => 0, code => 401, data => 'lock cookie mismatch'}
218             unless secure_compare $lock_cookie, $stored_lock;
219              
220 3         159 my $upd_coolock = $mhf->backend->upd_coolock($backend_id, 'no_lock');
221 3         202 my $upd_state = $mhf->backend->upd_lckstate($backend_id, 0);
222 3         217 $self->cookies_lock->delete($conf, $c);
223              
224 3 50 33     66 unless ($upd_coolock->{code} == 200 && $upd_state->{code} == 200) {
225 0         0 return {result => 0, code => 500, data => 'failed to unlock'};
226             }
227              
228 3         18 my $result = {result => 1, code => 200, data => {lock => 0}};
229             $conf->{callback}->{unlock}->($c, $result)
230 3 50       26 if ref $conf->{callback}->{unlock} eq 'CODE';
231 3         31 return $result;
232             }
233              
234             sub _sign_in {
235 5     5   21 my ($self, $conf, $mhf, $c, $identify, $opts) = @_;
236              
237 5         22 my $override = $self->_normalize_override($opts);
238 5         23 my $times = $self->_timeframe($conf, $override);
239 5         26 my $backend = $mhf->backend;
240 5         36 $self->_session_expiration($c, $times->{session});
241 5         881 $self->cookies_lock->delete($conf, $c);
242 5         17 my $cv = $self->cookies->create($conf, $c, $times->{cookies});
243              
244 5         69 return $backend->create($identify, $cv->[0], $cv->[1], $times->{cookies});
245             }
246              
247             sub _sign_out {
248 4     4   14 my ($self, $conf, $mhf, $c, $identify) = @_;
249              
250             # Session Destroy :
251 4         16 $c->session(expires => 1);
252              
253 4         95 $self->cookies_lock->delete($conf, $c);
254 4         18 my $cookie = $self->cookies->delete($conf, $c);
255 4         20 return $mhf->backend->delete($identify, $cookie);
256             }
257              
258             sub _has_auth {
259 30     30   95 my ($self, $conf, $mhf, $c) = @_;
260              
261 30         154 my $result = {result => 0, code => 404, data => 'empty'};
262 30         122 my $csrf_get = $conf->{'helper.prefix'} . '_csrf_get';
263 30         153 my $coo = $c->cookie($conf->{cookies}->{name});
264              
265 30 100       4925 return $result unless $coo;
266              
267 26         233 my $auth_check = $mhf->backend->check(1, $coo);
268              
269 26 50       1868 if ($auth_check->{result} == 1) {
270 26   50     230 my $csrf_ok = secure_compare($auth_check->{data}->{csrf} // '',
      50        
271             $c->$csrf_get() // '');
272 26 50       7733 if ($csrf_ok) {
273 26 50       113 my $locked = $conf->{lock} ? $auth_check->{data}->{lock} : 0;
274 26 100       67 if ($locked) {
275 6         34 my $lock_cookie = $c->cookie($conf->{cookies_lock}->{name});
276             my $match
277             = $lock_cookie
278             && $auth_check->{data}->{cookie_lock}
279 6   33     292 && secure_compare($lock_cookie, $auth_check->{data}->{cookie_lock});
280             $result = {
281             result => 2,
282             code => 423,
283             data => $auth_check->{data},
284 6 50       277 lock_cookie => $match ? 1 : 0
285             };
286             }
287             else {
288 20         144 $result = {result => 1, code => 200, data => $auth_check->{data}};
289             }
290             }
291             else {
292 0         0 $result = {result => 3, code => 406, data => ''};
293             }
294             $c->stash(
295 26         158 $conf->{'stash.prefix'} . '.backend-id' => $auth_check->{data}->{id});
296             $c->stash(
297 26         548 $conf->{'stash.prefix'} . '.identify' => $auth_check->{data}->{identify});
298             $c->stash(
299 26         388 $conf->{'stash.prefix'} . '.lock_state' => $auth_check->{data}->{lock});
300             }
301 26         385 return $result;
302             }
303              
304             sub _auth_update {
305 2     2   10 my ($self, $conf, $mhf, $c, $identify, $opts) = @_;
306              
307 2         10 my $override = $self->_normalize_override($opts);
308 2         10 my $times = $self->_timeframe($conf, $override);
309              
310 2         7 my $result = {result => 0};
311 2         8 my $update = $self->cookies->update($conf, $c, 1, $times->{cookies});
312 2 50       9 return $result unless $update;
313              
314 2 50       25 my $csrf = ref $update->[1] eq 'ARRAY' ? $update->[1]->[1] : $update->[1];
315 2         8 $result = $mhf->backend->update($identify, $update->[0], $csrf);
316 2         131 $self->_session_expiration($c, $times->{session});
317              
318 2         60 return $result;
319             }
320              
321             sub _csrf {
322 0     0   0 my ($self, $conf, $c) = @_;
323              
324             # Generate CSRF Token if not exists
325 0 0       0 unless ($c->session($conf->{'csrf.name'})) {
326 0         0 my $cook = $self->utils->gen_cookie(3);
327 0         0 my $csrf = $self->crand->random($cook, 2, 3);
328              
329 0         0 $c->session($conf->{'csrf.name'} => $csrf);
330 0         0 $c->res->headers->append((uc $conf->{'csrf.name'}) => $csrf);
331             }
332             }
333              
334             sub _csrfreset {
335 9     9   23 my ($self, $conf, $mhf, $c, $id) = @_;
336              
337 9         54 my $coon = $self->utils->gen_cookie(3);
338 9         1849 my $csrf = $self->crand->random($coon, 2, 3);
339              
340 9 100       16470 my $result = $mhf->backend->update_csrf($id, $csrf) if $id;
341              
342 9         171 $c->session($conf->{'csrf.name'} => $csrf);
343 9         678 $c->res->headers->header((uc $conf->{'csrf.name'}) => $csrf);
344 9         680 return [$result, $csrf];
345             }
346              
347             sub _csrf_get {
348 31     31   84 my ($self, $conf, $c) = @_;
349             return $c->session($conf->{'csrf.name'})
350 31   66     139 || $c->req->headers->header((uc $conf->{'csrf.name'}));
351             }
352              
353             sub _csrf_val {
354 0     0   0 my ($self, $conf, $c) = @_;
355              
356 0         0 my $get_csrf = $c->session($conf->{'csrf.name'});
357 0         0 my $csrf_header = $c->res->headers->header((uc $conf->{'csrf.name'}));
358 0 0       0 return $csrf_header if $csrf_header eq $get_csrf;
359             }
360              
361             package Mojolicious::Plugin::Hakkefuin::_cookies;
362 4     4   33 use Mojo::Base -base;
  4         7  
  4         24  
363              
364             has 'random';
365             has 'utils';
366              
367             sub _cookie_options {
368 7     7   19 my ($self, $base, $ttl) = @_;
369              
370 7 50       15 my %opts = %{$base || {}};
  7         50  
371 7 50       25 if (defined $ttl) {
372 7         20 $opts{expires} = time + $ttl;
373 7         22 $opts{max_age} = $ttl;
374             }
375 7         37 return \%opts;
376             }
377              
378             sub create {
379 5     5   77 my ($self, $conf, $app, $ttl) = @_;
380              
381 5         13 my $csrf_get = $conf->{'helper.prefix'} . '_csrf_get';
382 5         12 my $csrf_reg = $conf->{'helper.prefix'} . '_csrf_regen';
383 5   33     41 my $csrf = $app->$csrf_get() || $app->$csrf_reg()->[1];
384              
385 5         20 my $cookie_key = $conf->{'cookies'}->{name};
386 5         39 my $cookie_val
387             = Mojo::Util::hmac_sha1_sum($self->utils->gen_cookie(5), $csrf);
388              
389             my $duration
390 5 50       1365 = defined $ttl ? $ttl : $self->utils->time_convert($conf->{'c.time'});
391             $app->cookie($cookie_key, $cookie_val,
392 5         24 $self->_cookie_options($conf->{'cookies'}, $duration));
393 5         1169 [$cookie_val, $csrf];
394             }
395              
396             sub update {
397 2     2   18 my ($self, $conf, $app, $csrf_reset, $ttl) = @_;
398              
399 2 50       10 if ($self->check($app, $conf)) {
400             my $csrf
401 2 50       90 = $conf->{'helper.prefix'} . ($csrf_reset ? '_csrf_regen' : '_csrf_get');
402 2         10 $csrf = $app->$csrf();
403              
404 2         13 my $cookie_key = $conf->{'cookies'}->{name};
405 2         16 my $cookie_val
406             = Mojo::Util::hmac_sha1_sum($self->utils->gen_cookie(5), $csrf);
407             my $duration
408 2 50       825 = defined $ttl ? $ttl : $self->utils->time_convert($conf->{'c.time'});
409             $app->cookie($cookie_key, $cookie_val,
410 2         33 $self->_cookie_options($conf->{'cookies'}, $duration));
411 2         455 return [$cookie_val, $csrf];
412             }
413 0         0 return undef;
414             }
415              
416             sub delete {
417 4     4   31 my ($self, $conf, $app) = @_;
418              
419 4 50       21 if (my $cookie = $self->check($app, $conf)) {
420 4         136 $app->cookie($conf->{'cookies'}->{name} => '', {expires => 1});
421 4         747 return $cookie;
422             }
423 0         0 return undef;
424             }
425              
426             sub check {
427 6     6   13 my ($self, $app, $conf) = @_;
428 6         39 return $app->cookie($conf->{'cookies'}->{name});
429             }
430              
431             package Mojolicious::Plugin::Hakkefuin::_cookiesLock;
432 4     4   3761 use Mojo::Base -base;
  4         9  
  4         20  
433              
434             has 'random';
435             has 'utils';
436              
437             sub _cookie_options {
438 3     3   8 my ($self, $base, $ttl) = @_;
439              
440 3 50       7 my %opts = %{$base || {}};
  3         26  
441 3 50       34 if (defined $ttl) {
442 3         12 $opts{expires} = time + $ttl;
443 3         10 $opts{max_age} = $ttl;
444             }
445 3         22 return \%opts;
446             }
447              
448             sub create {
449 3     3   30 my ($self, $conf, $app, $seed, $ttl) = @_;
450              
451 3   33     12 my $base = $seed || $self->utils->gen_cookie(4);
452 3         14 my $cookie_val
453             = Mojo::Util::hmac_sha1_sum($self->utils->gen_cookie(6), $base);
454              
455             my $duration
456 3 50       1403 = defined $ttl ? $ttl : $self->utils->time_convert($conf->{'cl.time'});
457             $app->cookie($conf->{'cookies_lock'}->{name},
458 3         21 $cookie_val, $self->_cookie_options($conf->{'cookies_lock'}, $duration));
459 3         660 return $cookie_val;
460             }
461              
462             sub update {
463 0     0   0 my ($self, $conf, $app, $seed, $ttl) = @_;
464              
465 0 0       0 return undef unless $self->check($app, $conf);
466 0         0 return $self->create($conf, $app, $seed, $ttl);
467             }
468              
469             sub delete {
470 12     12   175 my ($self, $conf, $app) = @_;
471              
472 12 100       58 if ($self->check($app, $conf)) {
473 3         142 $app->cookie($conf->{'cookies_lock'}->{name} => '', {expires => 1});
474 3         590 return 1;
475             }
476 9         255 return 0;
477             }
478              
479             sub check {
480 15     15   56 my ($self, $app, $conf) = @_;
481 15         72 return $app->cookie($conf->{'cookies_lock'}->{name});
482             }
483              
484             1;
485              
486             =encoding utf8
487              
488             =head1 NAME
489              
490             Mojolicious::Plugin::Hakkefuin - Mojolicious Web Authentication.
491              
492             =head1 SYNOPSIS
493              
494             Mojolicious::Lite example (SQLite default):
495              
496             use Mojolicious::Lite;
497              
498             plugin 'Hakkefuin' => {
499             'helper.prefix' => 'fuin',
500             'stash.prefix' => 'fuin',
501             via => 'sqlite', # or mariadb / pg
502             dir => 'migrations',
503             'c.time' => '1w', # auth cookie TTL
504             's.time' => '1w', # session TTL
505             'lock' => 1, # enable lock/unlock helpers
506             };
507              
508             post '/login' => sub {
509             my $c = shift;
510             my $id = $c->param('user');
511             my $res = $c->fuin_signin($id);
512             return $c->render(status => $res->{code}, json => $res);
513             };
514              
515             # Override cookie/session TTL per request
516             post '/login-custom' => sub {
517             my $c = shift;
518             my $res = $c->fuin_signin($c->param('user'), {c_time => '2h', s_time => '30m'});
519             return $c->render(status => $res->{code}, json => $res);
520             };
521              
522             under sub {
523             my $c = shift;
524             my $auth = $c->fuin_has_auth; # checks cookie+csrf, stashes data
525             return $c->render(status => 423, json => $auth) if $auth->{result} == 2;
526             return $c->render(status => 401, text => 'Unauthorized')
527             unless $auth->{result} == 1;
528             $c->fuin_csrf; # ensure CSRF is in session/headers
529             return 1;
530             };
531              
532             get '/me' => sub {
533             my $c = shift;
534             $c->render(json => {user => $c->stash('fuin.identify')});
535             };
536              
537             # Rotate auth with custom TTLs without re-login
538             get '/auth-update-custom' => sub {
539             my $c = shift;
540             my $bid = $c->stash('fuin.backend-id');
541             my $res = $c->fuin_auth_update($bid, {c_time => '45m', s_time => '20m'});
542             $c->render(status => $res->{code}, json => $res);
543             };
544              
545             post '/logout' => sub {
546             my $c = shift;
547             my $res = $c->fuin_signout($c->stash('fuin.identify'));
548             $c->render(status => $res->{code}, json => $res);
549             };
550              
551             app->start;
552              
553             Mojolicious (non-Lite) menambahkan plugin di dalam C:
554              
555             sub startup {
556             my $self = shift;
557             $self->plugin(Hakkefuin => { via => 'pg', dir => 'migrations/pg' });
558             ...
559             }
560              
561             =head1 DESCRIPTION
562              
563             L is a L plugin for
564             Web Authentication. (Minimalistic and Powerful).
565              
566             =head1 OPTIONS
567              
568             =head2 helper.prefix
569              
570             # Mojolicious
571             $self->plugin('Hakkefuin' => {
572             'helper.prefix' => 'your_prefix_here'
573             });
574              
575             # Mojolicious Lite
576             plugin 'Hakkefuin' => {
577             'helper.prefix' => 'your_prefix_here'
578             };
579              
580             To change prefix of all helpers. By default, C is C.
581              
582             =head2 stash.prefix
583              
584             # Mojolicious
585             $self->plugin('Hakkefuin' => {
586             'stash.prefix' => 'your_stash_prefix_here'
587             });
588              
589             # Mojolicious Lite
590             plugin 'Hakkefuin' => {
591             'stash.prefix' => 'your_stash_prefix_here'
592             };
593              
594             To change prefix of stash. By default, C is C.
595              
596             =head2 csrf.name
597              
598             # Mojolicious
599             $self->plugin('Hakkefuin' => {
600             'csrf.name' => 'your_csrf_name_here'
601             });
602              
603             # Mojolicious Lite
604             plugin 'Hakkefuin' => {
605             'csrf.name' => 'your_csrf_name_here'
606             };
607              
608             To change csrf name in session and HTTP Headers. By default, C
609             is C.
610              
611             =head2 via
612              
613             # Mojolicious
614             $self->plugin('Hakkefuin' => {
615             via => 'mariadb', # OR
616             via => 'pg'
617             });
618              
619             # Mojolicious Lite
620             plugin 'Hakkefuin' => {
621             via => 'mariadb', # OR
622             via => 'pg'
623             };
624              
625             Use one of C<'mariadb'> or C<'pg'> or C<'sqlite'>. (For C<'sqlite'> option
626             does not need to be specified, as it would by default be using C<'sqlite'>
627             if option C is not specified).
628              
629             =head2 dir
630              
631             # Mojolicious
632             $self->plugin('Hakkefuin' => {
633             dir => 'your-custom-dirname-here'
634             });
635              
636             # Mojolicious Lite
637             plugin 'Hakkefuin' => {
638             dir => 'your-custom-dirname-here'
639             };
640              
641             Specified directory for L configure files.
642              
643             =head2 c.time
644              
645             # Mojolicious
646             $self->plugin('Hakkefuin' => {
647             'c.time' => '1w'
648             });
649              
650             # Mojolicious Lite
651             plugin 'Hakkefuin' => {
652             'c.time' => '1w'
653             };
654              
655             To set a cookie expires time. By default is 1 week.
656              
657             =head2 s.time
658              
659             # Mojolicious
660             $self->plugin('Hakkefuin' => {
661             's.time' => '1w'
662             });
663              
664             # Mojolicious Lite
665             plugin 'Hakkefuin' => {
666             's.time' => '1w'
667             };
668              
669             To set a cookie session expires time. By default is 1 week. For more
670             information of the abbreviation for time C and C helper,
671             see L.
672              
673             =head2 lock
674              
675             # Mojolicious
676             $self->plugin('Hakkefuin' => {
677             'lock' => 1
678             });
679              
680             # Mojolicious Lite
681             plugin 'Hakkefuin' => {
682             'lock' => 1
683             };
684              
685             To set C feature. By default is 1 (enable). If you won't use
686             that feature, you can give 0 (disable). This feature is additional
687             authentication method, beside C and C. When enabled a
688             dedicated lock cookie is issued and tracked in the backend.
689              
690             =head2 cl.time
691              
692             # Mojolicious
693             $self->plugin('Hakkefuin' => {
694             'cl.time' => '60m'
695             });
696              
697             # Mojolicious Lite
698             plugin 'Hakkefuin' => {
699             'cl.time' => '60m'
700             };
701              
702             To set cookie lock expires time. By default is 60 minutes for the lock
703             cookie used by C/C.
704              
705             =head1 HELPERS
706              
707             By default, prefix for all helpers using C, but you can do change that
708             with option C.
709              
710             =head2 mhf_lock
711              
712             $c->mhf_lock() # In the controllers
713              
714             Helper to lock the current authenticated session; sets lock cookie and
715             marks backend as locked.
716              
717             =head2 mhf_unlock
718              
719             $c->mhf_unlock(); # In the controllers
720              
721             Helper to unlock a locked session; clears lock cookie and unlocks backend.
722              
723             =head2 mhf_signin
724              
725             $c->mhf_signin('login-identify') # In the controllers
726              
727             Helper for action sign-in (login) web application.
728              
729             =head2 mhf_signout
730              
731             $c->mhf_signout('login-identify'); # In the controllers
732              
733             Helper for action sign-out (logout) web application.
734              
735             =head2 mhf_auth_update
736              
737             $c->mhf_auth_update('login-identify'); # In the controllers
738              
739             Helper for rotating authentication cookie and CSRF token.
740              
741             =head2 mhf_has_auth
742              
743             $c->mhf_has_auth; # In the controllers
744              
745             Helper for checking if routes has authenticated.
746              
747             =head2 mhf_csrf
748              
749             $c->mhf_csrf; # In the controllers
750             <%= mhf_csrf %> # In the template.
751              
752             Helper for generate csrf;
753              
754             =head2 mhf_csrf_val
755              
756             $c->mhf_csrf_val; # In the controllers
757              
758             Helper for comparing stored CSRF (session/header) and returning it when it
759             matches.
760              
761             =head2 mhf_csrf_get
762              
763             $c->mhf_csrf_get; # In the controllers
764              
765             Helper for retrieving the stored CSRF token.
766              
767             =head2 mhf_csrf_regen
768              
769             $c->mhf_csrf_regen; # In the controllers
770              
771             Helper for regenerating CSRF token and returning the new value.
772              
773             =head2 mhf_backend
774              
775             $c->mhf_backend; # In the controllers
776              
777             Helper for access to backend.
778              
779             =head1 METHODS
780              
781             L inherits all methods from
782             L and implements the following new ones.
783              
784             =head2 register
785              
786             $plugin->register(Mojolicious->new);
787              
788             Register plugin in L application.
789              
790             =head1 SEE ALSO
791              
792             L,
793             , L.
794              
795             =head1 AUTHOR
796              
797             Achmad Yusri Afandi, C
798              
799             =head1 COPYRIGHT AND LICENSE
800              
801             Copyright (C) 2025 by Achmad Yusri Afandi
802              
803             This program is free software, you can redistribute it and/or modify it
804             under the terms of the Artistic License version 2.0.
805              
806             =cut