Back | Next | Contents Cams Programmer's Guide

Cams JAAS Login Modules

This chapter describes how you use Java's JAAS API to create new authentication and authorization login modules to plug into Cams. If a standard Cams login module doesn't provide for the needs of your enterprise, you can use the JAAS API to implement a new login module to access virtually any type of user repository.

Who Should Read this Document

Most application programmers who use Cams to implement application security will not need to learn about the JAAS API, as authentication and authorization services are provided transparently by Cams. System administrators should read the Cams Administrator's Guide to understand how to configure Cams login modules. However, if a standard Cams login module is not available or an existing one does not quite suite your needs, this document is intended to guide experienced programmers through the process of creating new Cams JAAS login modules.

Related Documentation

This document provides terminology, architectural diagrams, and programmatic examples specific to writing login modules for use with Cams. Though the document is thorough within that scope, it is not intended to replace the abundance of documentation available on JAAS, Java Security, and Pluggable Authentication Modules (PAM). For more information on JAAS and related security topics, please see the links in the Resources section of this document.

Cams and JAAS

Cams uses an implementation of the Java Authentication and Authorization Service (JAAS), which was introduced as an optional package to the Java 2 SDK, Standard Edition (J2SDK) 1.3. This was also known as JAAS 1.0. With the release of J2SDK 1.4 JAAS was integrated into the Java Standard Edition.

JAAS implements a Java version of the Pluggable Authentication Module (PAM) framework, which permits applications to remain independent from underlying authentication technologies. The PAM framework allows the use of new or updated authentication technologies without requiring modifications to your application.

JAAS can be used for two purposes:

Login modules are plugged into the Cams server to provide a particular type of authentication. Currently, the standard Login modules included with Cams are:

If any of these standard login modules meet your requirements, then you can stop reading this document when your curiosity wanes. Otherwise, press on.

Conceptual Overview

A LoginModule is concerned with authentication, the mechanism by which callers prove that they are acting on behalf of specific users or systems. You use JAAS to establish trust by validating a user's Credentials (often a username and password) through creation of of a LoginContext. (see Figure 1). After successfully proving a caller's identity, a LoginContext is saved in a Cams session object, which allows an identified user or system to be authenticated to other entities.


Figure 1 - The Cams JAAS authentication flow

To create a new LoginModule, you implement the LoginModule interface. A Configuration specifies the LoginModules you will use with Cams. Because LoginModules are pluggable, you can implement them without modification to Cams. Because they are stackable, you can specify how authentication to one or more LoginModules is required to access any resource.

A resource is any entity you want to secure against unauthorized access. The level of granularity is up to you. For example, you can consider a resource to be: single web page or a collection of pages; a web application; an Enterprise JavaBean (EJB) within a web application; or a method within a EJB.

Initialization

The LoginContext reads the configuration and instantiates the specified LoginModules. Each LoginModule is initialized with a Subject, a CallbackHandler, shared LoginModule state, and LoginModule-specific options.

A Subject is the container that holds authentication information about the user or service being authenticated, including relevant Principals and Credentials (see Figure 2). A Principal is any entity such as an individual user, a login id, or groups to which a user belongs. A user usually represents a person. A group is a category of users, classified by common traits to facilitate administration.

Figure 2 - Joe is authenticated and belongs to groups Admin and Employee,
the associated Principals and Joe's digital certificate are saved in the Subject.

In the initialize method, the Cams LoginContext sends a Subject, a CallbackHandler, shared state, and options to the LoginModules. LoginModules use the CallbackHandler to communicate with users, often to prompt for usernames and passwords. Other Credentials may be required such as digital certificates, in which case the CallbackHandler may be null. LoginModules may also share state information, or define specific configuration values to control behavior. Options are defined using a key-value syntax, such as jdbcUrl=jdbc:mysql://host/database or debug=true. The LoginModule stores the options as a Map so that the values may be retrieved using the key.

Cams-specific Initialization

In addition to standard JAAS initialization, LoginModules used within Cams can also be initialized with a security domain-specific Logger and ServiceFinder. These objects enable a LoginModule to log messages to the security domain-specific trace log file and to find Cams Services hosted within the enclosing security domain. Specific examples will be shown in sections: Getting a Cams Logger, and Getting a Cams ServiceFinder.

Login

After initialization, the Cams LoginContext invokes the LoginModule's login method. Here, you write the code that will invoke the CallbackHandler (if required) and authenticate the user supplied Credentials against your repository. The login method performs the authentication and saves the result as private state information. The login method returns:

If a failure occurs, you must not retry or introduce delays. The responsibility of such tasks belongs to Cams. If Cams is configured to retry authentication (three times, for example), each relevant LoginModule's login method will be called again up to the limit. See Configuration for more information on how to modify the values that control behavior as authentication proceeds down the stack.

Commit

If the LoginContext's overall authentication succeeds, then the commit method for each relevant LoginModule is invoked. The commit method checks its privately saved state to see if its own authentication succeeded. If the overall LoginContext authentication succeeded and the LoginModule's own authentication succeeded, then the commit method associates the relevant user Principals and Credentials with the Subject.

Abort

The abort method is invoked to clean up the state when the LoginModule's login or commit method fails, or the LoginContext's overall authentication fails. In each case, the LoginModule may have different requirements to remove saved authentication state.

Logout

The logout method performs the logout procedures, such as removing Principals or Credentials from the Subject, or logging session information.

If you are familiar with UML sequence diagrams, Figure 3 shows a sequence diagram of the method invocation sequence for the Cams authentication service and relevant objects.
Figure 3 - Sequence diagram of Cams authentication flow

Authentication using the JAAS classes and the Cams authentication service is performed in the following manner:

  1. Cams initiates the authentication process by instantiating a LoginContext object. The LoginContext consults a Configuration to load the configured LoginModules.

  2. The LoginContext object initializes all the LoginModules configured for the relevant security domain.

  3. Cams invokes the LoginContext's login() method, which calls the initialize method of each configured LoginModules.

  4. The LoginModule's login method is invoked for each configured LoginModule.

  5. If login is successful, the commit method of the relevant LoginModules is invoked to associate the Principals and Credentials with the LoginContext's Subject.

  6. The Cams authentication service saves the LoginContext with its populated Subject in the Session object.

Throwing LoginExceptions

LoginModules must throw a LoginException through the login(), commit(), abort(), and logout() methods to indicate a failed request. This may serve not only to indicate that the request failed, but why the request failed. For example, a LoginModule might throw a LoginException for any of these reasons:

A LoginModule may throw any subclass of javax.security.auth.login.LoginException from its login(), commit(), abort(), and logout() methods. A subset of the following standard LoginException classes are used by typical LoginModule implementations.

In addition, you are free to create new LoginException subclasses to convey appropriate context for a failed authentication request.

NOTE: Cams provides a way for each security domain to define standard, customized LoginException messages based on the class or subclass of the LoginException that may be thrown by a LoginModule. Standard LoginException messages are provided in a file named login-exception.properties in each security domain's home directory. For more information, see the Cams Administrator's Guide, section: Customizing LoginException Messages.

Create a New Cams JAAS Login Module

Source code for the standard CAMS JAAS LoginModules is supplied with the distribution. You can use any Cams 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.

  1. Copy XmlLoginModule.java
  2. Modify MyLoginModule
    1. Setup state in the initialize method
    2. Getting a Cams Logger (optional)
    3. Getting a Cams ServiceFinder (optional)
    4. Add authentication calls to the login method
    5. Add Principals and Credentials to the Subject in the commit method
    6. Conditional clean up in the abort method
    7. Final clean up in logout method
  3. Test MyLoginModule
  4. Deploy MyLoginModule

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 name space conventions to avoid conflicts. You might 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.

Modify MyLoginModule

This section is divided 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;

Setup state in the initialize method

Initialize the Subject, CallbackHandler, sharedState, and options objects sent by the LoginContext. Use the options map to save an number of configuration values. Cams JAAS LoginModule options are stored in the relevant security domain's login-config.xml file (see the Cams Administrator's Guide). For example, all Cams 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

Getting a Cams Logger

The Cams Server may host multiple security domains and each has it's 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 it's trace information to this log file, you get the Cams 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 Cams security domain-specific Logger.
	 */
	public void setLogger(Logger logger)
	{
		this.logger = logger;
	}

	...
}
Example 2 - Implementing the Cams LoggerClient interface to get a Logger

If your LoginModule implements the LoggerClient interface, the Cams 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 Cams javadocs 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 Cams Logger

Getting a Cams ServiceFinder

A Cams ServiceFinder enables your LoginModule to find and use Cams Services hosted under the enclosing security domain. The Cams 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 Cams 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 Cams services required by this LoginModule.
	 */
	private ServiceFinder serviceFinder;

	...

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

	...
}
Example 4 - Implementing the Cams ServiceClient interface to get a ServiceFinder

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

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. In each case, the CallbackHandler must store the Credentials supplied by the application and pass them to the LoginModule. Hence, CallbackHandlers are specifically tasked to obtain exact Credentials.

The Cams JdbcLoginModule, LdapLoginModule, and XmlLoginModule all require the CallbackHandler to supply a username and password. These LoginModules are designed to work well with the Cams NamePasswordCallbackHandler. However, a pluggable framework allows you to associate any valid CallbackHandler with a corresponding LoginModule. If the NamePasswordCallbackHandler does not meet your needs, see the Cams JASS Callback Handlers chapter to understand how to create a new one. For example, you may want to specify a third authentication Credential in addition to username and password such as the last four digits of a user's social security number, or a pin.

Example 5 shows how the XmlLoginModule obtains the username and password supplied by the standard Cams NamePasswordCallbackHandler.

// This LoginModule requires a username and a password
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("XML username: ");
callbacks[1] = new PasswordCallback("XML password: ", false);

try
{
	// Get the username and password
	callbackHandler.handle(callbacks);

	// Username must be supplied
	this.username = ((NameCallback)callbacks[0]).getName();
	if (username == null || username.length() == 0)
		throw new LoginException(
			"[XmlLoginModule] Username was null or empty");

	// Treat a NULL password as an empty password
	char[] tmpPassword = ((PasswordCallback)callbacks[1]).getPassword();
	if (tmpPassword == null)
		tmpPassword = new char[0];

	// Save the password and clear the password callback
	this.password = new String(tmpPassword);
	((PasswordCallback)callbacks[1]).clearPassword();
}
Example 5 - Cams 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 3 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 - Cams LoginModule login method authentication

The code snippet in Example 6 also shows use of the Cams 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.

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 - Cams 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 - Cams 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 (e.g., 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 - Cams 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 - Cams LoginModule commit method add Principals

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 - Cams LoginModule abort method

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 - Cams LoginModule logout method

Congratulations, you're now ready to test MyLoginModule.

Test MyLoginModule

To test MyLoginModule, you'll need to setup Cams 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 Cams 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 Cams XML login module first, to make sure everything is working. Then, substitute MyLoginModule.

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 Cams class path. A "classes" directory that is in the Cams Server classpath is provided at: ${cams.home}/classes.

Resources

JASS Security Guides

JAAS Javadoc APIs

Java 2 Platform Security

PAM Guides

Back | Next | Contents