Python 身份验证:PAM 或 LDAP bind() 与 LDAP {SSHA} 检查?

Python Authentication: PAM or LDAP bind() vs. LDAP {SSHA} check?

我正在编写一个相当简单的内部应用程序(目前原型在 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 中,所需的问题引用存在并且处于适当的状态)。

自然地,我们要求用户在我发送将归因于他们的消息之前对我的应用程序进行身份验证。我们使用 LDAP,所有密码哈希都以 {SSHA} 格式存储。

我发现了至少三种不同的方法来执行身份验证:

这里的代码似乎正确地实现了其中的第一个:

#!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)

这似乎可行(假设我们的 .creds.ini 中有适当的值)。

这是实现第二个选项的一些代码:

#!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)

此处未显示,但我还测试了我可以独立于瞬态 chk LDAP 对象继续使用 ld LDAP 连接。所以我的长运行网络服务可以继续重复使用一个连接。

无论我使用两个 PAM 模块中的哪一个,最后的选项都几乎相同。这是一个使用 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)

我的问题是:我应该使用其中的哪一个。他们中的任何一个比其他人特别不安全吗? "best practices"对此有什么共识吗?

绝对使用绑定作为用户提供的用户 DN 和密码。

"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"

抱歉,我不知道 "Authenticate() functions" 的行为如何。

不绑定为 userDN,可能会绕过适用于密码策略或帐户限制和入侵者检测的内置服务器功能。

-吉姆