web-dev-qa-db-ja.com

Win32:Active Directoryに対して資格情報を検証する方法は?

質問 、および 。NETで回答 でしたが、今度はネイティブWin32コードの回答を取得します。

Windowsのユーザー名とパスワードを検証するにはどうすればよいですか?

i マネージコードについて前にこの質問をしました 。さて、ネイティブソリューションの時間です。


より一般的に提案されている解決策のいくつかの落とし穴を指摘する必要があります。

無効な方法1.偽装を使用してActiveDirectoryをクエリする

多くの人 Active Directoryにクエリを実行することをお勧めします 何かを探します。例外がスローされた場合は、資格情報が無効であることがわかります このスタックオーバーフローの質問 で提案されているように。

このアプローチにはいくつかの重大な欠点があります ただし:

  • ドメインアカウントを認証するだけでなく、暗黙的な承認チェックも実行します。つまり、偽装トークンを使用してADからプロパティを読み取っています。それ以外の場合は有効なアカウントにADから読み取る権限がない場合はどうなりますか?デフォルトでは、すべてのユーザーが読み取りアクセス権を持っていますが、ドメインポリシーを設定して、制限されたアカウント(またはグループ)のアクセス許可を無効にすることができます。

  • ADに対するバインドには重大なオーバーヘッドがあり、ADスキーマキャッシュをクライアントでロードする必要があります(DirectoryServicesが使用するADSIプロバイダーのADSIキャッシュ)。これはネットワークとADサーバーの両方であり、リソースを消費します。また、ユーザーアカウントの認証などの単純な操作にはコストがかかりすぎます。

  • 例外ではない場合の例外の失敗に依存しており、それが無効なユーザー名とパスワードを意味すると想定しています。その他の問題(ネットワーク障害、AD接続障害、メモリ割り当てエラーなど)は、認証障害として誤って解釈されます。

DirectoryEntry クラスの使用は.NETであり、資格情報を検証する誤った方法の例です。

無効なメソッド1a-.NET

DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;

無効なメソッド1b-.NET#2

public static Boolean CheckADUserCredentials(String accountName, String password, String domain)
{
    Boolean result;

    using (DirectoryEntry entry = new DirectoryEntry("LDAP://" + domain, accountName, password))
    {
        using (DirectorySearcher searcher = new DirectorySearcher(entry))
        {
            String filter = String.Format("(&(objectCategory=user)(sAMAccountName={0}))", accountName);
            searcher.Filter = filter;
            try
            {
                SearchResult adsSearchResult = searcher.FindOne();
                result = true;
            }
            catch (DirectoryServicesCOMException ex)
            {
                const int SEC_E_LOGON_DENIED = -2146893044; //0x8009030C;
                if (ex.ExtendedError == SEC_E_LOGON_DENIED)
                {
                    // Failed to authenticate. 
                    result = false;
                }
                else
                {
                    throw;
                }
            }
        }
    }

ADO接続を介してActiveDirectoryにクエリを実行するだけでなく:

無効なメソッド1c-ネイティブクエリ

connectionString = "Provider=ADsDSOObject;
       User ID=iboyd;Password=Tr0ub4dor&3;
       Encrypt Password=True;Mode=Read;
       Bind Flags=0;ADSI Flag=-2147483648';"

SELECT userAccountControl 
FROM 'LDAP://persuis/DC=stackoverflow,DC=com'
WHERE objectClass='user' and sAMAccountName = 'iboyd'

資格情報がvalidの場合でも、これらは両方とも失敗しますが、ディレクトリエントリを表示する権限がありません。

enter image description here

無効なメソッド2。LogonUserWin32API

OthersLogonUser() API関数の使用を提案しています。これは良さそうに聞こえますが、残念ながら、呼び出し元のユーザーは、通常、オペレーティングシステム自体にのみ与えられる許可を必要とする場合があります。

LogonUserを呼び出すプロセスには、SE_TCB_NAME特権が必要です。呼び出しプロセスにこの特権がない場合、LogonUserは失敗し、GetLastErrorはERROR_PRIVILEGE_NOT_HELDを返します。

場合によっては、LogonUserを呼び出すプロセスで、SE_CHANGE_NOTIFY_NAME特権も有効にする必要があります。それ以外の場合、LogonUserは失敗し、GetLastErrorはERROR_ACCESS_DENIEDを返します。この特権は、ローカルシステムアカウントまたはadministratorsグループのメンバーであるアカウントには必要ありません。デフォルトでは、SE_CHANGE_NOTIFY_NAMEはすべてのユーザーに対して有効になっていますが、一部の管理者はすべてのユーザーに対して無効にすることができます。

オペレーティングシステムの一部として機能する」特権を配布することは、Microsoftが ナレッジベースの記事で指摘しているように)意地悪にやりたいことではありません。

... LogonUserを呼び出すプロセスには、SE_TCB_NAME特権が必要です(ユーザーマネージャーでは、これは「オペレーティングシステムの一部として機能する」権限です)。 SE_TCB_NAME特権は非常に強力であり、アプリケーションを実行できるようにするためだけに任意のユーザーに付与しないでください資格情報を検証する必要があります。

さらに、空白のパスワードが指定されている場合、LogonUser()の呼び出しは失敗します。


有効な.NET3.5メソッド-PrincipalContext

.NET 3.5以降でのみ使用可能な検証方法があり、承認チェックを実行せずにユーザーによる認証を可能にします。

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "stackoverflow.com"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("iboyd", "Tr0ub4dor&3")
}

残念ながら、このコードは.NET3.5以降でのみ使用できます。

ネイティブに相当するものを見つける時が来ました。

16
Ian Boyd

これがMicrosoftの推奨事項です

他の答えについては、なぜあなたがそれらを撃墜しているのかよくわかりません。資格情報を検証しようとしているときに(比較的エッジケースの)失敗について不平を言っていますが、実際にそれらの資格情報を使用して何かを行う場合、その操作はとにかく失敗します。これらの資格情報を使用して実際に何かを行う予定がない場合、そもそもなぜそれらを検証する必要があるのでしょうか。やや不自然な状況のようですが、明らかにあなたが何を達成しようとしているのかわかりません。

9
Luke

有効な.NETソリューションのネイティブの同等物については、 this MSDNページおよびldap_bindを参照してください。

ただし、LogonUserは、LOGON32_LOGON_NETWORKとともに使用する場合のタスクに適したAPIだと思います。 SE_CHANGE_NOTIFY_NAMEの制限はWindows2000のみであり(したがって、Windows XP以降はこの特権を必要としません)、デフォルトでSE_CHANGE_NOTIFY_NAMEがすべてのユーザーに対して有効になっていることに注意してください。MSDNもページは言う

Passportアカウントにログオンしている場合を除き、この関数にはSE_TCB_NAME特権は必要ありません。

この場合、ADアカウントにログオンしているため、SE_TCB_NAMEは必要ありません。

4
John

ネイティブコードを投稿して、一連のWindows資格情報を検証することもできます。実装には時間がかかりました。

function TSSPLogon.LogonUser(username, password, domain: string; packageName: string='Negotiate'): HRESULT;
var
    ss: SECURITY_STATUS;
    packageInfo: PSecPkgInfoA;
    cbMaxToken: DWORD;
    clientBuf: PByte;
    serverBuf: PByte;
    authIdentity: SEC_WINNT_AUTH_IDENTITY;
    cbOut, cbIn: DWORD;
    asClient: AUTH_SEQ;
    asServer: AUTH_SEQ;
    Done: boolean;
begin
{
    If domain is blank will use the current domain.
    To force validation against the local database use domain "."

    sspiProviderName is the same of the Security Support Provider Package to use. Some possible choices are:
            - Negotiate (Preferred)
                        Introduced in Windows 2000 (secur32.dll)
                        Selects Kerberos and if not available, NTLM protocol.
                        Negotiate SSP provides single sign-on capability called as Integrated Windows Authentication.
                        On Windows 7 and later, NEGOExts is introduced which negotiates the use of installed
                        custom SSPs which are supported on the client and server for authentication.
            - Kerberos
                        Introduced in Windows 2000 and updated in Windows Vista to support AES) (secur32.dll)
                        Preferred for mutual client-server domain authentication in Windows 2000 and later.
            - NTLM
                        Introduced in Windows NT 3.51 (Msv1_0.dll)
                        Provides NTLM challenge/response authentication for client-server domains prior to
                        Windows 2000 and for non-domain authentication (SMB/CIFS)
            - Digest
                        Introduced in Windows XP (wdigest.dll)
                        Provides challenge/response based HTTP and SASL authentication between Windows and non-Windows systems where Kerberos is not available
            - CredSSP
                        Introduced in Windows Vista and available on Windows XP SP3 (credssp.dll)
                        Provides SSO and Network Level Authentication for Remote Desktop Services
            - Schannel
                        Introduced in Windows 2000 and updated in Windows Vista to support stronger AES encryption and ECC (schannel.dll)
                        Microsoft's implementation of TLS/SSL
                        Public key cryptography SSP that provides encryption and secure communication for
                        authenticating clients and servers over the internet. Updated in Windows 7 to support TLS 1.2.

    If returns false, you can call GetLastError to get the reason for the failure
}


    // Get the maximum authentication token size for this package
    ss := sspi.QuerySecurityPackageInfoA(PAnsiChar(packageName), packageInfo);
    if ss <> SEC_E_OK then
    begin
        RaiseWin32Error('QuerySecurityPackageInfo "'+PackageName+'" failed', ss);
        Result := ss;
        Exit;
    end;

    try
        cbMaxToken := packageInfo.cbMaxToken;
    finally
        FreeContextBuffer(packageInfo);
    end;

    // Initialize authorization identity structure
    ZeroMemory(@authIdentity, SizeOf(authIdentity));
    if Length(domain) > 0 then
    begin
        authIdentity.Domain := PChar(Domain);
        authIdentity.DomainLength := Length(domain);
    end;

    if Length(userName) > 0 then
    begin
        authIdentity.User := PChar(UserName);
        authIdentity.UserLength := Length(UserName);
    end;

    if Length(Password) > 0 then
    begin
        authIdentity.Password := PChar(Password);
        authIdentity.PasswordLength := Length(Password);
    end;

    AuthIdentity.Flags := SEC_WINNT_AUTH_IDENTITY_ANSI; //SEC_WINNT_AUTH_IDENTITY_UNICODE

    ZeroMemory(@asClient, SizeOf(asClient));
    ZeroMemory(@asServer, SizeOf(asServer));

    //Allocate buffers for client and server messages
    GetMem(clientBuf, cbMaxToken);
    GetMem(serverBuf, cbMaxToken);
    try
        done := False;
        try
            // Prepare client message (negotiate)
            cbOut := cbMaxToken;
            ss := Self.GenClientContext(@asClient, authIdentity, packageName, nil, 0, clientBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating client context for negotiate', ss);
                Result := ss;
                Exit;
            end;

            // Prepare server message (challenge).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
            if ss < 0 then
            begin
                {
                    Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED in the case of bad username or password.
                    Unexpected Result:   Logon will succeed if you pass in a bad username and the guest account is enabled in the specified domain.
                }
                RaiseWin32Error('Error generating server message for challenge', ss);
                Result := ss;
                Exit;
            end;

            // Prepare client message (authenticate).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenClientContext(@asClient, authIdentity, packageName, serverBuf, cbIn, clientBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating client client for authenticate', ss);
                Result := ss;
                Exit;
            end;

            // Prepare server message (authentication).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating server message for authentication', ss);
                Result := ss;
                Exit;
            end;
        finally
            //Free resources in client message
            if asClient.fHaveCtxtHandle then
                sspi.DeleteSecurityContext(@asClient.hctxt);

            if asClient.fHaveCredHandle then
                sspi.FreeCredentialHandle(@asClient.hcred);

            //Free resources in server message
            if asServer.fHaveCtxtHandle then
                sspi.DeleteSecurityContext(@asServer.hctxt);

            if asServer.fHaveCredHandle then
                sspi.FreeCredentialHandle(@asServer.hcred);
        end;
    finally
        FreeMem(clientBuf);
        FreeMem(serverBuf);
    end;

    Result := S_OK;
end;

:パブリックドメインにリリースされたコード。帰属は必要ありません。

2
Ian Boyd

Ldap_bind_sと呼ばれるwin32API関数があります。 ldap_bind_s関数は、LDAPに対してクライアントを認証します。詳細については、 [〜#〜] msdn [〜#〜] のドキュメントを参照してください。

1
Hans