Validar um nome de usuário e senha no Active Directory?


526

Como posso validar um nome de usuário e senha no Active Directory? Quero apenas verificar se um nome de usuário e senha estão corretos.

Respostas:


642

Se você trabalha no .NET 3.5 ou mais recente, pode usar o System.DirectoryServices.AccountManagementespaço para nome e verificar facilmente suas credenciais:

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "YOURDOMAIN"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "mypassword");
}

É simples, é confiável, é um código gerenciado 100% C # do seu lado - o que mais você pode pedir? :-)

Leia tudo sobre isso aqui:

Atualizar:

Conforme descrito nesta outra pergunta do SO (e suas respostas) , há um problema com essa chamada possivelmente retornando Truesenhas antigas de um usuário. Esteja ciente desse comportamento e não fique surpreso se isso acontecer :-) (obrigado a MikeGledhill por apontar isso!)


36
No meu domínio, tive que especificar pc.ValidateCredentials ("myuser", "mypassword", ContextOptions.Negotiate) ou obteria System.DirectoryServices.Protocols.DirectoryOperationException: o servidor não pode manipular solicitações de diretório.
Alex Peck

12
Se uma senha expirou ou as contas foram desativadas, ValidateCredentials retornará false. Infelizmente, ele não diz por que ele retornou falso (o que é uma pena, pois significa que não posso fazer algo sensato como redirecionar o usuário para alterar sua senha).
Chris J

64
Também tenha cuidado com a conta 'Convidado' - se a conta Convidado no nível do domínio estiver ativada, ValidateCredentials retornará true se você fornecer um usuário inexistente . Como resultado, convém ligar UserPrinciple.FindByIdentitypara verificar se o ID do usuário passado existe primeiro.
Chris J

7
@AlexPeck: a razão pela qual você fez isso (como eu) foi que o .NET usa as seguintes tecnologias por padrão: LDAP + SSL, Kerberos e RPC. Eu suspeito que o RPC está desativado na sua rede (bom!) E o Kerberos não é realmente usado pelo .NET, a menos que você o explique explicitamente ContextOptions.Negotiate.
Brett Veenstra

5
Esteja ciente de que, se o usuário ALTERAR sua senha do Active Directory, esse trecho de código continuará a autenticar felizmente o usuário usando sua senha antiga do AD. Sim, sério. Leia aqui: stackoverflow.com/questions/8949501/…
Mike Gledhill

70

Fazemos isso na nossa Intranet

Você precisa usar System.DirectoryServices;

Aqui estão as entranhas do código

using (DirectoryEntry adsEntry = new DirectoryEntry(path, strAccountId, strPassword))
{
    using (DirectorySearcher adsSearcher = new DirectorySearcher(adsEntry))
    {
        //adsSearcher.Filter = "(&(objectClass=user)(objectCategory=person))";
        adsSearcher.Filter = "(sAMAccountName=" + strAccountId + ")";

        try
        {
            SearchResult adsSearchResult = adsSearcher.FindOne();
            bSucceeded = true;

            strAuthenticatedBy = "Active Directory";
            strError = "User has been authenticated by Active Directory.";
        }
        catch (Exception ex)
        {
            // Failed to authenticate. Most likely it is caused by unknown user
            // id or bad strPassword.
            strError = ex.Message;
        }
        finally
        {
            adsEntry.Close();
        }
    }
}

9
O que você coloca no "caminho"? O nome do domínio? O nome do servidor? O caminho LDAP para o domínio? O caminho LDAP para o servidor?
Ian Boyd

3
Resposta1: Não, nós o executamos como um serviço da Web, para que possa ser chamado de vários locais no aplicativo Web principal. Resposta2: Path contém informações LDAP ... LDAP: // DC = DOMAINNAME1, DC = DOMAINNAME2, DC = com
DiningPhilanderer

3
Parece que isso pode permitir a injeção de LDAP. Você pode querer escapar ou remover qualquer parêntese no strAccountId
Brain2000 9/14

Isso significa que strPasswordestá armazenado no LDAP em texto sem formatação?
22615 Matt Hocajd

15
Nunca deve ser necessário chamar explicitamente Close()uma usingvariável.
Nyerguds 17/05/19

62

Várias soluções apresentadas aqui não têm a capacidade de diferenciar entre um usuário / senha incorretos e uma senha que precisa ser alterada. Isso pode ser feito da seguinte maneira:

using System;
using System.DirectoryServices.Protocols;
using System.Net;

namespace ProtocolTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                LdapConnection connection = new LdapConnection("ldap.fabrikam.com");
                NetworkCredential credential = new NetworkCredential("user", "password");
                connection.Credential = credential;
                connection.Bind();
                Console.WriteLine("logged in");
            }
            catch (LdapException lexc)
            {
                String error = lexc.ServerErrorMessage;
                Console.WriteLine(lexc);
            }
            catch (Exception exc)
            {
                Console.WriteLine(exc);
            }
        }
    }
}

Se a senha do usuário estiver incorreta ou o usuário não existir, o erro conterá

"8009030C: LdapErr: DSID-0C0904DC, comentário: erro AcceptSecurityContext, dados 52e, v1db1",

se a senha do usuário precisar ser alterada, ela conterá

"8009030C: LdapErr: DSID-0C0904DC, comentário: erro AcceptSecurityContext, dados 773, v1db1"

O lexc.ServerErrorMessagevalor dos dados é uma representação hexadecimal do código de erro do Win32. Esses são os mesmos códigos de erro que seriam retornados invocando a chamada da API Win32 LogonUser. A lista abaixo resume um intervalo de valores comuns com valores hexadecimais e decimais:

525 user not found ​(1317)
52e invalid credentials ​(1326)
530 not permitted to logon at this time (1328)
531 not permitted to logon at this workstation (1329)
532 password expired ​(1330)
533 account disabled ​(1331) 
701 account expired ​(1793)
773 user must reset password (1907)
775 user account locked (1909)

2
Infelizmente, algumas instalações do AD não retornam o sub-código LDAP, o que significa que esta solução não funcionará.
Søren Mors

4
Não se esqueça de adicionar algumas referências para o projeto: System.DirectoryServiceseSystem.DirectoryServices.Protocols
TomXP411

3
Minha pergunta, porém, é a seguinte: como você obtém o nome do servidor LDAP? Se você estiver escrevendo um aplicativo portátil, não poderá esperar que o usuário saiba ou precise fornecer os nomes dos servidores AD em todas as redes.
TomXP411

1
Eu tenho usuários restritos a efetuar login em estações de trabalho específicas; como faço para especificar a estação de trabalho na qual estou tentando fazer o login? (workstation1 falharia com dados de 531, workstation2 iria funcionar bem, por exemplo)
akohlsmith

1
Eu me sinto estranho, pois não acho que você esteja recebendo crédito suficiente. Este é um método totalmente gerenciado, sem o problema da chamada da API do Win32 para determinar se "o usuário deve redefinir a senha", o que claramente nenhuma das outras respostas obtidas. Existe alguma brecha nesse método que causa baixa taxa de apreciação? hmm ...
Lionet Chen

34

solução muito simples usando DirectoryServices:

using System.DirectoryServices;

//srvr = ldap server, e.g. LDAP://domain.com
//usr = user name
//pwd = user password
public bool IsAuthenticated(string srvr, string usr, string pwd)
{
    bool authenticated = false;

    try
    {
        DirectoryEntry entry = new DirectoryEntry(srvr, usr, pwd);
        object nativeObject = entry.NativeObject;
        authenticated = true;
    }
    catch (DirectoryServicesCOMException cex)
    {
        //not authenticated; reason why is in cex
    }
    catch (Exception ex)
    {
        //not authenticated due to some other exception [this is optional]
    }

    return authenticated;
}

o acesso NativeObject é necessário para detectar um usuário / senha incorreto


4
Esse código está incorreto porque também está fazendo uma verificação de autorização (verifique se o usuário tem permissão para ler informações do diretório ativo). O nome de usuário e a senha podem ser válidos, mas o usuário não tem permissão para ler informações - e obter uma exceção. Em outras palavras, você pode ter um nome de usuário e senha válidos, mas ainda obter uma exceção.
Ian Boyd

2
Na verdade, estou no processo de solicitar o equivalente nativo de PrincipleContext- que só existe no .NET 3.5. Mas se você estiver usando o .NET 3.5 ou mais recente, você deve usarPrincipleContext
Ian Boyd

28

Infelizmente, não há uma maneira "simples" de verificar as credenciais de um usuário no AD.

Com todos os métodos apresentados até agora, você pode obter um falso negativo: os cleds de um usuário serão válidos, no entanto, o AD retornará false sob certas circunstâncias:

  • É necessário que o usuário altere a senha no próximo logon.
  • A senha do usuário expirou.

O ActiveDirectory não permitirá que você use LDAP para determinar se uma senha é inválida devido ao fato de um usuário precisar alterar a senha ou se a senha expirou.

Para determinar a alteração da senha ou a senha expirada, você pode chamar o Win32: LogonUser () e verificar o código de erro do Windows para as 2 seguintes constantes:

  • ERROR_PASSWORD_MUST_CHANGE = 1907
  • ERROR_PASSWORD_EXPIRED = 1330

1
Posso perguntar onde você pegou os devinitions para Expirado e MUST_CHANGE ... Encontrado a lugar nenhum, mas aqui :)
mabstrei


Obrigado. Eu estava tentando descobrir como minha validação estava retornando falsa o tempo todo. Foi porque o usuário precisa alterar sua senha.
precisa

22

Provavelmente, a maneira mais fácil é PInvoke LogonUser Win32 API.eg

http://www.pinvoke.net/default.aspx/advapi32/LogonUser.html

Referência do MSDN aqui ...

http://msdn.microsoft.com/en-us/library/aa378184.aspx

Definitivamente deseja usar o tipo de logon

LOGON32_LOGON_NETWORK (3)

Isso cria apenas um token leve - perfeito para verificações de AuthN. (outros tipos podem ser usados ​​para criar sessões interativas etc.)


Como aponta o @Alan, a API LogonUser possui muitos traços úteis além de uma chamada System.DirectoryServices.
stephbu

3
@ cciotti: Não, isso está errado. A melhor maneira de autenticar alguém corretamente é usar LogonUserAPI como gravação @stephbu. Todos os outros métodos descritos neste post NÃO FUNCIONARÃO 100%. Apenas uma observação, no entanto, acredito que você precisa ingressar no domínio para ligar para o LogonUser.
197 Alan Alan

@ Alan para gerar credenciais, você precisa se conectar ao domínio entregando uma conta de domínio válida. No entanto, tenho certeza de que sua máquina não precisa necessariamente ser um membro do domínio.
stephbu

2
A LogonUserAPI exige que o usuário tenha o ato como parte da privelagem do sistema operacional ; o que não é algo que os usuários recebem - e não é algo que você deseja conceder a todos os usuários da organização. ( Msdn.microsoft.com/en-us/library/aa378184(v=vs.85).aspx )
Ian Boyd

1
O LogonUser precisa apenas Agir como parte do sistema operacional para Windows 2000 e versões posteriores de acordo com support.microsoft.com/kb/180548 ... Parece limpo para o Server 2003 e superior.
Chris J

18

Uma solução .Net completa é usar as classes do espaço para nome System.DirectoryServices. Eles permitem consultar um servidor AD diretamente. Aqui está uma pequena amostra que faria isso:

using (DirectoryEntry entry = new DirectoryEntry())
{
    entry.Username = "here goes the username you want to validate";
    entry.Password = "here goes the password";

    DirectorySearcher searcher = new DirectorySearcher(entry);

    searcher.Filter = "(objectclass=user)";

    try
    {
        searcher.FindOne();
    }
    catch (COMException ex)
    {
        if (ex.ErrorCode == -2147023570)
        {
            // Login or password is incorrect
        }
    }
}

// FindOne() didn't throw, the credentials are correct

Esse código se conecta diretamente ao servidor AD, usando as credenciais fornecidas. Se as credenciais forem inválidas, searcher.FindOne () lançará uma exceção. O ErrorCode é aquele que corresponde ao erro COM "nome de usuário / senha inválido".

Você não precisa executar o código como um usuário do AD. Na verdade, eu o utilizo com êxito para consultar informações em um servidor AD, de um cliente fora do domínio!


e quanto aos tipos de autenticação? Eu acho que você esqueceu no seu código acima. :-) por padrão DirectoryEntry.AuthenticationType está definido como Seguro, certo? esse código não funcionará em LDAPs que não são protegidos (anônimo ou nenhum, talvez). estou correto com isso?
jerbersoft

A desvantagem de consultar um servidor AD é que você tem permissão para consultar o servidor AD. Sua credencial pode ser válida, mas se você não tiver permissão para consultar o AD, receberá o erro. É por isso que o chamado Fast Bind foi criado; você valida credenciais sem autorizar a capacidade do usuário de fazer algo.
21715 Ian

2
isso não permitiria que alguém passasse caso uma COMException fosse lançada por qualquer outro motivo antes que as credenciais fossem verificadas?
Stefan Paul Noack

11

Ainda outra chamada .NET para autenticar rapidamente credenciais LDAP:

using System.DirectoryServices;

using(var DE = new DirectoryEntry(path, username, password)
{
    try
    {
        DE.RefreshCache(); // This will force credentials validation
    }
    catch (COMException ex)
    {
        // Validation failed - handle how you want
    }
}

Esta é a única solução que funcionou para mim, usando o PrincipalContext não funcionou para mim.
Daniel

PrincipalContext não é válido para uma conexão LDAP segura (também conhecida como LDAPS, que usa a porta 636
Kiquenet 19/12/16

10

Experimente este código (Observação: relatado para não funcionar no Windows Server 2000)

#region NTLogonUser
#region Direct OS LogonUser Code
[DllImport( "advapi32.dll")]
private static extern bool LogonUser(String lpszUsername, 
    String lpszDomain, String lpszPassword, int dwLogonType, 
    int dwLogonProvider, out int phToken);

[DllImport("Kernel32.dll")]
private static extern int GetLastError();

public static bool LogOnXP(String sDomain, String sUser, String sPassword)
{
   int token1, ret;
   int attmpts = 0;

   bool LoggedOn = false;

   while (!LoggedOn && attmpts < 2)
   {
      LoggedOn= LogonUser(sUser, sDomain, sPassword, 3, 0, out token1);
      if (LoggedOn) return (true);
      else
      {
         switch (ret = GetLastError())
         {
            case (126): ; 
               if (attmpts++ > 2)
                  throw new LogonException(
                      "Specified module could not be found. error code: " + 
                      ret.ToString());
               break;

            case (1314): 
               throw new LogonException(
                  "Specified module could not be found. error code: " + 
                      ret.ToString());

            case (1326): 
               // edited out based on comment
               //  throw new LogonException(
               //   "Unknown user name or bad password.");
            return false;

            default: 
               throw new LogonException(
                  "Unexpected Logon Failure. Contact Administrator");
              }
          }
       }
   return(false);
}
#endregion Direct Logon Code
#endregion NTLogonUser

exceto que você precisará criar sua própria exceção personalizada para "LogonException"


Não use manipulação de exceção para retornar informações de um método. "Nome de usuário desconhecido ou senha incorreta" não é excepcional, é um comportamento padrão para o LogonUser. Basta retornar falso.
Treb

sim ... isso foi uma porta de uma biblioteca VB6 velho ... escrito 2003 ou então ... (quando .Net primeiro saiu)
Charles Bretana

Se estiver sendo executado no Windows 2000, esse código não funcionará ( support.microsoft.com/kb/180548 )
Ian Boyd

1
Repensando isso. Logon O comportamento esperado do usuário, seu objetivo, é fazer logon no usuário . Se ele falhar para executar essa tarefa, ele é uma exceção. De fato, o método deve retornar nulo, não um booleano. Além disso, se você acabou de devolver um booleano, o consumidor do método não tem como informar ao usuário qual foi o motivo da falha.
Charles Bretana

5

Se você está preso ao .NET 2.0 e ao código gerenciado, aqui está outra maneira de funcionar com contas locais e de domínio:

using System;
using System.Collections.Generic;
using System.Text;
using System.Security;
using System.Diagnostics;

static public bool Validate(string domain, string username, string password)
{
    try
    {
        Process proc = new Process();
        proc.StartInfo = new ProcessStartInfo()
        {
            FileName = "no_matter.xyz",
            CreateNoWindow = true,
            WindowStyle = ProcessWindowStyle.Hidden,
            WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
            UseShellExecute = false,
            RedirectStandardError = true,
            RedirectStandardOutput = true,
            RedirectStandardInput = true,
            LoadUserProfile = true,
            Domain = String.IsNullOrEmpty(domain) ? "" : domain,
            UserName = username,
            Password = Credentials.ToSecureString(password)
        };
        proc.Start();
        proc.WaitForExit();
    }
    catch (System.ComponentModel.Win32Exception ex)
    {
        switch (ex.NativeErrorCode)
        {
            case 1326: return false;
            case 2: return true;
            default: throw ex;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return false;
}   

Funciona bem com contas locais da máquina que ele lançar o script
eka808

BTW, esse método é necessário para tornar público este trabalho estático SecureString ToSecureString (string PwString) {char [] PasswordChars = PwString.ToCharArray (); Senha do SecureString = new SecureString (); foreach (char c em PasswordChars) Password.AppendChar (c); ProcessStartInfo foo = novo ProcessStartInfo (); foo.Password = Senha; return foo.Password; }
eka808 29/11

Pelo contrário, deve-se usar o SecureString para senhas de qualquer maneira. WPF PasswordBox suporta.
Stephen Drew

5

A autenticação do Windows pode falhar por vários motivos: nome de usuário ou senha incorretos, conta bloqueada, senha expirada e muito mais. Para distinguir esses erros, chame a função API LogonUser via P / Invoke e verifique o código de erro se a função retornar false:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

public static class Win32Authentication
{
    private class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        private SafeTokenHandle() // called by P/Invoke
            : base(true)
        {
        }

        protected override bool ReleaseHandle()
        {
            return CloseHandle(this.handle);
        }
    }

    private enum LogonType : uint
    {
        Network = 3, // LOGON32_LOGON_NETWORK
    }

    private enum LogonProvider : uint
    {
        WinNT50 = 3, // LOGON32_PROVIDER_WINNT50
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr handle);

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool LogonUser(
        string userName, string domain, string password,
        LogonType logonType, LogonProvider logonProvider,
        out SafeTokenHandle token);

    public static void AuthenticateUser(string userName, string password)
    {
        string domain = null;
        string[] parts = userName.Split('\\');
        if (parts.Length == 2)
        {
            domain = parts[0];
            userName = parts[1];
        }

        SafeTokenHandle token;
        if (LogonUser(userName, domain, password, LogonType.Network, LogonProvider.WinNT50, out token))
            token.Dispose();
        else
            throw new Win32Exception(); // calls Marshal.GetLastWin32Error()
    }
}

Uso da amostra:

try
{
    Win32Authentication.AuthenticateUser("EXAMPLE\\user", "P@ssw0rd");
    // Or: Win32Authentication.AuthenticateUser("user@example.com", "P@ssw0rd");
}
catch (Win32Exception ex)
{
    switch (ex.NativeErrorCode)
    {
        case 1326: // ERROR_LOGON_FAILURE (incorrect user name or password)
            // ...
        case 1327: // ERROR_ACCOUNT_RESTRICTION
            // ...
        case 1330: // ERROR_PASSWORD_EXPIRED
            // ...
        case 1331: // ERROR_ACCOUNT_DISABLED
            // ...
        case 1907: // ERROR_PASSWORD_MUST_CHANGE
            // ...
        case 1909: // ERROR_ACCOUNT_LOCKED_OUT
            // ...
        default: // Other
            break;
    }
}

Nota: O LogonUser requer uma relação de confiança com o domínio contra o qual você está validando.


você pode explicar por que sua resposta é melhor que a resposta mais votada?
Mohammad Ali

1
@MohammadAli: Se você precisar saber por que a validação da credencial falhou (credenciais incorretas, uma conta bloqueada, uma senha expirada etc.), a função da API LogonUser informará. Por outro lado, o método PrincipalContext.ValidateCredentials (de acordo com comentários na resposta de marc_s) não; retorna falso em todos esses casos. Por outro lado, LogonUser requer uma relação de confiança com o domínio, mas PrincipalContext.ValidateCredentials (eu acho) não.
Michael Liu

2

Minha função simples

 private bool IsValidActiveDirectoryUser(string activeDirectoryServerDomain, string username, string password)
    {
        try
        {
            DirectoryEntry de = new DirectoryEntry("LDAP://" + activeDirectoryServerDomain, username + "@" + activeDirectoryServerDomain, password, AuthenticationTypes.Secure);
            DirectorySearcher ds = new DirectorySearcher(de);
            ds.FindOne();
            return true;
        }
        catch //(Exception ex)
        {
            return false;
        }
    }

1

Aqui minha solução completa de autenticação para sua referência.

Primeiro, adicione as quatro referências a seguir

 using System.DirectoryServices;
 using System.DirectoryServices.Protocols;
 using System.DirectoryServices.AccountManagement;
 using System.Net; 

private void AuthUser() { 


      try{
            string Uid = "USER_NAME";
            string Pass = "PASSWORD";
            if (Uid == "")
            {
                MessageBox.Show("Username cannot be null");
            }
            else if (Pass == "")
            {
                MessageBox.Show("Password cannot be null");
            }
            else
            {
                LdapConnection connection = new LdapConnection("YOUR DOMAIN");
                NetworkCredential credential = new NetworkCredential(Uid, Pass);
                connection.Credential = credential;
                connection.Bind();

                // after authenticate Loading user details to data table
                PrincipalContext ctx = new PrincipalContext(ContextType.Domain);
                UserPrincipal user = UserPrincipal.FindByIdentity(ctx, Uid);
                DirectoryEntry up_User = (DirectoryEntry)user.GetUnderlyingObject();
                DirectorySearcher deSearch = new DirectorySearcher(up_User);
                SearchResultCollection results = deSearch.FindAll();
                ResultPropertyCollection rpc = results[0].Properties;
                DataTable dt = new DataTable();
                DataRow toInsert = dt.NewRow();
                dt.Rows.InsertAt(toInsert, 0);

                foreach (string rp in rpc.PropertyNames)
                {
                    if (rpc[rp][0].ToString() != "System.Byte[]")
                    {
                        dt.Columns.Add(rp.ToString(), typeof(System.String));

                        foreach (DataRow row in dt.Rows)
                        {
                            row[rp.ToString()] = rpc[rp][0].ToString();
                        }

                    }  
                }
             //You can load data to grid view and see for reference only
                 dataGridView1.DataSource = dt;


            }
        } //Error Handling part
        catch (LdapException lexc)
        {
            String error = lexc.ServerErrorMessage;
            string pp = error.Substring(76, 4);
            string ppp = pp.Trim();

            if ("52e" == ppp)
            {
                MessageBox.Show("Invalid Username or password, contact ADA Team");
            }
            if ("775​" == ppp)
            {
                MessageBox.Show("User account locked, contact ADA Team");
            }
            if ("525​" == ppp)
            {
                MessageBox.Show("User not found, contact ADA Team");
            }
            if ("530" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this time, contact ADA Team");
            }
            if ("531" == ppp)
            {
                MessageBox.Show("Not permitted to logon at this workstation, contact ADA Team");
            }
            if ("532" == ppp)
            {
                MessageBox.Show("Password expired, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }
            if ("533​" == ppp)
            {
                MessageBox.Show("Account disabled, contact ADA Team");
            }



        } //common error handling
        catch (Exception exc)
        {
            MessageBox.Show("Invalid Username or password, contact ADA Team");

        }

        finally {
            tbUID.Text = "";
            tbPass.Text = "";

        }
    }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.