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