How To Authenticate Users With Active Directory

I recently needed to write an app to authenticate users via Active Directory. For this, I used the native LDAP classes in Java and rolled my own "ActiveDirectory" class. The class provides several static methods used to authenticate users and change passwords.

Authentication Example

Here's a really simple example of how to authenticate a user using a username and password. The ActiveDirectory class actually provides 3 different getConnection() methods for for authenticating users.
    try{
        LdapContext ctx = ActiveDirectory.getConnection("bob", "password");
        ctx.close();
    }
    catch(Exception e){
        //Failed to authenticate user!
        e.printStackTrace();
    }

Change Password

In addition to authenticating users, the ActiveDirectory class can be used to change a user's password. Here's an example:

    try{
        LdapContext conn = ActiveDirectory.getConnection("bob", "password");
        ActiveDirectory.getUser("bob", conn).changePassword("password", "NewPassword!", true, conn);
        conn.close();
        System.out.println("Success!");
    }
    catch(Exception e){
        //Failed to authenticate user or change password...
        e.printStackTrace();
    }
Note that there are several possible errors that you may encounter when changing a password. For starters, the Active Directory server must be LDAPS enabled. Secondly, the new password must meet certain requirements (e.g. password complexity, length, minimum password age, password history, etc.). Unfortunately, the Java/LDAP API doesn't tell you exactly what's wrong. It simply spits out a generic error:
LDAP: error code 19 - 0000052D: AtrErr: DSID-03190F00, #1:
        0: 0000052D: DSID-03190F00, problem 1005 (CONSTRAINT_ATT_TYPE), data 0, Att 9005a (unicodePwd)

If you are a domain administrator, you can view/update the password requirements on the Active Directory server via Administrative Tools->Domain Securitty Policy->Account Policies->Password Policy

The one policy that bit me in the ass when developing/testing the change password feature was the Minimum Password Age. For testing purposes, I changed the setting on my domain controller from "Undefined" to "0". After changing this, or any other password policy, remember to refresh the domain controller by running "gpupdate":
gpupdate /force

ActiveDirectory Class

Here's the source code for the ActiveDirectory Class. This is a standalone class and does not have any 3rd party dependencies.
package javaxt.security;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import static javax.naming.directory.SearchControls.SUBTREE_SCOPE;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.InitialLdapContext;

//Imports for changing password
import javax.naming.directory.ModificationItem;
import javax.naming.directory.BasicAttribute;
import javax.naming.ldap.StartTlsResponse;
import javax.naming.ldap.StartTlsRequest;
import javax.net.ssl.*;

//******************************************************************************
//**  ActiveDirectory
//*****************************************************************************/
/**
 *   Provides static methods to authenticate users, change passwords, etc. 
 *
 ******************************************************************************/

public class ActiveDirectory {

    private static String[] userAttributes = {
        "distinguishedName","cn","name","uid",
        "sn","givenname","memberOf","samaccountname",
        "userPrincipalName"
    };

    private ActiveDirectory(){}


  //**************************************************************************
  //** getConnection
  //*************************************************************************/
  /**  Used to authenticate a user given a username/password. Domain name is
   *   derived from the fully qualified domain name of the host machine.
   */
    public static LdapContext getConnection(String username, String password) throws NamingException {
        return getConnection(username, password, null, null);
    }


  //**************************************************************************
  //** getConnection
  //*************************************************************************/
  /**  Used to authenticate a user given a username/password and domain name.
   */
    public static LdapContext getConnection(String username, String password, String domainName) throws NamingException {
        return getConnection(username, password, domainName, null);
    }


  //**************************************************************************
  //** getConnection
  //*************************************************************************/
  /** Used to authenticate a user given a username/password and domain name.
   *  Provides an option to identify a specific a Active Directory server.
   */
    public static LdapContext getConnection(String username, String password, String domainName, String serverName) throws NamingException {

        if (domainName==null){
            try{
                String fqdn = java.net.InetAddress.getLocalHost().getCanonicalHostName();
                if (fqdn.split("\\.").length>1) domainName = fqdn.substring(fqdn.indexOf(".")+1);
            }
            catch(java.net.UnknownHostException e){}
        }
        
        //System.out.println("Authenticating " + username + "@" + domainName + " through " + serverName);

        if (password!=null){
            password = password.trim();
            if (password.length()==0) password = null;
        }

        //bind by using the specified username/password
        Hashtable props = new Hashtable();
        String principalName = username + "@" + domainName;
        props.put(Context.SECURITY_PRINCIPAL, principalName);
        if (password!=null) props.put(Context.SECURITY_CREDENTIALS, password);


        String ldapURL = "ldap://" + ((serverName==null)? domainName : serverName + "." + domainName) + '/';
        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        props.put(Context.PROVIDER_URL, ldapURL);
        try{
            return new InitialLdapContext(props, null);
        }
        catch(javax.naming.CommunicationException e){
            throw new NamingException("Failed to connect to " + domainName + ((serverName==null)? "" : " through " + serverName));
        }
        catch(NamingException e){
            throw new NamingException("Failed to authenticate " + username + "@" + domainName + ((serverName==null)? "" : " through " + serverName));
        }
    }


  //**************************************************************************
  //** getUser
  //*************************************************************************/
  /** Used to check whether a username is valid.
   *  @param username A username to validate (e.g. "peter", "peter@acme.com",
   *  or "ACME\peter").
   */
    public static User getUser(String username, LdapContext context) {
        try{
            String domainName = null;
            if (username.contains("@")){
                username = username.substring(0, username.indexOf("@"));
                domainName = username.substring(username.indexOf("@")+1);
            }
            else if(username.contains("\\")){
                username = username.substring(0, username.indexOf("\\"));
                domainName = username.substring(username.indexOf("\\")+1);
            }
            else{
                String authenticatedUser = (String) context.getEnvironment().get(Context.SECURITY_PRINCIPAL);
                if (authenticatedUser.contains("@")){
                    domainName = authenticatedUser.substring(authenticatedUser.indexOf("@")+1);
                }
            }

            if (domainName!=null){
                String principalName = username + "@" + domainName;
                SearchControls controls = new SearchControls();
                controls.setSearchScope(SUBTREE_SCOPE);
                controls.setReturningAttributes(userAttributes);
                NamingEnumeration<SearchResult> answer = context.search( toDC(domainName), "(& (userPrincipalName="+principalName+")(objectClass=user))", controls);
                if (answer.hasMore()) {
                    Attributes attr = answer.next().getAttributes();
                    Attribute user = attr.get("userPrincipalName");
                    if (user!=null) return new User(attr);
                }
            }
        }
        catch(NamingException e){
            //e.printStackTrace();
        }
        return null;
    }


  //**************************************************************************
  //** getUsers
  //*************************************************************************/
  /** Returns a list of users in the domain.
   */
    public static User[] getUsers(LdapContext context) throws NamingException {

        java.util.ArrayList<User> users = new java.util.ArrayList<User>();
        String authenticatedUser = (String) context.getEnvironment().get(Context.SECURITY_PRINCIPAL);
        if (authenticatedUser.contains("@")){
            String domainName = authenticatedUser.substring(authenticatedUser.indexOf("@")+1);
            SearchControls controls = new SearchControls();
            controls.setSearchScope(SUBTREE_SCOPE);
            controls.setReturningAttributes(userAttributes);
            NamingEnumeration answer = context.search( toDC(domainName), "(objectClass=user)", controls);
            try{
                while(answer.hasMore()) {
                    Attributes attr = ((SearchResult) answer.next()).getAttributes();
                    Attribute user = attr.get("userPrincipalName");
                    if (user!=null){
                        users.add(new User(attr));
                    }
                }
            }
            catch(Exception e){}
        }
        return users.toArray(new User[users.size()]);
    }


    private static String toDC(String domainName) {
        StringBuilder buf = new StringBuilder();
        for (String token : domainName.split("\\.")) {
            if(token.length()==0)   continue;   // defensive check
            if(buf.length()>0)  buf.append(",");
            buf.append("DC=").append(token);
        }
        return buf.toString();
    }


  //**************************************************************************
  //** User Class
  //*************************************************************************/
  /** Used to represent a User in Active Directory
   */
    public static class User {
        private String distinguishedName;
        private String userPrincipal;
        private String commonName;
        public User(Attributes attr) throws javax.naming.NamingException {
            userPrincipal = (String) attr.get("userPrincipalName").get();
            commonName = (String) attr.get("cn").get();
            distinguishedName = (String) attr.get("distinguishedName").get();

        }

        public String getUserPrincipal(){
            return userPrincipal;
        }

        public String getCommonName(){
            return commonName;
        }

        public String getDistinguishedName(){
            return distinguishedName;
        }

        public String toString(){
            return getDistinguishedName();
        }

      /** Used to change the user password. Throws an IOException if the Domain
       *  Controller is not LDAPS enabled.
       *  @param trustAllCerts If true, bypasses all certificate and host name
       *  validation. If false, ensure that the LDAPS certificate has been
       *  imported into a trust store and sourced before calling this method.
       *  Example:
          String keystore = "/usr/java/jdk1.5.0_01/jre/lib/security/cacerts";
          System.setProperty("javax.net.ssl.trustStore",keystore);
       */
        public void changePassword(String oldPass, String newPass, boolean trustAllCerts, LdapContext context) 
        throws java.io.IOException, NamingException {
            String dn = getDistinguishedName();


          //Switch to SSL/TLS
            StartTlsResponse tls = null;
            try{
                tls = (StartTlsResponse) context.extendedOperation(new StartTlsRequest());
            }
            catch(Exception e){
                //"Problem creating object: javax.naming.ServiceUnavailableException: [LDAP: error code 52 - 00000000: LdapErr: DSID-0C090E09, comment: Error initializing SSL/TLS, data 0, v1db0"
                throw new java.io.IOException("Failed to establish SSL connection to the Domain Controller. Is LDAPS enabled?");
            }


          //Exchange certificates
            if (trustAllCerts){
                tls.setHostnameVerifier(DO_NOT_VERIFY);
                SSLSocketFactory sf = null;
                try {
                    SSLContext sc = SSLContext.getInstance("TLS");
                    sc.init(null, TRUST_ALL_CERTS, null);
                    sf = sc.getSocketFactory();
                }
                catch(java.security.NoSuchAlgorithmException e) {}
                catch(java.security.KeyManagementException e) {}
                tls.negotiate(sf);
            }
            else{
                tls.negotiate();
            }


          //Change password
            try {
                //ModificationItem[] modificationItems = new ModificationItem[1];
                //modificationItems[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", getPassword(newPass)));

                ModificationItem[] modificationItems = new ModificationItem[2];
                modificationItems[0] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE, new BasicAttribute("unicodePwd", getPassword(oldPass)) );
                modificationItems[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute("unicodePwd", getPassword(newPass)) );
                context.modifyAttributes(dn, modificationItems);
            }
            catch(javax.naming.directory.InvalidAttributeValueException e){
                String error = e.getMessage().trim();
                if (error.startsWith("[") && error.endsWith("]")){
                    error = error.substring(1, error.length()-1);
                }
                System.err.println(error);
                //e.printStackTrace();
                tls.close();
                throw new NamingException(
                    "New password does not meet Active Directory requirements. " +
                    "Please ensure that the new password meets password complexity, " +
                    "length, minimum password age, and password history requirements."
                );
            }
            catch(NamingException e) {
                tls.close();
                throw e;
            }

          //Close the TLS/SSL session
            tls.close();
        }

        private static final HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };

        private static TrustManager[] TRUST_ALL_CERTS = new TrustManager[]{
        new X509TrustManager() {
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return null;
            }
            public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType) {
            }
            public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType) {
            }
        }
        };


        private byte[] getPassword(String newPass){
            String quotedPassword = "\"" + newPass + "\"";
            //return quotedPassword.getBytes("UTF-16LE");
            char unicodePwd[] = quotedPassword.toCharArray();
            byte pwdArray[] = new byte[unicodePwd.length * 2];
            for (int i=0; i<unicodePwd.length; i++) {
                pwdArray[i*2 + 1] = (byte) (unicodePwd[i] >>> 8);
                pwdArray[i*2 + 0] = (byte) (unicodePwd[i] & 0xff);
            }
            return pwdArray;
        }
    }
}

Acknowledgment

This code is based on work posted by DV on StackOverflow.