]> Dogcows Code - chaz/p5-CGI-Ex/blobdiff - lib/CGI/Ex/Auth.pm
CGI::Ex 2.00
[chaz/p5-CGI-Ex] / lib / CGI / Ex / Auth.pm
index e564efdaf38c606f0efe32926bd093ba870028e7..49c9a198e1ce3958713536d9a1bffe5118349306 100644 (file)
 package CGI::Ex::Auth;
 
-### CGI Extended Application
+=head1 NAME
+
+CGI::Ex::Auth - Handle logins nicely.
+
+=cut
 
 ###----------------------------------------------------------------###
-#  Copyright 2004 - Paul Seamons                                     #
+#  Copyright 2006 - Paul Seamons                                     #
 #  Distributed under the Perl Artistic License without warranty      #
 ###----------------------------------------------------------------###
 
-### See perldoc at bottom
-
-
 use strict;
-use vars qw($USE_PLAINTEXT
-            $CHECK_CRYPTED
-            $EXPIRE_LOGINS
-            $FAILED_SLEEP
-            $VERSION
-            );
-
-use CGI::Ex::Dump qw(debug);
+use vars qw($VERSION);
+
 use MIME::Base64 qw(encode_base64 decode_base64);
+use Digest::MD5 qw(md5_hex);
+use CGI::Ex;
 
-BEGIN {
-  $VERSION = '0.10';
-  $CHECK_CRYPTED = 1        if ! defined $CHECK_CRYPTED;
-  $FAILED_SLEEP  = 2        if ! defined $FAILED_SLEEP;
-  $EXPIRE_LOGINS = 6 * 3600 if ! defined $EXPIRE_LOGINS;
-  #if ($ENV{MOD_PERL}) {
-  #  require Digest::SHA1;
-  #  require Digest::MD5;
-  #}
-}
+$VERSION = '2.00';
 
 ###----------------------------------------------------------------###
 
 sub new {
-  my $class = shift || __PACKAGE__;
-  my $self  = ref($_[0]) ? shift : {@_};
-  bless $self, $class;
-  $self->init();
-  return $self;
+    my $class = shift || __PACKAGE__;
+    my $args  = shift || {};
+    return bless {%$args}, $class;
 }
 
-sub init {}
+sub get_valid_auth {
+    my $self = shift;
+    $self = $self->new(@_) if ! ref $self;
 
-###----------------------------------------------------------------###
+    ### shortcut that will print a js file as needed (such as the md5.js)
+    if ($self->script_name . $self->path_info eq $self->js_uri_path . "/CGI/Ex/md5.js") {
+        $self->cgix->print_js('CGI/Ex/md5.js');
+        eval { die "Printed Javascript" };
+        return;
+    }
 
-sub require_auth {
-  my $self = shift;
-  $self = __PACKAGE__->new($self) if ! UNIVERSAL::isa($self, __PACKAGE__);
+    my $form    = $self->form;
+    my $cookies = $self->cookies;
+    my $key_l   = $self->key_logout;
+    my $key_c   = $self->key_cookie;
+    my $has_cookies = scalar %$cookies;
+
+    ### allow for logout
+    if ($form->{$key_l}) {
+        $self->delete_cookie({key => $key_c});;
+        $self->location_bounce($self->logout_redirect);
+        eval { die "Logging out" };
+        return;
+    }
 
-  ### shortcut that will print a js file as needed
-  if ($ENV{PATH_INFO} && $ENV{PATH_INFO} =~ m|^/js/(CGI/Ex/\w+\.js)$|) {
-    $self->cgix->print_js($1);
-    return 0;
-  }
+    my $had_form_info;
+    foreach ([$form,    $self->key_user, 1],
+             [$cookies, $key_c,          0],
+             ) {
+        my ($hash, $key, $is_form) = @$_;
+        next if ! defined $hash->{$key};
+        $had_form_info ++ if $is_form;
+
+        ### if it looks like a bare username (as in they didn't have javascript)- add in other items
+        my $data;
+        if ($is_form
+            && $hash->{$key} !~ m|^[^/]+/|
+            && defined $hash->{ $self->key_pass }) {
+            $data = $self->verify_token({
+                token => {
+                    user        => delete $hash->{$key},
+                    test_pass   => delete $hash->{ $self->key_pass },
+                    expires_min => delete($hash->{ $self->key_save }) ? -1 : delete($hash->{ $self->key_expires_min }) || $self->expires_min,
+                    payload     => delete $hash->{ $self->key_payload } || '',
+                },
+                from => 'form',
+            }) || next;
+
+        } else {
+            $data = $self->verify_token({token => $hash->{$key}, from => ($is_form ? 'form' : 'cookie')}) || next;
+            delete $hash->{$key} if $is_form;
+        }
 
-  my $form    = $self->form;
-  my $cookies = $self->cookies;
-  my $key_l   = $self->key_logout;
-  my $key_c   = $self->key_cookie;
-  my $key_u   = $self->key_user;
-  my $key_p   = $self->key_pass;
-  my $key_chk = $self->key_cookie_check;
-  my $had_form_info = 0;
-
-  ### if they've passed us information - try and use it
-  if ($form->{$key_l}) {
-    $self->delete_cookie;
-
-  } elsif (exists($form->{$key_u}) && exists($form->{$key_p})) {
-    if ($self->verify_userpass($form->{$key_u}, $form->{$key_p})) {
-      my $has_cookies = scalar keys %$cookies;
-      my $user  = $form->{$key_u};
-      my $str   = encode_base64(join(":", delete($form->{$key_u}), delete($form->{$key_p})), "");
-      my $key_s = $self->key_save;
-      $self->set_cookie($str, delete($form->{$key_s}));
-      #return $self->success($user); # assume that cookies will work - if not next page will cause login
-      #### this may actually be the nicer thing to do in the common case - except for the nasty looking
-      #### url - all things considered - should really get location boucing to work properly while being
-      #### able to set a cookie at the same time
-
-      if ($has_cookies) {
-        return $self->success($user); # assuming if they have cookies - the one we set will work
-      } else {
-        $form->{$key_chk} = time();
-        my $key_r = $self->key_redirect;
-        if (! $form->{$key_r}) {
-          my $script = $ENV{SCRIPT_NAME} || die "Missing SCRIPT_NAME";
-          my $info   = $ENV{PATH_INFO} || '';
-          my $query  = $self->cgix->make_form($form);
-          $form->{$key_r} = $script . $info . ($query ? "?$query" : "");
+        ### generate a fresh cookie if they submitted info on plaintext types
+        if ($self->use_plaintext || ($data->{'type'} && $data->{'type'} eq 'crypt')) {
+            $self->set_cookie({
+                key        => $key_c,
+                val        => $self->generate_token($data),
+                no_expires => ($data->{ $self->key_save } ? 0 : 1), # make it a session cookie unless they ask for saving
+            }) if $is_form; # only set the cookie if we found info in the form - the cookie will be a session cookie after that
+
+        ### always generate a cookie on types that have expiration
+        } else {
+            $self->set_cookie({
+                key        => $key_c,
+                val        => $self->generate_token($data),
+                no_expires => 0,
+            });
+        }
+
+        ### successful login
+
+        ### bounce to redirect
+        if (my $redirect = $form->{ $self->key_redirect }) {
+            $self->location_bounce($redirect);
+            eval { die "Success login - bouncing to redirect" };
+            return;
+
+        ### if they have cookies we are done
+        } elsif ($has_cookies || $self->no_cookie_verify) {
+            return $self;
+
+        ### need to verify cookies are set-able
+        } elsif ($is_form) {
+            $form->{$self->key_verify} = $self->server_time;
+            my $query = $self->cgix->make_form($form);
+            my $url   = $self->script_name . $self->path_info . ($query ? "?$query" : "");
+
+            $self->location_bounce($url);
+            eval { die "Success login - bouncing to test cookie" };
+            return;
         }
-        $self->location_bounce($form->{$key_r});
-        return 0;
-      }
-    } else {
-      $had_form_info = 1;
-      $self->delete_cookie;
     }
 
-  ### otherwise look for an already set cookie
-  } elsif ($cookies->{$key_c}) {
-    my ($user, $pass) = split /:/, decode_base64($cookies->{$key_c}), 2;
-    return $self->success($user) if $self->verify_userpass($user, $pass);
-    $self->delete_cookie;
-
-  ### cases to handle no cookies
-  } elsif ($form->{$key_chk}) {
-    my $value = delete $form->{$key_chk};
-    if ($self->allow_htauth) {
-      die "allow_htauth is not implemented - yet";
-    } elsif (abs(time() - $value) < 3600) {
-      # fail down to below where we ask for auth
-      # this is assuming that all webservers in the cluster are within 3600 of each other
-    } else {
-      $self->hook_print("no_cookies", $form);
-      return 0;
+    ### make sure the cookie is gone
+    $self->delete_cookie({key => $key_c}) if $cookies->{$key_c};
+
+    ### nothing found - see if they have cookies
+    if (my $value = delete $form->{$self->key_verify}) {
+        if (abs(time() - $value) < 15) {
+            $self->no_cookies_print;
+            return;
+        }
     }
-  }
 
-  ### oh - you're still here - well then - ask for login credentials
-  my $key_r = $self->key_redirect;
-  if (! $form->{$key_r}) {
-    my $script = $ENV{SCRIPT_NAME} || die "Missing SCRIPT_NAME";
-    my $info   = $ENV{PATH_INFO} || '';
-    my $query  = $self->cgix->make_form($form);
-    $form->{$key_r} = $script . $info . ($query ? "?$query" : "");
-  }
-  $form->{login_error} = $had_form_info;
-  $self->hook_print("get_login_info", $form);
-  return 0;
+    ### oh - you're still here - well then - ask for login credentials
+    my $key_r = $self->key_redirect;
+    if (! $form->{$key_r}) {
+        my $query = $self->cgix->make_form($form);
+        $form->{$key_r} = $self->script_name . $self->path_info . ($query ? "?$query" : "");
+    }
+
+    $form->{'had_form_data'} = $had_form_info;
+    $self->login_print;
+    my $data = $self->last_auth_data;
+    eval { die defined($data) ? $data : "Requesting credentials" };
+    return;
 }
 
 ###----------------------------------------------------------------###
 
-sub hook_print {
-  my $self = shift;
-  my $page = shift;
-  my $form = shift;
-
-  ### copy the form and add various pieces
-  my $FORM = {%$form};
-  $FORM->{payload}      = $self->payload;
-  $FORM->{error}        = ($form->{login_error}) ? "Login Failed" : "";
-  $FORM->{key_user}     = $self->key_user;
-  $FORM->{key_pass}     = $self->key_pass;
-  $FORM->{key_save}     = $self->key_save;
-  $FORM->{key_redirect} = $self->key_redirect;
-  $FORM->{form_name}    = $self->form_name;
-  $FORM->{script_name}  = $ENV{SCRIPT_NAME};
-  $FORM->{path_info}    = $ENV{PATH_INFO} || '';
-  $FORM->{login_script} = $self->login_script($FORM);
-  delete $FORM->{$FORM->{key_pass}};
-
-  ### allow for custom hook
-  if (my $meth = $self->{hook_print}) {
-    $self->$meth($page, $FORM);
-    return 0;
-  }
+sub script_name { shift->{'script_name'} || $ENV{'SCRIPT_NAME'} || die "Missing SCRIPT_NAME" }
 
-  ### no hook - give basic functionality
-  my $content;
-  if ($page eq 'no_cookies') {
-    $content = qq{<div style="border: 2px solid black;background:red;color:white">You do not appear to have cookies enabled.</div>};
-  } elsif ($page eq 'get_login_info') {
-    $content = $self->basic_login_page($FORM);
-  } else {
-    $content = "No content for page \"$page\"";
-  }
+sub path_info { shift->{'path_info'} || $ENV{'PATH_INFO'} || '' }
 
-  $self->cgix->print_content_type();
-  print $content;
-  return 0;
-}
-
-###----------------------------------------------------------------###
+sub server_time { time }
 
-sub success {
-  my $self = shift;
-  my $user = shift;
-  $self->{user} = $ENV{REMOTE_USER} = $user;
-  $self->hook_success($user);
-  return 1;
+sub cgix {
+    my $self = shift;
+    $self->{'cgix'} = shift if $#_ != -1;
+    return $self->{'cgix'} ||= CGI::Ex->new;
 }
 
-sub user {
-  my $self = shift;
-  return $self->{user};
+sub form {
+    my $self = shift;
+    $self->{'form'} = shift if $#_ != -1;
+    return $self->{'form'} ||= $self->cgix->get_form;
 }
 
-sub hook_success {
-  my $self = shift;
-  my $user = shift;
-  my $meth;
-  if ($meth = $self->{hook_success}) {
-    $self->$meth($user);
-  }
+sub cookies {
+    my $self = shift;
+    $self->{'cookies'} = shift if $#_ != -1;
+    return $self->{'cookies'} ||= $self->cgix->get_cookies;
 }
 
-###----------------------------------------------------------------###
-
 sub delete_cookie {
-  my $self  = shift;
-  my $key_c = $self->key_cookie;
-  $self->cgix->set_cookie({
-    -name    => $key_c,
-    -value   => '',
-    -expires => '-10y',
-    -path    => '/',
-  });
-}      
+    my $self = shift;
+    my $args = shift;
+    my $key  = $args->{'key'};
+    $self->cgix->set_cookie({
+        -name    => $key,
+        -value   => '',
+        -expires => '-10y',
+        -path    => '/',
+    });
+    delete $self->cookies->{$key};
+}
 
 sub set_cookie {
-  my $self  = shift;
-  my $key_c = $self->key_cookie;
-  my $value = shift || '';
-  my $save_pass = shift;
-  $self->cgix->set_cookie({
-    -name    => $key_c,
-    -value   => $value,
-    ($save_pass ? (-expires => '+10y') : ()),
-    -path    => '/',
-  });
+    my $self = shift;
+    my $args = shift;
+    my $key  = $args->{'key'};
+    my $val  = $args->{'val'};
+    $self->cgix->set_cookie({
+        -name    => $key,
+        -value   => $val,
+        ($args->{'no_expires'} ? () : (-expires => '+20y')), # let the expires time take care of things for types that self expire
+        -path    => '/',
+    });
+    $self->cookies->{$key} = $val;
 }
 
 sub location_bounce {
-  my $self = shift;
-  my $url = shift;
-  return $self->cgix->location_bounce($url);
+    my $self = shift;
+    my $url  = shift;
+    return $self->cgix->location_bounce($url);
 }
 
 ###----------------------------------------------------------------###
 
-sub key_logout {
-  my $self = shift;
-  $self->{key_logout} = shift if $#_ != -1;
-  return $self->{key_logout} ||= 'logout';
+sub key_logout       { shift->{'key_logout'}       ||= 'cea_logout'   }
+sub key_cookie       { shift->{'key_cookie'}       ||= 'cea_user'     }
+sub key_user         { shift->{'key_user'}         ||= 'cea_user'     }
+sub key_pass         { shift->{'key_pass'}         ||= 'cea_pass'     }
+sub key_time         { shift->{'key_time'}         ||= 'cea_time'     }
+sub key_save         { shift->{'key_save'}         ||= 'cea_save'     }
+sub key_expires_min  { shift->{'key_expires_min'}  ||= 'cea_expires_min' }
+sub form_name        { shift->{'form_name'}        ||= 'cea_form'     }
+sub key_verify       { shift->{'key_verify'}       ||= 'cea_verify'   }
+sub key_redirect     { shift->{'key_redirect'}     ||= 'cea_redirect' }
+sub key_payload      { shift->{'key_payload'}      ||= 'cea_payload'  }
+sub secure_hash_keys { shift->{'secure_hash_keys'} ||= []             }
+sub no_cookie_verify { shift->{'no_cookie_verify'} ||= 0              }
+sub use_crypt        { shift->{'use_crypt'}        ||= 0              }
+sub use_blowfish     { shift->{'use_blowfish'}     ||= ''             }
+sub use_plaintext    { my $s = shift; $s->use_crypt || ($s->{'use_plaintext'} ||= 0) }
+sub use_base64       { my $s = shift; $s->{'use_base64'}  = 1      if ! defined $s->{'use_base64'};  $s->{'use_base64'}  }
+sub expires_min      { my $s = shift; $s->{'expires_min'} = 6 * 60 if ! defined $s->{'expires_min'}; $s->{'expires_min'} }
+
+sub logout_redirect {
+    my $self = shift;
+    return $self->{'logout_redirect'} || $self->script_name ."?loggedout=1";
 }
 
-sub key_cookie {
-  my $self = shift;
-  $self->{key_cookie} = shift if $#_ != -1;
-  return $self->{key_cookie} ||= 'ce_auth';
+sub js_uri_path {
+    my $self = shift;
+    return $self->{'js_uri_path'} ||= $self->script_name ."/js";
 }
 
-sub key_cookie_check {
-  my $self = shift;
-  $self->{key_cookie_check} = shift if $#_ != -1;
-  return $self->{key_cookie_check} ||= 'ccheck';
-}
+###----------------------------------------------------------------###
 
-sub key_user {
-  my $self = shift;
-  $self->{key_user} = shift if $#_ != -1;
-  return $self->{key_user} ||= 'ce_user';
+sub no_cookies_print {
+    my $self = shift;
+    $self->cgix->print_content_type;
+    print qq{<div style="border: 2px solid black;background:red;color:white">You do not appear to have cookies enabled.</div>};
+    return 1;
 }
 
-sub key_pass {
-  my $self = shift;
-  $self->{key_pass} = shift if $#_ != -1;
-  return $self->{key_pass} ||= 'ce_pass';
-}
+sub login_print {
+    my $self = shift;
+    my $hash = $self->login_hash_common;
+    my $template = $self->login_template;
 
-sub key_save {
-  my $self = shift;
-  $self->{key_save} = shift if $#_ != -1;
-  return $self->{key_save} ||= 'ce_save';
-}
+    ### allow for a hooked override
+    if (my $meth = $self->{'login_print'}) {
+        $meth->($self, $template, $hash);
+        return 0;
+    }
 
-sub key_redirect {
-  my $self = shift;
-  $self->{key_redirect} = shift if $#_ != -1;
-  return $self->{key_redirect} ||= 'redirect';
-}
+    ### process the document
+    require CGI::Ex::Template;
+    my $cet = CGI::Ex::Template->new($self->template_args);
+    my $out = '';
+    $cet->process_simple($template, $hash, \$out) || die $cet->error;
+
+    ### fill in form fields
+    require CGI::Ex::Fill;
+    CGI::Ex::Fill::fill({text => \$out, form => $hash});
+
+    ### print it
+    $self->cgix->print_content_type;
+    print $out;
 
-sub form_name {
-  my $self = shift;
-  $self->{form_name} = shift if $#_ != -1;
-  return $self->{form_name} ||= 'ce_form';
+    return 0;
 }
 
-sub allow_htauth {
-  my $self = shift;
-  $self->{allow_htauth} = shift if $#_ != -1;
-  return $self->{allow_htauth} ||= 0;
+sub template_args {
+    my $self = shift;
+    return $self->{'template_args'} ||= {
+        INCLUDE_PATH => $self->template_include_path,
+    };
 }
 
-sub payload {
-  my $self = shift;
-  my $user = shift;
-  my $time = shift || time();
-  my $meth;
-  my @payload = ($time);
-  if ($meth = $self->{hook_payload}) {
-    push @payload, $self->$meth($user);
-  }
-  return join "/", @payload;
+sub template_include_path { shift->{'template_include_path'} || '' }
+
+sub login_hash_common {
+    my $self = shift;
+    my $form = $self->form;
+    my $data = $self->last_auth_data;
+    $data = {} if ! defined $data;
+
+    return {
+        %$form,
+        error              => ($form->{'had_form_data'}) ? "Login Failed" : "",
+        login_data         => $data,
+        key_user           => $self->key_user,
+        key_pass           => $self->key_pass,
+        key_time           => $self->key_time,
+        key_save           => $self->key_save,
+        key_expires_min    => $self->key_expires_min,
+        key_payload        => $self->key_payload,
+        key_redirect       => $self->key_redirect,
+        form_name          => $self->form_name,
+        script_name        => $self->script_name,
+        path_info          => $self->path_info,
+        md5_js_path        => $self->js_uri_path ."/CGI/Ex/md5.js",
+        use_plaintext      => $self->use_plaintext,
+        $self->key_user    => $data->{'user'} || '',
+        $self->key_pass    => '', # don't allow for this to get filled into the form
+        $self->key_time    => $self->server_time,
+        $self->key_payload => $self->generate_payload({%$data, login_form => 1}),
+        $self->key_expires_min => $self->expires_min,
+
+    };
 }
 
 ###----------------------------------------------------------------###
 
-sub verify_userpass {
-  my $self = shift;
-  my $user = shift;
-  my $pass = shift;
-  my $host = shift || $self->host;
-  my $meth;
-  if ($meth = $self->{hook_verify_userpass}) {
-    return $self->$meth($user, $pass, $host);
-  } else {
-    return $self->hook_verify_userpass($user, $pass, $host);
-  }
-}
+sub verify_token {
+    my $self  = shift;
+    my $args  = shift;
+    my $token = delete $args->{'token'} || die "Missing token";
+    my $data  = $self->{'_last_auth_data'} = $self->new_auth_data({token => $token, %$args});
+
+    ### token already parsed
+    if (ref $token) {
+        $data->add_data({%$token, armor => 'none'});
 
-sub hook_verify_userpass {
-  my $self = shift;
-  my $user = shift;
-  my $pass_test = shift;
-  my $host = shift || $self->host;
-
-  return undef if ! defined $user;
-  return undef if ! defined $pass_test;
-  my $pass_real = $self->hook_get_pass_by_user($user, $host);
-  return undef if ! defined $pass_real;
-
-  my $type_real = ($pass_real =~ m/^(md5|sha1)\((.+)\)$/) ? $1 : 'plainorcrypt';
-  my $hash_real = $2;
-  my $type_test = ($pass_test =~ m/^(md5|sha1)\((.+)\)$/) ? $1 : 'plainorcrypt';
-  my $hash_test = $2;
-
-  ### if both types were plaintext - check if the equal
-  if ($type_real eq 'plainorcrypt'
-      && $type_test eq 'plainorcrypt') {
-    return 1 if $pass_real eq $pass_test;
-    if ($CHECK_CRYPTED && $pass_real =~ m|^([./0-9A-Za-z]{2})(.{,11})$|) {
-      return 1 if crypt($pass_test, $1) eq $pass_real;
+    ### parse token for info
+    } else {
+        my $found;
+        my $key;
+        for my $armor ('none', 'base64', 'blowfish') { # try with and without base64 encoding
+            my $copy = ($armor eq 'none')           ? $token
+                     : ($armor eq 'base64')         ? decode_base64($token)
+                     : ($key = $self->use_blowfish) ? decrypt_blowfish($token, $key)
+                     : next;
+            if ($copy =~ m|^ ([^/]+) / (\d+) / (-?\d+) / (.*) / ([a-fA-F0-9]{32}) (?: / (sh\.\d+\.\d+))? $|x) {
+                $data->add_data({
+                    user         => $1,
+                    cram_time    => $2,
+                    expires_min  => $3,
+                    payload      => $4,
+                    test_pass    => $5,
+                    secure_hash  => $6 || '',
+                    armor        => $armor,
+                });
+                $found = 1;
+                last;
+            } elsif ($copy =~ m|^ ([^/]+) / (.*) $|x) {
+                $data->add_data({
+                    user         => $1,
+                    test_pass    => $2,
+                    armor        => $armor,
+                });
+                $found = 1;
+                last;
+            }
+        }
+        if (! $found) {
+            $data->error('Invalid token');
+            return $data;
+        }
     }
-    return 0;
 
-  } else {
-    ### if test type is plaintext - then hash it and compare it alone
-    if ($type_test eq 'plainorcrypt') {
-      $pass_test = $self->enc_func($type_real, $pass_test); # encode same as real
-      $pass_test = "$type_real($pass_test)";
-      return $pass_test eq $pass_real;
-
-    ### if real type is plaintext - then hash it to get ready for test
-    } elsif ($type_real eq 'plainorcrypt') {
-      $pass_real = $self->enc_func($type_test, $pass_real);
-      $pass_real = "$type_test($pass_real)";
-      $type_real = $type_test;
+
+    ### verify the user and get the pass
+    my $pass;
+    if (! defined($data->{'user'})) {
+        $data->error('Missing user');
+
+    } elsif (! defined $data->{'test_pass'}) {
+        $data->error('Missing test_pass');
+
+    } elsif (! $self->verify_user($data->{'user'} = $self->cleanup_user($data->{'user'}))) {
+        $data->error('Invalid user');
+
+    } elsif (! defined($pass = eval { $self->get_pass_by_user($data->{'user'}) })) {
+        $data->add_data({details => $@});
+        $data->error('Could not get pass');
     }
-    
-    ### the types should be the same (unless a system stored sha1 and md5 passwords)
-    if ($type_real ne $type_test) {
-      warn "Test types for user \"$user\" are of two different types - very bad";
-      return 0;
+    return $data if $data->error;
+
+
+    ### store - to allow generate_token to not need to relookup the pass
+    $data->add_data({real_pass => $pass});
+
+
+    ### looks like a secure_hash cram
+    if ($data->{'secure_hash'}) {
+        $data->add_data(type => 'secure_hash_cram');
+        my $array = eval {$self->secure_hash_keys };
+        if (! $array) {
+            $data->error('secure_hash_keys not found');
+        } elsif (! @$array) {
+            $data->error('secure_hash_keys empty');
+        } elsif ($data->{'secure_hash'} !~ /^sh\.(\d+)\.(\d+)$/ || $1 > $#$array) {
+            $data->error('Invalid secure hash');
+        } else {
+            my $rand1 = $1;
+            my $rand2 = $2;
+            my $real  = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
+            my $str  = join("/", @{$data}{qw(user cram_time expires_min payload)});
+            my $sum = md5_hex($str .'/'. $real .('/sh.'.$array->[$rand1].'.'.$rand2));
+            if ($data->{'expires_min'} > 0
+                && ($self->server_time - $data->{'cram_time'}) > $data->{'expires_min'} * 60) {
+                $data->error('Login expired');
+            } elsif (lc($data->{'test_pass'}) ne $sum) {
+                $data->error('Invalid login');
+            }
+        }
+
+    ### looks like a normal cram
+    } elsif ($data->{'cram_time'}) {
+        $data->add_data(type => 'cram');
+        my $real = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
+        my $str  = join("/", @{$data}{qw(user cram_time expires_min payload)});
+        my $sum  = md5_hex($str .'/'. $real);
+        if ($data->{'expires_min'} > 0
+                 && ($self->server_time - $data->{'cram_time'}) > $data->{'expires_min'} * 60) {
+            $data->error('Login expired');
+        } elsif (lc($data->{'test_pass'}) ne $sum) {
+            $data->error('Invalid login');
+        }
+
+    ### plaintext_crypt
+    } elsif ($data->{'real_pass'} =~ m|^([./0-9A-Za-z]{2})([./0-9A-Za-z]{11})$|
+             && crypt($data->{'test_pass'}, $1) eq $data->{'real_pass'}) {
+        $data->add_data(type => 'crypt', was_plaintext => 1);
+
+    ### failed plaintext crypt
+    } elsif ($self->use_crypt) {
+        $data->error('Invalid login');
+        $data->add_data(type => 'crypt', was_plaintext => ($data->{'test_pass'} =~ /^[a-f0-9]{32}$/ ? 0 : 1));
+
+    ### plaintext and md5
+    } else {
+        my $is_md5_t = $data->{'test_pass'} =~ /^[a-f0-9]{32}$/;
+        my $is_md5_r = $data->{'real_pass'} =~ /^[a-f0-9]{32}$/;
+        my $test = $is_md5_t ? lc($data->{'test_pass'}) : md5_hex($data->{'test_pass'});
+        my $real = $is_md5_r ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'});
+        $data->add_data(type => ($is_md5_r ? 'md5' : 'plaintext'), was_plaintext => ($is_md5_t ? 0 : 1));
+        $data->error('Invalid login')
+            if $test ne $real;
     }
 
-    ### no payload - compare directly
-    if ($hash_test !~ m|^(.+)/([^/]+)$|) {
-      return lc($pass_test) eq lc($pass_real);
+    ### check the payload
+    if (! $data->error && ! $self->verify_payload($data->{'payload'})) {
+        $data->error('Invalid payload');
+    }
 
-    ### and finally - check the payload (allows for expiring login)
+    return $data;
+}
+
+sub new_auth_data {
+    my $self = shift;
+    return CGI::Ex::Auth::Data->new(@_);
+}
+
+sub last_auth_data { shift->{'_last_auth_data'} }
+
+sub generate_token {
+    my $self  = shift;
+    my $data  = shift || $self->last_auth_data;
+    die "Can't generate a token off of a failed auth" if ! $data;
+
+    my $token;
+
+    ### do kinds that require staying plaintext
+    if (   (defined($data->{'use_plaintext'}) ?  $data->{'use_plaintext'} : $self->use_plaintext) # ->use_plaintext is true if ->use_crypt is
+        || (defined($data->{'use_crypt'})     && $data->{'use_crypt'})
+        || (defined($data->{'type'})          && $data->{'type'} eq 'crypt')) {
+        $token = $data->{'user'} .'/'. $data->{'real_pass'};
+
+    ### all other types go to cram - secure_hash_cram, cram, plaintext and md5
     } else {
-      my $payload = $1; # payload can be anything
-      my $compare = $2; # a checksum which is the enc of the payload + '/' + enc of password
-      my @payload = split /\//, $payload;
+        my $user = $data->{'user'} || die "Missing user";
+        my $real = defined($data->{'real_pass'})   ? ($data->{'real_pass'} =~ /^[a-f0-9]{32}$/ ? lc($data->{'real_pass'}) : md5_hex($data->{'real_pass'}))
+                                                   : die "Missing real_pass";
+        my $exp  = defined($data->{'expires_min'}) ? $data->{'expires_min'} : $self->expires_min;
+        my $load = $self->generate_payload($data);
+        die "Payload can not contain a \"/\.  Please escape it in generate_payload." if $load =~ m|/|;
+        die "User can not contain a \"/\."                                           if $user =~ m|/|;
+
+        my $array;
+        if (! $data->{'prefer_cram'}
+            && ($array = eval { $self->secure_hash_keys })
+            && @$array) {
+            my $rand1 = int(rand @$array);
+            my $rand2 = int(rand 100000);
+            my $str = join("/", $user, $self->server_time, $exp, $load);
+            my $sum = md5_hex($str .'/'. $real .('/sh.'.$array->[$rand1].'.'.$rand2));
+            $token  = $str .'/'. $sum . '/sh.'.$rand1.'.'.$rand2;
+        } else {
+            my $str = join("/", $user, $self->server_time, $exp, $load);
+            my $sum = md5_hex($str .'/'. $real);
+            $token  = $str .'/'. $sum;
+        }
+    }
 
-      return 0 if $self->enc_func($type_test, "$payload/$hash_real") ne $compare;
+    if (my $key = $data->{'use_blowfish'} || $self->use_blowfish) {
+        $token = encrypt_blowfish($token, $key);
 
-      ### if no save password && greater than expire time- expire
-      if ($EXPIRE_LOGINS && ! $payload[1] && $payload[0] =~ m/^(\d+)/) {
-        return 0 if time() > $1 + $EXPIRE_LOGINS;
-      }
-      return 1;
+    } elsif (defined($data->{'use_base64'}) ? $data->{'use_base64'} : $self->use_base64) {
+        $token = encode_base64($token, '');
     }
-  }
-  return 0; # nothing should make it this far
-}
-
-sub enc_func {
-  my $self = shift;
-  my $type = shift;
-  my $str  = shift;
-  if ($type eq 'md5') {
-    require Digest::MD5;
-    return &Digest::MD5::md5_hex($str);
-  } elsif ($type eq 'sha1') {
-    require Digest::SHA1;
-    return &Digest::SHA1::sha1_hex($str);
-  }
+
+    return $token;
 }
 
-sub set_hook_get_pass_by_user {
-  my $self = shift;
-  $self->{hook_get_pass_by_user} = shift;
+sub generate_payload {
+    my $self = shift;
+    my $args = shift;
+    return defined($args->{'payload'}) ? $args->{'payload'} : '';
 }
 
-sub hook_get_pass_by_user {
-  my $self = shift;
-  my $user = shift;
-  my $host = shift || $self->host;
-  my $meth;
-  if ($meth = $self->{hook_get_pass_by_user}) {
-    return $self->$meth($user, $host);
-  }
-  die "hook_get_pass_by_user is a virtual method - please override - or use set_hook_get_pass_by_user";
+sub verify_user {
+    my $self = shift;
+    my $user = shift;
+    if (my $meth = $self->{'verify_user'}) {
+        return $meth->($self, $user);
+    }
+    return 1;
 }
 
-###----------------------------------------------------------------###
+sub cleanup_user {
+    my $self = shift;
+    my $user = shift;
+    if (my $meth = $self->{'cleanup_user'}) {
+        return $meth->($self, $user);
+    }
+    return $user;
+}
 
-sub cgix {
-  my $self = shift;
-  $self->{cgix} = shift if $#_ != -1;
-  return $self->{cgix} ||= do {
-    require CGI::Ex;
-    CGI::Ex->new(); # return of the do
-  };
+sub get_pass_by_user {
+    my $self = shift;
+    my $user = shift;
+    if (my $meth = $self->{'get_pass_by_user'}) {
+        return $meth->($self, $user);
+    }
+
+    die "Please override get_pass_by_user";
 }
 
-sub form {
-  my $self = shift;
-  if ($#_ != -1) {
-    $self->{form} = shift || die "Invalid form";
-  }
-  return $self->{form} ||= $self->cgix->get_form;
+sub verify_payload {
+    my $self    = shift;
+    my $payload = shift;
+    if (my $meth = $self->{'verify_payload'}) {
+        return $meth->($self, $payload);
+    }
+    return 1;
 }
 
-sub cookies {
-  my $self = shift;
-  if ($#_ != -1) {
-    $self->{cookies} = shift || die "Invalid cookies";
-  }
-  return $self->{cookies} ||= $self->cgix->get_cookies;
-}
-
-sub host {
-  my $self = shift;
-  return $self->{host} = shift if $#_ != -1;
-  return $self->{host} ||= do {
-    my $host = $ENV{HTTP_HOST} || die "Missing \$ENV{HTTP_HOST}";
-    $host = lc($host);
-    $host =~ s/:\d*$//;      # remove port number
-    $host =~ s/\.+$//;       # remove qualified dot
-    $host =~ s/[^\w\.\-]//g; # remove odd characters
-    $host; # return of the do
-  };
+###----------------------------------------------------------------###
+
+sub encrypt_blowfish {
+    my ($str, $key) = @_;
+
+    require Crypt::Blowfish;
+    my $cb = Crypt::Blowfish->new($key);
+
+    $str .= (chr 0) x (8 - length($str) % 8); # pad to multiples of 8
+
+    my $enc = '';
+    $enc .= unpack "H16", $cb->encrypt($1) while $str =~ /\G(.{8})/g; # 8 bytes at a time
+
+    return $enc;
+}
+
+sub decrypt_blowfish {
+    my ($enc, $key) = @_;
+
+    require Crypt::Blowfish;
+    my $cb = Crypt::Blowfish->new($key);
+
+    my $str = '';
+    $str .= $cb->decrypt(pack "H16", $1) while $enc =~ /\G([A-Fa-f0-9]{16})/g;
+    $str =~ y/\00//d;
+
+    return $str
 }
 
 ###----------------------------------------------------------------###
 
-sub basic_login_page {
-  my $self = shift;
-  my $form = shift;
+sub login_template {
+    my $self = shift;
+    return $self->{'login_template'} if $self->{'login_template'};
 
-  my $text = $self->basic_login_template();
-  $self->cgix->swap_template(\$text, $form);
-  $self->cgix->fill(\$text, $form);
+    my $text = ""
+        . $self->login_header
+        . $self->login_form
+        . $self->login_script
+        . $self->login_footer;
+    return \$text;
+}
+
+sub login_header {
+    return shift->{'login_header'} || q {
+    [%~ TRY ; PROCESS 'login_header.tt' ; CATCH %]<!-- [% error %] -->[% END ~%]
+    };
+}
 
-  return $text;
+sub login_footer {
+    return shift->{'login_footer'} || q {
+    [%~ TRY ; PROCESS 'login_footer.tt' ; CATCH %]<!-- [% error %] -->[% END ~%]
+    };
 }
 
-sub basic_login_template {
-  return qq{
-    [% header %]
-    <div align="center">
-    <span class="error" style="color:red">[% error %]</span>
-    <form name="[% form_name %]" method="get" action="[% script_name %]">
-    <table border="0" class="login_table">
-    <tr>
+sub login_form {
+    return shift->{'login_form'} || q {
+    <div class="login_chunk">
+    <span class="login_error">[% error %]</span>
+    <form class="login_form" name="[% form_name %]" method="post" action="[% script_name %][% path_info %]">
+    <input type="hidden" name="[% key_redirect %]" value="">
+    <input type="hidden" name="[% key_payload %]" value="">
+    <input type="hidden" name="[% key_time %]" value="">
+    <input type="hidden" name="[% key_expires_min %]" value="">
+    <table class="login_table">
+    <tr class="login_username">
       <td>Username:</td>
       <td><input name="[% key_user %]" type="text" size="30" value=""></td>
     </tr>
-    <tr>
+    <tr class="login_password">
       <td>Password:</td>
       <td><input name="[% key_pass %]" type="password" size="30" value=""></td>
     </tr>
-    <tr>
+    <tr class="login_save">
       <td colspan="2">
         <input type="checkbox" name="[% key_save %]" value="1"> Save Password ?
       </td>
     </tr>
-    <tr>
+    <tr class="login_submit">
       <td colspan="2" align="right">
-        <input type="hidden" name="[% key_redirect %]">
-        <input type="hidden" name="payload">
         <input type="submit" value="Submit">
       </td>
     </tr>
-    [% extra_table %]
     </table>
     </form>
     </div>
-    [% login_script %]
-    [% footer %]
-  };
+};
 }
 
-sub login_type {
-  my $self = shift;
-  if ($#_ != -1) {
-    $self->{login_type} = defined($_[0]) ? lc(shift) : undef;
-  }
-  $self->{login_type} = do {
-    my $type;
-    if ($USE_PLAINTEXT) {
-      $type = '';
-    } elsif (eval {require Digest::SHA1}) {
-      $type = 'sha1';
-    } elsif (eval {require Digest::MD5}) {
-      $type = 'md5';
-    } else {
-      $type = "";
-    }
-    $type; # return of the do
-  } if ! defined $self->{login_type};
-  return $self->{login_type};
-}
-
-
 sub login_script {
-  my $self = shift;
-  my $form = shift;
-  my $type = $self->login_type;
-  return if ! $type || $type !~ /^(sha1|md5)$/;
-
-  return qq{
-    <script src="$form->{script_name}/js/CGI/Ex/$type.js"></script>
+  return q {
+    [%~ IF ! use_plaintext %]
+    <script src="[% md5_js_path %]"></script>
     <script>
-    function send_it () {
-      var f = document.$form->{form_name};
-      var s = (f.$form->{key_save}.checked) ? 1 : 0;
-      var l = f.payload.value + '/' + s;
-      var r = f.$form->{key_redirect}.value;
-      var q = document.$form->{form_name}.action;
-      var sum = document.${type}_hex(l+'/'+document.${type}_hex(f.$form->{key_pass}.value));
-      q += '?$form->{key_user}='+escape(f.$form->{key_user}.value);
-      q += '&$form->{key_save}='+escape(s);
-      q += '&$form->{key_pass}='+escape('$type('+l+'/'+sum+')');
-      location.href = q;
+    if (document.md5_hex) document.[% form_name %].onsubmit = function () {
+      var f = document.[% form_name %];
+      var u = f.[% key_user %].value;
+      var p = f.[% key_pass %].value;
+      var t = f.[% key_time %].value;
+      var s = f.[% key_save %] && f.[% key_save %].checked ? -1 : f.[% key_expires_min %].value;
+      var l = f.[% key_payload %].value;
+      var r = f.[% key_redirect %].value;
+
+      var str = u+'/'+t+'/'+s+'/'+l;
+      var sum = document.md5_hex(str +'/' + document.md5_hex(p));
+      var loc = f.action + '?[% key_user %]='+escape(str +'/'+ sum)+'&[% key_redirect %]='+escape(r);
+
+      location.href = loc;
       return false;
     }
-    if (document.${type}_hex) document.$form->{form_name}.onsubmit = function () { return send_it() }
     </script>
+    [% END ~%]
   };
 }
 
 ###----------------------------------------------------------------###
 
-### return arguments to add on to a url to allow login (for emails)
-sub auth_string_sha1 {
-  my $self = shift;
-  my $user = shift;
-  my $pass = shift;
-  my $save = shift || 0;
-  my $time = shift || time;
-  my $payload = $self->payload($time);
-
-  require Digest::SHA1;
-
-  if ($pass =~ /^sha1\((.+)\)$/) {
-    $pass = $1;
-  } else {
-    $pass = &Digest::SHA1::sha1_hex($pass);
-  }
-  $pass = &Digest::SHA1::sha1_hex("$payload/$save/$pass");
+package CGI::Ex::Auth::Data;
 
-  return $self->cgix->make_form({
-    $self->key_user => $user,
-    $self->key_pass => "sha1($payload/$save/$pass)",
-    $self->key_save => $save,
-  });
+use strict;
+use overload
+    'bool'   => sub { ! shift->error },
+    '0+'     => sub { 1 },
+    '""'     => sub { shift->as_string },
+    fallback => 1;
+
+sub new {
+    my ($class, $args) = @_;
+    return bless {%{ $args || {} }}, $class;
+}
+
+sub add_data {
+    my $self = shift;
+    my $args = @_ == 1 ? shift : {@_};
+    @{ $self }{keys %$args} = values %$args;
+}
+
+sub error {
+    my $self = shift;
+    if (@_ == 1) {
+        $self->{'error'} = shift;
+        $self->{'error_caller'} = [caller];
+    }
+    return $self->{'error'};
+}
+
+sub as_string {
+    my $self = shift;
+    return $self->error || ($self->{'user'} && $self->{'type'}) ? "Valid auth data" : "Unverified auth data";
 }
 
 ###----------------------------------------------------------------###
@@ -581,65 +708,39 @@ sub auth_string_sha1 {
 
 __END__
 
-=head1 NAME
-
-CGI::Ex::Auth - Handle logins nicely.
-
 =head1 SYNOPSIS
 
   ### authorize the user
-  my $auth = $self->auth({
-    hook_get_pass_by_user => \&get_pass_by_user,
-    hook_print            => \&my_print,
-    login_type            => 'sha1',
+  my $auth = $self->get_valid_auth({
+    get_pass_by_user => \&get_pass_by_user,
   });
-  ### login_type may be sha1, md5, or plaintext
 
 
   sub get_pass_by_user {
     my $auth = shift;
-    my $username = shift;
-    my $host = shift;
-    my $password = some_way_of_getting_password;
-    return $password;
-  }
-
-  sub my_print {
-    my $auth = shift;
-    my $step = shift;
-    my $form = shift; # form includes login_script at this point
-    my $content = get_content_from_somewhere;
-    $auth->cgix->swap_template(\$content, $form);
-    $auth->cgix->print_content_type;
-    print $content;
+    my $user = shift;
+    my $pass = some_way_of_getting_password($user);
+    return $pass;
   }
 
 =head1 DESCRIPTION
 
-CGI::Ex::Auth allows for autoexpiring, safe logins.  Auth uses
-javascript modules that perform SHA1 and MD5 encoding to encode
-the password on the client side before passing them through the
-internet.
-
-If SHA1 is used the storage of the password can be described by
-the following code:
+CGI::Ex::Auth allows for auto-expiring, safe and easy web based logins.  Auth uses
+javascript modules that perform MD5 hashing to cram the password on
+the client side before passing them through the internet.
 
-  my $pass = "plaintextpassword";
-  my $save = ($save_the_password) ? 1 : 0;
-  my $time = time;
-  my $store = sha1_hex("$time/$save/" . sha1_hex($pass));
+For the stored cookie you can choose to use cram mechanisms,
+secure hash cram tokens, auto expiring logins (not cookie based),
+and Crypt::Blowfish protection.  You can also choose to keep
+passwords plaintext and to use perl's crypt for testing
+passwords.
 
-This allows for passwords to be stored as sha1 in a database.
-Passwords stored in the database this way are still susceptible to bruteforce
-attack, but are much more secure than storing plain text.
-
-If MD5 is used, the above procedure is replaced with md5_hex.
-
-A downside to this module is that it does not use a session to preserve state
-so authentication has to happen on every request.  A plus is that you don't
-need to use a session.  With later releases, a method will be added to allow
-authentication to look inside of a stored session somewhat similar to
-CGI::Session::Auth.
+A downside to this module is that it does not use a session to
+preserve state so get_pass_by_user has to happen on every request (any
+authenticated area has to verify authentication each time).  A plus is
+that you don't need to use a session if you don't want to.  It is up
+to the interested reader to add caching to the get_pass_by_user
+method.
 
 =head1 METHODS
 
@@ -647,116 +748,274 @@ CGI::Session::Auth.
 
 =item C<new>
 
-Constructor.  Takes a hash or hashref of properties as arguments.
-
-=item C<init>
-
-Called automatically near the end of new.
-
-=item C<require_auth>
+Constructor.  Takes a hashref of properties as arguments.
+
+Many of the methods which may be overridden in a subclass,
+or may be passed as properties to the new constuctor such as in the following:
+
+    CGI::Ex::Auth->new({
+        get_pass_by_user => \&my_pass_sub,
+        key_user         => 'my_user',
+        key_pass         => 'my_pass',
+        login_template   => \"<form><input name=my_user ... </form>",
+    });
+
+The following methods will look for properties of the same name.  Each of these will be
+defined separately.
+
+    cgix
+    cleanup_user
+    cookies
+    expires_min
+    form
+    form_name
+    get_pass_by_user
+    js_uri_path
+    key_cookie
+    key_expires_min
+    key_logout
+    key_pass
+    key_payload
+    key_redirect
+    key_save
+    key_time
+    key_user
+    key_verify
+    login_footer
+    login_form
+    login_header
+    login_script
+    login_template
+    no_cookie_verify
+    path_info
+    script_name
+    secure_hash_keys
+    template_args
+    template_include_path
+    use_base64
+    use_blowfish
+    use_crypt
+    use_plaintext
+    verify_payload
+    verify_user
+
+=item C<generate_token>
+
+Takes either an auth_data object from a auth_data returned by verify_token,
+or a hashref of arguments.
+
+Possible arguments are:
+
+    user           - the username we are generating the token for
+    real_pass      - the password of the user (if use_plaintext is false
+                     and use_crypt is false, the password can be an md5sum
+                     of the user's password)
+    use_blowfish   - indicates that we should use Crypt::Blowfish to protect
+                     the generated token.  The value of this argument is used
+                     as the key.  Default is false.
+    use_base64     - indicates that we should use Base64 encoding to protect
+                     the generated token.  Default is true.  Will not be
+                     used if use_blowfish is true.
+    use_plaintext  - indicates that we should keep the password in plaintext
+    use_crypt      - also indicates that we should keep the password in plaintext
+    expires_min    - says how many minutes until the generated token expires.
+                     Values <= 0 indicate to not ever expire.  Used only on cram
+                     types.
+    payload        - a payload that will be passed to generate_payload and then
+                     will be added to cram type tokens.  It cannot contain a /.
+    prefer_cram    - If the secure_hash_keys method returns keys, and it is a non-plaintext
+                     token, generate_token will create a secure_hash_cram.  Set
+                     this value to true to tell it to use a normal cram.  This
+                     is generally only useful in testing.
+
+The following are types of tokens that can be generated by generate_token.  Each type includes
+pseudocode and a sample of a generated that token.
+
+    plaintext:
+        user         := "paul"
+        real_pass    := "123qwe"
+        token        := join("/", user, real_pass);
+
+        use_base64   := 0
+        token        == "paul/123qwe"
+
+        use_base64   := 1
+        token        == "cGF1bC8xMjNxd2U="
+
+        use_blowfish := "foobarbaz"
+        token        == "6da702975190f0fe98a746f0d6514683"
+
+        Notes: This token will be used if either use_plaintext or use_crypt is set.
+        The real_pass can also be the md5_sum of the password.  If real_pass is an md5_sum
+        of the password but the get_pass_by_user hook returns the crypt'ed password, the
+        token will not be able to be verified.
+
+    cram:
+        user        := "paul"
+        real_pass   := "123qwe"
+        server_time := 1148512991         # a time in seconds since epoch
+        expires_min := 6 * 60
+        payload     := "something"
+
+        md5_pass    := md5_sum(real_pass) # if it isn't already a 32 digit md5 sum
+        str         := join("/", user, server_time, expires_min, payload, md5_pass)
+        md5_str     := md5(sum_str)
+        token       := join("/", user, server_time, expires_min, payload, md5_str)
+
+        use_base64  := 0
+        token       == "paul/1148512991/360/something/16d0ba369a4c9781b5981eb89224ce30"
+
+        use_base64  := 1
+        token       == "cGF1bC8xMTQ4NTEyOTkxLzM2MC9zb21ldGhpbmcvMTZkMGJhMzY5YTRjOTc4MWI1OTgxZWI4OTIyNGNlMzA="
+
+        Notes: use_blowfish is available as well
+
+    secure_hash_cram:
+        user        := "paul"
+        real_pass   := "123qwe"
+        server_time := 1148514034         # a time in seconds since epoch
+        expires_min := 6 * 60
+        payload     := "something"
+        secure_hash := ["aaaa", "bbbb", "cccc", "dddd"]
+        rand1       := 3                  # int(rand(length(secure_hash)))
+        rand2       := 39163              # int(rand(100000))
+
+        md5_pass    := md5_sum(real_pass) # if it isn't already a 32 digit md5 sum
+
+        sh_str1     := join(".", "sh", secure_hash[rand1], rand2)
+        sh_str2     := join(".", "sh", rand1, rand2)
+        str         := join("/", user, server_time, expires_min, payload, md5_pass, sh_str1)
+        md5_str     := md5(sum_str)
+        token       := join("/", user, server_time, expires_min, payload, md5_str, sh_str2)
+
+        use_base64  := 0
+        token       == "paul/1148514034/360/something/06db2914c9fd4e11499e0652bcf67dae/sh.3.39163"
+
+        Notes: use_blowfish is available as well.  The secure_hash keys need to be set in the
+        "secure_hash_keys" property of the CGI::Ex::Auth object.
+
+=item C<get_valid_auth>
+
+Performs the core logic.  Returns an auth object on successful login.
+Returns false on errored login (with the details of the error stored in
+$@).  If a false value is returned, execution of the CGI should be halted.
+get_valid_auth WILL NOT automatically stop execution.
+
+  $auth->get_valid_auth || exit;
+
+Optionally, the class and a list of arguments may be passed.  This will create a
+new object using the passed arguments, and then run get_valid_auth.
+
+  CGI::Ex::Auth->get_valid_auth({key_user => 'my_user'}) || exit;
+
+=item C<login_print>
+
+Called if login errored.  Defaults to printing a very basic (but
+adequate) page loaded from login_template..
 
-Performs the core logic.  Returns true on successful login.
-Returns false on failed login.  If a false value is returned,
-execution of the CGI should be halted.  require_auth WILL
-NOT automatically stop execution.
-
-  $auth->require_auth || exit;
-
-=item C<hook_print>
-
-Called if login failed.  Defaults to printing a very basic page.
 You will want to override it with a template from your own system.
 The hook that is called will be passed the step to print (currently
 only "get_login_info" and "no_cookies"), and a hash containing the
 form variables as well as the following:
 
-  payload      - $self->payload
-  error        - The error that occurred (if any)
-  key_user     - $self->key_user;
-  key_pass     - $self->key_pass;
-  key_save     - $self->key_save;
-  key_redirect - $self->key_redirect;
-  form_name    - $self->form_name;
-  script_name  - $ENV{SCRIPT_NAME}
-  path_info    - $ENV{PATH_INFO} || ''
-  login_script - $self->login_script($FORM); # The javascript that does the login
-
-=item C<success>
-
-Method called on successful login.  Sets $self->user as well as $ENV{REMOTE_USER}.
-
-=item C<user>
-
-Returns the user that was successfully logged in (undef if no success).
-
-=item C<hook_success>
-
-Called from success.  May be overridden or a subref may be given as a property.
+=item C<login_hash_common>
+
+Passed to the template swapped during login_print.
+
+    %$form,            # any keys passed to the login script
+    error              # The text "Login Failed" if a login occurred
+    login_data         # A login data object if they failed authentication.
+    key_user           # $self->key_user,        # the username fieldname
+    key_pass           # $self->key_pass,        # the password fieldname
+    key_time           # $self->key_time,        # the server time field name
+    key_save           # $self->key_save,        # the save password checkbox field name
+    key_payload        # $self->key_payload,     # the payload fieldname
+    key_redirect       # $self->key_redirect,    # the redirect fieldname
+    form_name          # $self->form_name,       # the name of the form
+    script_name        # $self->script_name,     # where the server will post back to
+    path_info          # $self->path_info,       # $ENV{PATH_INFO} if any
+    md5_js_path        # $self->js_uri_path ."/CGI/Ex/md5.js", # script for cramming
+    use_plaintext      # $self->use_plaintext,   # used to avoid cramming
+    $self->key_user    # $data->{'user'},        # the username (if any)
+    $self->key_pass    # '',                     # intentional blankout
+    $self->key_time    # $self->server_time,     # the server's time
+    $self->key_payload # $data->{'payload'}      # the payload (if any)
+    $self->key_expires_min # $self->expires_min  # how many minutes crams are valid
 
 =item C<key_logout>
 
-If a key is passed the form hash that matches this key, the current user will
-be logged out.  Default is "logout".
+If the form hash contains a true value in this field name, the current user will
+be logged out.  Default is "cea_logout".
 
 =item C<key_cookie>
 
-The name of the auth cookie.  Default is "ce_auth".
+The name of the auth cookie.  Default is "cea_user".
 
-=item C<key_cookie_check>
+=item C<key_verify>
 
-A field name used during a bounce to see if cookies exist.  Default is "ccheck".
+A field name used during a bounce to see if cookies exist.  Default is "cea_verify".
 
 =item C<key_user>
 
-The form field name used to pass the username.  Default is "ce_user".
+The form field name used to pass the username.  Default is "cea_user".
 
 =item C<key_pass>
 
-The form field name used to pass the password.  Default is "ce_pass".
+The form field name used to pass the password.  Default is "cea_pass".
 
 =item C<key_save>
 
-The form field name used to pass whether they would like to save the cookie for
-a longer period of time.  Default is "ce_save".  The value of this form field
-should be 1 or 0.  If it is zero, the cookie installed will be a session cookie
-and will expire in $EXPIRE_LOGINS seconds (default of 6 hours).
+Works in conjunction with key_expires_min.  If key_save is true, then
+the cookie will be set to be saved for longer than the current session
+(If it is a plaintext variety it will be given a 20 year life rather
+than being a session cookie.  If it is a cram variety, the expires_min
+portion of the cram will be set to -1).  If it is set to false, the cookie
+will be available only for the session (If it is a plaintext variety, the cookie
+will be session based and will be removed on the next loggout.  If it is
+a cram variety then the cookie will only be good for expires_min minutes.
 
-=item C<form_name>
+Default is "cea_save".
 
-The name of the html login form to attach the javascript to.  Default is "ce_form".
+=item C<key_expires_min>
 
-=item C<payload>
+The name of the form field that contains how long cram type cookies will be valid
+if key_save contains a false value.
 
-Additional variables to store in the cookie.  Can be used for anything.  Should be
-kept small.  Default is time (should always use time as the first argument).  Used
-for autoexpiring the cookie and to prevent bruteforce attacks.
+Default key name is "cea_expires_min".  Default field value is 6 * 60 (six hours).
 
-=item C<verify_userpass>
+This value will have no effect when use_plaintext or use_crypt is set.
 
-Called to verify the passed form information or the stored cookie.  Calls hook_verify_userpass.
+A value of -1 means no expiration.
 
-=item C<hook_verify_userpass>
+=item C<form_name>
 
-Called by verify_userpass.  Arguments are the username, cookie or info to be tested,
-and the hostname.  Default method calls hook_get_pass_by_user to get the real password.
-Then based upon how the real password is stored (sha1, md5, plaintext, or crypted) and
-how the login info was passed from the html form (or javascript), will attempt to compare
-the two and return success or failure.  It should be noted that if the javascript method
-used is SHA1 and the password is stored crypted or md5'ed - the comparison will not work
-and the login will fail.  SHA1 logins require either plaintext password or sha1 stored passwords.
-MD5 logins require either plaintext password or md5 stored passwords.  Plaintext logins
-allow for SHA1 or MD5 or crypted or plaintext storage - but should be discouraged because
-they are plaintext and the users password can be discovered.
+The name of the html login form to attach the javascript to.  Default is "cea_form".
 
-=item C<hook_get_pass_by_user>
+=item C<verify_token>
 
-Called by hook_verify_userpass.  Arguments are the username and hostname.  Should return
-a sha1 password, md5 password, plaintext password, or crypted password depending
-upon which system is being used to get the information from the user.
+This method verifies the token that was passed either via the form or via cookies.
+It will accept plaintext or crammed tokens (A listing of the available algorithms
+for creating tokes is listed below).  It also allows for armoring the token with
+base64 encoding, or using blowfish encryption.  A listing of creating these tokens
+can be found under generate_token.
 
-=item C<set_hook_get_pass_by_user>
+=item C<cleanup_user>
 
-Allows for setting the subref used by hook_get_pass_by_user.x
+Called by verify_token.  Default is to do no modification.  Allows for usernames to
+be lowercased, or canonized in some other way.  Should return the cleaned username.
+
+=item C<verify_user>
+
+Called by verify_token.  Single argument is the username.  May or may not be an
+initial check to see if the username is ok.  The username will already be cleaned at
+this point.  Default return is true.
+
+=item C<get_pass_by_user>
+
+Called by verify_token.  Given the cleaned, verified username, should return a
+valid password for the user.  It can always return plaintext.  If use_crypt is
+enabled, it should return the crypted password.  If use_plaintext and use_crypt
+are not enabled, it may return the md5 sum of the password.
 
 =item C<cgix>
 
@@ -770,59 +1029,45 @@ A hash of passed form info.  Defaults to CGI::Ex::get_form.
 
 The current cookies.  Defaults to CGI::Ex::get_cookies.
 
-=item C<host>
-
-What host are we on.  Defaults to a cleaned $ENV{HTTP_HOST}.
+=item C<login_template>
 
-=item C<basic_login_page>
+Should return either a template filename to use for the login template, or it
+should return a reference to a string that contains the template.  The contents
+will be used in login_print and passed to the template engine.
 
-Calls the basic_login_template, swaps in the form variables (including
-form name, login_script, etc).  Then prints content_type, the content, and
-returns.
+Default login_template is the values of login_header, login_form, login_script, and
+login_script concatenated together.
 
-=item C<basic_login_template>
+Values from login_hash_common will be passed to the template engine, and will
+also be used to fill in the form.
 
-Returns a bare essentials form that will handle the login.  Has place
-holders for all of the form name, and login variables, and errors and
-login javascript.  Variable place holders are of the form
-[% login_script %] which should work with Template::Toolkit or CGI::Ex::swap_template.
+The basic values are capable of handling most needs so long as appropriate
+headers and css styles are used.
 
-=item C<login_type>
+=item C<login_header>
 
-Either sha1, md5, or plaintext.  If global $USE_PLAINTEXT is set,
-plaintext password will be used.  login_type will then look for
-Digest::SHA1, then Digest::MD5, and then fail to plaintext.
+Should return a header to use in the default login_template.  The default
+value will try to PROCESS a file called login_header.tt that should be
+located in directory specified by the template_include_path method.
 
-SHA1 comparison will work with passwords stored as plaintext password,
-or stored as the string "sha1(".sha1_hex($password).")".
+It should ideally supply css styles that format the login_form as desired.
 
-MD5 comparison will work with passwords stored as plaintext password,
-or stored as the string "md5(".md5_hex($password).")".
+=item C<login_footer>
 
-Plaintext comparison will work with passwords stored as sha1(string),
-md5(string), plaintext password string, or crypted password.
+Same as login_header - but for the footer.  Will look for login_footer.tt by
+default.
 
-=item C<login_script>
-
-Returns a chunk of javascript that will encode the password before
-the html form is ever submitted.  It does require that $ENV{PATH_TRANSLATED}
-is not modified before calling the require_auth method so that any
-external javascript files may be served (also by the require_auth).
-
-=item C<auth_string_sha1>
+=item C<login_form>
 
-Arguments are username, password, save_password, and time.  This will
-return a valid login string.  You probably will want to pass 1 for the
-save_password or else the login will only be good for 6 hours.
+An html chunk that contains the necessary form fields to login the user.  The
+basic chunk has a username text entry, password text entry, save password checkbox,
+and submit button, and any hidden fields necessary for logging in the user.
 
-  my $login = $self->auth->auth_string_sha1($user, $pass, 1);
-  my $url   = "http://$ENV{HTTP_HOST}$ENV{SCRIPT_NAME}?$login";
-
-=head1 TODO
+=item C<login_script>
 
-Using plaintext allows for the password to be passed in the querystring.
-It should at least be Base64 encoded.  I'll add that soon - BUT - really
-you should be using the SHA1 or MD5 login types.
+Contains javascript that will attach to the form from login_form.  This script
+is capable of taking the login_fields and creating an md5 cram which prevents
+the password from being passed plaintext.
 
 =head1 AUTHORS
 
This page took 0.05104 seconds and 4 git commands to generate.