Hello all,
I would like to submit another plugin for your consideration. This
plugin is different from auth_ldap_bind in that it supports CRAM-MD5
authentication. The two plugins have different applications (mostly due
to different security requirements,) but I could put some effort into
merging the two plugins together if it is so desired.
As with the last ldap plugin I submitted, please let me know if there
are any changes that are desired (tests?) If the plugin will not be
accepted into the distribution, I would like to push for an
'unsupported/contrib' area of svn.
Thanks,
Elliot
#!/usr/bin/perl -Tw
sub register {
my ( $self, $qp, @args ) = @_;
# pull config in from file
%{ $self->{ldconf} } = map { (split /\s+/, $_, 2)[0,1] }
$self->qp->config('ldap');
# override ldap config with plugin args
for my $ldap_arg (@args) {
%{ $self->{ldconf} } = map { (split /\s+/, $_, 2)[0,1] } $ldap_arg;
}
# allow insecure methods only if explicitly configured
if ($self->{ldconf}->{ldap_allow_plain}) {
$self->register_hook( "auth-plain", "authldap" );
$self->register_hook( "auth-login", "authldap" );
}
$self->register_hook( "auth-cram-md5", "authldap" );
# do light validation of ldap_host and ldap_port to satisfy -T
my $ldhost = $self->{ldconf}->{ldap_host};
my $ldport = $self->{ldconf}->{ldap_port};
if (($ldhost) && ($ldhost =~ m/^(([a-z0-9]+\.?)+)$/)) {
$self->{ldconf}->{ldap_host} = $1
} else {
undef $self->{ldconf}->{ldap_host};
}
if (($ldport) && ($ldport =~ m/^(\d+)$/)) {
$self->{ldconf}->{ldap_port} = $1
} else {
undef $self->{ldconf}->{ldap_port};
}
# set any values that are not already
$self->{ldconf}->{ldap_host} ||= "127.0.0.1";
$self->{ldconf}->{ldap_port} ||= 389;
$self->{ldconf}->{ldap_timeout} ||= 5;
$self->{ldconf}->{ldap_clearpassword_attr} ||= "clearPassword";
$self->{ldconf}->{ldap_auth_filter_attr} ||= "uid";
}
sub authldap {
use Net::LDAP qw(:all);
use Qpsmtpd::Constants;
use Digest::HMAC_MD5 qw(hmac_md5_hex);
my ( $self, $transaction, $method, $user, $passClear, $passHash, $ticket ) =
@_;
my ($ldhost, $ldport, $ldwait, $ldbase, $ldbinddn, $ldbindpw, $ldmattr,
$ldpattr, $lduserdn, $ldh, $mesg);
# pull values in from config
$ldhost = $self->{ldconf}->{ldap_host};
$ldport = $self->{ldconf}->{ldap_port};
$ldbase = $self->{ldconf}->{ldap_base};
$ldwait = $self->{ldconf}->{ldap_timeout};
$ldbinddn = $self->{ldconf}->{ldap_bind_dn};
$ldbindpw = $self->{ldconf}->{ldap_bind_password};
$ldpattr = $self->{ldconf}->{ldap_clearpassword_attr};
$ldmattr = $self->{ldconf}->{ldap_auth_filter_attr};
# log error here and DECLINE if no baseDN, because a custom baseDN is
required:
unless ($ldbase && $ldbinddn && $ldbindpw) {
$self->log(LOGERROR, "authldap/$method - please configure ldap_base,
ldap_bind_dn, and ldap_bind_password" ) &&
return ( DECLINED, "authldap/$method - temporary auth error" );
}
my ( $pw_name, $pw_domain ) = split "@", lc($user);
# find dn of user matching supplied username
$ldh = Net::LDAP->new($ldhost, port => $ldport, timeout => $ldwait ) or
$self->log(LOGERROR, "authldap/$method - error in initial conn" ) &&
return ( DECLINED, "authldap/$method - temporary auth error" );
# bind to the directory using configured credentials
$mesg = $ldh->bind($ldbinddn, password => "$ldbindpw", timeout => $ldwait);
if ( $mesg->code ) {
$self->log(LOGERROR, "authldap/$method - error '" . $mesg->code . "' in
bind" );
return ( DECLINED, "authldap/$method - wrong username or password" );
}
# find the user's DN
$mesg = $ldh->search(
base=>$ldbase,
scope=>'sub',
filter=>"$ldmattr=$pw_name",
attrs=>[$ldpattr],
timeout=>$ldwait,
sizelimit=>'1') or
$self->log(LOGERROR, "authldap/$method - err in search for user" ) &&
return ( DECLINED, "authldap/$method - temporary auth error" );
# deal with errors if they exist
if ( $mesg->code ) {
$self->log(LOGERROR, "authldap/$method - err " . $mesg->code . " in search
for user" );
return ( DECLINED, "authldap/$method - temporary auth error" );
}
# unbind
$ldh->unbind if ($ldh);
$ldh->disconnect;
# if all went well and we got an entry back
if (($mesg->count == 1)) {
$self->log(LOGDEBUG, "authldap/$method - dn '" . $mesg->entry->dn . "'
matched '$ldmattr=$pw_name'" );
# if we didn't get the password attribute
my @clearpass = $mesg->entry->get_value( $ldpattr );
unless (scalar(@clearpass)) {
$self->log(LOGERROR, "authldap/$method - could not retrieve clear text
password attr '$ldpattr'" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
# if clear pass is configured (allowed) and clearpass is given
if (($self->{ldconf}->{ldap_allow_plain}) and (defined ($passClear))) {
# if given pass matches stored
my $match = 0;
foreach (@clearpass) {
# since it could be a multi-valued attr
if ($passClear eq $_) {
$match = 1;
last;
}
}
if ($match) {
# auth'd
$self->log(LOGNOTICE, "authldap/$method - user '$pw_name' authenticated
successfully" ) &&
return ( OK, "authldap/$method" );
}
else {
# failed
$self->log(LOGNOTICE, "authldap/$method - given clear pass did not
match stored pass" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
}
# if we have a hash
elsif (defined ($passHash)) {
# if hash of given pass matches hash of stored
my $match = 0;
foreach (@clearpass) {
# since it could be a multi-valued attr
if ($passHash eq hmac_md5_hex($ticket, $_)) {
$match = 1;
last;
}
}
if ($match) {
$self->log(LOGNOTICE, "authldap/$method - user '$pw_name' authenticated
successfully" ) &&
return ( OK, "authldap/$method" );
}
else {
$self->log(LOGNOTICE, "authldap/$method - given clear pass did not
match stored pass" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
}
# err out
else {
$self->log(LOGNOTICE, "authldap/$method - no hash given, and not
configured to accept plain/login auth" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
}
else {
# if we didn't get any entries
if ($mesg->count < 1) {
$self->log(LOGNOTICE, "authldap/$method - no entries matched
'$ldmattr=$pw_name'" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
# if we got too many entries
if ($mesg->count > 1) {
$self->log(LOGNOTICE, "authldap/$method - too many entries matched
'$ldmattr=$pw_name'" ) &&
return ( DECLINED, "authldap/$method - wrong username or password" );
}
}
}
=head1 NAME
auth_ldap_clearpassword - Authenticate user against an LDAP Directory
=head1 DESCRIPTION
This plugin authenticates users against an LDAP Directory. The plugin
first performs a lookup for an entry matching the connecting user. This
lookup uses the 'ldap_auth_filter_attr' attribute to match the connecting
user to their LDAP DN. As part of the query, the user's cleartext password
is returned ('ldap_clearpassword_attr'.) The clearpassword is used either
in comparison, or used as part of a hash scheme (CRAM-MD5).
=head1 CONFIGURATION
Configuration items can be held in either the 'ldap' configuration file, or as
arguments to the plugin.
Configuration items in the 'ldap' configuration file are set one per line,
starting the line with the configuration item key, followed by a space,
then the values associated with the configuration item.
Configuration items given as arguments to the plugin are keys and values
separated by spaces. Be sure to quote any values that have spaces in them.
The only required configuration items are 'ldap_base', 'ldap_bind_dn', and
'ldap_bind_password'. 'ldap_base' specifies the base which should be used when
querying your Directory, 'ldap_bind_dn' and 'ldap_bind_password' specify the
DN and password to be used when binding to your directory. The plugin will not
work until these three attributes have been configured.
The configuration items 'ldap_host' and 'ldap_port' specify the host and port
at which your Directory server may be contacted. If these are not specified,
the plugin will use '127.0.0.1' and '389' respectively.
The configuration item 'ldap_timeout' specifies how long the plugin should
wait for a response from your Directory server. By default, the value is 5
seconds.
The configuration item 'ldap_clearpassword_attr' specifies what attribute
contains the user's clear text password. The default value is 'clearPassword'.
The configuration item 'ldap_auth_filter_attr' specifies how the plugin should
locate the user in your Directory. By default, the plugin will look up the user
based on the 'uid' attribute.
The configuration item 'ldap_allow_plain' specifies whether or not PLAIN/LOGIN
authentication should be advertised/allowed. If set to a true value, PLAIN
and LOGIN methods will be allowed. The default is set to not allow PLAIN or
LOGIN, only CRAM is available.
=head1 NOTES
WARNING
allowing retrieval of cleartext passwords from your Directory is a potentially
dangerous decision. Please consider this in your implementation.
=head1 FUTURE DIRECTION
A configurable LDAP filter should be made available, to account for users
who are over quota, have had their accounts disabled, or whatever other
arbitrary requirements.
A configurable DN template (uid=$USER,ou=$DOMAIN,$BASE). This would allow
a lookup with a scope of 'base', providing a performance benefit, as well as
allowing the possibility of virtual domain support.
=head1 CONFIGURATION EXAMPLE
#
# Required settings and example values
#
ldap_base ou=People,dc=domain,dc=com
ldap_bind_dn uid=qpsmtpd,dc=domain,dc=com
ldap_bind_password password
#
# Optional settings and their defaults
#
ldap_clearpassword_attr clearPassword
ldap_auth_filter_attr uid
ldap_host 127.0.0.1
ldap_port 389
ldap_timeout 5
ldap_allow_plain 0
=head1 AUTHOR
Elliot Foster <[EMAIL PROTECTED]>
=head1 COPYRIGHT AND LICENSE
Copyright (c) 2006 Elliot Foster
This plugin is licensed under the same terms as the qpsmtpd package itself.
Please see the LICENSE file included with qpsmtpd for details.
=cut