ChatGPT解决这个技术问题 Extra ChatGPT

Validate a username and password against Active Directory?

How can I validate a username and password against Active Directory? I simply want to check if a username and password are correct.


C
Community

If you work on .NET 3.5 or newer, you can use the System.DirectoryServices.AccountManagement namespace and easily verify your credentials:

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

It's simple, it's reliable, it's 100% C# managed code on your end - what more can you ask for? :-)

Read all about it here:

Managing Directory Security Principals in the .NET Framework 3.5

MSDN docs on System.DirectoryServices.AccountManagement

Update:

As outlined in this other SO question (and its answers), there is an issue with this call possibly returning True for old passwords of a user. Just be aware of this behavior and don't be too surprised if this happens :-) (thanks to @MikeGledhill for pointing this out!)


In my domain, I had to specify pc.ValidateCredentials("myuser", "mypassword", ContextOptions.Negotiate) or I would get System.DirectoryServices.Protocols.DirectoryOperationException: The server cannot handle directory requests.
If a password's expired or the accounts disabled, then ValidateCredentials will return false. Unfortuantly, it doesn't tell you why it's returned false (which is a pity as it means I can't do something sensible like redirect the user to change their password).
Also beware the 'Guest' account -- if the domain-level Guest account is enabled, ValidateCredentials returns true if you give it a non-existant user. As a result, you may want to call UserPrinciple.FindByIdentity to see if the passed in user ID exists first.
@AlexPeck: the reason why you had to do this (like me) was that .NET uses the following technologies by default: LDAP+SSL, Kerberos, then RPC. I suspect RPC is off in your network (good!) and Kerberos doesn't actually get used by .NET unless you explicitly tell it using ContextOptions.Negotiate.
Do be aware that if the user CHANGES their Active Directory password, this piece of code will continue to happily authenticate the user using their old AD password. Yup, really. Have a read here: stackoverflow.com/questions/8949501/…
C
Community

We do this on our Intranet

You have to use System.DirectoryServices;

Here are the guts of the code

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

What do you put in "path"? The name of the domain? The name of the server? The LDAP path to the domain? The LDAP path to the server?
Answer1: No we run it as a web service so it can be called from multiple locations in the main web app. Answer2: Path contains LDAP info... LDAP://DC=domainname1,DC=domainname2,DC=com
It would seem that this could allow LDAP injection. You may want to make sure to escape or remove any parenthesis in the strAccountId
Does this mean that strPassword is stored in LDAP in plain text?
There should never be a need to explicitly call Close() on a using variable.
c
codechurn

Several solutions presented here lack the ability to differentiate between a wrong user / password, and a password that needs to be changed. That can be done in the following way:

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

If the users password is wrong, or the user doesn't exists, error will contain

"8009030C: LdapErr: DSID-0C0904DC, comment: AcceptSecurityContext error, data 52e, v1db1",

if the users password needs to be changed, it will contain

"8009030C: LdapErr: DSID-0C0904DC, comment: AcceptSecurityContext error, data 773, v1db1"

The lexc.ServerErrorMessage data value is a hex representation of the Win32 Error Code. These are the same error codes which would be returned by otherwise invoking the Win32 LogonUser API call. The list below summarizes a range of common values with hex and decimal values:

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)

Unfortunately, some AD installations doesn't return the LDAP sub error code, which means that this solution won't work.
Don't forget to add some references to the project: System.DirectoryServices and System.DirectoryServices.Protocols
My question, though, is this: how do you get the LDAP server name? If you're writing a portable application, you can't expect the user to know or need to supply the names of AD servers on every network.
I have users which are restricted to logging in to specific workstations; how do I specify the workstation that I am trying the login for? (workstation1 would fail with data 531, workstation2 would work fine, for example)
I feel weird as I don't think you're getting enough credit. This is a fully managed method without the trouble of Win32 API call to determine if "user must reset password" which clearly none of the other answers achieved. Is there any loophole in this method causing it's low appreciation rate? hmm...
M
Markus Safar

very simple solution using 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;
}

the NativeObject access is required to detect a bad user/password


This code is bad because it's also doing an authorization check (check if the user is allowed to read active directory information). The username and password can be valid, but the user not allowed to read info - and get an exception. In other words you can have a valid username&password, but still get an exception.
i am actually in the process of asking for the native equivalent of PrincipleContext - which only exists in .NET 3.5. But if you are using .NET 3.5 or newer you should use PrincipleContext
A
Alan

Unfortunately there is no "simple" way to check a users credentials on AD.

With every method presented so far, you may get a false-negative: A user's creds will be valid, however AD will return false under certain circumstances:

User is required to Change Password at Next Logon.

User's password has expired.

ActiveDirectory will not allow you to use LDAP to determine if a password is invalid due to the fact that a user must change password or if their password has expired.

To determine password change or password expired, you may call Win32:LogonUser(), and check the windows error code for the following 2 constants:

ERROR_PASSWORD_MUST_CHANGE = 1907

ERROR_PASSWORD_EXPIRED = 1330


May I ask where you got the devinitions for Expired and Must_Change... Found them nowhere but here :)
Thanks. I was trying to find out way my validation was returning false all the time. It was because the user needs to change his password.
s
stephbu

Probably easiest way is to PInvoke LogonUser Win32 API.e.g.

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

MSDN Reference here...

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

Definitely want to use logon type

LOGON32_LOGON_NETWORK (3)

This creates a lightweight token only - perfect for AuthN checks. (other types can be used to build interactive sessions etc.)


As @Alan points out, LogonUser API has many useful traits beyond a System.DirectoryServices call.
@cciotti: No, that's wrong. The BEST way to correctly authenticate someone is to use LogonUserAPI as @stephbu write. All other methods described in this post will NOT WORK 100%. Just a note however, I do believe you have to be domain joined inorder to call LogonUser.
@Alan to generate credentials you have to be able to connect to the domain by handing in a valid domain account. However I'm pretty sure your machine doesn't necessarily need to be a member of the domain.
The LogonUser API requires the user to have the Act as a part of the operating system privelage; which isn't something that users get - and not something you want to be granting to every user in the organization. (msdn.microsoft.com/en-us/library/aa378184(v=vs.85).aspx)
LogonUser only needs Act as part of the operating system for Windows 2000 and below according to support.microsoft.com/kb/180548 ... It looks clean for Server 2003 and higher.
s
surfmuggle

A full .Net solution is to use the classes from the System.DirectoryServices namespace. They allow to query an AD server directly. Here is a small sample that would do this:

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

This code directly connects to the AD server, using the credentials provided. If the credentials are invalid, searcher.FindOne() will throw an exception. The ErrorCode is the one corresponding to the "invalid username/password" COM error.

You don't need to run the code as an AD user. In fact, I succesfully use it to query informations on an AD server, from a client outside the domain !


how about authentication types? i think you forgot it in your code above. :-) by default DirectoryEntry.AuthenticationType is set to Secured right? that code is not going to work on LDAPs that are not secured (Anonymous or None perhaps). am i correct with this?
The down-side to querying an AD server is that you have permission to query the AD server. Your credential can be valid, but if you don't have permission to query AD, then you will get the error. That is why the so-called Fast Bind was created; you validate credentials without authorizing the user's ability to do something.
wouldn't this allow anyone to pass in case a COMException is thrown for any other reason before the credentials are checked?
p
palswim

Yet another .NET call to quickly authenticate LDAP credentials:

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

This is the only solution that has worked for me, using PrincipalContext hasn't worked for me.
PrincipalContext not valid for a secure LDAP connection (aka LDAPS, which uses port 636
C
Charles Bretana

Try this code (NOTE: Reported to not work on 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

except you'll need to create your own custom exception for "LogonException"


Don't use exception handling for returning information from a method. "Unknown user name or bad password" is not exceptional, it is standard behaviour for LogonUser. Just return false.
yes... this was a port from an old VB6 library... written 2003 or so... (when .Net first came out)
If running on Windows 2000 this code will not work (support.microsoft.com/kb/180548)
Rethinking this. Logon User's expected behavior, it's purpose, is to log the user on. If it fails to perform that task, it IS an exception. In fact, the method should return void, not a Boolean. Plus, if you just returned a Boolean the method's consumer has no way to inform the user what the reason for the failure was.
M
Michael Liu

Windows authentication can fail for various reasons: an incorrect user name or password, a locked account, an expired password, and more. To distinguish between these errors, call the LogonUser API function via P/Invoke and check the error code if the function returns 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()
    }
}

Sample usage:

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

Note: LogonUser requires a trust relationship with the domain you're validating against.


can you explain why your answer is better than the highest voted answer?
@MohammadAli: If you need to know why credential validation failed (incorrect credentials, a locked account, an expired password, etc.), the LogonUser API function will tell you. In contrast, the PrincipalContext.ValidateCredentials method (according to comments on marc_s's answer) won't; it returns false in all these cases. On the other hand, LogonUser requires a trust relationship with the domain, but PrincipalContext.ValidateCredentials (I think) does not.
c
chauwel

If you are stuck with .NET 2.0 and managed code, here is another way that works whith local and domain accounts:

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

Works well with local accounts of the machine he launch the script
BTW, this method is needed to make this works public static SecureString ToSecureString(string PwString) { char[] PasswordChars = PwString.ToCharArray(); SecureString Password = new SecureString(); foreach (char c in PasswordChars) Password.AppendChar(c); ProcessStartInfo foo = new ProcessStartInfo(); foo.Password = Password; return foo.Password; }
On the contrary, one should use SecureString for passwords anyway. WPF PasswordBox supports it.
h
hossein andarkhora

My Simple Function

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

d
dnxit

For me both of these below worked, make sure your Domain is given with LDAP:// in start

//"LDAP://" + domainName
private void btnValidate_Click(object sender, RoutedEventArgs e)
{
    try
    {
        DirectoryEntry de = new DirectoryEntry(txtDomainName.Text, txtUsername.Text, txtPassword.Text);
        DirectorySearcher dsearch = new DirectorySearcher(de);
        SearchResult results = null;

        results = dsearch.FindOne();

        MessageBox.Show("Validation Success.");
    }
    catch (LdapException ex)
    {
        MessageBox.Show($"Validation Failure. {ex.GetBaseException().Message}");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Validation Failure. {ex.GetBaseException().Message}");
    }
}

private void btnValidate2_Click(object sender, RoutedEventArgs e)
{
    try
    {
        LdapConnection lcon = new LdapConnection(new LdapDirectoryIdentifier((string)null, false, false));
        NetworkCredential nc = new NetworkCredential(txtUsername.Text,
                               txtPassword.Text, txtDomainName.Text);
        lcon.Credential = nc;
        lcon.AuthType = AuthType.Negotiate;
        lcon.Bind(nc);

        MessageBox.Show("Validation Success.");
    }
    catch (LdapException ex)
    {
        MessageBox.Show($"Validation Failure. {ex.GetBaseException().Message}");
    }
    catch (Exception ex)
    {
        MessageBox.Show($"Validation Failure. {ex.GetBaseException().Message}");
    }
}

G
Gayan Chinthaka Dharmarathna

Here my complete authentication solution for your reference.

First, add the following four references

 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 = "";

        }
    }

F
Fábio Nascimento

I'm using this procedure as a DLL to login in other app that we developed... (We are currently using this with OpenEdge Progress)

public static string AzureLogin(string user, string password) {

    string status;

    try {
        new DirectorySearcher(new DirectoryEntry("LDAP://yourdomain.com", user, password) {
            AuthenticationType = AuthenticationTypes.Secure,
            Username = user,
            Password = password
        })  {
            Filter = "(objectclass=user)"
        }.FindOne().Properties["displayname"][0].ToString();

        status = $"SUCCESS - User {user} has logged in.";

    } catch(System.Exception e) {
        status = $"ERROR - While logging in: {e}";
    }

    return status;
}