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

Reply via email to