I'm writing a fairly simple internal application (currently prototyped in Bottle) which will send notification for incidents and change management events to an internal mailing list while forcing these notices to conform to a couple of standard templates (and ensuring, through the Python Jira API that the required issue references exist and are in the appropriate state).
Naturally we require that users authenticate to my application before I send messages which will be attributed to them. We use LDAP and all password hashes are stored in {SSHA} format.
I've found at least three different ways to perform the authentication:
Here's code which seems to correctly implement the first of these:
#!python
import hashlib
import ConfigParser, os
from base64 import encodestring as encode
from base64 import decodestring as decode
import ldap
config = ConfigParser.ConfigParser()
config.read(os.path.expanduser('~/.creds.ini'))
uid = config.get('LDAP', 'uid')
pwd = config.get('LDAP', 'pwd')
svr = config.get('LDAP', 'svr')
bdn = config.get('LDAP', 'bdn')
ld = ldap.initialize(svr)
ld.protocol_version = ldap.VERSION3
ld.simple_bind_s(uid, pwd)
def chk(prop, pw):
pw=decode(pw[6:]) # Base64 decode after stripping off {SSHA}
digest = pw[:20] # Split digest/hash of PW from salt
salt = pw[20:] # Extract salt
chk = hashlib.sha1(prop) # Hash the string presented
chk.update(salt) # Salt to taste:
return chk.digest() == digest
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
if len(user_dn) < 1:
print 'No DN found for %s' % name
sys.exit(126)
pw = user_dn[0][1].get('userPassword', [''])[0]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, pw):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
This seems to work (assuming we have the appropriate values in our .creds.ini
).
Here's a bit of code implementing the second option:
#!python
# ...
### Same ConfigParser and LDAP initialization as before
# ...
def chk(prop, dn):
chk = ldap.initialize(svr)
chk.protocol_version = ldap.VERSION3
try:
chk.simple_bind_s(dn, prop)
except ldap.INVALID_CREDENTIALS:
return False
chk.unbind()
return True
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
user_dn = ld.search_s(bdn, ldap.SCOPE_SUBTREE, '(uid=%s)' % name)
if len(user_dn) < 1:
print 'No distinguished name (DN) found for %s' % name
sys.exit(126)
dn = user_dn[0][0]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, dn):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
Not shown here but I also tested that I can continue to use the ld
LDAP connection independently of the transient chk
LDAP object. So my long-running web service can keep re-using the one connection.
The last options are almost identical regardless of which of the two PAM modules I use. Here's an example using python-pam:
#!/usr/bin/env python
import pam
pam_conn = pam.pam()
def chk(prop, name):
return pam_conn.authenticate(name, prop)
if __name__ == '__main__':
import sys
from getpass import getpass
max_attempts = 3
if len(sys.argv) < 2:
print 'Must supply username against which to authenticate'
sys.exit(127)
name = sys.argv[1]
exit_value = 1
attempts = 0
while attempts < max_attempts:
prop = getpass('Password: ')
if chk(prop, name):
print 'Authentication successful'
exit_value = 0
break
else:
print 'Authentication failed'
attempts += 1
else:
print 'Maximum retries exceeded'
sys.exit(exit_value)
My question is: which of these should I use. Are any of them particularly less secure than the others? Is there any consensus on "best practices" for this?
Definitely use a bind as the userDN and password presented by the user.
"bind to LDAP with a service account with limited search privileges, use that to find the user's dn and then attempt to bind to LDAP using that dn and the users proposed password"
Sorry, I do not know how the "Authenticate() functions" behave.
Not binding as the userDN, may bypass the builtin server functions that apply to password policy or account restrictions and intruder detection.
-jim