JNDI — Java Naming & Directory Interface

Introduction

While researching an upcoming JAAS article, the decision was made to use JNDI to interact with the Openldap server that was stored user information. JNDI is used extensively in the J2EE landscape; so, exploring JNDI separately from JAAS seemed in order.

JNDI-Java Naming and Directory Interface-provides naming and directory information services to applications written in Java. So, what does this mean? It provides a Java API for applications interacting with services such DNS, LDAP, NIS/NIS+, etc in an implementation independent fashion. JNDI also provides an SPI-Service Provider interface-that allows JNDI to interact with a variety of naming and directory implementations by writing plugins that implement the appropriate details, but keep those hidden from the API-side of JNDI.

The Service Providers that JNDI provides support for can be found here. These provide Java applications with the ability to interact with LDAP, DNS, NIS, NDS, RMI, Corba, etc through the JNDI API.Specifications & Architecture

Sun publishes a JNDI API and JNDI SPI architecture documents. For easier reading, there is also an Executive Summary that is worth reading. JNDI API & SPI Specifications are given here. Of course, this is part of the larger JSE Specification Javadoc.

Naming

For Java code that is going to use the JNDI API, the first step is to establish an Initial Context using a javax.naming.InitialContext object. For anyone who has ever done any programming in a J2EE environment, this should sound familiar. Establishing an Initial Context to the J2EE Container’s JNDI Context is the first step to performing any non-trivial task.

The InitialContext object implements the Context Interface. A Context represents the starting point for Naming operations.

The InitialContext object provides a starting point for performing naming operations with JNDI. Whether connecting to a remote LDAP server or interacting with a local J2EE JNDI Context, the same basic naming operations are being performed.

Naming services are an important part of any computer system–a JVM in no exception. The Naming Services provided by JNDI provides for mapping names to objects and finding objects based upon resolving names.

Names are resolved to objects by the naming service. Name syntax is defined by the naming service; every

The association between a name and an object is called a binding. Depending on the nature of the objects, it may not be possible to place the objects in the naming service. In this case, a pointer or reference would need to be stored instead.

The set of name-to-object bindings makes up the Context. The Context provides a lookup, binding, unbinding, and listing operations. One Context can be contained within another Context that uses the same naming syntax–making a subcontext. A connected set of Contexts that have the same naming syntax and operations make up a naming system. A set of names in a naming system make up a namespace. A well-known namespace example, would be the JNDI tree in a J2EE Container.

See the Sun Documentation for more information on Naming concepts.

Directory

A Directory Service is a Naming Service that also allows attributes to be associated with the objects that provides a set of operations for manipulating and searching Attributes attached to Directory Objects. An LDAP Server is a good example of this, but, by no means is the only directory.

A Directory Object represents an entity in an environment–very abstract, but very flexible as well. A Directory Object could be workstations, servers, users, printers, etc.

An Attribute is associated with a Directory Object. An Attribute has an identifier, Attribute Identifier, and values, Attribute Values. An Attribute Identifier can uniquely identify an Attribute independently of of the Attribute Values. Semantic meaning is given to Attribute Values by the applications that use the information.

A Directory is a connected set of Directory Objects–in a similar way to a set of names making up a namespace. If a Directory arranges its objects in a hierarchical fashion, then it also serves as a Naming Service.

See the Sun documentation for more information on Directory Concepts.

JNDI Use In J2EE

As mentioned in the Introduction section, JNDI is used extensively in J2EE; thus, the interest in it. JNDI has been called the glue that holds J2EE together.

The JNDI Tree represents the Namespace or Context available in the J2EE Container.

To access a Datasource, EJB, JMS Connection Factory, or numerous other J2EE resources, a JNDI InitialContext must be be created and lookup on the object’s name must be performed.

Note, in more recent J2EE Spec versions, names in the JNDI Tree can be referenced via Resource References defined in applications’ deployment descriptors; this decouples the application from the JNDI names. This is where lookups on names such as “java:comp/env/jdbc/DS1” for a Resource Reference called “jdbc/DS1” which maps to the JNDI name “jdbc/myDataSource” (where this JNDI name belongs to a datasource configured in the J2EE container). This is the recommended approach to accessing J2EE resources.

Examples

There are many different JNDI examples that can be given. Here, we will concentrate on using JNDI to access an LDAP Server. In another article, a brief introduction to Openldap will be given. There are many different ways to use LDAP with Java code (and JNDI). For example, you could simply use JNDI to create an InitialContext using a valid LDAP userid & password for authentication purposes, you could lookup attributes about a user stored as an InetOrgPerson schema object, or you could store Java objects in the LDAP server.

Two examples will be given here: authenticating users and storing/retrieving Java objects.

Authenticating Users

The need to authenticate users is a common scenario. There are several ways to accomplish this. If the users are defined in an LDAP Server, you can either verify a user’s credentials (id & password) by binding to LDAP with the supplied credentials or by comparing the password attribute value against that supplied by the entity being authenticated. The pros and cons of using each of these methods for authenticating users with an LDAP server will be left for another discussion. But, in this example, it is necessary to bind to LDAP as the provided user and subsequently look up group memberships to make authorization decisions.

Assuming that the standard InetOrgPerson schema is defined in your LDAP server and the user is valid InetOrgPerson object in the LDAP tree, then the first can be done–this method will be presented here. The details of setting up an LDAP Server are not important here, but an assumption is being made that all users have a suffix of the form “ou=Users,dc=thinkmiddleware,dc=com”. Furthermore, an InetOrgPerson object can be uniquely identified in this LDAP tree as “cn=userName,ou=Users,dc=thinkmiddleware,dc=com”. Five thousand users were loaded into LDAP in the “ou=Users,dc=thinkmiddleware,dc=com” branch; the usernames correspond to the first 5000 natural numbers–1,2,3…4999, 5000. The password for each user is “secret”. The LDAP Server is listening on localhost:389.

The details of using JNDI to connect to LDAP with a user’s credentials are implemented inJndiLdapConnection.java. A test program, LDAPTest.java, uses a JndiLdapConnection object to authenticate a username and password, provided by a user, by attempting to bind to LDAP using a JNDI InitialContext. If the bind operation is successful, the user’s credentials must be valid–the user has been authenticated. Otherwise, the user is not authenticated.

An Ant build.xml file is provided. All three files can be downloaded in a JAR file given at the bottom of this article.

Compiling and running this programing using “ant run” produces the following output:

$ ant run
Buildfile: build.xml

init:

compile:

copies:

jars:

build:

run:
Enter username:
1234
Enter password:
secret
Authentication Successful.

BUILD SUCCESSFUL

So, the username/password combination 1234/secret is a valid userid. If an invalid username/password were entered, the output would look similar to the following:

$ ant run
Buildfile: build.xml

init:

compile:

copies:

jars:

build:

run:
Enter username:
1234
Enter password:
welcome
javax.naming.AuthenticationException: [LDAP: error code 49 – Invalid Credentials]
at com.sun.jndi.ldap.LdapCtx.mapErrorCode(LdapCtx.java:3023)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2969)
at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2771)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2685)
at com.sun.jndi.ldap.LdapCtx.(LdapCtx.java:305)
at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:193)
at com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:211)
at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:154)
at com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:84)
at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:684)
at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:305)
at javax.naming.InitialContext.init(InitialContext.java:240)
at javax.naming.InitialContext.(InitialContext.java:214)
at javax.naming.directory.InitialDirContext.(InitialDirContext.java:99)
at com.tm.jndi.ldap.JndiLdapConnection.connect(JndiLdapConnection.java:94)
at com.tm.jndi.ldap.JndiLdapConnection.connect(JndiLdapConnection.java:64)
at com.tm.jndi.tests.LDAPTest.authenticate(LDAPTest.java:25)
at com.tm.jndi.tests.LDAPTest.main(LDAPTest.java:39)
com.tm.jndi.ldap.ConnectionException
at com.tm.jndi.ldap.JndiLdapConnection.connect(JndiLdapConnection.java:98)
at com.tm.jndi.ldap.JndiLdapConnection.connect(JndiLdapConnection.java:64)
at com.tm.jndi.tests.LDAPTest.authenticate(LDAPTest.java:25)
at com.tm.jndi.tests.LDAPTest.main(LDAPTest.java:39)

BUILD SUCCESSFUL
Total time: 7 seconds

The LDAP Server returned an “Invalid Credentials” error.

Storing & Retrieving Java Objects From LDAP

The bases of a JNDI tree in a J2EE Container could be implemented in an LDAP Server; although, there are many different ways to do it. The next example demonstrates storing a J2EE Object, an Integer, into an LDAP Directory, retrieving it, and finally removing it (otherwise, it would be hard to rerun the same example).

Once again, the LDAP Server details are abstracted away from this tutorial. The Java Schema would need to be added to the LDAP Server in order to put serialized Java objects in the directory. A user with administrative privileges will also be needed to perform writes to the LDAP database.

The Storage.java class provides an abstraction for interacting with Java Objects stored in LDAP. The JNDI LDAP calls needed to support this class are implemented in JndiLdapConnection.java. A simple test program, StorageTest.java, has been written to test this scenario.

The output of running the Ant “storageRun” rule is:

$ ant storageRun
Buildfile: build.xml

init:

compile:

copyConfig:

copies:

jars:

build:

storageRun:
icf=com.sun.jndi.ldap.LdapCtxFactory
url=ldap://localhost:389/dc=thinkmiddleware,dc=com
dn=cn=Manager,dc=thinkmiddleware,dc=com
password=secret
Context object appears to be valid.
j=12345

BUILD SUCCESSFUL
Total time: 1 second

So, an Integer object, i, with a value of 12345 is allocated and stored in LDAP with a Distinguished Name of “cn=12345,dc=thinkmiddleware,dc=com”. A lookup on this DN is then performed; the resulting Object is cast as an Integer object and assigned to j.

If the Storage.destroy() call is removed from StorageTest.java, the “cn=12345,dc=thinkmiddleware,dc=com” object will remain in LDAP. Before the test is run, the LDAP tree looks like:

After the test is run, the LDAP tree has a new node:

The object’s attributes listed in the right-hand pane shows the Java Object’s details. Note, the screenshots show the Ldap Browser tool available from http://www.mcs.anl.gov/~gawor/ldap/

Downloads

The LDAP authentication example’s source code and build.xml can be found here.

The “storing and retrieving Java objects from an LDAP server” example’s source can be found here.

References

[1] http://www.openldap.org
[2] http://java.sun.com/products/jndi/
[3] http://java.sun.com/j2se/1.5.0/docs/guide/jndi/index.html
[4] http://java.sun.com/products/jndi/tutorial/index.html
[5] http://java.sun.com/j2se/1.5.0/docs/guide/jndi/spec/jndi/index.html
[6] http://java.sun.com/j2se/1.5.0/docs/guide/jndi/spec/spi/spicover.frame.html
[7] http://java.sun.com/j2se/1.5.0/docs/guide/jndi/reference.html
[8] http://java.sun.com/j2se/1.5.0/docs/api/javax/naming/Context.html
[9] http://java.sun.com/products/jndi/tutorial/getStarted/concepts/directory.html
[10] http://java.sun.com/products/jndi/tutorial/getStarted/concepts/naming.html
[11] http://java.sun.com/j2ee/tutorial/1_3-fcs/doc/Resources2.html
[12] http://www.mcs.anl.gov/~gawor/ldap/