See wam Menu

Creating a New WAM JAAS Login Module

Source code for the standard WAM JAAS LoginModules is supplied with the distribution. You can use any WAM JAAS LoginModule as a starting point to make a new version customized to your needs.

This section provides a generalized example of the how you might go about creating a new LoginModule, using the XmlLoginModule as a starting point.

The LoginModules you create must implement the standard JAAS LoginModule methods provided in the javax.security.auth.spi.LoginModule package.

Step 1 - Copy XmlLoginModule

You should create your new LoginModule in a new directory. This can be any directory you like, however, best practice suggests that you should use standard Java namespace conventions to avoid conflicts. You might want to use XmlLoginModule.java as a template. These instructions will call the new LoginModule MyLoginModule. After you’ve created MyLoginModule, open it in a text editor.

Step 2 - Modify MyLoginModule

This step and its substeps are organized by the LoginContext methods you’ll need to implement. Before getting into the methods, make sure that you change the package name to reflect the new file location. If you are only making a minor modification, you may not need to make any other changes to the imports. Otherwise, modify the imports as required. For example: package com.mycompany.login.module;

Step 2a - Setup state in the initialize method

Initialize the Subject, CallbackHandler, sharedState, and options objects sent by the LoginContext.

Use the options map to save any number of configuration values. WAM JAAS LoginModule options are stored in the relevant security domain’s login-config.xml file (see the WAM Administrator’s Guide).

For example, all WAM JAAS LoginModules should have a debug option that allows verbose debug messages to be displayed. You might also use the options map to retrieve JDBC connection parameters, LDAP repository data, and more.

NOTE: You may want to fetch most option values in the login or commit methods where you have better control to handle errors.

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


   // get debug flag
   this.debug = "true".equalsIgnoreCase((String)options.get("debug"));
}

Example 1 - LoginModule initialize method

Step 2b - Getting a WAM Logger

The WAM Policy Server may host multiple security domains and each has its own log files. DEBUG, INFO, WARNING, ERROR, and FATAL messages from services and components hosted within a security domain are written to a security domain-specific “trace” log file.

If you’d like your LoginModule to log its trace information to this log file, you get the WAM Logger you’ll need by implementing the LoggerClient interface as shown in Example 2.

import javax.security.auth.spi.LoginModule;


import com.cafesoft.core.log.LoggerClient;


...


public class MyLoginModule implements LoginModule, LoggerClient

   ...

   /**
    * Used to log security domain-specific messages.
    */
   private Logger logger;

   ...

   /**
    * Sets the logger.
    *
    * @param logger the Logger to be used when logging messages, which will
    *    be the WAM security domain-specific Logger.
    */
   public void setLogger(Logger logger)
   {
      this.logger = logger;
   }

   ...
}

Example 2 - Implementing the WAM LoggerClient interface to get a Logger

If your LoginModule implements the LoggerClient interface, the WAM Policy Server will invoke the setLogger() first, before invoking any other method. Once your code has a Logger, using it is easy.

Example 3 shows some sample code that logs DEBUG, and ERROR-level messages. The Logger also has: info, warning, and fatal methods. See the WAM Javadoc on com/cafesoft/core/log/Logger for more details.

// DEBUG-level messages should always be invoked conditinally based on
// a component-specific debug flag
if (debug)
   logger.debug(this, "Attempting to authenticate user=" + username);

// ERROR-level messages will generally include an Exception
...
catch (java.io.IOException e)
{
   logger.error(this, "Error handling callbacks", e);
   throw new LoginException("[XmlLoginModule] Error: " +
      e.getMessage() + "\n" + e.toString());
}

Example 3 - Using a WAM Logger

Step 2c - Getting a WAM ServiceFinder

A WAM ServiceFinder enables your LoginModule to find and use WAM Services hosted under the enclosing security domain. The WAM LdapLoginModule makes use of a ServiceFinder to use the LdapConnectionPoolService, which provides a pool of ready to use LDAP connections, which boosts performance and minimizes resource utilization.

Your LoginModule will be given a WAM ServiceFinder appropriate for the enclosing security domain if it implements the ServiceClient interface as shown in Example 4.

import javax.security.auth.spi.LoginModule;


import com.cafesoft.core.service.ServiceClient;


...


public class MyLoginModule implements LoginModule, ServiceClient
{
   ...

   /**
    * Used to lookup WAM services required by this LoginModule.
    */
   private ServiceFinder serviceFinder;

   ...

   /**
    * Set the ServiceFinder.
    *
    * @param finder the object used to lookup WAM services needed by
    *       this LoginModule. The ServiceFinder will be specific to the
    *       enclosing WAM security domain.
    */
   public void setServiceFinder(ServiceFinder finder)
   {
      this.serviceFinder = finder;
   }

   ...
}

Example 4 - Implementing the WAM ServiceClient interface to get a ServiceFinder

More information on use of the ServiceFinder is available in Programming with WAM Services.

Step 2d - Add authentication calls to the login method

In the login method, you obtain the user’s Credentials using the CallbackHandler supplied by the LoginContext. A CallbackHandler provides the mechanism for the calling application to pass Credentials to the LoginModule.

For example, the calling application will typically prompt for a username and password, but can also transparently require submission of a digital certificate or other credentials. In each case, the CallbackHandler must store the Credentials supplied by the application and pass them to the LoginModule.

The WAM JdbcLoginModule, LdapLoginModule and XmlLoginModule all require the CallbackHandler to supply a username and password. All WAM LoginModules are designed to work well with the WAM MapCallbackHandler, which flexibly stores any type of creditial passed to it and makes them available to the LoginModules.

Example 5 shows how the XmlLoginModule obtains the username and password supplied by the standard WAM MapCallbackHandler.

// This LoginModule requires a username and a password
Callback[] callbacks = new Callback[1];
callbacks[0] = new MapCallback();
         try
{
   // Get the callback values and clear the password.
   callbackHandler.handle(callbacks);
   this.username = ((MapCallback)callbacks[0]).getCallbackValue("username");
   this.password = ((MapCallback)callbacks[0]).getCallbackValue("password");
   ((MapCallback)callbacks[0]).setCallbackValue("password", null);
}

Example 5 - WAM LoginModule login method callbacks

The XmlLoginModule builds an in-memory representation of the XML repository. You probably won’t need to do this as MyLoginModule will most likely be garnering information from an existing repository.

Depending upon what you’re doing, the try/catch block shown in Example 6 represents the real work engine for the login method. A connection is made to the repository to fetch a password for comparison against the username and password garnered by the CallbackHandler. If there is a match, the instance variable succeeded is set to true.

try
{
   // Try to get the user
   RepositoryUser ru = userRepository.getUser(username);

   // If the user exists, get the user's password
   if (ru != null)
   {
      String rp = ru.getPassword();

      // The repository password may be "digested" and "salted" by use
      // of a "Message Digest" algorithm, if so, then we'll need to
      // digest and salt the user-entered password before comparison.
      String digestString = password;
      DigestString ds = new DigestString(rp);

      // Is the password clear text (or does it have unknown
      // algorithm)?
      if (ds.isDigestString())
      {
         // Get the digestString from the user-entered password using
         // the same ingredients found in the repository password;
         // the password, the digest algorithm, and possibly "salt"
         digestString = DigestString.createDigestString(password,
            ds.getAlgorithm(), ds.getSalt(), ds.getLabel());
      }

      // Compare the cleartext or digested passwords
      loginSucceeded = rp.equals(digestString);
   }
}

Example 6 - WAM LoginModule login method authentication

The code snippet in Example 6 also shows use of the WAM DigestString class. This utility class enables you to easily compare input passwords against repository passwords that have been saved as CRYPT, SHA, salted SHA, MD5, and salted MD5 hashes.

Step 2e - Add Principals and Credentials to the Subject in the commit method

The commit method is always called, but is short lived if MyLoginModule’s login method failed. Example 7 shows this case as well as the transaction ArrayList to which valid Principals will be added. Using an ArrayList to build the list of Principals enables us to wait to commit all Principals in a single transaction ensuring that the state of MyLoginModule will not be half-baked.

// The login failed, clean up state
if (!loginSucceeded)
   return false;


// add all principals to an ArrayList, then commit in single transaction
ArrayList principalList = new ArrayList(5);

Example 7 - WAM LoginModule commit method failed login and Principal ArrayList

The try block in Example 8 shows a call to the XML repository that returns groups to which the user belongs. If there are groups, then they are saved to the ArrayList as CSRolePrincipals. This is where you’ll write the code in MyLoginModule to retrieve groups from your repository for the user.

Notice the use of the if statement to determine if the CSRolePrincipal is already in the the list. If it is, there’s no need to add it again.

try
{
   // Get the user and the user roles
   RepositoryUser ru = userRepository.getUser(username);
   String[] r = ru.getRoles();

   // Add the user roles
   if ((r != null) && (r.length > 0))
   {
      for (int i = 0; i < r.length; i++)
      {
         // Add the CSRolePrincipal to the Subject
         // only if it does not exist
         Principal rolePrincipal = new CSRolePrincipal(r[i]);
         if (!subject.getPrincipals(CSRolePrincipal.class).contains(rolePrincipal))
         {
            principalList.add(rolePrincipal);

            if (debug)
               logger.debug(this,
                  "[XmlLoginModule] Added CSRolePrincipal " +
                  r[i] + " to principal ArrayList for user '" +
                  username + "'");
         }
         else if (debug)
         {
            logger.debug(this,
               "[XmlLoginModule] CSRolePrincipal '" + r[i] +
               "' already in Subject for '" + username +
               "', not added to " + "principal ArrayList");
         }
      }
   }
}

Example 8 - WAM LoginModule commit method CSRolePrincipal

A CSUserPrincipal will always be relevant for an authenticated user. By adding the CSUserPrincipal after any CSRolePrincipals, you ensure that the commitSucceeded flag doesn’t end up half-baked. For example, if you added CSUserPrincipal first and the role repository operation failed, you’d have a corrupted state.

Example 9 shows that you add the CSUserPrincipal using a similar construct to the way the CSRolePrincipal is added.

// If commit gets this far, there is always a relevant CSUserPrincipal
// for this user, but only add it to the Subject if it does not exist
Principal userPrincipal = new CSUserPrincipal(username);
if (!subject.getPrincipals(CSUserPrincipal.class).contains(userPrincipal))
{
   principalList.add(userPrincipal);

   if (debug)
      logger.debug(this, "[XmlLoginModule] Added CSUserPrincipal '" +
         username + "' to principal ArrayList");
}
else if (debug)
{
   logger.debug(this, "[XmlLoginModule] CSUserPrincipal '" + username +
      "' already in Subject, not added to principal ArrayList");
}

Example 9 - WAM LoginModule commit method CSUserPrincipal

You are now ready to commit the transaction by adding the all of the Principals to the Subject (see Example 10). You can also null the instance variables that will no longer be needed before setting and returning the commitSucceeded value of true.

// Add all principals to subject
for (int i = 0; i < principalList.size(); i++)
   subject.getPrincipals().add(principalList.get(i));

this.commitSucceeded = true;

// Clean up
this.username = null;
this.password = null;

return commitSucceeded;

Example 10 - WAM LoginModule commit method add Principals

Step 2f - Conditional clean up in the abort method

The abort method is called if the LoginModule’s login or commit methods fail, or if the LoginModule succeed but another relevant LoginModule fails. The code snippet in Example 11 shows how XmlLoginModule handles this for each case.

public boolean abort() throws LoginException
{
   // Authentication for this LoginModule did not succeed
   if (!loginSucceeded)
   {
      return false;
   }
   else if (!commitSucceeded)
   {
      // Authentication for this LoginModule loginSucceeded,
      // but commit did not, so clean up
      this.loginSucceeded = false;
      this.username = null;
      this.password = null;
      this.serviceFinder = null;
      this.logger = null;
   }
   else
   {
      // Authentication and commit for this LoginModule's loginSucceeded,
      // but another relevant LoginModule failed
      logout();
   }

   return true;
}

Example 11 - WAM LoginModule abort method

Step 2g - Final clean up in logout method

The logout method is called if the LoginModule’s login or commit succeed, but there is a logout request. The code snippet in Example 12 shows how XmlLoginModule handles the cleanup by clearing Principals and nulling any remaining instance variables. You do not need to keep track of which Principals were added by this LoginModule to the Subject, as a logout request is global for all relevant, configured LoginModules.

public boolean logout() throws LoginException
{
   // Cleanup Object references
   this.subject.getPrincipals().clear();
   this.subject = null;
   this.serviceFinder = null;
   this.logger = null;
   this.callbackHandler = null;
   this.username = null;
   this.password = null;
   this.options = null;
   this.loginSucceeded = false;
   this.commitSucceeded = false;
   this.debug = false;


   return true;
}

Example 12 - WAM LoginModule logout method

Congratulations, you’re now ready to test MyLoginModule.

Step 3 - Test MyLoginModule

To test MyLoginModule, you’ll need to setup WAM on a development system and make sure that MyLoginModule is in the classpath. Also, you need to ensure that at least one security domain’s Configuration is setup to use MyLoginModule.

You do this the same way you would with a production system. Hence, please reference the WAM Administrator’s Guide, which has complete instructions on configuring a login module for use.

NOTE: You may want to setup the test environment with the production WAM XML login module first, to make sure everything is working. Then, substitute MyLoginModule.

Step 4 - Deploy MyLoginModule

You can package MyCallbackHandler in a jar file with any other classes you may have made, or put the individual classes in a directory that is in the WAM class path. A classes directory that is in the WAM Policy Server classpath is provided at: ${cams.home}/classes.


Have a Question?

Have a how-to question? Seeing a weird error? Contact us.

Found a bug? Submit a support ticket.

Have a product idea or request? Share it with us in our Ideas Portal.