Come convalidare le credenziali di dominio?


87

Voglio convalidare una serie di credenziali rispetto al controller di dominio. per esempio:

Username: STACKOVERFLOW\joel
Password: splotchy

Metodo 1. Query Active Directory con rappresentazione

Molte persone suggeriscono di interrogare Active Directory per qualcosa. Se viene generata un'eccezione, allora sai che le credenziali non sono valide, come suggerito in questa domanda su stackoverflow .

Tuttavia, questo approccio presenta alcuni seri inconvenienti :

  1. Non stai solo autenticando un account di dominio, ma stai anche eseguendo un controllo di autorizzazione implicito. Cioè, stai leggendo le proprietà dall'ANNUNCIO utilizzando un token di rappresentazione. Cosa succede se l'account altrimenti valido non ha il diritto di leggere dall'AD? Per impostazione predefinita, tutti gli utenti hanno accesso in lettura, ma i criteri di dominio possono essere impostati per disabilitare le autorizzazioni di accesso per account (e / o gruppi) limitati.

  2. Il binding con AD ha un sovraccarico serio, la cache dello schema AD deve essere caricata sul client (cache ADSI nel provider ADSI utilizzato da DirectoryServices). Questa è sia rete che server AD, consuma risorse ed è troppo costosa per un'operazione semplice come l'autenticazione di un account utente.

  3. Stai facendo affidamento su un errore di eccezione per un caso non eccezionale e presumendo che ciò significhi nome utente e password non validi. Altri problemi (ad es. Errore di rete, errore di connettività AD, errore di allocazione della memoria, ecc.) Vengono quindi interpretati erroneamente come errore di autenticazione.

Metodo 2. LogonUser Win32 API

Altri hanno suggerito di utilizzare la LogonUser()funzione API. Sembra carino, ma sfortunatamente l'utente chiamante a volte ha bisogno di un'autorizzazione solitamente data solo al sistema operativo stesso:

Il processo che chiama LogonUser richiede il privilegio SE_TCB_NAME. Se il processo chiamante non dispone di questo privilegio, LogonUser non riesce e GetLastError restituisce ERROR_PRIVILEGE_NOT_HELD.

In alcuni casi, il processo che chiama LogonUser deve anche avere il privilegio SE_CHANGE_NOTIFY_NAME abilitato; in caso contrario, LogonUser non riesce e GetLastError restituisce ERROR_ACCESS_DENIED. Questo privilegio non è richiesto per l'account di sistema locale o per gli account membri del gruppo di amministratori. Per impostazione predefinita, SE_CHANGE_NOTIFY_NAME è abilitato per tutti gli utenti, ma alcuni amministratori potrebbero disabilitarlo per tutti.

Distribuire il privilegio " Agisci come parte del sistema operativo " non è qualcosa che vuoi fare volenti o nolenti - come Microsoft sottolinea in un articolo della knowledge base :

... il processo che chiama LogonUser deve avere il privilegio SE_TCB_NAME (in User Manager, questo è il diritto " Agisci come parte del sistema operativo "). Il privilegio SE_TCB_NAME è molto potente e non dovrebbe essere concesso a nessun utente arbitrario solo per poter eseguire un'applicazione che deve convalidare le credenziali.

Inoltre, una chiamata a LogonUser()fallirà se viene specificata una password vuota.


Qual è il modo corretto per autenticare una serie di credenziali di dominio?


Mi capita di chiamare da codice gestito, ma questa è una domanda generale di Windows. Si può presumere che i clienti abbiano installato .NET Framework 2.0.


1
I lettori devono tenere presente che a partire da Windows XP, LogonUser non richiede più SE_TCB_NAME (a meno che non si acceda a un account Passport).
Harry Johnston

Risposte:


130

C # in .NET 3.5 utilizzando System.DirectoryServices.AccountManagement .

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

Questo convaliderà rispetto al dominio corrente. Controlla il costruttore PrincipalContext parametrizzato per altre opzioni.


@tvanfosson: DirectoryServices non usa AD?
Mitch Wheat

1
Sì. Ma la documentazione indica che questo è un modo veloce per convalidare le credenziali. È anche diverso dal metodo di associazione menzionato nella domanda poiché non stai leggendo alcuna proprietà dall'oggetto. Notare che il metodo si trova nel contesto, non in un oggetto directory.
tvanfosson

Correzione: System.DirectoryServices.AccountManagement richiede .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ian Boyd

19
Funziona anche con gli utenti locali se hai usato new PrincipalContext(ContextType.Machine)invece.
VansFannel

Qualcuno sa se funziona con le credenziali memorizzate nella cache o richiede la connessione al controller di dominio? Ho bisogno di saperlo per alcune implementazioni su cui sto lavorando ora e non sono attualmente su alcun dominio da testare
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);
        }
    }
}

7
questo contiene differenze significative rispetto alla risposta di @tvanfosson 3 anni prima?
gbjbaanb

5
@gbjbaanb Sì, poiché contiene il Domainparametro durante la creazione del PrincipalContext, qualcosa che mi interessava sapere e che ho trovato in questa risposta.
Rudi Visser

1
@RudiVisser tvanfosson ti ha suggerito di "Controlla il costruttore PrincipalContext parametrizzato per altre opzioni" - leggi sempre i documenti, non prendere mai solo la parola di Internet per niente! :)
gbjbaanb

4
@gbjbaanb Sì certo, ma fornendo un esempio di lavoro, piuttosto che un link e il suggerimento di leggere altrove è il mantra StackOverflow, è per questo che accettiamo presentazioni multiple di risposte: D Semplicemente dicendo che questo non fornire più.
Rudi Visser

Qualcuno sa come potremmo fare qualcosa di simile in un'app UWP? (con AD regolare e non con Azure AD). Ho chiesto una domanda qui: stackoverflow.com/questions/42821447
slayernoah

7

Sto usando il seguente codice per convalidare le credenziali. Il metodo mostrato di seguito confermerà se le credenziali sono corrette e in caso contrario se la password è scaduta o deve essere modificata.

Ho cercato qualcosa di simile per anni ... Quindi spero che questo aiuti qualcuno!

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;
        }
    }

questo è il "metodo 2" descritto nella domanda ... quindi ... non risponde veramente alla domanda
Robert Levy,

1

Ecco come determinare un utente locale:

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

Modifica di Ian Boyd

Non dovresti più usare NTLM. È così vecchio e così brutto che Application Verifier di Microsoft (che viene utilizzato per rilevare errori di programmazione comuni) lancerà un avviso se rileva che stai utilizzando NTLM.

Ecco un capitolo della documentazione di Application Verifier sul motivo per cui viene eseguito un test se qualcuno utilizza erroneamente NTLM:

Perché è necessario il plug-in NTLM

NTLM è un protocollo di autenticazione obsoleto con difetti che potenzialmente compromettono la sicurezza delle applicazioni e del sistema operativo. Il difetto più importante è la mancanza di autenticazione del server, che potrebbe consentire a un utente malintenzionato di indurre gli utenti a connettersi a un server contraffatto. Come corollario della mancanza dell'autenticazione del server, le applicazioni che utilizzano NTLM possono anche essere vulnerabili a un tipo di attacco noto come attacco "riflesso". Quest'ultimo consente a un utente malintenzionato di dirottare la conversazione di autenticazione di un utente su un server legittimo e utilizzarlo per autenticare l'attaccante sul computer dell'utente. Le vulnerabilità di NTLM e le modalità per sfruttarle sono l'obiettivo di aumentare l'attività di ricerca nella comunità della sicurezza.

Sebbene Kerberos sia disponibile da molti anni, molte applicazioni sono ancora scritte per utilizzare solo NTLM. Ciò riduce inutilmente la sicurezza delle applicazioni. Kerberos non può tuttavia sostituire NTLM in tutti gli scenari, principalmente quelli in cui un client deve autenticarsi a sistemi che non fanno parte di un dominio (una rete domestica forse è la più comune di queste). Il pacchetto di sicurezza Negotiate consente una compromissione compatibile con le versioni precedenti che utilizza Kerberos quando possibile e torna a NTLM solo quando non sono disponibili altre opzioni. Cambiare il codice per utilizzare Negotiate invece di NTLM aumenterà notevolmente la sicurezza per i nostri clienti, introducendo al contempo poche o nessuna compatibilità delle applicazioni. Negoziare di per sé non è un proiettile d'argento: ci sono casi in cui un utente malintenzionato può forzare il downgrade a NTLM, ma questi sono significativamente più difficili da sfruttare. Tuttavia, un miglioramento immediato è che le applicazioni scritte per utilizzare correttamente Negotiate sono automaticamente immuni agli attacchi di riflessione NTLM.

Un ultimo avvertimento contro l'utilizzo di NTLM: nelle future versioni di Windows sarà possibile disabilitare l'utilizzo di NTLM a livello del sistema operativo. Se le applicazioni hanno una forte dipendenza da NTLM, semplicemente non riusciranno ad autenticarsi quando NTLM è disabilitato.

Come funziona il plug-in

Il plug Verifier rileva i seguenti errori:

  • Il pacchetto NTLM viene specificato direttamente nella chiamata ad AcquireCredentialsHandle (o API wrapper di livello superiore).

  • Il nome di destinazione nella chiamata a InitializeSecurityContext è NULL.

  • Il nome di destinazione nella chiamata a InitializeSecurityContext non è un nome di dominio in stile SPN, UPN o NetBIOS formato correttamente.

Gli ultimi due casi costringeranno Negotiate a ripiegare su NTLM direttamente (il primo caso) o indirettamente (il controller di dominio restituirà un errore "principal not found" nel secondo caso causando il fallback di Negotiate).

Il plug-in registra anche gli avvisi quando rileva downgrade a NTLM; ad esempio, quando un SPN non viene trovato dal controller di dominio. Questi vengono registrati solo come avvisi poiché spesso sono casi legittimi, ad esempio durante l'autenticazione a un sistema che non fa parte di un dominio.

NTLM si ferma

5000 - L'applicazione ha selezionato in modo esplicito il pacchetto NTLM

Gravità - Errore

L'applicazione o il sottosistema seleziona esplicitamente NTLM invece di Negotiate nella chiamata ad AcquireCredentialsHandle. Anche se può essere possibile per il client e il server autenticarsi utilizzando Kerberos, ciò è impedito dalla selezione esplicita di NTLM.

Come risolvere questo errore

La correzione di questo errore consiste nel selezionare il pacchetto Negozia al posto di NTLM. Il modo in cui ciò viene eseguito dipenderà dal particolare sottosistema di rete utilizzato dal client o dal server. Di seguito vengono forniti alcuni esempi. È necessario consultare la documentazione sulla particolare libreria o set di API che si sta utilizzando.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”

-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


Lo spazio dei nomi System.DirectoryServices.AccountManagement era nuovo in .NET 3.5
Jeremy Gray il

1
So che ha quasi 4 anni, ma se stai convalidando un utente locale, dovrai assicurarti di impostare ContextType su ContextType.Machine quando crei un PrincipalContext. Altrimenti penserà che il nome della macchina fornito nella variabile Domain sia effettivamente un server di dominio.
SolidRegardless
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.