Active Directory + python3でユーザー認証を行う
環境
- Windows Server 2019(評価版)
- Windows 10 Enterprise(評価版)
Active Directoryの動作確認用にドメイン参加させてみるのに利用
構築した環境
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
- エラー一覧は以下が参考になる
Ldapsearchエラー コード
接続法 その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を検索
- ldapsearchで利用できる条件式
まずは全検索
- 以下のコマンドを実行するとActive Directoryに登録されているユーザー全てが出力される
- Active Directory側の制限で1000件までしか返してくれない
Active Directory で LDAP ポリシーを表示および設定するには、次のNtdsutil.exe
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))"