skydum

個人的な作業記録とか備忘録代わりのメモ

python3 + ldap3 + Active Directory(LDAP)で認証を行う

Active Directory + python3でユーザー認証を行う

環境

構築した環境

Windows Server 2019

項目名 設定内容
IPv4アドレス 192.168.0.155/24
IPv6アドレス ネットワークのタブでチェックを外して未使用状態に変更
Active Directory ad.example.com

作成したユーザーとグループ

ユーザー名 所属グループ名 パスワード
test-user Domain Users, TestGroups PASSWORD
test-user2 TestSecondGroups PASSWORD
グループ名 所属グループ名
TestGroups Remote Desktop Users
TestSecondGroups
コンピューター名 備考欄
WIN10LTS Windows10をActive Directoryドメイン参加

登録ユーザー

  • test-user

  • test-user2

  • TestGroups

  • TestSecondGroups

Windows Server 2019上でユーザーの登録内容の確認

  • PowerShelleを起動して以下のコマンドを入力
PS C:\Users\Administrator> Get-ADUser -Filter { SamAccountName -eq "test-user" } -Properties *


AccountExpirationDate                : 
accountExpires                       : 9223372036854775807
AccountLockoutTime                   : 
AccountNotDelegated                  : False
AllowReversiblePasswordEncryption    : False
AuthenticationPolicy                 : {}
AuthenticationPolicySilo             : {}
BadLogonCount                        : 0
badPasswordTime                      : 0
badPwdCount                          : 0
CannotChangePassword                 : True
CanonicalName                        : ad.example.com/Users/FirstName LastName イニシャル.
Certificates                         : {}
City                                 : 
CN                                   : FirstName LastName イニシャル.
codePage                             : 0
Company                              : 
CompoundIdentitySupported            : {}
Country                              : 
countryCode                          : 0
Created                              : 2022/07/19 20:44:21
createTimeStamp                      : 2022/07/19 20:44:21
Deleted                              : 
Department                           : 
Description                          : 
DisplayName                          : FirstName LastName イニシャル.
DistinguishedName                    : CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com
Division                             : 
DoesNotRequirePreAuth                : False
dSCorePropagationData                : {2022/07/19 20:44:21, 1601/01/01 9:00:00}
EmailAddress                         : 
EmployeeID                           : 
EmployeeNumber                       : 
Enabled                              : True
Fax                                  : 
GivenName                            : LastName
HomeDirectory                        : 
HomedirRequired                      : False
HomeDrive                            : 
HomePage                             : 
HomePhone                            : 
Initials                             : イニシャル
instanceType                         : 4
isDeleted                            : 
KerberosEncryptionType               : {}
LastBadPasswordAttempt               : 
LastKnownParent                      : 
lastLogoff                           : 0
lastLogon                            : 0
LastLogonDate                        : 
LockedOut                            : False
logonCount                           : 0
LogonWorkstations                    : 
Manager                              : 
MemberOf                             : {CN=TestGroups,CN=Users,DC=ad,DC=example,DC=com}
MNSLogonAccount                      : False
MobilePhone                          : 
Modified                             : 2022/07/19 20:44:21
modifyTimeStamp                      : 2022/07/19 20:44:21
msDS-User-Account-Control-Computed   : 0
Name                                 : FirstName LastName イニシャル.
nTSecurityDescriptor                 : System.DirectoryServices.ActiveDirectorySecurity
ObjectCategory                       : CN=Person,CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
ObjectClass                          : user
ObjectGUID                           : 1248700d-e14c-4589-8408-b44298854cbb
objectSid                            : S-1-5-21-4206833561-140562442-4113086211-1610
Office                               : 
OfficePhone                          : 
Organization                         : 
OtherName                            : 
PasswordExpired                      : False
PasswordLastSet                      : 2022/07/19 20:44:21
PasswordNeverExpires                 : True
PasswordNotRequired                  : False
POBox                                : 
PostalCode                           : 
PrimaryGroup                         : CN=Domain Users,CN=Users,DC=ad,DC=example,DC=com
primaryGroupID                       : 513
PrincipalsAllowedToDelegateToAccount : {}
ProfilePath                          : 
ProtectedFromAccidentalDeletion      : False
pwdLastSet                           : 133027046616261250
SamAccountName                       : test-user
sAMAccountType                       : 805306368
ScriptPath                           : 
sDRightsEffective                    : 15
ServicePrincipalNames                : {}
SID                                  : S-1-5-21-4206833561-140562442-4113086211-1610
SIDHistory                           : {}
SmartcardLogonRequired               : False
sn                                   : FirstName
State                                : 
StreetAddress                        : 
Surname                              : FirstName
Title                                : 
TrustedForDelegation                 : False
TrustedToAuthForDelegation           : False
UseDESKeyOnly                        : False
userAccountControl                   : 66048
userCertificate                      : {}
UserPrincipalName                    : test-user@ad.example.com
uSNChanged                           : 28775
uSNCreated                           : 28767
whenChanged                          : 2022/07/19 20:44:21
whenCreated                          : 2022/07/19 20:44:21




PS C:\Users\Administrator> 

Active DirectoryからLDAPで検索

ldapsearchでLDAPを検索してみる

  • CentOS 7ならyum install openldap-clientを入れると利用できる
    • 何のパッケージに入っているのかわからないときはyum provides ldapsearchみたいにすると良い

接続法 その1

  • ldapsearchを使って接続する場合はPowerShellの結果に含まれるDistinguishedNameを利用する
DistinguishedName                    : CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com
  • -h は接続先のIPアドレス
  • -D はActive Direcotryへ接続するユーザー名
  • -w はActive Directoryへ接続するユーザーのパスワード

  • 成功した場合は以下のようなレスポンスが返ってくる

$ ldapsearch -h 192.168.0.155 -D 'CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com' -w "PASSWORD"
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# search result
search: 2
result: 32 No such object
text: 0000208D: NameErr: DSID-03100220, problem 2001 (NO_OBJECT), data 0, best
 match of:
        ''


# numResponses: 1
  • パスワードを間違えた場合やユーザーが見つからないなとの場合以下のようなエラーが発生する
$ ldapsearch -h 192.168.0.155 -D 'CN=FirstName LastName イニシャル.,CN=Users,DC=ad,DC=example,DC=com' -w "PASSWORDZ"
ldap_bind: Invalid credentials (49)
        additional info: 80090308: LdapErr: DSID-0C090439, comment: AcceptSecurityContext error, data 52e, v4563

接続法 その2

  • こちらのほうがわかりやすくて良いかも
$ ldapsearch -h 192.168.0.155 -D 'test-user@ad.example.com' -w 'PASSWORD'

python3 + ldap3でLDAPで認証

# pip install ldap3

python3 + ldap3で認証テスト

  • ConnectionでLDAPのサーバへ接続する
    • user=ドメイン名\ユーザー名
    • password=パスワード
    • authentication=NTML(NTLM形式で認証を行う)
    • auto_bind=True(サーバへ接続を自動的に行う)
    • read_ony=読み込み専用モードで接続する
  • conn.extend.standard.who_am_i
    • 接続したユーザーが誰かをLDAPのサーバに問い合わせる
    • 成功した場合u:AD-SERVER\test-userの様な値が返ってくる
from ldap3 import ALL, NTLM, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPBindError


SERVER = "192.168.0.155"
USERNAME = "ad.example.com\\test-user"
PASSWORD = "PASSWORD"


try:
    server = Server(SERVER, get_info=ALL)

    # サーバーへの接続に失敗した場合Raiseする
    # connには接続に使用した情報が全て含まれている
    conn = Connection(server, user=USERNAME, password=PASSWORD, authentication=NTLM, auto_bind=True, read_only=True)

    # 接続に成功するとauthにu:AD-SERVER\test-userが入る
    auth = conn.extend.standard.who_am_i()

    # u:AD-SERVER\test-user
    print(auth)

    # ldap://192.168.0.155:389 - cleartext - user: ad.example.com\test-user - not lazy - bound - open - <local: 192.168.0.50:63201 - remote: 192.168.0.155:389> - tls not started - listening - SyncStrategy - internal decoder
    print(conn)

    # DSA info (from DSE):
    # Supported LDAP versions: 3, 2
    # Naming contexts:
    #     DC=ad,DC=example,DC=com
    #     CN=Configuration,DC=ad,DC=example,DC=com
    #     CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    #     DC=DomainDnsZones,DC=ad,DC=example,DC=com
    #     DC=ForestDnsZones,DC=ad,DC=example,DC=com
    # Supported controls:
    #     1.2.840.113556.1.4.1338 - Verify name - Control - MICROSOFT
    #     1.2.840.113556.1.4.1339 - Domain scope - Control - MICROSOFT
    #     1.2.840.113556.1.4.1340 - Search options - Control - MICROSOFT
    #     1.2.840.113556.1.4.1341 - RODC DCPROMO - Control - MICROSOFT
    #     1.2.840.113556.1.4.1413 - Permissive modify - Control - MICROSOFT
    #     1.2.840.113556.1.4.1504 - Attribute scoped query - Control - MICROSOFT
    #     1.2.840.113556.1.4.1852 - User quota - Control - MICROSOFT
    #     1.2.840.113556.1.4.1907 - Server shutdown notify - Control - MICROSOFT
    #     1.2.840.113556.1.4.1948 - Range retrieval no error - Control - MICROSOFT
    #     1.2.840.113556.1.4.1974 - Server force update - Control - MICROSOFT
    #     1.2.840.113556.1.4.2026 - Input DN - Control - MICROSOFT
    #     1.2.840.113556.1.4.2064 - Show recycled - Control - MICROSOFT
    #     1.2.840.113556.1.4.2065 - Show deactivated link - Control - MICROSOFT
    #     1.2.840.113556.1.4.2066 - Policy hints [DEPRECATED] - Control - MICROSOFT
    #     1.2.840.113556.1.4.2090 - DirSync EX - Control - MICROSOFT
    #     1.2.840.113556.1.4.2204 - Tree deleted EX - Control - MICROSOFT
    #     1.2.840.113556.1.4.2205 - Updates stats - Control - MICROSOFT
    #     1.2.840.113556.1.4.2206 - Search hints - Control - MICROSOFT
    #     1.2.840.113556.1.4.2211 - Expected entry count - Control - MICROSOFT
    #     1.2.840.113556.1.4.2239 - Policy hints - Control - MICROSOFT
    #     1.2.840.113556.1.4.2255 - Set owner - Control - MICROSOFT
    #     1.2.840.113556.1.4.2256 - Bypass quota - Control - MICROSOFT
    #     1.2.840.113556.1.4.2309
    #     1.2.840.113556.1.4.2330
    #     1.2.840.113556.1.4.2354
    #     1.2.840.113556.1.4.319 - LDAP Simple Paged Results - Control - RFC2696
    #     1.2.840.113556.1.4.417 - LDAP server show deleted objects - Control - MICROSOFT
    #     1.2.840.113556.1.4.473 - Sort Request - Control - RFC2891
    #     1.2.840.113556.1.4.474 - Sort Response - Control - RFC2891
    #     1.2.840.113556.1.4.521 - Cross-domain move - Control - MICROSOFT
    #     1.2.840.113556.1.4.528 - Server search notification - Control - MICROSOFT
    #     1.2.840.113556.1.4.529 - Extended DN - Control - MICROSOFT
    #     1.2.840.113556.1.4.619 - Lazy commit - Control - MICROSOFT
    #     1.2.840.113556.1.4.801 - Security descriptor flags - Control - MICROSOFT
    #     1.2.840.113556.1.4.802 - Range option - Control - MICROSOFT
    #     1.2.840.113556.1.4.805 - Tree delete - Control - MICROSOFT
    #     1.2.840.113556.1.4.841 - Directory synchronization - Control - MICROSOFT
    #     1.2.840.113556.1.4.970 - Get stats - Control - MICROSOFT
    #     2.16.840.1.113730.3.4.10 - Virtual List View Response - Control - IETF
    #     2.16.840.1.113730.3.4.9 - Virtual List View Request - Control - IETF
    # Supported extensions:
    #     1.2.840.113556.1.4.1781 - Fast concurrent bind - Extension - MICROSOFT
    #     1.2.840.113556.1.4.2212 - Batch request - Extension - MICROSOFT
    #     1.3.6.1.4.1.1466.101.119.1 - Dynamic Refresh - Extension - RFC2589
    #     1.3.6.1.4.1.1466.20037 - StartTLS - Extension - RFC4511-RFC4513
    #     1.3.6.1.4.1.4203.1.11.3 - Who am I - Extension - RFC4532
    # Supported features:
    #     1.2.840.113556.1.4.1670 - Active directory V51 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.1791 - Active directory LDAP Integration - Feature - MICROSOFT
    #     1.2.840.113556.1.4.1935 - Active directory V60 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.2080 - Active directory V61 R2 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.2237 - Active directory W8 - Feature - MICROSOFT
    #     1.2.840.113556.1.4.800 - Active directory - Feature - MICROSOFT
    # Supported SASL mechanisms:
    #     GSSAPI, GSS-SPNEGO, EXTERNAL, DIGEST-MD5
    # Schema entry:
    #     CN=Aggregate,CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    # Other:
    # domainFunctionality:
    #     7
    # forestFunctionality:
    #     7
    # domainControllerFunctionality:
    #     7
    # rootDomainNamingContext:
    #     DC=ad,DC=example,DC=com
    # ldapServiceName:
    #     ad.example.com:win-2fechokc8i8$@AD.EXAMPLE.COM
    # isGlobalCatalogReady:
    #     TRUE
    # supportedLDAPPolicies:
    #     MaxPoolThreads
    #     MaxPercentDirSyncRequests
    #     MaxDatagramRecv
    #     MaxReceiveBuffer
    #     InitRecvTimeout
    #     MaxConnections
    #     MaxConnIdleTime
    #     MaxPageSize
    #     MaxBatchReturnMessages
    #     MaxQueryDuration
    #     MaxDirSyncDuration
    #     MaxTempTableSize
    #     MaxResultSetSize
    #     MinResultSets
    #     MaxResultSetsPerConn
    #     MaxNotificationPerConn
    #     MaxValRange
    #     MaxValRangeTransitive
    #     ThreadMemoryLimit
    #     SystemMemoryLimitPercent
    # serverName:
    # CN=WIN-2FECHOKC8I8,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=example,DC=com
    # schemaNamingContext:
    #     CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com
    # isSynchronized:
    #     TRUE
    # highestCommittedUSN:
    #     28797
    # dsServiceName:
    #     CN=NTDS Settings,CN=WIN-2FECHOKC8I8,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=ad,DC=example,DC=com
    # dnsHostName:
    # WIN-2FECHOKC8I8.ad.example.com
    # defaultNamingContext:
    #     DC=ad,DC=example,DC=com
    # currentTime:
    #     20220719123933.0Z
    # configurationNamingContext:
    #     CN=Configuration,DC=ad,DC=example,DC=com#
    print(server.info)

    entories = conn.extend.standard.paged_search(
        "dc=ad,dc=example,dc=com",
        "(objectclass=person)",
        attributes=["cn", "displayName", "description"],
        paged_size=100,
    )

    # generatorが返ってくる
    print(entories)

    list_entries = list(entories)

    for entry in list_entries:
        if entry.get("attributes", None) is None:
            continue

        # {'cn': 'FirstName LastName イニシャル.', 'description': ['説明'], 'displayName': 'FirstName LastName イニシャル.'}
        # {'cn': 'admin', 'displayName': 'admin', 'description': []}
        # {'cn': 'WIN10LTS', 'displayName': [], 'description': []}
        # {'cn': 'krbtgt', 'description': ['キー配布センター サービス アカウント'], 'displayName': []}
        # {'cn': 'WIN-2FECHOKC8I8', 'displayName': [], 'description': []}
        # {'cn': 'Guest', 'description': ['コンピューター/ドメインへのゲスト アクセス用 (ビルトイン アカウント)'], 'displayName': []}
        # {'cn': 'Administrator', 'description': ['コンピューター/ドメインの管理用 (ビルトイン アカウント)'], 'displayName': []}
        print(entry["attributes"])
except LDAPBindError as e:
    # 認証エラーが発生した場合ここに来る
    # automatic bind not successful - invalidCredentials
    print(e)

python3 + ldap3でLDAPを検索

  • ConnectionでLDAPのサーバに接続した後でconn.extend.standard.paged_searchを使って検索を行う
    この時に利用できる条件などはldapsearchで利用できる検索条件と同等なため、毎回pythonを実行するよりldapsearchで条件を決めてからpythonに組み込んだほうが楽だと思う。
  • attibutes=["*"]としているので取得できるすべての項目が取得される。
    attributes=["cn", "displayName", "description"]の様にすると記載した3個の条件に一致する項目だけが取得できる。
from ldap3 import ALL, NTLM, SUBTREE, Connection, Server
from ldap3.core.exceptions import LDAPBindError

SERVER = "192.168.0.155"
USERNAME = "ad.example.com\\test-user"
PASSWORD = "PASSWORD"

try:
    server = Server(SERVER, get_info=ALL)
    conn = Connection(
        server,
        user=USERNAME,
        password=PASSWORD,
        authentication=NTLM,
        auto_bind=True,
        read_only=True,
    )

    entories = conn.extend.standard.paged_search(
        "dc=ad,dc=example,dc=com",
        "(&(objectclass=person)(sAMAccountName=test-user))",
        attributes=["*"],
        paged_size=100,
    )

    for entry in entories:
        if entry.get("attributes", None) is None:
            continue

        print(entry["attributes"])
except LDAPBindError as e:
    print(e)

ldapsearchでの検索方法

ldapsearchでActive Directoryを検索

まずは全検索

MaxPageSize - この値は、返される各オブジェクトのサイズに依存して、1 つの検索結果で返されるオブジェクトの最大数を制御します。 この数を超える可能性がある検索を実行するには、クライアントがページ検索コントロールを指定する必要があります。 返される結果を、MaxPageSize 値より大きいグループにグループ化します。 要約すると、MaxPageSize は、1 つの検索結果で返されるオブジェクトの数を制御します。

既定値: 1,000

  • 検索コマンド
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com"

検索結果

$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com"
# extended LDIF
#
# LDAPv3
# base <cn=Users,dc=ad,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
...省略

# FirstName LastName \E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB., Users, ad.example.com

...省略
  • FiresName Lastnameの所で# FirstName LastName \E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB., Users, ad.の様に表示されている部分はデコードすると「イニシャル」になる

  • bytes.fromhex(r"\E3\82\A4\E3\83\8B\E3\82\B7\E3\83\A3\E3\83\AB".replace("\\", "")).decode('utf-8')

単一項目で絞って検索する

  • objectclassがpersonの物を検索
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "objectclass=person"

複数項目(AND)で絞って検索する

  • objectclassがperson and sAMAccountNameがtest-user
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "(&(objectclass=person)(sAMAccountName=test-user))"

複数項目(OR)で絞って検索

  • UserPrincipalNameがtest-user or UserPrincipalNameがtest-user2のものを検索
$ ldapsearch -h 192.168.0.155 -D "test-user@ad.example.com" -w "PASSWORD" -b "cn=Users,dc=ad,dc=example,dc=com" "(|(UserPrincipalName=test-user@ad.example.com)(UserPrincipalName=test-user2@ad.example.com))"