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