Como validar credenciais de domínio?

86

Quero validar um conjunto de credenciais em relação ao controlador de domínio. por exemplo:

Username: STACKOVERFLOW\joel
Password: splotchy

Método 1. Consultar o Active Directory com representação

Muitas pessoas sugerem consultar o Active Directory para algo. Se uma exceção for lançada, você saberá que as credenciais não são válidas - como é sugerido nesta questão stackoverflow .

No entanto, existem algumas desvantagens sérias nessa abordagem :

  1. Você não está apenas autenticando uma conta de domínio, mas também fazendo uma verificação de autorização implícita. Ou seja, você está lendo propriedades do AD usando um token de representação. E se a conta válida de outra forma não tiver direitos de leitura do AD? Por padrão, todos os usuários têm acesso de leitura, mas as políticas de domínio podem ser definidas para desabilitar as permissões de acesso para contas restritas (e / ou grupos).

  2. A vinculação ao AD tem uma sobrecarga séria, o cache do esquema do AD deve ser carregado no cliente (cache ADSI no provedor ADSI usado por DirectoryServices). Isso consome recursos da rede e do servidor AD - e é muito caro para uma operação simples como a autenticação de uma conta de usuário.

  3. Você está contando com uma falha de exceção para um caso não excepcional e presumindo que isso significa nome de usuário e senha inválidos. Outros problemas (por exemplo, falha de rede, falha de conectividade AD, erro de alocação de memória, etc.) são então interpretados incorretamente como falha de autenticação.

Método 2. API LogonUser Win32

Outros sugeriram usar a LogonUser()função API. Isso parece bom, mas infelizmente o usuário que faz a chamada às vezes precisa de uma permissão geralmente concedida apenas ao próprio sistema operacional:

O processo de chamada de LogonUser requer o privilégio SE_TCB_NAME. Se o processo de chamada não tiver esse privilégio, LogonUser falha e GetLastError retorna ERROR_PRIVILEGE_NOT_HELD.

Em alguns casos, o processo que chama LogonUser também deve ter o privilégio SE_CHANGE_NOTIFY_NAME habilitado; caso contrário, LogonUser falha e GetLastError retorna ERROR_ACCESS_DENIED. Este privilégio não é necessário para a conta do sistema local ou contas que são membros do grupo de administradores. Por padrão, SE_CHANGE_NOTIFY_NAME está habilitado para todos os usuários, mas alguns administradores podem desabilitá-lo para todos.

Distribuir o privilégio " Agir como parte do sistema operacional " não é algo que você queira fazer quer queira quer não - como a Microsoft aponta em um artigo da base de conhecimento :

... o processo que está chamando LogonUser deve ter o privilégio SE_TCB_NAME (no Gerenciador de usuários, esse é o direito " Agir como parte do sistema operacional "). O privilégio SE_TCB_NAME é muito poderoso e não deve ser concedido a nenhum usuário arbitrário apenas para que ele possa executar um aplicativo que precise validar credenciais.

Além disso, uma chamada para LogonUser()falhará se uma senha em branco for especificada.


Qual é a maneira correta de autenticar um conjunto de credenciais de domínio?


Acontece que estou ligando de código gerenciado, mas esta é uma questão geral do Windows. Pode-se presumir que os clientes tenham o .NET Framework 2.0 instalado.

Ian Boyd
fonte
1
Os leitores devem observar que, a partir do Windows XP, LogonUser não requer mais SE_TCB_NAME (a menos que você esteja fazendo login em uma conta do Passport).
Harry Johnston

Respostas:

130

C # no .NET 3.5 usando System.DirectoryServices.AccountManagement .

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

Isso irá validar contra o domínio atual. Confira o construtor PrincipalContext parametrizado para outras opções.

Tvanfosson
fonte
@tvanfosson: DirectoryServices não usa AD?
Mitch Wheat
1
Sim. Mas a documentação indica que esta é uma maneira rápida de validar credenciais. Também é diferente do método de vinculação mencionado na pergunta, pois você não está lendo nenhuma propriedade do objeto. Observe que o método está no contexto, não em um objeto de diretório.
tvanfosson
Correção: System.DirectoryServices.AccountManagement requer .NET 3.5. ( msdn.microsoft.com/en-us/library/… )
Ian Boyd
19
Ele também funciona com usuários locais, se você o usou new PrincipalContext(ContextType.Machine).
VansFannel
Alguém sabe se isso funciona em credenciais em cache ou requer conexão com o DC? Preciso saber disso para alguma implementação na qual estou trabalhando agora e não estou em nenhum domínio para testar
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
fonte
7
isso contém alguma diferença significativa com a resposta de @tvanfosson 3 anos antes?
gbjbaanb
5
@gbjbaanb Sim, pois contém o Domainparâmetro ao criar o PrincipalContext, algo que eu estava interessado em saber e encontrei nesta resposta.
Rudi Visser
1
@RudiVisser tvanfosson sugeriu que você "Verifique o construtor PrincipalContext parametrizado para outras opções" - sempre leia os documentos, nunca confie apenas na palavra da Internet para nada! :)
gbjbaanb
4
@gbjbaanb Sim, claro, mas fornecendo um exemplo de trabalho em vez de um link e sugestão de ler em outros lugares é o mantra StackOverflow, é por isso que aceitam submissões múltiplas de respostas: D Basta dizer que este não fornecer mais.
Rudi Visser
Alguém sabe como poderíamos fazer algo semelhante em um aplicativo UWP? (com AD normal e não com Azure AD). Eu fiz uma pergunta aqui: stackoverflow.com/questions/42821447
slayernoah
7

Estou usando o seguinte código para validar as credenciais. O método mostrado abaixo irá confirmar se as credenciais estão corretas e, caso contrário, se a senha expirou ou precisa ser alterada.

Há anos que procuro algo assim ... Espero que ajude alguém!

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
fonte
este é o "método 2" descrito na pergunta ... então ... não está realmente respondendo à pergunta
Robert Levy
1

Veja como determinar um usuário local:

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

Editado por Ian Boyd

Você não deve mais usar NTLM. É tão antigo e tão ruim que o Application Verifier da Microsoft (usado para detectar erros de programação comuns) emitirá um aviso se detectar o uso de NTLM.

Aqui está um capítulo da documentação do Application Verifier sobre por que eles têm um teste se alguém está usando NTLM por engano:

Por que o plug-in NTLM é necessário

NTLM é um protocolo de autenticação desatualizado com falhas que podem comprometer a segurança dos aplicativos e do sistema operacional. A deficiência mais importante é a falta de autenticação do servidor, que pode permitir que um invasor engane os usuários para que se conectem a um servidor falsificado. Como corolário da falta de autenticação do servidor, os aplicativos que usam NTLM também podem ser vulneráveis ​​a um tipo de ataque conhecido como ataque de “reflexão”. Este último permite que um invasor sequestre a conversa de autenticação de um usuário para um servidor legítimo e use-a para autenticar o invasor no computador do usuário. As vulnerabilidades do NTLM e as formas de explorá-las são o alvo do aumento da atividade de pesquisa na comunidade de segurança.

Embora o Kerberos esteja disponível há muitos anos, muitos aplicativos ainda são escritos para usar apenas NTLM. Isso reduz desnecessariamente a segurança dos aplicativos. O Kerberos, entretanto, não pode substituir o NTLM em todos os cenários - principalmente aqueles em que um cliente precisa se autenticar em sistemas que não estão associados a um domínio (uma rede doméstica talvez seja a mais comum). O pacote de segurança Negotiate permite um compromisso compatível com versões anteriores que usa Kerberos sempre que possível e só reverte para NTLM quando não há outra opção. Mudar o código para usar Negotiate em vez de NTLM aumentará significativamente a segurança para nossos clientes, ao mesmo tempo que apresenta pouca ou nenhuma compatibilidade de aplicativos. Negociar por si só não é uma solução mágica - há casos em que um invasor pode forçar o downgrade para NTLM, mas esses são significativamente mais difíceis de explorar. No entanto, uma melhoria imediata é que os aplicativos escritos para usar o Negotiate corretamente são automaticamente imunes a ataques de reflexão NTLM.

A título de advertência final contra o uso de NTLM: em versões futuras do Windows será possível desabilitar o uso de NTLM no sistema operacional. Se os aplicativos dependerem fortemente do NTLM, eles simplesmente falharão na autenticação quando o NTLM for desativado.

Como funciona o plug-in

O plugue do verificador detecta os seguintes erros:

  • O pacote NTLM é especificado diretamente na chamada para AcquireCredentialsHandle (ou API de wrapper de nível superior).

  • O nome de destino na chamada para InitializeSecurityContext é NULL.

  • O nome de destino na chamada para InitializeSecurityContext não é um nome de domínio no estilo SPN, UPN ou NetBIOS devidamente formado.

Os dois últimos casos forçarão o Negotiate a retroceder para NTLM diretamente (o primeiro caso) ou indiretamente (o controlador de domínio retornará um erro “principal não encontrado” no segundo caso, fazendo com que o Negotiate retroceda).

O plug-in também registra avisos ao detectar downgrades para NTLM; por exemplo, quando um SPN não é encontrado pelo controlador de domínio. Eles são registrados apenas como avisos, pois costumam ser casos legítimos - por exemplo, ao autenticar em um sistema que não faz parte do domínio.

NTLM para

5000 - O aplicativo selecionou explicitamente o pacote NTLM

Gravidade - Erro

O aplicativo ou subsistema seleciona explicitamente NTLM em vez de Negotiate na chamada para AcquireCredentialsHandle. Mesmo que seja possível para o cliente e o servidor autenticarem usando Kerberos, isso é impedido pela seleção explícita de NTLM.

Como corrigir este erro

A correção para esse erro é selecionar o pacote de negociação no lugar do NTLM. A forma como isso é feito dependerá do subsistema de rede específico usado pelo cliente ou servidor. Alguns exemplos são fornecidos a seguir. Você deve consultar a documentação da biblioteca específica ou conjunto de APIs que está usando.

APIs(parameter) Used by Application    Incorrect Value  Correct Value  
=====================================  ===============  ========================
AcquireCredentialsHandle (pszPackage)  “NTLM”           NEGOSSP_NAME “Negotiate”
Alan nicholas
fonte
-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, Canadá

Markus Safar
fonte
O namespace System.DirectoryServices.AccountManagement era novo no .NET 3.5
Jeremy Gray
1
Eu sei que isso tem quase 4 anos, mas se você estiver validando um usuário local, precisará garantir que defina ContextType como ContextType.Machine ao construir um PrincipalContext. Caso contrário, ele pensará que o nome da máquina fornecido na variável Domínio é na verdade um servidor de domínio.
SolidRegardless