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