consolidate auth logic into Qpsmtpd::Auth
These 3 auth plugins all have a data store they fetch the reference password or hash from. They then match the attemped password or hash against the reference. This consolidates the latter portion (validating the password/hash) into Auth.pm. * less duplicated code in the plugins. * Pass validation consistently handled for these 3 plugins. * less work to create new auth plugins Also caches the CRAM-MD5 ticket. It could also cache user/pass info if this was desirable.
This commit is contained in:
parent
b95b74bf48
commit
35e1ce9883
@ -4,9 +4,11 @@ package Qpsmtpd::Auth;
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
use MIME::Base64;
|
|
||||||
use Qpsmtpd::Constants;
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
|
use Digest::HMAC_MD5 qw(hmac_md5_hex);
|
||||||
|
use MIME::Base64;
|
||||||
|
|
||||||
sub e64 {
|
sub e64 {
|
||||||
my ($arg) = @_;
|
my ($arg) = @_;
|
||||||
my $res = encode_base64($arg);
|
my $res = encode_base64($arg);
|
||||||
@ -144,6 +146,7 @@ sub get_auth_details_cram_md5 {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$session->{auth}{ticket} = $ticket;
|
||||||
return ($ticket, $user, $passHash);
|
return ($ticket, $user, $passHash);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -159,6 +162,58 @@ sub get_base64_response {
|
|||||||
return $answer;
|
return $answer;
|
||||||
};
|
};
|
||||||
|
|
||||||
# tag: qpsmtpd plugin that sets RELAYCLIENT when the user authentifies
|
sub validate_password {
|
||||||
|
my ( $self, %a ) = @_;
|
||||||
|
|
||||||
|
my ($pkg, $file, $line) = caller();
|
||||||
|
$file = (split '/', $file)[-1]; # strip off the path
|
||||||
|
|
||||||
|
my $src_clear = $a{src_clear};
|
||||||
|
my $src_crypt = $a{src_crypt};
|
||||||
|
my $attempt_clear = $a{attempt_clear};
|
||||||
|
my $attempt_hash = $a{attempt_hash};
|
||||||
|
my $method = $a{method} or die "missing method";
|
||||||
|
my $ticket = $a{ticket} || $self->{auth}{ticket};
|
||||||
|
my $deny = $a{deny} || DENY;
|
||||||
|
|
||||||
|
if ( ! $src_crypt && ! $src_clear ) {
|
||||||
|
$self->log(LOGINFO, "fail: missing password");
|
||||||
|
return ( $deny, "$file - no such user" );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( ! $src_clear && $method =~ /CRAM-MD5/i ) {
|
||||||
|
$self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass");
|
||||||
|
return ( DECLINED, $file );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( defined $attempt_clear ) {
|
||||||
|
if ( $src_clear && $src_clear eq $attempt_clear ) {
|
||||||
|
$self->log(LOGINFO, "pass: clear match");
|
||||||
|
return ( OK, $file );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( $src_crypt && $src_crypt eq crypt( $attempt_clear, $src_crypt ) ) {
|
||||||
|
$self->log(LOGINFO, "pass: crypt match");
|
||||||
|
return ( OK, $file );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( defined $attempt_hash && $src_clear ) {
|
||||||
|
if ( ! $ticket ) {
|
||||||
|
$self->log(LOGERROR, "skip: missing ticket");
|
||||||
|
return ( DECLINED, $file );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( $attempt_hash eq hmac_md5_hex( $ticket, $src_clear ) ) {
|
||||||
|
$self->log(LOGINFO, "pass: hash match");
|
||||||
|
return ( OK, $file );
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->log(LOGINFO, "fail: wrong password");
|
||||||
|
return ( $deny, "$file - wrong password" );
|
||||||
|
};
|
||||||
|
|
||||||
|
# tag: qpsmtpd plugin that sets RELAYCLIENT when the user authenticates
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package Qpsmtpd::Plugin;
|
package Qpsmtpd::Plugin;
|
||||||
use Qpsmtpd::Constants;
|
|
||||||
use strict;
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
|
use Digest::HMAC_MD5 qw(hmac_md5_hex);
|
||||||
|
|
||||||
# more or less in the order they will fire
|
# more or less in the order they will fire
|
||||||
our @hooks = qw(
|
our @hooks = qw(
|
||||||
|
@ -30,7 +30,11 @@ algorithm so no password is transfered in the clear.
|
|||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
use Digest::HMAC_MD5 qw(hmac_md5_hex);
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Auth;
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
my ( $self, $qp ) = @_;
|
my ( $self, $qp ) = @_;
|
||||||
@ -45,35 +49,35 @@ sub auth_flat_file {
|
|||||||
@_;
|
@_;
|
||||||
|
|
||||||
if ( ! defined $passClear && ! defined $passHash ) {
|
if ( ! defined $passClear && ! defined $passHash ) {
|
||||||
|
$self->log(LOGINFO, "fail: missing password");
|
||||||
return ( DENY, "authflat - missing password" );
|
return ( DENY, "authflat - missing password" );
|
||||||
}
|
}
|
||||||
|
|
||||||
my ( $pw_name, $pw_domain ) = split '@', lc($user);
|
my ( $pw_name, $pw_domain ) = split '@', lc($user);
|
||||||
|
|
||||||
unless ( defined $pw_domain ) {
|
unless ( defined $pw_domain ) {
|
||||||
|
$self->log(LOGINFO, "fail: missing domain");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
my ($auth_line) = grep {/^$pw_name\@$pw_domain:/} $self->qp->config('flat_auth_pw');
|
my ($auth_line) = grep {/^$pw_name\@$pw_domain:/} $self->qp->config('flat_auth_pw');
|
||||||
|
|
||||||
if ( ! defined $auth_line) {
|
if ( ! defined $auth_line) {
|
||||||
$self->log(LOGINFO, "User not found: $pw_name\@$pw_domain");
|
$self->log(LOGINFO, "fail: no such user: $user");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGINFO, "Authentication for: $pw_name\@$pw_domain");
|
|
||||||
|
|
||||||
my ($auth_user, $auth_pass) = split(/:/, $auth_line, 2);
|
my ($auth_user, $auth_pass) = split(/:/, $auth_line, 2);
|
||||||
|
|
||||||
# at this point we can assume the user name matched
|
# at this point we can assume the user name matched
|
||||||
if ( defined $passClear && $auth_pass eq $passClear ) {
|
return Qpsmtpd::Auth::validate_password( $self,
|
||||||
return ( OK, "authflat" );
|
src_clear => $auth_pass,
|
||||||
};
|
src_crypt => undef,
|
||||||
|
attempt_clear => $passClear,
|
||||||
if ( defined $passHash && $passHash eq hmac_md5_hex($ticket, $auth_pass) ) {
|
attempt_hash => $passHash,
|
||||||
return ( OK, "authflat" );
|
method => $method,
|
||||||
};
|
ticket => $ticket,
|
||||||
|
deny => DENY,
|
||||||
return ( DENY, "authflat - wrong password" );
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +42,9 @@ Please see the LICENSE file included with qpsmtpd for details.
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
|
use Qpsmtpd::Auth;
|
||||||
use Qpsmtpd::Constants;
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
use Digest::HMAC_MD5 qw(hmac_md5_hex);
|
|
||||||
#use vpopmail; # we eval this in $test_vpopmail
|
#use vpopmail; # we eval this in $test_vpopmail
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
@ -60,47 +60,26 @@ sub register {
|
|||||||
sub auth_vpopmail {
|
sub auth_vpopmail {
|
||||||
my ($self, $transaction, $method, $user, $passClear, $passHash, $ticket) =
|
my ($self, $transaction, $method, $user, $passClear, $passHash, $ticket) =
|
||||||
@_;
|
@_;
|
||||||
my ($pw_name, $pw_domain) = split "@", lc($user);
|
|
||||||
|
|
||||||
$self->log(LOGINFO, "Authenticating against vpopmail: $user");
|
my $pw = vauth_getpw( split '@', lc($user) );
|
||||||
|
|
||||||
my $pw = vauth_getpw($pw_name, $pw_domain);
|
|
||||||
my $pw_clear_passwd = $pw->{pw_clear_passwd};
|
my $pw_clear_passwd = $pw->{pw_clear_passwd};
|
||||||
my $pw_passwd = $pw->{pw_passwd};
|
my $pw_passwd = $pw->{pw_passwd};
|
||||||
|
|
||||||
# make sure the user exists
|
|
||||||
if (!$pw || (!$pw_clear_passwd && !$pw_passwd)) {
|
if (!$pw || (!$pw_clear_passwd && !$pw_passwd)) {
|
||||||
|
$self->log(LOGINFO, "fail: invalid user $user");
|
||||||
return (DENY, "auth_vpopmail - invalid user");
|
return (DENY, "auth_vpopmail - invalid user");
|
||||||
|
|
||||||
# change DENY to DECLINED to support multiple auth plugins
|
# change DENY to DECLINED to support multiple auth plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
return (OK, "auth_vpopmail")
|
return Qpsmtpd::Auth::validate_password( $self,
|
||||||
if $pw_passwd eq crypt($passClear, $pw_passwd);
|
src_clear => $pw->{pw_clear_passwd},
|
||||||
|
src_crypt => $pw->{pw_passwd},
|
||||||
# simplest case: clear text passwords
|
attempt_clear => $passClear,
|
||||||
if (defined $passClear && defined $pw_clear_passwd) {
|
attempt_hash => $passHash,
|
||||||
return (DENY, "auth_vpopmail - incorrect password")
|
method => $method,
|
||||||
if $passClear ne $pw_clear_passwd;
|
ticket => $ticket,
|
||||||
return (OK, "auth_vpopmail");
|
deny => DENY,
|
||||||
}
|
);
|
||||||
|
|
||||||
if ($method =~ /CRAM-MD5/i) {
|
|
||||||
|
|
||||||
# clear_passwd isn't defined so we cannot support CRAM-MD5
|
|
||||||
return (DECLINED, "auth_vpopmail") if !defined $pw_clear_passwd;
|
|
||||||
|
|
||||||
if (defined $passHash
|
|
||||||
and $passHash eq hmac_md5_hex($ticket, $pw_clear_passwd))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (OK, "auth_vpopmail")
|
|
||||||
if (defined $passHash
|
|
||||||
&& $passHash eq hmac_md5_hex($ticket, $pw_clear_passwd));
|
|
||||||
|
|
||||||
return (DENY, "auth_vpopmail - unknown error");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub test_vpopmail_module {
|
sub test_vpopmail_module {
|
||||||
|
@ -66,9 +66,10 @@ Please see the LICENSE file included with qpsmtpd for details.
|
|||||||
use strict;
|
use strict;
|
||||||
use warnings;
|
use warnings;
|
||||||
|
|
||||||
use DBI;
|
use Qpsmtpd::Auth;
|
||||||
use Qpsmtpd::Constants;
|
use Qpsmtpd::Constants;
|
||||||
use Digest::HMAC_MD5 qw(hmac_md5_hex);
|
|
||||||
|
use DBI;
|
||||||
|
|
||||||
sub register {
|
sub register {
|
||||||
my ( $self, $qp ) = @_;
|
my ( $self, $qp ) = @_;
|
||||||
@ -122,45 +123,27 @@ sub auth_vmysql {
|
|||||||
my ( $self, $transaction, $method, $user, $passClear, $passHash, $ticket ) = @_;
|
my ( $self, $transaction, $method, $user, $passClear, $passHash, $ticket ) = @_;
|
||||||
|
|
||||||
my $dbh = $self->get_db_handle() or return DECLINED;
|
my $dbh = $self->get_db_handle() or return DECLINED;
|
||||||
my $db_user = $self->get_vpopmail_user($dbh, $user) or return DECLINED;
|
my $u = $self->get_vpopmail_user($dbh, $user) or return DECLINED;
|
||||||
|
|
||||||
# if vpopmail was not built with '--enable-clear-passwd=y'
|
# if vpopmail was not built with '--enable-clear-passwd=y'
|
||||||
# then pw_clear_passwd may not even exist
|
# then pw_clear_passwd may not even exist
|
||||||
my $pw_clear_passwd = $db_user->{'pw_clear_passwd'};
|
# my $pw_clear_passwd = $db_user->{'pw_clear_passwd'};
|
||||||
my $pw_passwd = $db_user->{'pw_passwd'}; # always present
|
|
||||||
|
|
||||||
if ( ! $pw_passwd && ! $pw_clear_passwd ) {
|
if ( ! $u->{pw_passwd} && ! $u->{pw_clear_passwd} ) {
|
||||||
$self->log(LOGINFO, "fail: no such user");
|
$self->log(LOGINFO, "fail: no such user");
|
||||||
return ( DENY, "auth_vmysql - no such user" );
|
return ( DENY, "auth_vmysql - no such user" );
|
||||||
};
|
};
|
||||||
|
|
||||||
# at this point, the user name has matched
|
# at this point, the user name has matched
|
||||||
|
|
||||||
if ( ! $pw_clear_passwd && $method =~ /CRAM-MD5/i ) {
|
return Qpsmtpd::Auth::validate_password( $self,
|
||||||
$self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass");
|
src_clear => $u->{pw_clear_passwd},
|
||||||
return ( DECLINED, "auth_vmysql" );
|
src_crypt => $u->{pw_passwd},
|
||||||
}
|
attempt_clear => $passClear,
|
||||||
|
attempt_hash => $passHash,
|
||||||
if ( defined $passClear ) {
|
method => $method,
|
||||||
if ( $pw_clear_passwd && $pw_clear_passwd eq $passClear ) {
|
ticket => $ticket,
|
||||||
$self->log(LOGINFO, "pass: clear match");
|
deny => DENY,
|
||||||
return ( OK, "auth_vmysql" );
|
);
|
||||||
};
|
|
||||||
|
|
||||||
if ( $pw_passwd eq crypt( $passClear, $pw_passwd ) ) {
|
|
||||||
$self->log(LOGINFO, "pass: crypt match");
|
|
||||||
return ( OK, "auth_vmysql" );
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( defined $passHash && $pw_clear_passwd &&
|
|
||||||
$passHash eq hmac_md5_hex( $ticket, $pw_clear_passwd )
|
|
||||||
) {
|
|
||||||
$self->log(LOGINFO, "pass: hash match");
|
|
||||||
return ( OK, "auth_vmysql" );
|
|
||||||
};
|
|
||||||
|
|
||||||
$self->log(LOGINFO, "fail: wrong password");
|
|
||||||
return ( DENY, "auth_vmysql - wrong password" );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ package Test::Qpsmtpd::Plugin;
|
|||||||
# Additional plugin methods used during testing
|
# Additional plugin methods used during testing
|
||||||
package Qpsmtpd::Plugin;
|
package Qpsmtpd::Plugin;
|
||||||
|
|
||||||
use Test::More;
|
|
||||||
use strict;
|
use strict;
|
||||||
|
use Test::More;
|
||||||
|
use Qpsmtpd::Constants;
|
||||||
|
|
||||||
sub register_tests {
|
sub register_tests {
|
||||||
# Virtual base method - implement in plugin
|
# Virtual base method - implement in plugin
|
||||||
@ -37,4 +38,55 @@ sub run_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub validate_password {
|
||||||
|
my ( $self, %a ) = @_;
|
||||||
|
|
||||||
|
my ($pkg, $file, $line) = caller();
|
||||||
|
|
||||||
|
my $src_clear = $a{src_clear};
|
||||||
|
my $src_crypt = $a{src_crypt};
|
||||||
|
my $attempt_clear = $a{attempt_clear};
|
||||||
|
my $attempt_hash = $a{attempt_hash};
|
||||||
|
my $method = $a{method} or die "missing method";
|
||||||
|
my $ticket = $a{ticket};
|
||||||
|
my $deny = $a{deny} || DENY;
|
||||||
|
|
||||||
|
if ( ! $src_crypt && ! $src_clear ) {
|
||||||
|
$self->log(LOGINFO, "fail: missing password");
|
||||||
|
return ( $deny, "$file - no such user" );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( ! $src_clear && $method =~ /CRAM-MD5/i ) {
|
||||||
|
$self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass");
|
||||||
|
return ( DECLINED, $file );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( defined $attempt_clear ) {
|
||||||
|
if ( $src_clear && $src_clear eq $attempt_clear ) {
|
||||||
|
$self->log(LOGINFO, "pass: clear match");
|
||||||
|
return ( OK, $file );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( $src_crypt && $src_crypt eq crypt( $attempt_clear, $src_crypt ) ) {
|
||||||
|
$self->log(LOGINFO, "pass: crypt match");
|
||||||
|
return ( OK, $file );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( defined $attempt_hash && $src_clear ) {
|
||||||
|
if ( ! $ticket ) {
|
||||||
|
$self->log(LOGERROR, "skip: missing ticket");
|
||||||
|
return ( DECLINED, $file );
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( $attempt_hash eq hmac_md5_hex( $ticket, $src_clear ) ) {
|
||||||
|
$self->log(LOGINFO, "pass: hash match");
|
||||||
|
return ( OK, $file );
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$self->log(LOGINFO, "fail: wrong password");
|
||||||
|
return ( $deny, "$file - wrong password" );
|
||||||
|
};
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
Loading…
Reference in New Issue
Block a user