Securing EJB Applications with Custom JBoss Login Modules

Posted 1721 days ago

JBoss comes out-of-the-box with a handful of great login modules, but lets face it, you need a custom one to integrate with your existing security infrastructure. This overview shows you how to secure your EJB application with a custom server login module, and also to use a custom client login module to authenticate your EJB client applications.

JBoss makes use of the Java Authentication and Authorization Service API to authenticate and authorize users in your EJB applications. If you are not already familiar with creating JAAS login modules, I would recommend that you read my entry on Creating JAAS Login Modules to get an understanding of how they work.

This overview will show you how to secure your EJB Applications in JBoss using JAAS, and how to authenticate your client applications to the JBoss server. I will assume that since you are at the level of creating custom login modules, that you already know how to set up your XML Descriptor file for role based authorization, and have already done so. If you haven't, you should... otherwise, it makes it difficult to test your login module! We won't go over the steps of creating an EJB, but for this example we'll assume that we have a stateless session bean called HelloBean. The HelloBean's remote interface defines one method, hello(), that returns the String "Hello" to the client. HelloBean's XML Descriptor is set up such that the caller must be in the role "ExampleRole".

So here's the scenario, you have an existing security infrastructure that provides username and password based authentication. You want to perform client side authentication, and have the server lookup what roles the user is in from some database or other part of the existing security infrastructure. My entry on Creating JAAS Login Modules will show you how to set up the client side login module that performs client side authentication. Everything in that entry holds true for this tutorial, with only a couple additional steps needed for JBoss. The first thing that is required is that in addition to your own custom login module, you also use JBoss's ClientLoginModule. To do this, your security domain configuration file should look like this:

exampleDomain {
     net.eskeeter.jaasexample.CustomLoginModule required;
     org.jboss.security.ClientLoginModule required;
};

JBoss's ClientLoginModule will get the username and password from the callback handler you initialized the login context with. If you look carefully at the implementation of the CustomCallbackHandler (in the jaas login module entry), you'll see that it only interacts with the user the first time it is called. Since each login module in the context shares the same instance of the callback handler, the user is not prompted for their credentials twice. There are other (and probably "better") ways to accomplish this using the shared state map, but this way is effective as well.

So now that your client is authenticated, you need to get the principal and credentials to the server. Note that if you do not need to perform further password authentication on the server side, there is no reason to pass the actual password to the server, although for JBoss, you do need to pass something to the server. In the implementation I wrote for jETIA, I pass the hard-coded string "IDontNeedThePassword" to the server as the credentials. So how do you get this information to the server? It's easy. JBoss's ClientLoginModule takes the username and password and puts them in the org.jboss.security.SecurityAssociation class. You get the information to the server by initializing your JNDI Naming Context with them as follows:

HashTable props = new HashTable();
props.put( Context.SECURITY_PRINCIPAL, 
           SecurityAssociation.getPrincipal() );
props.put( Context.SECURITY_CREDENTIALS, 
           SecurityAssociation.getCredential() ); 

InitialContext initialContext = new InitialContext( props );

From then on, when you use that naming context to lookup EJB Home and Remote references, that information can be obtained by the server in the caller's context. Using a Service Locator pattern (Core J2EE Patterns, Prentice Hall, Alur, Crupi and Malks) simplifies this, since you only have one initial context in the whole application.

So that's all there is to the client side additions. By requiring JBoss's ClientLoginModule and initializing the JNDI naming context with the principal and credentials, you have fulfilled all your client side obligations for using custom JBoss login modules. Implementing the server side login module is no more difficult than it was to do the client side.

To write the server side login module, all you need to do is implement the JAAS module, set up your EJB Application in a security domain, and tell JBoss to use your module for that domain. Recall that our client's CustomLoginModule subclasses the JAAS javax.security.auth.login.LoginModule class. The server side login module should subclass org.jboss.security.auth.spi.AbstractLoginModule, which is a subclass of javax.security.auth.login.LoginModule. The reason we subclass JBoss's class instead of directly extending the JAAS class is because JBoss expects to find the user and role principals arranged in a specific manner. Using the AbstractLoginModule ensures that these are set up properly for JBoss. Our CustomServerLoginModule looks like this:

package net.eskeeter.jbossjaasexample;

import java.security.acl.Group;
import javax.security.auth.login.LoginException
import org.jboss.security.auth.api.AbstractLoginModule;

public class CustomServerLoginModule 
  extends AbstractServerLoginModule
{
    private Principal identity;
	
    public Principal getIdentity() {
        return identity;
    }
	
    public Group[] getRoleSets() {
        ...
    }
	
    public boolean login() throws LoginException {
        ...
    }
} 

The getIdentity() and getRoleSets() methods are abstract in the AbstractServerLoginModule, and must be implemented by our server login module. The login() method has a default implementation, but you'll typically want to override this do anything you need to do on the server side. This login method is the very same as the one in the JAAS login module, and thus follows the same rules: return true if successful, false if this module should be ignored, or throw a LoginException if an error occurs.

public boolean login() throws LoginException
{
    identity = 
      org.jboss.security.SecurityAssociation.getPrincipal();
	
    if ( identity == null )
    { 
        throw new LoginException( 
            "The principal was not found in " +
            "the SecurityAssociation." );
    }
	
    loginOk = true;
    return true;
}

Our simple sets the identity principal, and returns true. The boolean loginOk is a protected member of the AbstractServerLoginModule class, and is checked by the commit() method to determine if the login() method succeeded. The getIdentity() method simply returns the principal that should be used as the identity of the subject on the server. The getRoleSets method returns an array of Groups of Principals, our sample implementation is as follows:

public Group[] getRoleSets() 
{
    // Your implementation should lookup what roles
    // the user, identified by the identity principal, is in 
    // from a database, or some group management 
    // system you have in place.
	
    Group rolesGroup = new SimpleGroup( "Roles" );
    rolesGroup.addMember( 
        new SimplePrincipal( "ExampleRole" ) );
	
    return new Group[]{ rolesGroup };
}

JBoss expects to find the user's roles in a group called "Roles". If you have a need for them, you can add other groups as well. Our simple implementation is not very dynamic, it always returns the Roles group with a single role principal, "ExampleRole".

That's all there is to writing the custom login module for the server. All that's left to do now is tell the JBoss what security domain your EJB Application should run in, and to use your server login module for that domain. Setting your EJB Application's security domain is as easy as adding a tag to your jboss.xml descriptor file:

<jboss>
<security-domain>ExampleDomain</security-domain>
</jboss>

Note that this security domain does not have anything to do with the security domain you set up on the client. When you build your EJB jar file, put the jboss.xml file along side your ejb-jar.xml file in the jar files META-INF directory. JBoss will process this file with the ejb-jar.xml descriptor file, appending any information found in it to the information found in the ejb-jar.xml file. To tell JBoss to use your login module, you need to edit the /server/

Writing Custom JAAS Login Modules

Posted 1721 days ago

This tutorial will show you in detail the steps required to implement a custom JAAS login module for a Java application.

When a client application needs to authenticate the user, the only thing it needs to do is obtain a LoginContext, and execute its login() method, as follows:

package net.eskeeter.jaasexample;

import javax.security.auth.login.LoginException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.callback.CallbackHandler;

public class JAASExample
{
    public static void main( String[] args )
    {
        String securityDomain = "exampleDomain";
	CallbackHandler callbackHandler = 
		new CustomCallbackHandler();

	try
	{
	    LoginContext lc = 
	    new LoginContext( securityDomain, 
			      callbackHandler );
	    lc.login();
	}
	catch ( LoginException e )
	{
	    System.exit( -1 );
	}
    }
}

The two relevant lines of code are in the try block. First, the application creates a LoginContext, then calls the login() method. Although this sounds simple enough, there is quite a bit going on here, so lets start from the top:

First, the securityDomain string. The security domain is used to determine what login modules should be used by the LoginContext instance. On the client, you will need to create a security domain configuration file that defines this security domain; I called mine auth.conf (the name is not importatant) and it simply contains the following:

exampleDomain {
     net.eskeeter.jaasexample.CustomLoginModule required;
};

The contents of the file are simple. The exampleDomain label identifies a security domain, and the modules it needs are enclosed in the curly braces. In this example, there is only one module, MyCustomLoginModule, and it is required. We will see the implementation of the MyCustomLoginModule class shortly.

Second is the instantiation of a CallbackHandler. The callback handler is used by the login modules to prompt the user for their username and password. This allows you to write Login Modules independent of how the client application interacts with the user, wether it be console based, Swing, network communications, or even a database lookup. We will come back to callback handlers later. For now, it is enough that you understand that the handler will get the username and password from the user, and then give that information back to the LoginModule.

The next thing we do is instantiate the LoginContext with these two pieces of information. When the LoginContext is created, it looks at your security domain configuration file and creates an instance of each login module specified. Before we can understand how the LoginContext interacts with each Login Module, perhaps we should take a look at what goes into the login module itself. Here is the MyCustomLoginModule implementation, details have been taken out for brevity, but we will come back to them in a bit:

public class CustomLoginModule 
  implements javax.security.auth.spi.LoginModule
{
    private Subject subject;
    private CallbackHandler handler;
    private Map sharedState;
    private Map options;

    private String username = null;

    // Saves the state of phase 1, login().
    private boolean loginOk = false;  

    private SimplePrincipal usernameprincipal;
    private Object password;

    public void initialize( Subject subject, 
			    CallbackHandler callbackHandler, 
			    Map sharedState, 
			    Map options)
    {
	this.subject = subject;
	this.handler = callbackHandler;
	this.sharedState = sharedState;
	this.options = options;
    }

    public boolean login() throws LoginException
    {
        ...
    }

    public boolean commit() throws LoginException
    {
        ...
    }

    public boolean abort() throws LoginException
    {
        ...
    }

    public boolean logout() throws LoginException
    {
        ...
    }
}

Each of these methods are defined in the LoginModule interface, so they must all be overridden. When the LoginContext creates an instance of the login module, it uses the default no-parameter constructor, and then calls the initialize() method. The only purpose that initialize() serves is to store away the parameters for the other methods to use later. The implementation shown here is exactly how you should expect to see it in any subclass of LoginModule. The Subject that is passed in represents the "thing" being authenticated (i.e., the user). The CallbackHandler is a reference to the handler you passed in to the LoginContext. The sharedState map lets you pass parameters between login modules, such as the username and password, so that the user does not need to be prompted for them with each module. The options map allows you to configure options at runtime; we do not use it in our sample implementation.

The next thing that happens in our JAASExample class is that we call the LoginContext's login() method. As you might have figured out by now, this will in turn call the login() method on each of the LoginModule instances it created. The login method looks like this:

public boolean login() throws LoginException
{
    NameCallback namecallback = 
	new NameCallback( "Enter username" );
    PasswordCallback passwordcallback = 
	new PasswordCallback( "Enter password", false );

    try
    {
        handler.handle( new Callback[]{ namecallback, 
					passwordcallback } );

	username = namecallback.getName();
	password = new String( passwordcallback.getPassword() );

	System.out.println( "TODO\t" + 
		            this.getClass().getName() + 
			    ": Call Authentication Code." );

	loginOk = true;
	return true;
    }
    catch ( UnsupportedCallbackException e )
    {
    }
    catch ( java.io.IOException e )
    {
    }
    finally
    {
    }

    return false;
}

The execution of the login method completes phase 1 of authentication. The login method creates a NameCallback and PasswordCallback and passes them to the CallbackHandler; we will look at this in more depth a little later. The only other thing that login does is to save the state of the callbacks by storing the username and password in its private instance variables. The SimplePrincipal class is defined as follows:

public class SimplePrincipal implements Principal
{
    private String name;
    private boolean destroyed = false;

    public SimplePrincipal( String name )
    {
	this.name = name;
    }

    public String getName()
    {
	return name;
    }
}

The implementation in the download is a little more complicated than this one because of some JBoss requirements. For more information on that, see my entry on creating custom JBoss login modules with JAAS. Note that nothing has been added to the Subject yet, this is done in commit().

public boolean commit() throws LoginException
{
    if ( ! loginOk )
	return false;

    usernameprincipal = new SimplePrincipal( username );
    password = new String( "idontusethis" );

    subject.getPrincipals().add( usernameprincipal );
    subject.getPublicCredentials().add( password );

    this.username = null;
    return true;
}

Commit stores the state saved in login in the subject being authenticated. Note that in the implementation I did for jETIA (http://www.jetia.com), I pass a phony password to the server. Since I do my authentication to the corporate user database on the client side, there is no reason to pass the actual password to the server. For JBoss though, it is important that you pass SOMETHING in the subject's credientials (we'll talk more about JBoss after we understand JAAS in general.).

The abort method should simply destroy all internal state saved in the LoginModule. The logout method should remove any principals or credentials from the subject that were added by that LoginModule.

The ONLY thing left to understand of the client side of things is the CallbackHandler. The CallbackHandler interface only defined one method, handle, that takes an array of Callback objects. For a description of the different types of callback objects, see the javax.security.auth.callback package in the J2SE API. Our CallbackHandle, CustomCallbackHandler, looks like this:

package net.eskeeter.jaasexample;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import java.io.IOException;

public class CustomCallbackHandler 
  implements javax.security.auth.callback.CallbackHandler
{
    private String username = null;
    private char[] password = null;

    public void handle(Callback[] callbacks) 
	throws IOException, UnsupportedCallbackException
    {
        prompt();

        for ( int i = 0; i < callbacks.length; i++ )
	{
	    Callback callback = callbacks[i];

	    if ( callback instanceof NameCallback )
	    {
	        NameCallback ncb = (NameCallback) callback;
		ncb.setName( username );
	    }
	    else if ( callback instanceof PasswordCallback )
	    {
	        PasswordCallback pcb = (PasswordCallback) callback;
                pcb.setPassword( password );
	    }
	    else
	    {
	        System.out.println( 
			"Unsupported callback: " + 
			callback.getClass().getName() );
	        throw new UnsupportedCallbackException( callback );
	    }
	}
    }

    /**
     * Prompts the user for username and password with the 
     * AuthenticationDialog dialog. This method may only be
     * called once per instance.
     */
    private void prompt()
    {
	AuthenticationDialog dlg = 
            new AuthenticationDialog( null );
	dlg.show();

	username = dlg.getUserName();
        password = dlg.getPassword();
    }
}

The only thing to do now is compile everything together, and run it. The one detail we did not cover above, is telling the LoginContext where exactly to find you security domain configuration file. Fortunately, this is as easy as passing a parameter to the JVM when you run it. To start this application, type the following:

java -Djava.security.auth.login.config=/auth.conf 
    net.eskeeter.JAASExample

About

My name is Tim Fanelli, I am a software engineer in Northern NY. I spend most of my time working, and when I can, I try to post interesting things here.

Cigar Dossiers