X-Git-Url: https://git.dogcows.com/gitweb?a=blobdiff_plain;ds=sidebyside;f=lib%2FCGI%2FEx%2FAuth.pm;h=7362ed1c3be750352526e39db8656a90016c9c91;hb=b6e904ff7b346908d0662aae9a9c5f7d976dd85e;hp=e564efdaf38c606f0efe32926bd093ba870028e7;hpb=85070b46d0a93ddbeef07341421adb8389a55418;p=chaz%2Fp5-CGI-Ex
diff --git a/lib/CGI/Ex/Auth.pm b/lib/CGI/Ex/Auth.pm
index e564efd..7362ed1 100644
--- a/lib/CGI/Ex/Auth.pm
+++ b/lib/CGI/Ex/Auth.pm
@@ -1,578 +1,724 @@
package CGI::Ex::Auth;
-### CGI Extended Application
+=head1 NAME
+
+CGI::Ex::Auth - Handle logins nicely.
+
+=cut
###----------------------------------------------------------------###
-# Copyright 2004 - Paul Seamons #
+# Copyright 2007 - 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.17';
###----------------------------------------------------------------###
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 $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" : "");
+ 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;
}
- $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;
+ ### 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;
+ }
}
- }
- ### 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;
-}
+ ### 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;
+ }
+ }
-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;
- }
+ ### 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" };
- ### no hook - give basic functionality
- my $content;
- if ($page eq 'no_cookies') {
- $content = qq{
You do not appear to have cookies enabled.
};
- } elsif ($page eq 'get_login_info') {
- $content = $self->basic_login_page($FORM);
- } else {
- $content = "No content for page \"$page\"";
- }
+ ### allow for a sleep to help prevent brute force
+ sleep($self->failed_sleep) if defined($data) && $data->error ne 'Login expired' && $self->failed_sleep;
- $self->cgix->print_content_type();
- print $content;
- return 0;
+ return;
}
###----------------------------------------------------------------###
-sub success {
- my $self = shift;
- my $user = shift;
- $self->{user} = $ENV{REMOTE_USER} = $user;
- $self->hook_success($user);
- return 1;
-}
+sub script_name { shift->{'script_name'} || $ENV{'SCRIPT_NAME'} || die "Missing SCRIPT_NAME" }
+
+sub path_info { shift->{'path_info'} || $ENV{'PATH_INFO'} || '' }
+
+sub server_time { time }
-sub user {
- my $self = shift;
- return $self->{user};
+sub cgix {
+ my $self = shift;
+ $self->{'cgix'} = shift if $#_ != -1;
+ return $self->{'cgix'} ||= CGI::Ex->new;
}
-sub hook_success {
- my $self = shift;
- my $user = shift;
- my $meth;
- if ($meth = $self->{hook_success}) {
- $self->$meth($user);
- }
+sub form {
+ my $self = shift;
+ $self->{'form'} = shift if $#_ != -1;
+ return $self->{'form'} ||= $self->cgix->get_form;
}
-###----------------------------------------------------------------###
+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 failed_sleep { shift->{'failed_sleep'} ||= 0 }
+
+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{You do not appear to have cookies enabled.
};
+ 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});
-sub form_name {
- my $self = shift;
- $self->{form_name} = shift if $#_ != -1;
- return $self->{form_name} ||= 'ce_form';
+ ### print it
+ $self->cgix->print_content_type;
+ print $out;
+
+ 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,
+ text_user => $self->text_user,
+ text_pass => $self->text_pass,
+ text_save => $self->text_save,
+ };
}
###----------------------------------------------------------------###
-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 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;
+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'});
+
+ ### 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') ? eval { local $^W; 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');
+ } elsif (ref $pass eq 'HASH') {
+ my $extra = $pass;
+ $pass = exists($extra->{'real_pass'}) ? delete($extra->{'real_pass'})
+ : exists($extra->{'password'}) ? delete($extra->{'password'})
+ : do { $data->error('Data returned by get_pass_by_user did not contain real_pass or password'); undef };
+ $data->error('Invalid login') if ! defined $pass && ! $data->error;
+ $data->add_data($extra);
+ }
+ 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;
}
-
- ### 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;
+
+ ### check the payload
+ if (! $data->error && ! $self->verify_payload($data->{'payload'})) {
+ $data->error('Invalid payload');
}
- ### no payload - compare directly
- if ($hash_test !~ m|^(.+)/([^/]+)$|) {
- return lc($pass_test) eq lc($pass_real);
+ 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;
- ### and finally - check the payload (allows for expiring login)
+ ### 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')) {
+ my $pass = defined($data->{'test_pass'}) ? $data->{'test_pass'} : $data->{'real_pass'};
+ $token = $data->{'user'} .'/'. $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
+
+ return $token;
}
-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);
- }
+sub generate_payload {
+ my $self = shift;
+ my $args = shift;
+ return defined($args->{'payload'}) ? $args->{'payload'} : '';
}
-sub set_hook_get_pass_by_user {
- my $self = shift;
- $self->{hook_get_pass_by_user} = shift;
+sub verify_user {
+ my $self = shift;
+ my $user = shift;
+ if (my $meth = $self->{'verify_user'}) {
+ return $meth->($self, $user);
+ }
+ return 1;
}
-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 cleanup_user {
+ my $self = shift;
+ my $user = shift;
+ if (my $meth = $self->{'cleanup_user'}) {
+ return $meth->($self, $user);
+ }
+ return $user;
}
-###----------------------------------------------------------------###
+sub get_pass_by_user {
+ my $self = shift;
+ my $user = shift;
+ if (my $meth = $self->{'get_pass_by_user'}) {
+ return $meth->($self, $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
- };
+ 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->login_header
+ . $self->login_form
+ . $self->login_script
+ . $self->login_footer;
+ return \$text;
+}
- my $text = $self->basic_login_template();
- $self->cgix->swap_template(\$text, $form);
- $self->cgix->fill(\$text, $form);
+sub login_header {
+ return shift->{'login_header'} || q {
+ [%~ TRY ; PROCESS 'login_header.tt' ; CATCH %][% END ~%]
+ };
+}
- return $text;
+sub login_footer {
+ return shift->{'login_footer'} || q {
+ [%~ TRY ; PROCESS 'login_footer.tt' ; CATCH %][% END ~%]
+ };
}
-sub basic_login_template {
- return qq{
- [% header %]
-