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

时间:2015-03-29 11:34:34

标签: ldap pam

我正在编写一个相当简单的内部应用程序(目前在Bottle中进行原型设计),它会将事件通知和更改管理事件发送到内部邮件列表,同时强制这些通知符合几个标准模板(并通过Python Jira API确保所需的问题参考存在且处于适当的状态。)

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

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

  • 使用具有足够LDAP ACI的服务帐户绑定到LDAP以获取密码哈希值(与我们的 /etc/sssd/sssd.conf 系统级别身份验证相同),使用它来查找 dn 并提取'userPassword'属性;然后使用 hashlib
  • 对提议的密码进行验证
  • 使用具有有限搜索权限的服务帐户绑定到LDAP,使用该帐户查找用户的 dn ,然后尝试使用 dn 绑定到LDAP并建议用户密码
  • 使用simplepam(纯Python / ctypes包装器)或python-pam模块
  • 中的 authenticate()函数

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

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

此处未显示,但我还测试了我可以继续使用ld LDAP连接,而不依赖于临时chk LDAP对象。因此,我长期运行的Web服务可以继续重用一个连接。

无论我使用哪两个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)

我的问题是:我应该使用哪些。它们中的任何一个都不如其他人安全吗?关于“最佳实践”是否有任何共识?

1 个答案:

答案 0 :(得分:2)

绝对使用bind作为用户提供的用户名和密码。

“使用具有有限搜索权限的服务帐户绑定到LDAP,使用该帐户查找用户的dn,然后尝试使用该dn和用户建议的密码绑定到LDAP”

抱歉,我不知道“Authenticate()函数”的表现如何。

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

-Jim