Skip to main content

Active Directory authentication with Hudson

Posted by kohsuke on January 10, 2008 at 12:48 PM PST

This post is a little rant about the sorry state of Java when it comes to interfacing with native libraries, and my baby step to fight that problem.

Rant

As you know, "write once run anywhere" is one of the mantra of Java, but it seems to me that this is often used in a wrong way. Yes, having the ability to write a program that runs anywhere is great, and clearly there are many situations where I'd like this. But the mantra is often used to actively sabotage those who want to write a program that takes advantages of some environments, and this reduces the value of Java, as opposed to strengthen it.

When I look at what Sun has been doing, we clearly don't believe in letting people use Java to write a great program that takes advantages of a particular environment, despite the fact that not every program needs to run everywhere.

There are many examples for this; Java hasn't even let me access environment variables until recently. I still can't write a good behaving Unix daemon in Java, because it requires POSIX API. Or I can't reliably tell if a file is a symbolic link or not. In contrast, doing this is a breeze in many other languages, like Perl or Ruby.

The latest incarnation of this pain for me is in the context of Hudson around Active Directory authentication. I'm sure many people deploy AD for their network, and in such a situation, it makes sense to authenticate the user against AD. AD handles group/user membership, as well as other useful information about the user like e-mail address or name, and in that way users can have a single point of identity management.

So now, how does a Java program talks to AD? One is to talk to AD as LDAP. This can be made to work and Java has good support for speaking LDAP, but it takes a lot of effort for my users to make this work; they would first have to manually configure where their AD servers are. Then they'd have to give me the admin username and password (because AD doesn't allow anonymous LDAP access out of the box.) Early AD implementations in Windows 2000 don't support the inetOrgPerson schema for user information, so finally one needs additional configuration so that LDAP queries will be performed in the correct way.

All in all, it adds up to quite a lot of options that you have to get right. You can see how bad it is by looking at documents like this, and you can see the typical symptom of bad engineering — too many things that need the right configuration.

We can do this a lot better, if only we can talk to the API that Microsoft designed to access AD, called ADSI. With ADSI, you don't need to specify where the servers are. AD is LDAP+Kerberos+DNS, and it auto-discovers nearby AD server to talk to (this also means the outage of a particular AD server won't affect the program as long as other AD servers are online.) Access to ADSI happens under the owner of the process (roughly speaking), so no separate "manager user/password" is necessary. If a Java program could only use ADSI, it can eliminate the configuration completely. The user would only need to say "use Active Directory", and everything else just works.

This is what I wanted for Hudson. Every configuration that the user needs to do will make Hudson that much harder to use. It's very important for me that Hudson just works.

Active Directory integration in Hudson

To make this combination "just work", this support builds on top of my recent com4j improvements. com4j is another one of my hobby projects, and it provides easy way to talk to COM API by taking advantages of Java5 features. ADSI is available as COM components, so by using com4j I can call into ADSI and do the authentication.

The result is extremely simple configuration — you just select "Active Directory" option, and that's it.

How this works?

Making things simpler for users normally means doing more work on my side, and this is no exception. Since I thought other people might be interested in integrating AD to Java program, here's how you can do it.

First you need to figure out the domain name of AD. The following code lets you do this. The resulting defaultNamingContext is the domain name in LDAP distinguished name format, like DC=acme,DC=com

import com4j.typelibs.activeDirectory.*;

IADs rootDSE = COM4J.getObject(IADs.class, "LDAP://RootDSE", null);
defaultNamingContext = (String)rootDSE.get("defaultNamingContext");

Searching LDAP requires ADO, so it's good to create a connection upfront for reuse.

import com4j.typelibs.ado20.*;

con = ClassFactory.createConnection();
con.provider("ADsDSOObject");
con.open("Active Directory Provider",""/*default*/,""/*default*/,-1/*default*/);

Now, authenticating an user is a three step process. First, you query LDAP to find out the LDAP DN for the given user from the login ID, then check the password by trying to bind as that user. Once that succeeds, you can further query LDAP to find out more about this user, such as the groups s/he belongs to, phone number, name, e-mail address, etc. You'll notice that type-safe interfaces like IADsUser lets you see other information available in LDAP.

<br />_Command cmd = ClassFactory.createCommand();<br />cmd.activeConnection(con);<br /><br />cmd.commandText("<LDAP://"+defaultNamingContext+">;(sAMAccountName="+username+");distinguishedName;subTree");<br />_Recordset rs = cmd.execute(null, Variant.MISSING, -1/*default*/);<br />if(rs.eof())<br />    throw new UsernameNotFoundException("No such user: "+username);<br /><br />String dn = rs.fields().item("distinguishedName").value().toString();<br /><br /><br />// now we got the DN of the user<br />IADsOpenDSObject dso = COM4J.getObject(IADsOpenDSObject.class,"LDAP:",null);<br /><br />// to do bind with DN as the user name, the flag must be 0<br />IADsUser usr;<br />try {<br />    usr = dso.openDSObject("LDAP://"+dn, dn, password, 0).queryInterface(IADsUser.class);<br />} catch (ComException e) {<br />    throw new BadCredentialsException("Incorrect password for "+username);<br />}<br /><br />for( Com4jObject g : usr.groups() ) {<br />    IADsGroup grp = g.queryInterface(IADsGroup.class);<br />    System.out.println("Belong to group "+grp.name());<br />}<br />

Conclusion

So that's it. I hope I showed you that Java's lack of good native library support can result in bad usability, and having more investments (like com4j) in this space can improve this situation. I want a library that can call shared libraries (like this, and I want a library that can call POSIX APIs, like this. I hope the Java community as a whole would learn more about what the native libraries offer, and I hope Sun would give us something better than JNI.

Related Topics >>

Comments

Hi! I am struggling how to find a way to connect to AD in ...

Hi! I am struggling how to find a way to connect to AD in java till I found this post and direct me to your blog. I am currently using Spring LDAP to connect and authenticate user in AD since I am already in spring framework. Now, someone asked instead of LDAP, use Active Directory Web Service. My question is, will that be possible? As of my research, ADWS is offered to Servers and allows them to connect to AD using an Interface (which is ADWS itself) and does not offer APIs to use in java. My conclusion is that using ADWS in java is not possible. Can you give me some thoughts or confirmation with this? Thank you very much!

ldaps

We followed your instructions and it worked like a charm. Thank You! For our main environments we are required to use ldaps (ldap over ssl). Just wondering if you could give us a couple of pointers on how to configure hudson to use ldaps.

x97mdr, can you share the stack trace?

I have had a problem using this on Windows Server 2003 x64 edition. When I set the authentication mode to Active Directory and hit the 'Save' button an exception message pops up saying that it cannot create the ActiveDirectory bean ... ?

jbaruch -- I don't understand your question. This is about how you authenticate a user ID and password against AD. It's your business to ask your user to enter those values. I'm just showing how you can verify the username and password pair against AD.

kgolomb -- I don't know what ADS_SECURE_AUTHENTICATION does. Does LDAP send the user name and password in clear text? Is that the point of using this flag?

azeemirshad -- COM is a Windows technology. It only works on Windows. If you need to authenticate against AD from Unix, see my other post where I show how.

I am trying to use com4j in a jsp application deployed on websphere 6.1 on solaris platform. It gives the exception for com4j.dll. will com4j work on websphere Thanks, Azeem

openDSObject is used without the ADS_SECURE_AUTHENTICATION option. Any reason for the secure option being missing. Maybe make it an option for those who want to secure the passwords of users authenticating via this plugin?

I saw you use username and password fields in your code. How can I set their values? There are no textfields for them in the UI. Thanks, Baruch.

I think my point is that today the only way to get support for those is by having them standardized in JavaSE --- it's awfully hard to do this as a library because of the lack of the good native library interfacing.

So you are right that if JavaSE could provide support for all of them, it would be great and my problem is fixed, but I think more practical approach is for JavaSE to define good native interface, then let all those things happen in the "user space."

It looks to me like symbolic links, daemons/services and AD can be implemented in a cross platform way in any case. Symbolic links are new to Windows Vista, and reparse points have been around since Windows 2000, so Java should support them anyway; Unix daemons and Windows services could use a custom executable, like java.exe vs. javaw.exe on Windows, that worked correctly on Mac OS X, Linux, Solaris, AIX, Windows, etc.; and ADSI could be supported through JNDI like DNS and LDAP.

Hi, I like what you are trying to do. I have been trying to get your sample code going and I keep running into the exception below. Exception in thread "main" com4j.ComException: 8000500d (Unknown error) : The directory property cannot be found in the cache. : .\invoke.cpp:460 at com4j.Wrapper.invoke(Wrapper.java:122) at $Proxy5.get(Unknown Source) at com.cibc.sso.ActiveDirectorySSO.main(ActiveDirectorySSO.java:22) Caused by: com4j.ComException: 8000500d (Unknown error) : The directory property cannot be found in the cache. : .\invoke.cpp:460 at com4j.Native.invoke(Native Method) at com4j.StandardComMethod.invoke(StandardComMethod.java:95) at com4j.Wrapper$InvocationThunk.call(Wrapper.java:258) at com4j.Task.invoke(Task.java:44) at com4j.ComThread.run0(ComThread.java:149) at com4j.ComThread.run(ComThread.java:125) Also when I switch my active directory root to DC=com instead of the full DN of the root I get this exception: Exception in thread "main" com4j.ExecutionException: com4j.ComException: 8007202b Failed to MkParseDisplayName : A referral was returned from the server. : .\com4j.cpp:196 at com4j.ComThread.execute(ComThread.java:189) at com4j.Task.execute(Task.java:23) at com4j.COM4J.getObject(COM4J.java:224) at com.cibc.sso.ActiveDirectorySSO.main(ActiveDirectorySSO.java:21) Caused by: com4j.ComException: 8007202b Failed to MkParseDisplayName : A referral was returned from the server. : .\com4j.cpp:196 at com4j.Native.getObject(Native Method) at com4j.COM4J$GetObjectTask.call(COM4J.java:239) at com4j.COM4J$GetObjectTask.call(COM4J.java:227) at com4j.Task.invoke(Task.java:44) at com4j.ComThread.run0(ComThread.java:149) at com4j.ComThread.run(ComThread.java:125) In only get the first exception if I add the root properly. It seems it can't handle referals properly? Also it seems that you need to specify the username and password you want to use. It seems there is no way to integrate with the Windows Integrated Authentication to automatically sign in to Active Directory with your machines username and password as ADSI can.