]> Dogcows Code - chaz/p5-CGI-Ex/blob - lib/CGI/Ex/Auth.pm
70d754e47f0bf7ba9d68d75001f2807cb1df00ef
[chaz/p5-CGI-Ex] / lib / CGI / Ex / Auth.pm
1 package CGI::Ex::Auth;
2
3 =head1 NAME
4
5 CGI::Ex::Auth - Handle logins nicely.
6
7 =cut
8
9 ###----------------------------------------------------------------###
10 # Copyright 2007 - Paul Seamons #
11 # Distributed under the Perl Artistic License without warranty #
12 ###----------------------------------------------------------------###
13
14 use strict;
15 use vars qw($VERSION);
16
17 use MIME::Base64 qw(encode_base64 decode_base64);
18 use Digest::MD5 qw(md5_hex);
19 use CGI::Ex;
20
21 $VERSION = '2.18';
22
23 ###----------------------------------------------------------------###
24
25 sub new {
26 my $class = shift || __PACKAGE__;
27 my $args = shift || {};
28 return bless {%$args}, $class;
29 }
30
31 sub get_valid_auth {
32 my $self = shift;
33 $self = $self->new(@_) if ! ref $self;
34
35 ### shortcut that will print a js file as needed (such as the md5.js)
36 if ($self->script_name . $self->path_info eq $self->js_uri_path . "/CGI/Ex/md5.js") {
37 $self->cgix->print_js('CGI/Ex/md5.js');
38 eval { die "Printed Javascript" };
39 return;
40 }
41
42 my $form = $self->form;
43 my $cookies = $self->cookies;
44 my $key_l = $self->key_logout;
45 my $key_c = $self->key_cookie;
46 my $has_cookies = scalar %$cookies;
47
48 ### allow for logout
49 if ($form->{$key_l}) {
50 $self->delete_cookie({key => $key_c});;
51 $self->location_bounce($self->logout_redirect);
52 eval { die "Logging out" };
53 return;
54 }
55
56 my $had_form_info;
57 foreach ([$form, $self->key_user, 1],
58 [$cookies, $key_c, 0],
59 ) {
60 my ($hash, $key, $is_form) = @$_;
61 next if ! defined $hash->{$key};
62 $had_form_info ++ if $is_form;
63
64 ### if it looks like a bare username (as in they didn't have javascript) - add in other items
65 my $data;
66 if ($is_form
67 && $hash->{$key} !~ m|^[^/]+/|
68 && defined $hash->{ $self->key_pass }) {
69 $data = $self->verify_token({
70 token => {
71 user => delete $hash->{$key},
72 test_pass => delete $hash->{ $self->key_pass },
73 expires_min => delete($hash->{ $self->key_save }) ? -1 : delete($hash->{ $self->key_expires_min }) || $self->expires_min,
74 payload => delete $hash->{ $self->key_payload } || '',
75 },
76 from => 'form',
77 }) || next;
78
79 } else {
80 $data = $self->verify_token({token => $hash->{$key}, from => ($is_form ? 'form' : 'cookie')}) || next;
81 delete $hash->{$key} if $is_form;
82 }
83
84 ### generate a fresh cookie if they submitted info on plaintext types
85 if ($self->use_plaintext || ($data->{'type'} && $data->{'type'} eq 'crypt')) {
86 $self->set_cookie({
87 key => $key_c,
88 val => $self->generate_token($data),
89 no_expires => ($data->{ $self->key_save } ? 0 : 1), # make it a session cookie unless they ask for saving
90 }) if $is_form; # only set the cookie if we found info in the form - the cookie will be a session cookie after that
91
92 ### always generate a cookie on types that have expiration
93 } else {
94 $self->set_cookie({
95 key => $key_c,
96 val => $self->generate_token($data),
97 no_expires => 0,
98 });
99 }
100
101 ### successful login
102
103 ### bounce to redirect
104 if (my $redirect = $form->{ $self->key_redirect }) {
105 $self->location_bounce($redirect);
106 eval { die "Success login - bouncing to redirect" };
107 return;
108
109 ### if they have cookies we are done
110 } elsif ($has_cookies || $self->no_cookie_verify) {
111 return $self;
112
113 ### need to verify cookies are set-able
114 } elsif ($is_form) {
115 $form->{$self->key_verify} = $self->server_time;
116 my $query = $self->cgix->make_form($form);
117 my $url = $self->script_name . $self->path_info . ($query ? "?$query" : "");
118
119 $self->location_bounce($url);
120 eval { die "Success login - bouncing to test cookie" };
121 return;
122 }
123 }
124
125 ### make sure the cookie is gone
126 $self->delete_cookie({key => $key_c}) if $cookies->{$key_c};
127
128 ### nothing found - see if they have cookies
129 if (my $value = delete $form->{$self->key_verify}) {
130 if (abs(time() - $value) < 15) {
131 $self->no_cookies_print;
132 return;
133 }
134 }
135
136 ### oh - you're still here - well then - ask for login credentials
137 my $key_r = $self->key_redirect;
138 if (! $form->{$key_r}) {
139 my $query = $self->cgix->make_form($form);
140 $form->{$key_r} = $self->script_name . $self->path_info . ($query ? "?$query" : "");
141 }
142
143 $form->{'had_form_data'} = $had_form_info;
144 $self->login_print;
145 my $data = $self->last_auth_data;
146 eval { die defined($data) ? $data : "Requesting credentials" };
147
148 ### allow for a sleep to help prevent brute force
149 sleep($self->failed_sleep) if defined($data) && $data->error ne 'Login expired' && $self->failed_sleep;
150
151 return;
152 }
153
154 ###----------------------------------------------------------------###
155
156 sub script_name { shift->{'script_name'} || $ENV{'SCRIPT_NAME'} || die "Missing SCRIPT_NAME" }
157
158 sub path_info { shift->{'path_info'} || $ENV{'PATH_INFO'} || '' }
159
160 sub server_time { time }
161
162 sub cgix {
163 my $self = shift;
164 $self->{'cgix'} = shift if $#_ != -1;
165 return $self->{'cgix'} ||= CGI::Ex->new;
166 }
167
168 sub form {
169 my $self = shift;
170 $self->{'form'} = shift if $#_ != -1;
171 return $self->{'form'} ||= $self->cgix->get_form;
172 }
173
174 sub cookies {
175 my $self = shift;
176 $self->{'cookies'} = shift if $#_ != -1;
177 return $self->{'cookies'} ||= $self->cgix->get_cookies;
178 }
179
180 sub delete_cookie {
181 my $self = shift;
182 my $args = shift;
183 return $self->{'delete_cookie'}->($self, $args) if $self->{'delete_cookie'};
184 my $key = $args->{'key'};
185 $self->cgix->set_cookie({
186 -name => $key,
187 -value => '',
188 -expires => '-10y',
189 -path => '/',
190 });
191 delete $self->cookies->{$key};
192 }
193
194 sub set_cookie {
195 my $self = shift;
196 my $args = shift;
197 return $self->{'set_cookie'}->($self, $args) if $self->{'set_cookie'};
198 my $key = $args->{'key'};
199 my $val = $args->{'val'};
200 $self->cgix->set_cookie({
201 -name => $key,
202 -value => $val,
203 ($args->{'no_expires'} ? () : (-expires => '+20y')), # let the expires time take care of things for types that self expire
204 -path => '/',
205 });
206 $self->cookies->{$key} = $val;
207 }
208
209 sub location_bounce {
210 my $self = shift;
211 my $url = shift;
212 return $self->{'location_bounce'}->($self, $url) if $self->{'location_bounce'};
213 return $self->cgix->location_bounce($url);
214 }
215
216 ###----------------------------------------------------------------###
217
218 sub key_logout { shift->{'key_logout'} ||= 'cea_logout' }
219 sub key_cookie { shift->{'key_cookie'} ||= 'cea_user' }
220 sub key_user { shift->{'key_user'} ||= 'cea_user' }
221 sub key_pass { shift->{'key_pass'} ||= 'cea_pass' }
222 sub key_time { shift->{'key_time'} ||= 'cea_time' }
223 sub key_save { shift->{'key_save'} ||= 'cea_save' }
224 sub key_expires_min { shift->{'key_expires_min'} ||= 'cea_expires_min' }
225 sub form_name { shift->{'form_name'} ||= 'cea_form' }
226 sub key_verify { shift->{'key_verify'} ||= 'cea_verify' }
227 sub key_redirect { shift->{'key_redirect'} ||= 'cea_redirect' }
228 sub key_payload { shift->{'key_payload'} ||= 'cea_payload' }
229 sub secure_hash_keys { shift->{'secure_hash_keys'} ||= [] }
230 sub no_cookie_verify { shift->{'no_cookie_verify'} ||= 0 }
231 sub use_crypt { shift->{'use_crypt'} ||= 0 }
232 sub use_blowfish { shift->{'use_blowfish'} ||= '' }
233 sub use_plaintext { my $s = shift; $s->use_crypt || ($s->{'use_plaintext'} ||= 0) }
234 sub use_base64 { my $s = shift; $s->{'use_base64'} = 1 if ! defined $s->{'use_base64'}; $s->{'use_base64'} }
235 sub expires_min { my $s = shift; $s->{'expires_min'} = 6 * 60 if ! defined $s->{'expires_min'}; $s->{'expires_min'} }
236 sub failed_sleep { shift->{'failed_sleep'} ||= 0 }
237
238 sub logout_redirect {
239 my $self = shift;
240 return $self->{'logout_redirect'} || $self->script_name ."?loggedout=1";
241 }
242
243 sub js_uri_path {
244 my $self = shift;
245 return $self->{'js_uri_path'} ||= $self->script_name ."/js";
246 }
247
248 ###----------------------------------------------------------------###
249
250 sub no_cookies_print {
251 my $self = shift;
252 $self->cgix->print_content_type;
253 print qq{<div style="border: 2px solid black;background:red;color:white">You do not appear to have cookies enabled.</div>};
254 return 1;
255 }
256
257 sub login_print {
258 my $self = shift;
259 my $hash = $self->login_hash_common;
260 my $template = $self->login_template;
261
262 ### allow for a hooked override
263 if (my $meth = $self->{'login_print'}) {
264 $meth->($self, $template, $hash);
265 return 0;
266 }
267
268 ### process the document
269 require CGI::Ex::Template;
270 my $cet = CGI::Ex::Template->new($self->template_args);
271 my $out = '';
272 $cet->process_simple($template, $hash, \$out) || die $cet->error;
273
274 ### fill in form fields
275 require CGI::Ex::Fill;
276 CGI::Ex::Fill::fill({text => \$out, form => $hash});
277
278 ### print it
279 $self->cgix->print_content_type;
280 print $out;
281
282 return 0;
283 }
284
285 sub template_args {
286 my $self = shift;
287 return $self->{'template_args'} ||= {
288 INCLUDE_PATH => $self->template_include_path,
289 };
290 }
291
292 sub template_include_path { shift->{'template_include_path'} || '' }
293
294 sub login_hash_common {
295 my $self = shift;
296 my $form = $self->form;
297 my $data = $self->last_auth_data;
298 $data = {} if ! defined $data;
299
300 return {
301 %$form,
302 error => ($form->{'had_form_data'}) ? "Login Failed" : "",
303 login_data => $data,
304 key_user => $self->key_user,
305 key_pass => $self->key_pass,
306 key_time => $self->key_time,
307 key_save => $self->key_save,
308 key_expires_min => $self->key_expires_min,
309 key_payload => $self->key_payload,
310 key_redirect => $self->key_redirect,
311 form_name => $self->form_name,
312 script_name => $self->script_name,
313 path_info => $self->path_info,
314 md5_js_path => $self->js_uri_path ."/CGI/Ex/md5.js",
315 use_plaintext => $self->use_plaintext,
316 $self->key_user => $data->{'user'} || '',
317 $self->key_pass => '', # don't allow for this to get filled into the form
318 $self->key_time => $self->server_time,
319 $self->key_payload => $self->generate_payload({%$data, login_form => 1}),
320 $self->key_expires_min => $self->expires_min,
321 text_user => $self->text_user,
322 text_pass => $self->text_pass,
323 text_save => $self->text_save,
324 };
325 }
326
327 ###----------------------------------------------------------------###
328
329 sub verify_token {
330 my $self = shift;
331 my $args = shift;
332 my $token = delete $args->{'token'} || die "Missing token";
333 my $data = $self->{'_last_auth_data'} = $self->new_auth_data({token => $token, %$args});
334
335 ### token already parsed
336 if (ref $token) {
337 $data->add_data({%$token, armor => 'none'});
338
339 ### parse token for info
340 } else {
341 my $found;
342 my $key;
343 for my $armor ('none', 'base64', 'blowfish') { # try with and without base64 encoding
344 my $copy = ($armor eq 'none') ? $token
345 : ($armor eq 'base64') ? eval { local $^W; decode_base64($token) }
346 : ($key = $self->use_blowfish) ? decrypt_blowfish($token, $key)
347 : next;
348 if ($copy =~ m|^ ([^/]+) / (\d+) / (-?\d+) / (.*) / ([a-fA-F0-9]{32}) (?: / (sh\.\d+\.\d+))? $|x) {
349 $data->add_data({
350 user => $1,
351 cram_time => $2,
352 expires_min => $3,
353 payload => $4,
354 test_pass => $5,
355 secure_hash => $6 || '',
356 armor => $armor,
357 });
358 $found = 1;
359 last;
360 } elsif ($copy =~ m|^ ([^/]+) / (.*) $|x) {
361 $data->add_data({
362 user => $1,
363 test_pass => $2,
364 armor => $armor,
365 });
366 $found = 1;
367 last;
368 }
369 }
370 if (! $found) {
371 $data->error('Invalid token');
372 return $data;
373 }
374 }
375
376
377 ### verify the user and get the pass
378 my $pass;
379 if (! defined($data->{'user'})) {
380 $data->error('Missing user');
381
382 } elsif (! defined $data->{'test_pass'}) {
383 $data->error('Missing test_pass');
384
385 } elsif (! $self->verify_user($data->{'user'} = $self->cleanup_user($data->{'user'}))) {
386 $data->error('Invalid user');
387
388 } elsif (! defined($pass = eval { $self->get_pass_by_user($data->{'user'}) })) {
389 $data->add_data({details => $@});
390 $data->error('Could not get pass');
391 } elsif (ref $pass eq 'HASH') {
392 my $extra = $pass;
393 $pass = exists($extra->{'real_pass'}) ? delete($extra->{'real_pass'})
394 : exists($extra->{'password'}) ? delete($extra->{'password'})
395 : do { $data->error('Data returned by get_pass_by_user did not contain real_pass or password'); undef };
396 $data->error('Invalid login') if ! defined $pass && ! $data->error;
397 $data->add_data($extra);
398 }
399 return $data if $data->error;
400
401
402 ### store - to allow generate_token to not need to relookup the pass
403 $data->add_data({real_pass => $pass});
404
405
406 ### looks like a secure_hash cram
407 if ($data->{'secure_hash'}) {
408 $data->add_data(type => 'secure_hash_cram');
409 my $array = eval {$self->secure_hash_keys };
410 if (! $array) {
411 $data->error('secure_hash_keys not found');
412 } elsif (! @$array) {
413 $data->error('secure_hash_keys empty');
414 } elsif ($data->{'secure_hash'} !~ /^sh\.(\d+)\.(\d+)$/ || $1 > $#$array) {
415 $data->error('Invalid secure hash');
416 } else {
417 my $rand1 = $1;
418 my $rand2 = $2;
419 my $real = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
420 my $str = join("/", @{$data}{qw(user cram_time expires_min payload)});
421 my $sum = md5_hex($str .'/'. $real .('/sh.'.$array->[$rand1].'.'.$rand2));
422 if ($data->{'expires_min'} > 0
423 && ($self->server_time - $data->{'cram_time'}) > $data->{'expires_min'} * 60) {
424 $data->error('Login expired');
425 } elsif (lc($data->{'test_pass'}) ne $sum) {
426 $data->error('Invalid login');
427 }
428 }
429
430 ### looks like a normal cram
431 } elsif ($data->{'cram_time'}) {
432 $data->add_data(type => 'cram');
433 my $real = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
434 my $str = join("/", @{$data}{qw(user cram_time expires_min payload)});
435 my $sum = md5_hex($str .'/'. $real);
436 if ($data->{'expires_min'} > 0
437 && ($self->server_time - $data->{'cram_time'}) > $data->{'expires_min'} * 60) {
438 $data->error('Login expired');
439 } elsif (lc($data->{'test_pass'}) ne $sum) {
440 $data->error('Invalid login');
441 }
442
443 ### plaintext_crypt
444 } elsif ($data->{'real_pass'} =~ m|^([./0-9A-Za-z]{2})([./0-9A-Za-z]{11})$|
445 && crypt($data->{'test_pass'}, $1) eq $data->{'real_pass'}) {
446 $data->add_data(type => 'crypt', was_plaintext => 1);
447
448 ### failed plaintext crypt
449 } elsif ($self->use_crypt) {
450 $data->error('Invalid login');
451 $data->add_data(type => 'crypt', was_plaintext => ($data->{'test_pass'} =~ /^[a-f0-9]{32}$/ ? 0 : 1));
452
453 ### plaintext and md5
454 } else {
455 my $is_md5_t = $data->{'test_pass'} =~ /^[a-f0-9]{32}$/;
456 my $is_md5_r = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/;
457 my $test = $is_md5_t ? lc($data->{'test_pass'}) : md5_hex($data->{'test_pass'});
458 my $real = $is_md5_r ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
459 $data->add_data(type => ($is_md5_r ? 'md5' : 'plaintext'), was_plaintext => ($is_md5_t ? 0 : 1));
460 $data->error('Invalid login')
461 if $test ne $real;
462 }
463
464 ### check the payload
465 if (! $data->error && ! $self->verify_payload($data->{'payload'})) {
466 $data->error('Invalid payload');
467 }
468
469 return $data;
470 }
471
472 sub new_auth_data {
473 my $self = shift;
474 return CGI::Ex::Auth::Data->new(@_);
475 }
476
477 sub last_auth_data { shift->{'_last_auth_data'} }
478
479 sub generate_token {
480 my $self = shift;
481 my $data = shift || $self->last_auth_data;
482 die "Can't generate a token off of a failed auth" if ! $data;
483
484 my $token;
485
486 ### do kinds that require staying plaintext
487 if ( (defined($data->{'use_plaintext'}) ? $data->{'use_plaintext'} : $self->use_plaintext) # ->use_plaintext is true if ->use_crypt is
488 || (defined($data->{'use_crypt'}) && $data->{'use_crypt'})
489 || (defined($data->{'type'}) && $data->{'type'} eq 'crypt')) {
490 my $pass = defined($data->{'test_pass'}) ? $data->{'test_pass'} : $data->{'real_pass'};
491 $token = $data->{'user'} .'/'. $pass;
492
493 ### all other types go to cram - secure_hash_cram, cram, plaintext and md5
494 } else {
495 my $user = $data->{'user'} || die "Missing user";
496 my $real = defined($data->{'real_pass'}) ? ($data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'}))
497 : die "Missing real_pass";
498 my $exp = defined($data->{'expires_min'}) ? $data->{'expires_min'} : $self->expires_min;
499 my $load = $self->generate_payload($data);
500 die "Payload can not contain a \"/\. Please escape it in generate_payload." if $load =~ m|/|;
501 die "User can not contain a \"/\." if $user =~ m|/|;
502
503 my $array;
504 if (! $data->{'prefer_cram'}
505 && ($array = eval { $self->secure_hash_keys })
506 && @$array) {
507 my $rand1 = int(rand @$array);
508 my $rand2 = int(rand 100000);
509 my $str = join("/", $user, $self->server_time, $exp, $load);
510 my $sum = md5_hex($str .'/'. $real .('/sh.'.$array->[$rand1].'.'.$rand2));
511 $token = $str .'/'. $sum . '/sh.'.$rand1.'.'.$rand2;
512 } else {
513 my $str = join("/", $user, $self->server_time, $exp, $load);
514 my $sum = md5_hex($str .'/'. $real);
515 $token = $str .'/'. $sum;
516 }
517 }
518
519 if (my $key = $data->{'use_blowfish'} || $self->use_blowfish) {
520 $token = encrypt_blowfish($token, $key);
521
522 } elsif (defined($data->{'use_base64'}) ? $data->{'use_base64'} : $self->use_base64) {
523 $token = encode_base64($token, '');
524 }
525
526 return $token;
527 }
528
529 sub generate_payload {
530 my $self = shift;
531 my $args = shift;
532 return defined($args->{'payload'}) ? $args->{'payload'} : '';
533 }
534
535 sub verify_user {
536 my $self = shift;
537 my $user = shift;
538 if (my $meth = $self->{'verify_user'}) {
539 return $meth->($self, $user);
540 }
541 return 1;
542 }
543
544 sub cleanup_user {
545 my $self = shift;
546 my $user = shift;
547 if (my $meth = $self->{'cleanup_user'}) {
548 return $meth->($self, $user);
549 }
550 return $user;
551 }
552
553 sub get_pass_by_user {
554 my $self = shift;
555 my $user = shift;
556 if (my $meth = $self->{'get_pass_by_user'}) {
557 return $meth->($self, $user);
558 }
559
560 die "Please override get_pass_by_user";
561 }
562
563 sub verify_payload {
564 my $self = shift;
565 my $payload = shift;
566 if (my $meth = $self->{'verify_payload'}) {
567 return $meth->($self, $payload);
568 }
569 return 1;
570 }
571
572 ###----------------------------------------------------------------###
573
574 sub encrypt_blowfish {
575 my ($str, $key) = @_;
576
577 require Crypt::Blowfish;
578 my $cb = Crypt::Blowfish->new($key);
579
580 $str .= (chr 0) x (8 - length($str) % 8); # pad to multiples of 8
581
582 my $enc = '';
583 $enc .= unpack "H16", $cb->encrypt($1) while $str =~ /\G(.{8})/g; # 8 bytes at a time
584
585 return $enc;
586 }
587
588 sub decrypt_blowfish {
589 my ($enc, $key) = @_;
590
591 require Crypt::Blowfish;
592 my $cb = Crypt::Blowfish->new($key);
593
594 my $str = '';
595 $str .= $cb->decrypt(pack "H16", $1) while $enc =~ /\G([A-Fa-f0-9]{16})/g;
596 $str =~ y/\00//d;
597
598 return $str
599 }
600
601 ###----------------------------------------------------------------###
602
603 sub login_template {
604 my $self = shift;
605 return $self->{'login_template'} if $self->{'login_template'};
606
607 my $text = ""
608 . $self->login_header
609 . $self->login_form
610 . $self->login_script
611 . $self->login_footer;
612 return \$text;
613 }
614
615 sub login_header {
616 return shift->{'login_header'} || q {
617 [%~ TRY ; PROCESS 'login_header.tt' ; CATCH %]<!-- [% error %] -->[% END ~%]
618 };
619 }
620
621 sub login_footer {
622 return shift->{'login_footer'} || q {
623 [%~ TRY ; PROCESS 'login_footer.tt' ; CATCH %]<!-- [% error %] -->[% END ~%]
624 };
625 }
626
627 sub login_form {
628 return shift->{'login_form'} || q {
629 <div class="login_chunk">
630 <span class="login_error">[% error %]</span>
631 <form class="login_form" name="[% form_name %]" method="post" action="[% script_name %][% path_info %]">
632 <input type="hidden" name="[% key_redirect %]" value="">
633 <input type="hidden" name="[% key_payload %]" value="">
634 <input type="hidden" name="[% key_time %]" value="">
635 <input type="hidden" name="[% key_expires_min %]" value="">
636 <table class="login_table">
637 <tr class="login_username">
638 <td>[% text_user %]</td>
639 <td><input name="[% key_user %]" type="text" size="30" value=""></td>
640 </tr>
641 <tr class="login_password">
642 <td>[% text_pass %]</td>
643 <td><input name="[% key_pass %]" type="password" size="30" value=""></td>
644 </tr>
645 <tr class="login_save">
646 <td colspan="2">
647 <input type="checkbox" name="[% key_save %]" value="1"> [% text_save %]
648 </td>
649 </tr>
650 <tr class="login_submit">
651 <td colspan="2" align="right">
652 <input type="submit" value="Submit">
653 </td>
654 </tr>
655 </table>
656 </form>
657 </div>
658 };
659 }
660
661 sub text_user { my $self = shift; return defined($self->{'text_user'}) ? $self->{'text_user'} : 'Username:' }
662 sub text_pass { my $self = shift; return defined($self->{'text_pass'}) ? $self->{'text_pass'} : 'Password:' }
663 sub text_save { my $self = shift; return defined($self->{'text_save'}) ? $self->{'text_save'} : 'Save Password ?' }
664
665 sub login_script {
666 return q {
667 [%~ IF ! use_plaintext %]
668 <script src="[% md5_js_path %]"></script>
669 <script>
670 if (document.md5_hex) document.[% form_name %].onsubmit = function () {
671 var f = document.[% form_name %];
672 var u = f.[% key_user %].value;
673 var p = f.[% key_pass %].value;
674 var t = f.[% key_time %].value;
675 var s = f.[% key_save %] && f.[% key_save %].checked ? -1 : f.[% key_expires_min %].value;
676 var l = f.[% key_payload %].value;
677 var r = f.[% key_redirect %].value;
678
679 var str = u+'/'+t+'/'+s+'/'+l;
680 var sum = document.md5_hex(str +'/' + document.md5_hex(p));
681 var loc = f.action + '?[% key_user %]='+escape(str +'/'+ sum)+'&[% key_redirect %]='+escape(r);
682
683 location.href = loc;
684 return false;
685 }
686 </script>
687 [% END ~%]
688 };
689 }
690
691 ###----------------------------------------------------------------###
692
693 package CGI::Ex::Auth::Data;
694
695 use strict;
696 use overload
697 'bool' => sub { ! shift->error },
698 '0+' => sub { 1 },
699 '""' => sub { shift->as_string },
700 fallback => 1;
701
702 sub new {
703 my ($class, $args) = @_;
704 return bless {%{ $args || {} }}, $class;
705 }
706
707 sub add_data {
708 my $self = shift;
709 my $args = @_ == 1 ? shift : {@_};
710 @{ $self }{keys %$args} = values %$args;
711 }
712
713 sub error {
714 my $self = shift;
715 if (@_ == 1) {
716 $self->{'error'} = shift;
717 $self->{'error_caller'} = [caller];
718 }
719 return $self->{'error'};
720 }
721
722 sub as_string {
723 my $self = shift;
724 return $self->error || ($self->{'user'} && $self->{'type'}) ? "Valid auth data" : "Unverified auth data";
725 }
726
727 ###----------------------------------------------------------------###
728
729 1;
730
731 __END__
732
733 =head1 SYNOPSIS
734
735 use CGI::Ex::Auth;
736
737 ### authorize the user
738 my $auth = CGI::Ex::Auth->get_valid_auth({
739 get_pass_by_user => \&get_pass_by_user,
740 });
741
742
743 sub get_pass_by_user {
744 my $auth = shift;
745 my $user = shift;
746 my $pass = some_way_of_getting_password($user);
747 return $pass;
748 }
749
750 ### OR - if you are using a OO based CGI or Application
751
752 sub require_authentication {
753 my $self = shift;
754
755 return $self->{'auth'} = CGI::Ex::Auth->get_valid_auth({
756 get_pass_by_user => sub {
757 my ($auth, $user) = @_;
758 return $self->get_pass($user);
759 },
760 });
761 }
762
763 sub get_pass {
764 my ($self, $user) = @_;
765 return $self->loopup_and_cache_pass($user);
766 }
767
768 =head1 DESCRIPTION
769
770 CGI::Ex::Auth allows for auto-expiring, safe and easy web based logins. Auth uses
771 javascript modules that perform MD5 hashing to cram the password on
772 the client side before passing them through the internet.
773
774 For the stored cookie you can choose to use cram mechanisms,
775 secure hash cram tokens, auto expiring logins (not cookie based),
776 and Crypt::Blowfish protection. You can also choose to keep
777 passwords plaintext and to use perl's crypt for testing
778 passwords.
779
780 A downside to this module is that it does not use a session to
781 preserve state so get_pass_by_user has to happen on every request (any
782 authenticated area has to verify authentication each time). A plus is
783 that you don't need to use a session if you don't want to. It is up
784 to the interested reader to add caching to the get_pass_by_user
785 method.
786
787 =head1 METHODS
788
789 =over 4
790
791 =item C<new>
792
793 Constructor. Takes a hashref of properties as arguments.
794
795 Many of the methods which may be overridden in a subclass,
796 or may be passed as properties to the new constuctor such as in the following:
797
798 CGI::Ex::Auth->new({
799 get_pass_by_user => \&my_pass_sub,
800 key_user => 'my_user',
801 key_pass => 'my_pass',
802 login_template => \"<form><input name=my_user ... </form>",
803 });
804
805 The following methods will look for properties of the same name. Each of these will be
806 defined separately.
807
808 cgix
809 cleanup_user
810 cookies
811 expires_min
812 form
813 form_name
814 get_pass_by_user
815 js_uri_path
816 key_cookie
817 key_expires_min
818 key_logout
819 key_pass
820 key_payload
821 key_redirect
822 key_save
823 key_time
824 key_user
825 key_verify
826 login_footer
827 login_form
828 login_header
829 login_script
830 login_template
831 no_cookie_verify
832 path_info
833 script_name
834 secure_hash_keys
835 template_args
836 template_include_path
837 text_user
838 text_pass
839 text_save
840 use_base64
841 use_blowfish
842 use_crypt
843 use_plaintext
844 verify_payload
845 verify_user
846
847 =item C<generate_token>
848
849 Takes either an auth_data object from a auth_data returned by verify_token,
850 or a hashref of arguments.
851
852 Possible arguments are:
853
854 user - the username we are generating the token for
855 real_pass - the password of the user (if use_plaintext is false
856 and use_crypt is false, the password can be an md5sum
857 of the user's password)
858 use_blowfish - indicates that we should use Crypt::Blowfish to protect
859 the generated token. The value of this argument is used
860 as the key. Default is false.
861 use_base64 - indicates that we should use Base64 encoding to protect
862 the generated token. Default is true. Will not be
863 used if use_blowfish is true.
864 use_plaintext - indicates that we should keep the password in plaintext
865 use_crypt - also indicates that we should keep the password in plaintext
866 expires_min - says how many minutes until the generated token expires.
867 Values <= 0 indicate to not ever expire. Used only on cram
868 types.
869 payload - a payload that will be passed to generate_payload and then
870 will be added to cram type tokens. It cannot contain a /.
871 prefer_cram - If the secure_hash_keys method returns keys, and it is a non-plaintext
872 token, generate_token will create a secure_hash_cram. Set
873 this value to true to tell it to use a normal cram. This
874 is generally only useful in testing.
875
876 The following are types of tokens that can be generated by generate_token. Each type includes
877 pseudocode and a sample of a generated that token.
878
879 plaintext:
880 user := "paul"
881 real_pass := "123qwe"
882 token := join("/", user, real_pass);
883
884 use_base64 := 0
885 token == "paul/123qwe"
886
887 use_base64 := 1
888 token == "cGF1bC8xMjNxd2U="
889
890 use_blowfish := "foobarbaz"
891 token == "6da702975190f0fe98a746f0d6514683"
892
893 Notes: This token will be used if either use_plaintext or use_crypt is set.
894 The real_pass can also be the md5_sum of the password. If real_pass is an md5_sum
895 of the password but the get_pass_by_user hook returns the crypt'ed password, the
896 token will not be able to be verified.
897
898 cram:
899 user := "paul"
900 real_pass := "123qwe"
901 server_time := 1148512991 # a time in seconds since epoch
902 expires_min := 6 * 60
903 payload := "something"
904
905 md5_pass := md5_sum(real_pass) # if it isn't already a 32 digit md5 sum
906 str := join("/", user, server_time, expires_min, payload, md5_pass)
907 md5_str := md5(sum_str)
908 token := join("/", user, server_time, expires_min, payload, md5_str)
909
910 use_base64 := 0
911 token == "paul/1148512991/360/something/16d0ba369a4c9781b5981eb89224ce30"
912
913 use_base64 := 1
914 token == "cGF1bC8xMTQ4NTEyOTkxLzM2MC9zb21ldGhpbmcvMTZkMGJhMzY5YTRjOTc4MWI1OTgxZWI4OTIyNGNlMzA="
915
916 Notes: use_blowfish is available as well
917
918 secure_hash_cram:
919 user := "paul"
920 real_pass := "123qwe"
921 server_time := 1148514034 # a time in seconds since epoch
922 expires_min := 6 * 60
923 payload := "something"
924 secure_hash := ["aaaa", "bbbb", "cccc", "dddd"]
925 rand1 := 3 # int(rand(length(secure_hash)))
926 rand2 := 39163 # int(rand(100000))
927
928 md5_pass := md5_sum(real_pass) # if it isn't already a 32 digit md5 sum
929
930 sh_str1 := join(".", "sh", secure_hash[rand1], rand2)
931 sh_str2 := join(".", "sh", rand1, rand2)
932 str := join("/", user, server_time, expires_min, payload, md5_pass, sh_str1)
933 md5_str := md5(sum_str)
934 token := join("/", user, server_time, expires_min, payload, md5_str, sh_str2)
935
936 use_base64 := 0
937 token == "paul/1148514034/360/something/06db2914c9fd4e11499e0652bcf67dae/sh.3.39163"
938
939 Notes: use_blowfish is available as well. The secure_hash keys need to be set in the
940 "secure_hash_keys" property of the CGI::Ex::Auth object.
941
942 =item C<get_valid_auth>
943
944 Performs the core logic. Returns an auth object on successful login.
945 Returns false on errored login (with the details of the error stored in
946 $@). If a false value is returned, execution of the CGI should be halted.
947 get_valid_auth WILL NOT automatically stop execution.
948
949 $auth->get_valid_auth || exit;
950
951 Optionally, the class and a list of arguments may be passed. This will create a
952 new object using the passed arguments, and then run get_valid_auth.
953
954 CGI::Ex::Auth->get_valid_auth({key_user => 'my_user'}) || exit;
955
956 =item C<login_print>
957
958 Called if login errored. Defaults to printing a very basic (but
959 adequate) page loaded from login_template..
960
961 You will want to override it with a template from your own system.
962 The hook that is called will be passed the step to print (currently
963 only "get_login_info" and "no_cookies"), and a hash containing the
964 form variables as well as the following:
965
966 =item C<login_hash_common>
967
968 Passed to the template swapped during login_print.
969
970 %$form, # any keys passed to the login script
971 error # The text "Login Failed" if a login occurred
972 login_data # A login data object if they failed authentication.
973 key_user # $self->key_user, # the username fieldname
974 key_pass # $self->key_pass, # the password fieldname
975 key_time # $self->key_time, # the server time field name
976 key_save # $self->key_save, # the save password checkbox field name
977 key_payload # $self->key_payload, # the payload fieldname
978 key_redirect # $self->key_redirect, # the redirect fieldname
979 form_name # $self->form_name, # the name of the form
980 script_name # $self->script_name, # where the server will post back to
981 path_info # $self->path_info, # $ENV{PATH_INFO} if any
982 md5_js_path # $self->js_uri_path ."/CGI/Ex/md5.js", # script for cramming
983 use_plaintext # $self->use_plaintext, # used to avoid cramming
984 $self->key_user # $data->{'user'}, # the username (if any)
985 $self->key_pass # '', # intentional blankout
986 $self->key_time # $self->server_time, # the server's time
987 $self->key_payload # $data->{'payload'} # the payload (if any)
988 $self->key_expires_min # $self->expires_min # how many minutes crams are valid
989 text_user # $self->text_user # template text Username:
990 text_pass # $self->text_pass # template text Password:
991 text_save # $self->text_save # template text Save Password ?
992
993 =item C<key_logout>
994
995 If the form hash contains a true value in this field name, the current user will
996 be logged out. Default is "cea_logout".
997
998 =item C<key_cookie>
999
1000 The name of the auth cookie. Default is "cea_user".
1001
1002 =item C<key_verify>
1003
1004 A field name used during a bounce to see if cookies exist. Default is "cea_verify".
1005
1006 =item C<key_user>
1007
1008 The form field name used to pass the username. Default is "cea_user".
1009
1010 =item C<key_pass>
1011
1012 The form field name used to pass the password. Default is "cea_pass".
1013
1014 =item C<key_save>
1015
1016 Works in conjunction with key_expires_min. If key_save is true, then
1017 the cookie will be set to be saved for longer than the current session
1018 (If it is a plaintext variety it will be given a 20 year life rather
1019 than being a session cookie. If it is a cram variety, the expires_min
1020 portion of the cram will be set to -1). If it is set to false, the cookie
1021 will be available only for the session (If it is a plaintext variety, the cookie
1022 will be session based and will be removed on the next loggout. If it is
1023 a cram variety then the cookie will only be good for expires_min minutes.
1024
1025 Default is "cea_save".
1026
1027 =item C<key_expires_min>
1028
1029 The name of the form field that contains how long cram type cookies will be valid
1030 if key_save contains a false value.
1031
1032 Default key name is "cea_expires_min". Default field value is 6 * 60 (six hours).
1033
1034 This value will have no effect when use_plaintext or use_crypt is set.
1035
1036 A value of -1 means no expiration.
1037
1038 =item C<failed_sleep>
1039
1040 Number of seconds to sleep if the passed tokens are invalid. Does not apply
1041 if validation failed because of expired tokens. Default value is 0.
1042 Setting to 0 disables any sleeping.
1043
1044 =item C<form_name>
1045
1046 The name of the html login form to attach the javascript to. Default is "cea_form".
1047
1048 =item C<verify_token>
1049
1050 This method verifies the token that was passed either via the form or via cookies.
1051 It will accept plaintext or crammed tokens (A listing of the available algorithms
1052 for creating tokes is listed below). It also allows for armoring the token with
1053 base64 encoding, or using blowfish encryption. A listing of creating these tokens
1054 can be found under generate_token.
1055
1056 =item C<cleanup_user>
1057
1058 Called by verify_token. Default is to do no modification. Allows for usernames to
1059 be lowercased, or canonized in some other way. Should return the cleaned username.
1060
1061 =item C<verify_user>
1062
1063 Called by verify_token. Single argument is the username. May or may not be an
1064 initial check to see if the username is ok. The username will already be cleaned at
1065 this point. Default return is true.
1066
1067 =item C<get_pass_by_user>
1068
1069 Called by verify_token. Given the cleaned, verified username, should return a
1070 valid password for the user. It can always return plaintext. If use_crypt is
1071 enabled, it should return the crypted password. If use_plaintext and use_crypt
1072 are not enabled, it may return the md5 sum of the password.
1073
1074 get_pass_by_user => sub {
1075 my ($auth_obj, $user) = @_;
1076 my $pass = $some_obj->get_pass({user => $user});
1077 return $pass;
1078 }
1079
1080 Alternately, get_pass_by_user may return a hashref of data items that
1081 will be added to the data object if the token is valid. The hashref
1082 must also contain a key named real_pass or password that contains the
1083 password. Note that keys passed back in the hashref that are already
1084 in the data object will override those in the data object.
1085
1086 get_pass_by_user => sub {
1087 my ($auth_obj, $user) = @_;
1088 my ($pass, $user_id) = $some_obj->get_pass({user => $user});
1089 return {
1090 password => $pass,
1091 user_id => $user_id,
1092 };
1093 }
1094
1095 =item C<cgix>
1096
1097 Returns a CGI::Ex object.
1098
1099 =item C<form>
1100
1101 A hash of passed form info. Defaults to CGI::Ex::get_form.
1102
1103 =item C<cookies>
1104
1105 The current cookies. Defaults to CGI::Ex::get_cookies.
1106
1107 =item C<login_template>
1108
1109 Should return either a template filename to use for the login template, or it
1110 should return a reference to a string that contains the template. The contents
1111 will be used in login_print and passed to the template engine.
1112
1113 Default login_template is the values of login_header, login_form, login_script, and
1114 login_script concatenated together.
1115
1116 Values from login_hash_common will be passed to the template engine, and will
1117 also be used to fill in the form.
1118
1119 The basic values are capable of handling most needs so long as appropriate
1120 headers and css styles are used.
1121
1122 =item C<login_header>
1123
1124 Should return a header to use in the default login_template. The default
1125 value will try to PROCESS a file called login_header.tt that should be
1126 located in directory specified by the template_include_path method.
1127
1128 It should ideally supply css styles that format the login_form as desired.
1129
1130 =item C<login_footer>
1131
1132 Same as login_header - but for the footer. Will look for login_footer.tt by
1133 default.
1134
1135 =item C<login_form>
1136
1137 An html chunk that contains the necessary form fields to login the user. The
1138 basic chunk has a username text entry, password text entry, save password checkbox,
1139 and submit button, and any hidden fields necessary for logging in the user.
1140
1141 =item C<login_script>
1142
1143 Contains javascript that will attach to the form from login_form. This script
1144 is capable of taking the login_fields and creating an md5 cram which prevents
1145 the password from being passed plaintext.
1146
1147 =item C<text_user, text_pass, text_save>
1148
1149 The text items shown in the default login template. The default values are:
1150
1151 text_user "Username:"
1152 text_pass "Password:"
1153 text_save "Save Password ?"
1154
1155 =back
1156
1157 =head1 LICENSE
1158
1159 This module may be distributed under the same terms as Perl itself.
1160
1161 =head1 AUTHORS
1162
1163 Paul Seamons <perl at seamons dot com>
1164
1165 =cut
This page took 0.108829 seconds and 3 git commands to generate.