I was having a bit hard time to pick up the title for this post, since not wanting to create the impression that it would talk about using LDAP server to achieve SSO for any applications. This post is actually about implementing SSO for a client application that happens to connect to any LDAP server (specifically servers with LDAPv3 capability).
Over years, I have the need to consult the corporate LDAP server (running from Windows 2003 AD) to get user information, such as email address, status, group membership. I was also granted the permission to update a portion of our LDAP structure to support LDAP authentication for Documentum users. To alleviate my burden to query LDAP server for so many users, I have implemented a Java utility to search and modify the portion of LDAP server. Not surprisingly, I had used simple binding for years. Since that’s the only binding option I knew at the very beginning and it had worked for me quite well.
During the procedure to work on SSO solution for Documentum Webtop and Kerberos setup, the word SASL popped up many times during my searching and cought my eyes. From then I had been thinking that since LDAPv3 supports SASL, there must be a way to use it to talk to LDAP server. Given the fact that SASL GSSAPI mechanism is actually using Kerberos, it was very likely that SSO could be achieved for my LDAP utility. Here is the journey for me to reach that goal.
Simple binding
With the help of JNDI/LDAP service provider, binding to LDAP server is very straitforward. Here is the way to do simple binding:
// Set up the environment for creating the initial context Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); // Must use fully qualified domain name of the LDAP server within the URL env.put(Context.PROVIDER_URL, "ldap://FQDN:389); // Authenticate as mypersonal account using the password env.put(Context.SECURITY_PRINCIPAL, "CN=mypersonal,DC=corpnet,DC=mycorp,DC=com"); env.put(Context.SECURITY_CREDENTIALS, "mypassword"); // Specify simple binding is used env.put(Context.SECURITY_AUTHENTICATION, "simple"); // Create the initial context DirContext ctx = new InitialDirContext(env); // ... do something useful with ctx
When using above ‘simple’ binding, the credential is passed through network as plain text. With the following line adding to the environment configuration, you could see the password is just passed around for the authentication.
env.put("com.sun.jndi.ldap.trace.ber", System.out);
Fortunately, we have SASL supported by many commonly used LDAP servers, which provides a security layer for authentication to LDAP server.
DIGEST-MD5 SASL mechanism
SASL authentication supports several mechanisms, such as DIGEST-MD5, EXTERNAL, and GSSAPI. Many literatures talked about how to use DIGEST-MD5 mechanism to bind to LDAP server. A discussion topic started by SteveHB from Why DIGEST-MD5 Authentication Does Work covers many aspects of this mechanism. Interestingly, all the three formats of security pricipal listed by SteveHB for Windows 2003 AD worked for me under my environment with both JDK1.6.0_18 and JDK1.5.0_12. Maybe the AD configuration contributed to the difference. In order to use SASL DIGEST-MD5, the only change to the above simple binding example is to use the following line for Context.SECURITY_AUTHENTICATION (both successful and failed principal formats are also listed):
// Specify SASL DIGEST5-MD authentication env.put(Context.SECURITY_AUTHENTICATION, "DIGEST5-MD"); // Principal formats successful env.put(Context.SECURITY_PRINCIPAL, "mypersonal"); env.put(Context.SECURITY_PRINCIPAL, "FULL_DOMAIN_NAME\\mypersonal"); env.put(Context.SECURITY_PRINCIPAL, "mypersonal@REALM_NAME"); // Principal format failed for me // env.put(Context.SECURITY_PRINCIPAL, "DN for mypersonal");
You may refer to SteveHB’s post for other formats to provide Context.SECURITY_PRINCIPAL for SunOne Directory Server or AD2000.
GSS-API SASL mechanism
The GSS-API SASL mechanism was actually retrofitted to mean only Kerberos v5, although it was originally intended to support any GSS-API implementation. Most of the configuration here is similar to that presented in my previous post. For the sake of completeness, all the necessary configuration is listed below.
krb5.ini file
File krb5.ini is saved under C:\Windows, and it contains the following:
[libdefaults] default_realm = CORPNET.MYCORP.COM default_tkt_enctypes = rc4-hmac default_tgs_enctypes = rc4-hmac [realms] CORPNET.MYCORP.COM = { kdc = myonedomaincontroller.corpnet.mycorp.com default_domain = CORPNET.MYCORP.COM } [domain_realm] .CORPNET.MYCORP.COM = CORPNET.MYCORP.COM
gssapi-jaas.conf
The content of the file is as following:
SaslSso2Ldap { com.sun.security.auth.module.Krb5LoginModule required client=TRUE // debug=true useTicketCache=true principal="mypersonal"; };
It should be put under the same location as the JNDI/LDAP client class file.
import javax.naming.*; import javax.naming.directory.*; import javax.security.auth.login.*; import javax.security.auth.Subject; import java.util.Hashtable; class SaslSso2Ldap { public static void main(String[] args) { LoginContext context = null; try { // 1. Log in (to Kerberos) context = new LoginContext(SaslSso2Ldap.class.getName()); context.login(); // 2. Perform JNDI work as logged in Subject Subject.doAs(context.getSubject(), new JndiAction()); // 3. Logout the Subject context.logout(); } catch (LoginException e) { e.printStackTrace(); } } } /** * The application must supply a PrivilegedAction that is to be run * inside a Subject.doAs() or Subject.doAsPrivileged(). */ class JndiAction implements java.security.PrivilegedAction { public Object run() { performJndiOperation(args); return null; } private static void performJndiOperation() { // Set up environment for creating initial context Hashtable env = new Hashtable(11); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); // Must use fully qualified hostname env.put(Context.PROVIDER_URL, "ldap://myldapserver.corpnet.mycorp.com:389"); // Request the use of the "GSSAPI" SASL mechanism // Authenticate by using already established Kerberos credentials env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); env.put("com.sun.jndi.ldap.trace.ber", System.out); try { /* Create initial context */ DirContext ctx = new InitialDirContext(env); System.out.println(ctx.getAttributes("")); // do something useful with ctx // Close the context when we're done ctx.close(); } catch (NamingException e) { e.printStackTrace(); } } }
You may notice that there are probably more comments than the code itself. It’s just that simple to achieve SSO for JNDI/LDAP client. The following command is used to test the program:
java -Djava.security.auth.login.config=gssapi_jaas.conf SaslSso2Ldap
Something could go wrong
– Make sure the LDAP server you are testing against supports SASL. Since Windows AD does not support anonymous binding, you would need some other means to connect to AD to get the information. The following was obtained using LDAP Browser.
– The value provided for Context.PROVIDER_URL has to be a welformed URL, and the server host must be the fully qualified domain name. The reason for this is that the Java Service provider will derive the service principal from the URL (not surprisingly, the service name is ldap). You may refer to How should I configure my DNS for Kerberos? for further explanation.
– Make sure the LDAP server is known to KDC and SPN associated with it should be resolvable from the URL of the LDAP server.
With the above configuration, my LDAP client never needs me to provide my login credential. In addition to increase the security of my connection to AD, I do not need worry about the password change periodically for my utility any more.
References
JNDI/LDAP Service Provider
GSS-API/Kerberos v5 Authentication
SASL Authentication
JNDI/LDAP SASL Examples
How should I configure my DNS for Kerberos?
What is SASL?