1#!perl -w 2 3=head1 NAME 4 5auth_ldap_bind - Authenticate user via an LDAP bind 6 7=head1 DESCRIPTION 8 9This plugin authenticates users against an LDAP Directory. The plugin 10first performs a lookup for an entry matching the connecting user. This 11lookup uses the 'ldap_auth_filter_attr' attribute to match the connecting 12user to their LDAP DN. Once the plugin has found the user's DN, the plugin 13will attempt to bind to the Directory as that DN with the password that has 14been supplied. 15 16=head1 CONFIGURATION 17 18Configuration items can be held in either the 'ldap' configuration file, or as 19arguments to the plugin. 20 21Configuration items in the 'ldap' configuration file 22are set one per line, starting the line with the configuration item key, 23followed by a space, then the values associated with the configuration item. 24 25Configuration items given as arguments to the plugin are keys and values 26separated by spaces. Be sure to quote any values that have spaces in them. 27 28The only configuration item which is required is 'ldap_base'. This tells the 29plugin what your base DN is. The plugin will not work until it has been 30configured. 31 32The configuration items 'ldap_host' and 'ldap_port' specify the host and port 33at which your Directory server may be contacted. If these are not specified, 34the plugin will use port '389' on 'localhost'. 35 36The configuration item 'ldap_timeout' specifies how long the plugin should 37wait for a response from your Directory server. By default, the value is 5 38seconds. 39 40The configuration item 'ldap_auth_filter_attr' specifies how the plugin should 41find the user in your Directory. By default, the plugin will look up the user 42based on the 'uid' attribute. 43 44=head1 NOTES 45 46Each auth requires an initial lookup to find the user's DN. Ideally, the 47plugin would simply bind as the user without the need for this lookup (see 48FUTURE DIRECTION below). 49 50This plugin requires that the Directory allow anonymous bind (see FUTURE 51DIRECTION below). 52 53=head1 FUTURE DIRECTION 54 55A configurable LDAP filter should be made available, to account for users 56who are over quota, have had their accounts disabled, or whatever other 57arbitrary requirements. 58 59A configurable DN template (uid=$USER,ou=$DOMAIN,$BASE). This would prevent 60the need of the initial user lookup, as the DN is created from the template. 61 62A configurable bind DN, for Directories that do not allow anonymous bind. 63 64Another plugin ('ldap_auth_cleartext'?), to allow retrieval of plain-text 65passwords from the Directory, permitting CRAM-MD5 or other hash algorithm 66authentication. 67 68=head1 AUTHOR 69 70Elliot Foster <elliotf@gratuitous.net> 71 72=head1 COPYRIGHT AND LICENSE 73 74Copyright (c) 2005 Elliot Foster 75 76This plugin is licensed under the same terms as the qpsmtpd package itself. 77Please see the LICENSE file included with qpsmtpd for details. 78 79=cut 80 81use strict; 82use warnings; 83 84use Net::LDAP qw(:all); 85use Qpsmtpd::Constants; 86 87sub register { 88 my ($self, $qp, @args) = @_; 89 90 $self->register_hook("auth-plain", "authldap"); 91 $self->register_hook("auth-login", "authldap"); 92 93 # pull config defaults in from file 94 %{$self->{"ldconf"}} = 95 map { (split /\s+/, $_, 2)[0, 1] } $self->qp->config('ldap'); 96 97 # override ldap config defaults with plugin args 98 for my $ldap_arg (@args) { 99 %{$self->{"ldconf"}} = map { (split /\s+/, $_, 2)[0, 1] } $ldap_arg; 100 } 101 102 # do light validation of ldap_host and ldap_port to satisfy -T 103 my $ldhost = $self->{"ldconf"}->{'ldap_host'}; 104 my $ldport = $self->{"ldconf"}->{'ldap_port'}; 105 if (($ldhost) && ($ldhost =~ m/^(([a-z0-9]+\.?)+)$/)) { 106 $self->{"ldconf"}->{'ldap_host'} = $1; 107 } 108 else { 109 undef $self->{"ldconf"}->{'ldap_host'}; 110 } 111 if (($ldport) && ($ldport =~ m/^(\d+)$/)) { 112 $self->{"ldconf"}->{'ldap_port'} = $1; 113 } 114 else { 115 undef $self->{"ldconf"}->{'ldap_port'}; 116 } 117 118 # set any values that are not already 119 $self->{"ldconf"}->{"ldap_host"} ||= "127.0.0.1"; 120 $self->{"ldconf"}->{"ldap_port"} ||= 389; 121 $self->{"ldconf"}->{"ldap_timeout"} ||= 5; 122 $self->{"ldconf"}->{"ldap_auth_filter_attr"} ||= "uid"; 123} 124 125sub authldap { 126 my ($self, $transaction, $method, $user, $passClear, $passHash, $ticket) = 127 @_; 128 my ($ldhost, $ldport, $ldwait, $ldbase, $ldmattr, $lduserdn, $ldh, $mesg); 129 130 # pull values in from config 131 $ldhost = $self->{"ldconf"}->{"ldap_host"}; 132 $ldport = $self->{"ldconf"}->{"ldap_port"}; 133 $ldbase = $self->{"ldconf"}->{"ldap_base"}; 134 135 # log error here and DECLINE if no baseDN, because a custom baseDN is required: 136 unless ($ldbase) { 137 $self->log(LOGERROR, "skip: please configure ldap_base"); 138 return (DECLINED, "authldap - temporary auth error"); 139 } 140 $ldwait = $self->{"ldconf"}->{'ldap_timeout'}; 141 $ldmattr = $self->{"ldconf"}->{'ldap_auth_filter_attr'}; 142 143 my ($pw_name, $pw_domain) = split "@", lc($user); 144 145 # find dn of user matching supplied username 146 $ldh = Net::LDAP->new($ldhost, port => $ldport, timeout => $ldwait) or do { 147 $self->log(LOGALERT, "skip: error in initial conn"); 148 return (DECLINED, "authldap - temporary auth error"); 149 }; 150 151 # find the user's DN 152 $mesg = $ldh->search( 153 base => $ldbase, 154 scope => 'sub', 155 filter => "$ldmattr=$pw_name", 156 attrs => ['uid'], 157 timeout => $ldwait, 158 sizelimit => '1' 159 ) 160 or do { 161 $self->log(LOGALERT, "skip: err in search for user"); 162 return (DECLINED, "authldap - temporary auth error"); 163 }; 164 165 # deal with errors if they exist 166 if ($mesg->code) { 167 $self->log(LOGALERT, 168 "skip: err " . $mesg->code . " in search for user"); 169 return (DECLINED, "authldap - temporary auth error"); 170 } 171 172 # unbind, so as to allow a rebind below 173 $ldh->unbind if $ldh; 174 175 # bind against directory as user with password supplied 176 if (!$mesg->count || $lduserdn = $mesg->entry->dn) { 177 $self->log(LOGALERT, "fail: user not found"); 178 return (DECLINED, "authldap - wrong username or password"); 179 } 180 181 $ldh = Net::LDAP->new($ldhost, port => $ldport, timeout => $ldwait) or do { 182 $self->log(LOGALERT, "skip: err in user conn"); 183 return (DECLINED, "authldap - temporary auth error"); 184 }; 185 186 # here's the whole reason for the script 187 $mesg = $ldh->bind($lduserdn, password => $passClear, timeout => $ldwait); 188 $ldh->unbind if $ldh; 189 190 # deal with errors if they exist, or allow success 191 if ($mesg->code) { 192 $self->log(LOGALERT, "fail: error in user bind"); 193 return (DECLINED, "authldap - wrong username or password"); 194 } 195 196 $self->log(LOGINFO, "pass: $user auth success"); 197 $self->log(LOGDEBUG, "user: $user, pass: $passClear"); 198 return (OK, "authldap"); 199} 200 201