Comment valider les identifiants de domaine?

86

Je souhaite valider un ensemble d'informations d'identification sur le contrôleur de domaine. par exemple:

Username: STACKOVERFLOW\joel
Password: splotchy

Méthode 1. Interroger Active Directory avec l'emprunt d'identité

Beaucoup de gens suggèrent d'interroger Active Directory pour quelque chose. Si une exception est levée, vous savez que les informations d'identification ne sont pas valides - comme suggéré dans cette question de stackoverflow .

Cette approche présente cependant de sérieux inconvénients :

  1. Vous authentifiez non seulement un compte de domaine, mais vous effectuez également une vérification d'autorisation implicite. Autrement dit, vous lisez les propriétés de l'AD à l'aide d'un jeton d'emprunt d'identité. Que faire si le compte par ailleurs valide n'a aucun droit de lecture à partir de l'AD? Par défaut, tous les utilisateurs ont un accès en lecture, mais les stratégies de domaine peuvent être définies pour désactiver les autorisations d'accès pour les comptes restreints (et / ou les groupes).

  2. La liaison avec l'AD a une surcharge importante, le cache de schéma AD doit être chargé au niveau du client (cache ADSI dans le fournisseur ADSI utilisé par DirectoryServices). C'est à la fois le réseau et le serveur AD, qui consomme des ressources - et est trop cher pour une opération simple comme l'authentification d'un compte utilisateur.

  3. Vous comptez sur un échec d'exception pour un cas non exceptionnel, et supposez que cela signifie un nom d'utilisateur et un mot de passe invalides. D'autres problèmes (par exemple une panne de réseau, une panne de connectivité AD, une erreur d'allocation de mémoire, etc.) sont alors interprétés à tort comme un échec d'authentification.

Méthode 2. API LogonUser Win32

D'autres ont suggéré d'utiliser la LogonUser()fonction API. Cela semble bien, mais malheureusement, l'utilisateur appelant a parfois besoin d'une autorisation généralement donnée uniquement au système d'exploitation lui-même:

Le processus appelant LogonUser requiert le privilège SE_TCB_NAME. Si le processus appelant ne dispose pas de ce privilège, LogonUser échoue et GetLastError renvoie ERROR_PRIVILEGE_NOT_HELD.

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

Attribuer le privilège « Agir en tant que partie du système d'exploitation » n'est pas quelque chose que vous voulez faire bon gré mal gré - comme Microsoft le souligne dans un article de la base de connaissances :

... le processus qui appelle LogonUser doit avoir le privilège SE_TCB_NAME (dans le Gestionnaire des utilisateurs, il s'agit du droit « Agir en tant que partie du système d'exploitation »). Le privilège SE_TCB_NAME est très puissant et ne doit être accordé à aucun utilisateur arbitraire uniquement pour qu'il puisse exécuter une application qui doit valider les informations d'identification.

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


Quelle est la bonne façon d'authentifier un ensemble d'informations d'identification de domaine?


Il se trouve que j'appelle à partir de code managé, mais c'est une question Windows générale. On peut supposer que les clients ont installé .NET Framework 2.0.

Ian Boyd
la source
1
Les lecteurs doivent noter qu'à partir de Windows XP, LogonUser n'a plus besoin de SE_TCB_NAME (sauf si vous vous connectez à un compte Passport).
Harry Johnston

Réponses:

130

C # dans .NET 3.5 à l'aide de System.DirectoryServices.AccountManagement .

 bool valid = false;
 using (PrincipalContext context = new PrincipalContext(ContextType.Domain))
 {
     valid = context.ValidateCredentials( username, password );
 }

Cela validera par rapport au domaine actuel. Consultez le constructeur PrincipalContext paramétré pour d'autres options.

Tvanfosson
la source
@tvanfosson: DirectoryServices n'utilise-t-il pas AD?
Mitch Wheat
1
Oui. Mais la documentation indique qu'il s'agit d'un moyen rapide de valider les informations d'identification. Elle est également différente de la méthode de liaison mentionnée dans la question car vous ne lisez aucune propriété de l'objet. Notez que la méthode est sur le contexte, pas sur un objet d'annuaire.
tvanfosson
Correction: System.DirectoryServices.AccountManagement nécessite .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ian Boyd
19
Il fonctionne également avec les utilisateurs locaux si vous avez utilisé à la new PrincipalContext(ContextType.Machine)place.
VansFannel
Est-ce que quelqu'un sait si cela fonctionne sur les informations d'identification mises en cache ou nécessite-t-il une connexion au contrôleur de domaine? J'ai besoin de savoir ceci pour une implémentation sur laquelle je travaille maintenant et je ne suis actuellement sur aucun domaine à tester
Jcl
21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.DirectoryServices.AccountManagement;

public struct Credentials
{
    public string Username;
    public string Password;
}

public class Domain_Authentication
{
    public Credentials Credentials;
    public string Domain;

    public Domain_Authentication(string Username, string Password, string SDomain)
    {
        Credentials.Username = Username;
        Credentials.Password = Password;
        Domain = SDomain;
    }

    public bool IsValid()
    {
        using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, Domain))
        {
            // validate the credentials
            return pc.ValidateCredentials(Credentials.Username, Credentials.Password);
        }
    }
}
kantanomo
la source
7
cela contient-il une différence significative par rapport à la réponse de @ tvanfosson 3 ans plus tôt?
gbjbaanb
5
@gbjbaanb Oui, car il contient le Domainparamètre lors de la création du PrincipalContext, quelque chose que je voulais savoir et que j'ai trouvé dans cette réponse.
Rudi Visser
1
@RudiVisser tvanfosson vous a suggéré "Consultez le constructeur Paramétré PrincipalContext pour d'autres options" - lisez toujours la documentation, ne prenez jamais le mot d'Internet pour quoi que ce soit! :)
gbjbaanb
4
@gbjbaanb Oui bien sûr, mais fournit un exemple de travail plutôt qu'un lien et suggestion de lire ailleurs est le mantra StackOverflow, c'est pourquoi nous acceptons les soumissions multiples de réponses: D Il suffit de dire que cela ne fournit plus.
Rudi Visser le
Est-ce que quelqu'un sait comment nous pourrions faire quelque chose de similaire dans une application UWP? (avec AD standard et non avec Azure AD). J'ai posé une question ici: stackoverflow.com/questions/42821447
slayernoah
7

J'utilise le code suivant pour valider les informations d'identification. La méthode indiquée ci-dessous confirmera si les informations d'identification sont correctes et si ce n'est pas le cas, le mot de passe a expiré ou doit être modifié.

Je cherche quelque chose comme ça depuis des lustres ... J'espère donc que cela aide quelqu'un!

using System;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Runtime.InteropServices;

namespace User
{
    public static class UserValidation
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool LogonUser(string principal, string authority, string password, LogonTypes logonType, LogonProviders logonProvider, out IntPtr token);
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr handle);
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }
        enum LogonTypes : uint
        {
            Interactive = 2,
            Network = 3,
            Batch = 4,
            Service = 5,
            Unlock = 7,
            NetworkCleartext = 8,
            NewCredentials = 9
        }
        public  const int ERROR_PASSWORD_MUST_CHANGE = 1907;
        public  const int ERROR_LOGON_FAILURE = 1326;
        public  const int ERROR_ACCOUNT_RESTRICTION = 1327;
        public  const int ERROR_ACCOUNT_DISABLED = 1331;
        public  const int ERROR_INVALID_LOGON_HOURS = 1328;
        public  const int ERROR_NO_LOGON_SERVERS = 1311;
        public  const int ERROR_INVALID_WORKSTATION = 1329;
        public  const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
        public  const int ERROR_ACCOUNT_EXPIRED = 1793;
        public  const int ERROR_PASSWORD_EXPIRED = 1330;

        public static int CheckUserLogon(string username, string password, string domain_fqdn)
        {
            int errorCode = 0;
            using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain_fqdn, "ADMIN_USER", "PASSWORD"))
            {
                if (!pc.ValidateCredentials(username, password))
                {
                    IntPtr token = new IntPtr();
                    try
                    {
                        if (!LogonUser(username, domain_fqdn, password, LogonTypes.Network, LogonProviders.Default, out token))
                        {
                            errorCode = Marshal.GetLastWin32Error();
                        }
                    }
                    catch (Exception)
                    {
                        throw;
                    }
                    finally
                    {
                        CloseHandle(token);
                    }
                }
            }
            return errorCode;
        }
    }
Kevinrr3
la source
c'est la "méthode 2" décrite dans la question ... donc ... pas vraiment de réponse à la question
Robert Levy
1

Voici comment déterminer un utilisateur local:

    public bool IsLocalUser()
    {
        return windowsIdentity.AuthenticationType == "NTLM";
    }

Édité par Ian Boyd

Vous ne devriez plus du tout utiliser NTLM. Il est si ancien et si mauvais que le vérificateur d'application de Microsoft (qui est utilisé pour détecter les erreurs de programmation courantes) lancera un avertissement s'il vous détecte en utilisant NTLM.

Voici un chapitre de la documentation Application Verifier expliquant pourquoi ils ont un test si quelqu'un utilise par erreur NTLM:

Pourquoi le plug-in NTLM est nécessaire

NTLM est un protocole d'authentification obsolète avec des failles susceptibles de compromettre la sécurité des applications et du système d'exploitation. Le défaut le plus important est le manque d'authentification du serveur, qui pourrait permettre à un attaquant de tromper les utilisateurs pour qu'ils se connectent à un serveur usurpé. Comme corollaire d'une authentification de serveur manquante, les applications utilisant NTLM peuvent également être vulnérables à un type d'attaque appelé attaque «par réflexion». Ce dernier permet à un attaquant de détourner la conversation d'authentification d'un utilisateur vers un serveur légitime et de l'utiliser pour authentifier l'attaquant sur l'ordinateur de l'utilisateur. Les vulnérabilités de NTLM et les moyens de les exploiter sont la cible d'une activité de recherche croissante dans la communauté de la sécurité.

Bien que Kerberos soit disponible depuis de nombreuses années, de nombreuses applications sont encore écrites pour n'utiliser que NTLM. Cela réduit inutilement la sécurité des applications. Kerberos ne peut cependant pas remplacer NTLM dans tous les scénarios - principalement ceux où un client a besoin de s'authentifier auprès de systèmes qui ne sont pas joints à un domaine (un réseau domestique étant peut-être le plus courant d'entre eux). Le package de sécurité Negotiate permet un compromis rétrocompatible qui utilise Kerberos chaque fois que possible et revient uniquement à NTLM lorsqu'il n'y a pas d'autre option. Changer de code pour utiliser Negotiate au lieu de NTLM augmentera considérablement la sécurité de nos clients tout en introduisant peu ou pas de compatibilités d'application. Négocier en soi n'est pas une solution miracle - il existe des cas où un attaquant peut forcer une rétrogradation vers NTLM, mais ceux-ci sont beaucoup plus difficiles à exploiter. Cependant, une amélioration immédiate est que les applications écrites pour utiliser correctement Negotiate sont automatiquement immunisées contre les attaques par réflexion NTLM.

En guise d'avertissement final contre l'utilisation de NTLM: dans les futures versions de Windows, il sera possible de désactiver l'utilisation de NTLM au niveau du système d'exploitation. Si les applications ont une forte dépendance à NTLM, elles échoueront tout simplement à s'authentifier lorsque NTLM est désactivé.

Fonctionnement du plug-in

La prise Verifier détecte les erreurs suivantes:

  • Le package NTLM est directement spécifié dans l'appel à AcquireCredentialsHandle (ou à l'API wrapper de niveau supérieur).

  • Le nom cible dans l'appel à InitializeSecurityContext est NULL.

  • Le nom cible dans l'appel à InitializeSecurityContext n'est pas un nom de domaine de style SPN, UPN ou NetBIOS correctement formé.

Les deux derniers cas forceront Negotiate à revenir à NTLM soit directement (le premier cas), soit indirectement (le contrôleur de domaine renverra une erreur «principal non trouvé» dans le second cas provoquant le repli de Negotiate).

Le plug-in enregistre également des avertissements lorsqu'il détecte des rétrogradations vers NTLM; par exemple, lorsqu'un SPN n'est pas trouvé par le contrôleur de domaine. Celles-ci ne sont consignées que comme des avertissements car il s'agit souvent de cas légitimes - par exemple, lors de l'authentification auprès d'un système qui n'est pas joint au domaine.

Arrêts NTLM

5000 - L'application a explicitement sélectionné le package NTLM

Gravité - Erreur

L'application ou le sous-système sélectionne explicitement NTLM au lieu de Negotiate dans l'appel à AcquireCredentialsHandle. Même s'il est possible pour le client et le serveur de s'authentifier à l'aide de Kerberos, cela est empêché par la sélection explicite de NTLM.

Comment réparer cette erreur

Le correctif pour cette erreur consiste à sélectionner le package Negotiate à la place de NTLM. La manière de procéder dépendra du sous-système de réseau particulier utilisé par le client ou le serveur. Quelques exemples sont donnés ci-dessous. Vous devriez consulter la documentation sur la bibliothèque ou l'ensemble d'API que vous utilisez.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”
Alan Nicholas
la source
-1
using System;
using System.Collections.Generic;
using System.Text;
using System.DirectoryServices.AccountManagement;

class WindowsCred
{
    private const string SPLIT_1 = "\\";

    public static bool ValidateW(string UserName, string Password)
    {
        bool valid = false;
        string Domain = "";

        if (UserName.IndexOf("\\") != -1)
        {
            string[] arrT = UserName.Split(SPLIT_1[0]);
            Domain = arrT[0];
            UserName = arrT[1];
        }

        if (Domain.Length == 0)
        {
            Domain = System.Environment.MachineName;
        }

        using (PrincipalContext context = new PrincipalContext(ContextType.Domain, Domain)) 
        {
            valid = context.ValidateCredentials(UserName, Password);
        }

        return valid;
    }
}

Kashif Mushtaq Ottawa, Canada

Markus Safar
la source
L'espace de noms System.DirectoryServices.AccountManagement était nouveau dans .NET 3.5
Jeremy Gray
1
Je sais que cela a presque 4 ans, mais si vous validez un utilisateur local, vous devrez vous assurer que vous définissez le ContextType sur ContextType.Machine lorsque vous construisez un PrincipalContext. Sinon, il pensera que le nom de la machine fourni dans la variable Domain est en fait un serveur de domaine.
SolidRegardless