Custom JAAS Login Modules

In Java, JavaEE by timfanelliLeave a Comment

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

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.