40

I need to do LDAP Authentication for an application.

I tried the following program:

import java.util.Hashtable;  

import javax.naming.Context;  
import javax.naming.NamingException;  
import javax.naming.ldap.InitialLdapContext;  
import javax.naming.ldap.LdapContext;  


public class LdapContextCreation {  
    public static void main(String[] args) {  
        LdapContextCreation ldapContxCrtn = new LdapContextCreation();  
        LdapContext ctx = ldapContxCrtn.getLdapContext();  
    }  
    public LdapContext getLdapContext(){  
        LdapContext ctx = null;  
        try{  
            Hashtable env = new Hashtable();  
            env.put(Context.INITIAL_CONTEXT_FACTORY,  "com.sun.jndi.ldap.LdapCtxFactory");  
            env.put(Context.SECURITY_AUTHENTICATION, "Simple");  
            //it can be <domain\\userid> something that you use for windows login  
            //it can also be  
            env.put(Context.SECURITY_PRINCIPAL, "username@domain.com");  
            env.put(Context.SECURITY_CREDENTIALS, "password");  
            //in following property we specify ldap protocol and connection url.  
            //generally the port is 389  
            env.put(Context.PROVIDER_URL, "ldap://server.domain.com");  
            ctx = new InitialLdapContext(env, null);  
            System.out.println("Connection Successful.");  
        }catch(NamingException nex){  
            System.out.println("LDAP Connection: FAILED");  
            nex.printStackTrace();  
        }  
        return ctx;  
    }  

}

Getting following exception:

LDAP Connection: FAILED
javax.naming.AuthenticationException: [LDAP: error code 49 - Invalid Credentials]
    at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3053)
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2999)
    at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2801)
    at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2715)
    at com.sun.jndi.ldap.LdapCtx.(LdapCtx.java:305)
    at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:187)
    at com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:205)
    at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:148)
    at com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:78)
    at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:235)
    at javax.naming.InitialContext.initializeDefaultInitCtx(InitialContext.java:318)
    at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:348)
    at javax.naming.InitialContext.internalInit(InitialContext.java:286)
    at javax.naming.InitialContext.init(InitialContext.java:308)
    at javax.naming.ldap.InitialLdapContext.(InitialLdapContext.java:99)
    at LdapContextCreation.getLdapContext(LdapContextCreation.java:27)
    at LdapContextCreation.main(LdapContextCreation.java:12)

Few more points to consider:

  1. Earlier I was using tomcat 5.3.5 but somebody told me that only tomcat 6 supports it so I downloaded tomcat 6.0.35 and currently using this version only.

  2. Configured server.xml and added the following code -

    <Realm className="org.apache.catalina.realm.JNDIRealm" 
                       debug="99" 
                       connectionURL="ldap://server.domain.com:389/"  
                       userPattern="{0}" />
    
  3. Commented the following code from server.xml -

    <!-- Commenting for LDAP
      <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
         resourceName="UserDatabase"/> -->
    
  4. Steps 2 and 3 from article

  5. Someone suggested that there are some jar files that are supposed to be copied to tomcat in order to run ldap authentication, is that something I need to do? And which jar files?

  6. Also, I am using the correct credentials for sure, then what is causing this issue?

  7. Is there a way I can figure out the correct attributes for LDAP in case I am using incorrect ones?

Gary
  • 13,303
  • 18
  • 49
  • 71
anujin
  • 773
  • 7
  • 24
  • 36
  • There are better libraries for this, but here is a Similar Question http://stackoverflow.com/a/12165647/1286621 I agree with @jasim about the principal. You need to figure out what format your LDAP server is using. Here's an example of my Active Directory Server "CN=bindUserName,CN=Users,DC=myDepartment,DC=myNetwork". The LDAP folks should pretty quickly be able to tell you what the format is. There are also gui tools out there that can connect to LDAP and browse the directories. Talk to your Admins first though. – Mike Sep 07 '12 at 14:21
  • 1
    Just one more comment, you are aware that there is usually a "Bind" user/password, one that has permission to lookup info in the LDAP Server correct? Once you Bind to the Server, you can then authenticate the users credentials. – Mike Sep 07 '12 at 14:26

4 Answers4

37

Following Code authenticates from LDAP using pure Java JNDI. The Principle is:-

  1. First Lookup the user using a admin or DN user.
  2. The user object needs to be passed to LDAP again with the user credential
  3. No Exception means - Authenticated Successfully. Else Authentication Failed.

Code Snippet

public static boolean authenticateJndi(String username, String password) throws Exception{
    Properties props = new Properties();
    props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    props.put(Context.PROVIDER_URL, "ldap://LDAPSERVER:PORT");
    props.put(Context.SECURITY_PRINCIPAL, "uid=adminuser,ou=special users,o=xx.com");//adminuser - User with special priviledge, dn user
    props.put(Context.SECURITY_CREDENTIALS, "adminpassword");//dn user password


    InitialDirContext context = new InitialDirContext(props);

    SearchControls ctrls = new SearchControls();
    ctrls.setReturningAttributes(new String[] { "givenName", "sn","memberOf" });
    ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    NamingEnumeration<javax.naming.directory.SearchResult> answers = context.search("o=xx.com", "(uid=" + username + ")", ctrls);
    javax.naming.directory.SearchResult result = answers.nextElement();

    String user = result.getNameInNamespace();

    try {
        props = new Properties();
        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        props.put(Context.PROVIDER_URL, "ldap://LDAPSERVER:PORT");
        props.put(Context.SECURITY_PRINCIPAL, user);
        props.put(Context.SECURITY_CREDENTIALS, password);

   context = new InitialDirContext(props);
    } catch (Exception e) {
        return false;
    }
    return true;
}
Atanu Sarkar
  • 371
  • 3
  • 2
  • 15
    I know this entry is a bit old, but I really have this burning question: Why do we need an Admin user, why can't we simple try to bind with the user credentials that we want to authenticate in the first place? – Marci-man Mar 01 '18 at 16:48
  • 3
    @Marci-man It is to simplify the parameter to be passed in your login() method. The `Context.SECURITY_PRINCIPAL` parameter may require the complete LDAP object address, or some specific naming convention, which is *not* simple to type. Also, the user object may be reorganized (e.g. user appointed to another department within the company) so the LDAP path may change. – aff Jun 10 '19 at 08:06
  • Question. When on secure port, LDAP connection times out after being idle for certain time. How do we prevent it from happening? Some SO posts point to using `ldap_abandon` and `LDAP_OPT_RECONNECT` but am not sure how to set these values here – JMD Feb 19 '20 at 20:51
29

This is my LDAP Java login test application supporting LDAP:// and LDAPS:// self-signed test certificate. Code is taken from few SO posts, simplified implementation and removed legacy sun.java.* imports.

Usage
I have run this in Windows7 and Linux machines against WinAD directory service. Application prints username and member groups.

$ java -cp classes test.LoginLDAP url=ldap://1.2.3.4:389 username=myname@company.fi password=mypwd

$ java -cp classes test.LoginLDAP url=ldaps://1.2.3.4:636 username=myname@company.fi password=mypwd

Test application supports temporary self-signed test certificates for ldaps:// protocol, this DummySSLFactory accepts any server cert so man-in-the-middle is possible. Real life installation should import server certificate to a local JKS keystore file and not using dummy factory.

Application uses enduser's username+password for initial context and ldap queries, it works for WinAD but don't know if can be used for all ldap server implementations. You could create context with internal username+pwd then run queries to see if given enduser is found.

LoginLDAP.java

package test;

import java.util.*;
import javax.naming.*;
import javax.naming.directory.*;

public class LoginLDAP {

    public static void main(String[] args) throws Exception {
        Map<String,String> params = createParams(args);

        String url = params.get("url"); // ldap://1.2.3.4:389 or ldaps://1.2.3.4:636
        String principalName = params.get("username"); // firstname.lastname@mydomain.com
        String domainName = params.get("domain"); // mydomain.com or empty

        if (domainName==null || "".equals(domainName)) {
            int delim = principalName.indexOf('@');
            domainName = principalName.substring(delim+1);
        }

        Properties props = new Properties();
        props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        props.put(Context.PROVIDER_URL, url); 
        props.put(Context.SECURITY_PRINCIPAL, principalName); 
        props.put(Context.SECURITY_CREDENTIALS, params.get("password")); // secretpwd
        if (url.toUpperCase().startsWith("LDAPS://")) {
            props.put(Context.SECURITY_PROTOCOL, "ssl");
            props.put(Context.SECURITY_AUTHENTICATION, "simple");
            props.put("java.naming.ldap.factory.socket", "test.DummySSLSocketFactory");         
        }

        InitialDirContext context = new InitialDirContext(props);
        try {
            SearchControls ctrls = new SearchControls();
            ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> results = context.search(toDC(domainName),"(& (userPrincipalName="+principalName+")(objectClass=user))", ctrls);
            if(!results.hasMore())
                throw new AuthenticationException("Principal name not found");

            SearchResult result = results.next();
            System.out.println("distinguisedName: " + result.getNameInNamespace() ); // CN=Firstname Lastname,OU=Mycity,DC=mydomain,DC=com

            Attribute memberOf = result.getAttributes().get("memberOf");
            if(memberOf!=null) {
                for(int idx=0; idx<memberOf.size(); idx++) {
                    System.out.println("memberOf: " + memberOf.get(idx).toString() ); // CN=Mygroup,CN=Users,DC=mydomain,DC=com
                    //Attribute att = context.getAttributes(memberOf.get(idx).toString(), new String[]{"CN"}).get("CN");
                    //System.out.println( att.get().toString() ); //  CN part of groupname
                }
            }
        } finally {
            try { context.close(); } catch(Exception ex) { }
        }       
    }

    /**
     * Create "DC=sub,DC=mydomain,DC=com" string
     * @param domainName    sub.mydomain.com
     * @return
     */
    private static String toDC(String domainName) {
        StringBuilder buf = new StringBuilder();
        for (String token : domainName.split("\\.")) {
            if(token.length()==0) continue;
            if(buf.length()>0)  buf.append(",");
            buf.append("DC=").append(token);
        }
        return buf.toString();
    }

    private static Map<String,String> createParams(String[] args) {
        Map<String,String> params = new HashMap<String,String>();  
        for(String str : args) {
            int delim = str.indexOf('=');
            if (delim>0) params.put(str.substring(0, delim).trim(), str.substring(delim+1).trim());
            else if (delim==0) params.put("", str.substring(1).trim());
            else params.put(str, null);
        }
        return params;
    }

}

And SSL helper class.

package test;

import java.io.*;
import java.net.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;    
import javax.net.*;
import javax.net.ssl.*;

public class DummySSLSocketFactory extends SSLSocketFactory {
    private SSLSocketFactory socketFactory;
    public DummySSLSocketFactory() {
        try {
          SSLContext ctx = SSLContext.getInstance("TLS");
          ctx.init(null, new TrustManager[]{ new DummyTrustManager()}, new SecureRandom());
          socketFactory = ctx.getSocketFactory();
        } catch ( Exception ex ){ throw new IllegalArgumentException(ex); }
    }

      public static SocketFactory getDefault() { return new DummySSLSocketFactory(); }

      @Override public String[] getDefaultCipherSuites() { return socketFactory.getDefaultCipherSuites(); }
      @Override public String[] getSupportedCipherSuites() { return socketFactory.getSupportedCipherSuites(); }

      @Override public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException {
        return socketFactory.createSocket(socket, string, i, bln);
      }
      @Override public Socket createSocket(String string, int i) throws IOException, UnknownHostException {
        return socketFactory.createSocket(string, i);
      }
      @Override public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException, UnknownHostException {
        return socketFactory.createSocket(string, i, ia, i1);
      }
      @Override public Socket createSocket(InetAddress ia, int i) throws IOException {
        return socketFactory.createSocket(ia, i);
      }
      @Override public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException {
        return socketFactory.createSocket(ia, i, ia1, i1);
      }
}

class DummyTrustManager implements X509TrustManager {
    @Override public void checkClientTrusted(X509Certificate[] xcs, String str) {
        // do nothing
    }
    @Override public void checkServerTrusted(X509Certificate[] xcs, String str) {
        /*System.out.println("checkServerTrusted for authType: " + str); // RSA
        for(int idx=0; idx<xcs.length; idx++) {
            X509Certificate cert = xcs[idx];
            System.out.println("X500Principal: " + cert.getSubjectX500Principal().getName());
        }*/
    }
    @Override public X509Certificate[] getAcceptedIssuers() {
        return new java.security.cert.X509Certificate[0];
    }
}
Community
  • 1
  • 1
Whome
  • 10,181
  • 6
  • 53
  • 65
7

You will have to provide the entire user dn in SECURITY_PRINCIPAL

like this

env.put(Context.SECURITY_PRINCIPAL, "cn=username,ou=testOu,o=test"); 
Ahmed Ashour
  • 5,179
  • 10
  • 35
  • 56
Andromeda
  • 12,659
  • 20
  • 77
  • 103
  • Not true, it depends on the server implementation. – Michael-O Sep 07 '12 at 12:05
  • 1
    @Michael-O Certainly it's true. (1) It's in the JNDI Specification. (2) LDAP authentication is accomplished via an LDAP 'bind' operation, on all LDAP server implementations. (3) It is the function of JNDI to overcome differences in server implementation. – user207421 Sep 07 '12 at 12:23
  • @EJP, I was talking about using the DN only for a bind process. – Michael-O Sep 07 '12 at 12:30
  • @Jasim, I tried your suggestion but still no luck:(. `env.put(Context.SECURITY_PRINCIPAL, "cn=username@domain.com,ou=ouname,o=oname"); ` Is there a way to figure out the correct attributes, just in case I am using incorrect ones. – anujin Sep 07 '12 at 12:36
  • @anujin you can try this tool http://ldaptool.sourceforge.net/ to get the correct dns. Michael-O EJP I was not aware of that...when i faced the same issue this is how i got it resolved..Thanks for pointing it out... – Andromeda Sep 07 '12 at 12:58
  • @Jasim - I will definitely try it in a while. – anujin Sep 07 '12 at 16:46
  • 1
    @Michael-O Using the entire DN in the bind process is specified in the LDAP protocol, RFC 4513. It is not server-dependent. If you know of an exception please provide it. – user207421 Sep 08 '12 at 00:13
  • Active Directory does not require that. There are several patterns to perform a bind in AD. – Michael-O Sep 08 '12 at 09:22
  • @Michael-O OK, thanks, but it isn't really 'server-dependent': you just have to make the usual exception for a Microsoft product not complying with the relevant specification. – user207421 Sep 09 '12 at 01:26
  • Well, what user will ever memory his DN and actually enter it? None. – Michael-O Sep 09 '12 at 08:40
1
// this class will authenticate LDAP UserName or Email

// simply call LdapAuth.authenticateUserAndGetInfo (username,password);

//Note: Configure ldapURI ,requiredAttributes ,ADSearchPaths,accountSuffex 

import java.util.*;

import javax.naming.*;

import java.util.regex.*;

import javax.naming.directory.*;

import javax.naming.ldap.InitialLdapContext;

import javax.naming.ldap.LdapContext;

public class LdapAuth {


private final static String ldapURI = "ldap://20.200.200.200:389/DC=corp,DC=local";

private final static String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";

private  static String[] requiredAttributes = {"cn","givenName","sn","displayName","userPrincipalName","sAMAccountName","objectSid","userAccountControl"};


// see you active directory user OU's hirarchy 

private  static String[] ADSearchPaths = 

{ 

    "OU=O365 Synced Accounts,OU=ALL USERS",

    "OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",

    "OU=In-House,OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",

    "OU=Torbram Users,OU=Users,OU=O365 Synced Accounts,OU=ALL USERS",

    "OU=Migrated Users,OU=TES-Users"

};


private static String accountSuffex = "@corp.local"; // this will be used if user name is just provided


private static void authenticateUserAndGetInfo (String user, String password) throws Exception {

    try {


        Hashtable<String,String> env = new Hashtable <String,String>();

        env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);

        env.put(Context.PROVIDER_URL, ldapURI);     

        env.put(Context.SECURITY_AUTHENTICATION, "simple");

        env.put(Context.SECURITY_PRINCIPAL, user);

        env.put(Context.SECURITY_CREDENTIALS, password);

        DirContext ctx = new InitialDirContext(env);

        String filter = "(sAMAccountName="+user+")";  // default for search filter username

        if(user.contains("@"))  // if user name is a email then
        {
            //String parts[] = user.split("\\@");
            //use different filter for email
            filter = "(userPrincipalName="+user+")";
        }

        SearchControls ctrl = new SearchControls();
        ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
        ctrl.setReturningAttributes(requiredAttributes);

        NamingEnumeration userInfo = null;


        Integer i = 0;
        do
        {
            userInfo = ctx.search(ADSearchPaths[i], filter, ctrl);
            i++;

        } while(!userInfo.hasMore() && i < ADSearchPaths.length );

        if (userInfo.hasMore()) {

            SearchResult UserDetails = (SearchResult) userInfo.next();
            Attributes userAttr = UserDetails.getAttributes();System.out.println("adEmail = "+userAttr.get("userPrincipalName").get(0).toString());

            System.out.println("adFirstName = "+userAttr.get("givenName").get(0).toString());

            System.out.println("adLastName = "+userAttr.get("sn").get(0).toString());

            System.out.println("name = "+userAttr.get("cn").get(0).toString());

            System.out.println("AdFullName = "+userAttr.get("cn").get(0).toString());

        }

        userInfo.close();

    }
    catch (javax.naming.AuthenticationException e) {

    }
}   
}
gdrt
  • 3,160
  • 4
  • 37
  • 56
Qasim Mirza
  • 71
  • 1
  • 2