Win32: comment valider les justificatifs d'identité contre Active Directory?

Il a été demande et répondu .NET, mais maintenant il est temps pour obtenir une réponse natif Win32 code:

comment valider un nom d'utilisateur et un mot de passe Windows?

i a posé cette question avant pour le code géré. Maintenant, c'est l'Heure de la solution autochtone.


Il faut souligner les écueils les plus fréquemment proposé des solutions:

non Valide Méthode 1. La requête Active Directory avec l'emprunt d'identité

beaucoup de gens suggèrent l'interrogation de l'annuaire Active Directory pour quelque chose. Si une exception est lancée, alors vous savez que les justificatifs d'identité ne sont pas valides - comme suggéré dans c'stackoverflow question.

Il y a peu de sérieux inconvénients à cette approche par contre:

  • vous n'êtes pas seulement en train d'authentifier un compte de domaine, je fais aussi une vérification d'autorisation implicite. C'est-à-dire que vous lisez les propriétés de la publicité en utilisant un jeton d'imitation. Que se passe-t-il si le compte autrement valide n'a aucun droit de lecture de L'annonce? Par défaut, tous les utilisateurs ont accès en lecture, mais les politiques de domaine peuvent être paramétrées pour désactiver les permissions d'accès pour les comptes (et / ou groupes) restreints.

  • reliure contre la publicité a un grave overhead, le cache du schéma publicitaire doit être chargé au client (cache ADSI dans L'ADSI fournisseur utilisé par DirectoryServices). Il s'agit à la fois d'un réseau, et D'un serveur publicitaire, consommant des ressources - et est trop coûteux pour une opération simple comme l'authentification d'un compte d'utilisateur.

  • vous comptez sur un échec d'exception pour un cas non exceptionnel, et en supposant que cela signifie un nom d'utilisateur et un mot de passe invalides. D'autres problèmes (par exemple, défaillance du réseau, défaillance de la connectivité publicitaire, erreur d'allocation de mémoire, etc.) sont alors mal introduits en tant qu'échec d'authentification.

l'utilisation du DirectoryEntry classe est .NET est un exemple d'une mauvaise façon de vérifier les informations d'identification:

non valide méthode 1a-.NET

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

non valide méthode 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;
                }
            }
        }
    }

ainsi que l'interrogation Active Directory via une connexion ADO:

Méthode non Valide 1c - Requête Native

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'

ceux-ci échouent tous les deux même lorsque votre les informations d'identification sont valide, mais vous n'avez pas la permission de voir votre entrée de répertoire:

enter image description here

Méthode Invalide 2. Win32 LogonUser API

Autres ont suggéré d'utiliser l' LogonUser () fonction API. Cela sonne bien, mais malheureusement l'utilisateur appelant parfois besoin d'une autorisation en général uniquement pour le système d'exploitation lui-même:

le processus appelant LogonUser nécessite le privilège SE_TCB_NAME. Si l' processus appelant n'a pas cette privilège, LogonUser échoue et Retour de GetLastError ERROR_PRIVILEGE_NOT_HELD.

Dans certains cas, le processus qui appelle LogonUser doit également avoir le Se_change_notify_name privilège activé; sinon, LogonUser échoue et GetLastError retourne ERROR_ACCESS_DENIED. Ce privilège est pas nécessaire pour le système local compte ou comptes membres des administrateurs groupe. Par par défaut, SE_CHANGE_NOTIFY_NAME est activé pour tous les utilisateurs, mais certains les administrateurs peuvent désactiver pour chacun.

distribuer le "Agir comme une partie du système d'exploitation" privelage n'est pas quelque chose que vous voulez faire de bon gré mal gré - que Microsoft points dans un article de la base de connaissances:

...le processus appelant LogonUser doit avoir le nom de SE_TCB_NAME le privilège (dans le Gestionnaire D'utilisateur, c'est le "<!-- 93 - Système" à droite). Le SE_TCB_NAME le privilège est très puissant et ne doit pas être accordé à un utilisateur arbitraire juste pour qu'ils puissent lancer une application qui doit valider les références.

de plus, un appel à LogonUser() échouera si un mot de passe vide est spécifié.


Valid .NET 3.5 Method -PrincipalContext

il y a une méthode de validation, seulement disponible en .net 3.5 et versions plus récentes, qui permet l'authentification par un utilisateur sans effectuer de vérification d'autorisation:

// 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")
}

malheureusement ce code n'est disponible qu'à partir de .net 3.5.

Il est temps de trouver le maternelle équivalent.

16
demandé sur Community 2011-08-18 21:33:21

5 réponses

Voici la recommandation de Microsoft.

pour ce qui est des autres réponses, Je ne sais pas vraiment pourquoi vous les abattez. Vous vous plaignez des échecs (cas relativement limite) tout en essayant de valider les justificatifs d'identité, mais si vous allez réellement faire quelque chose avec ces justificatifs d'identité alors cette opération va juste échouer de toute façon. Si vous n'allez pas réellement faire quelque chose avec ces informations d'identification, alors pourquoi avez-vous besoin de les valider dans le premier la place? Il semble un peu artificiel de la situation, mais évidemment je ne sais pas ce que vous essayez d'accomplir.

9
répondu Luke 2011-08-18 21:34:06

Pour les natifs equivalnt de votre pièce .NET solution reportez-vous à MSDN page and ldap_bind

Mais je pense que LogonUser c'est le droit de l'API pour la tâche lors de l'utilisation avec LOGON32_LOGON_NETWORK. Notez que la limitation de SE_CHANGE_NOTIFY_NAME est seulement pour Windows 2000 (donc Windows XP et les versions plus récentes ne requièrent pas ce privilège) et que par défaut SE_CHANGE_NOTIFY_NAME est activé pour tous les utilisateurs. La page MSDN dit aussi

le privilège SE_TCB_NAME est cette fonction n'est pas requise à moins que vous ne vous connectiez à un compte Passeport.

dans ce cas, vous ouvrez une session sur un compte AD, donc SE_TCB_NAME n'est pas nécessaire.

4
répondu John 2011-08-18 18:57:43

je pourrais aussi bien mettre le code natif pour valider un ensemble d'informations d'identification Windows. Il a fallu un certain temps à mettre en œuvre.

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;

Remarque:: tout code publié dans le domaine public. Aucune attribution n'est requise.

2
répondu Ian Boyd 2016-01-15 01:53:45

il existe une fonction API win32 appelée ldap_bind_s. La fonction ldap_bind_s authentifie un client contre LDAP. Voir MSDN documentation pour plus d'information.

1
répondu Hans 2011-08-18 17:44:27

j'ai authentifié l'utilisateur , par nom d'utilisateur et mot de passe comme ceci:

nom d'utilisateur est la valeur de l'attribut SN de l'utilisateur dans le serveur Ldap, comme U12345

userDN est l'utilisateur DistinguishedName dans LdapServer

public bool AuthenticateUser(string username, string password)
{
try
{
var ldapServerNameAndPort = "Servername:389";
var userDN = string.Format("CN=0},OU=Users,OU=MyOU,DC=MyDC,DC=com",username);
var conn = new LdapConnection(ldapServerNameAndPort)
{
 AuthType = AuthType.Basic
};
conn.Bind(new NetworkCredential(userDN , password));
return true;
}
catch (Exception e)
{
 return false;
}

}

-1
répondu nahidf 2014-04-04 17:56:07