Developing a Login Module

We have used a specific login module, KeyStoreLoginModule, earlier in Example #4 to illustrate how a Java program can prompt a user for authentication information and use an external user account management system to get username and password. We also discussed how JAAS allows any user account management system to be used for authentication through configured login modules.

KeyStoreLoginModule is great for illustration due to its platform independence and the fact that the tools to manage the underlying datastore are available within J2SE. However, it suffers from a big limitation: it has no concept of roles or groups. As a result, it is not possible to create different roles, assign multiple users to these roles and specify access control rules for the roles. This would greatly simplify the administration of access control policies and this is how access control is managed in practice. In most situations, specifying access rules to specific users is just not practical.

There are other login modules, as shown in Table 5-1, that are more functional and support the concept of roles. However, they either require platform-specific user management systems (NTLoginModule and UnixLoginModule) or complex setup with external software (JndiLoginModule and Krb5LoginModule). So, for subsequent examples in this and later chapters, we create a simple user account management system and write a login module for it. This also helps us understand the steps in writing our own login module.

Because this account management system is part of JSTK, we call it JSTK User Account Management system.

JSTK User Account Management System

This system essentially maintains a database of users and roles where a role can be associated with multiple users. All the source files implementing this system can be found under the srcorgjstkuam subdirectory of JSTK installation directory. Here we cover only those aspects that are required for using the system and writing the corresponding login module.

The main functionality of JSTK UAM, the management of users and roles and login validations, is exposed through the class UserAccountManager. A partial listing of the public methods of this class is given here:

public void addUser(String loginName, String userName, String passWord);
public void changePassWord(String loginName, String passWord);
public java.security.Principal getUser(String loginName);
public void addRole(String roleName, String desc);
public java.security.Principal getRole(String roleName);
public java.util.Iterator userRoles(String loginName);
public void addRoleToUser(String roleName, String loginName);
public void validate(String loginName, String passWord);

You can see that the UserAccountManager class keeps track of users and roles. A user is identified by a login or user name and has a password, whereas a role is identified by a role name and has no password. New users and roles are added to the system by invoking addUser() and addRole(), respectively. Multiple users can be added to the same role, i.e., a role can be assigned to zero or more users. This is done through the method addRoleToUser(). It is also possible to remove users, roles or user role associations, although these methods are not shown here. All these methods are for use by a tool, either visual or text-based, to administer the system. One such text-based, shell-like program, UAMShell.java, is made available as part of JSTK. This program can be launched by running the script uamsh.bat, kept in the %JSTK_HOME%in directory.

Given the loginName and the passWord, the method validate() can verify whether they match with the stored values or not. Besides this, you can also retrieve a user, a role or all roles assigned to a user as java.security.Principal objects through the getUser(), getRole() and userRoles() calls. Recall that the Principal interface is the mechanism to store an identification value and is used in policy files to specify access permissions.

These methods are used by a JSTKLoginModule to authenticate a user and associate the user Principal and all role Principals with the Subject, representing the current user. Internally, two concrete classes, JSTKUserPrincipal and JSTKRolePrincipal, implement Principal interface for users and roles. The method getName() of interface Principal returns the login name for a user Principal and the role name for a role Principal.

A user account management system must persist information about users and roles on secondary storage. JSTK User Account Management system accomplishes this by simply writing the serialized UserAccountManager object to a file.

Enough description! Let us see these programs in action by executing the UAMShell program. As usual, we work from the JSTK home directory and launch the program by running the script uamsh.bat.

Listing 5-6. A session with JSTK User Account Management Shell
C:jstk>binuamshell -uamfile %JSTK_HOME%configuamdb.ser
uam>adduser pankaj pankajpass
							-- user pankaj with password pankaj
User Added: pankaj
uam>adduser veena veenapass
							-- user veena with password veena
User Added: veena
uam>adduser akriti akritipass
							-- user akriti with password akriti
User Added: akriti
uam>addrole admin
Role Added: admin
uam>addrole customer
Role Added: user
uam>assignrole admin veena     –
							user veena assigned role admin
Role admin added to User veena
uam>assignrole customer pankaj –
							user pankaj assigned role customer
Role user added to User pankaj
uam>assignrole customer akriti –
							user akriti assigned role customer
Role user added to User akriti
uam>assignrole customer veena
							-- user veena assigned role customer
Role user added to User veena
uam>users
							-- list all users with their roles
----- All Users -----
pankaj: user
veena: user admin
akriti: user
uam>exit
c:jstk>

The value of the command line option –uamfile, %JSTK_HOME%configuamdb.ser, is passed to the UserAccountManager as the path of the file to persist the relevant information. If this file already exists, existing users and roles are loaded and new additions/modifications are made to the in-memory copy and is persisted on successful completion of the command. If the file doesn't exist, a new file is created with the specified name.

The sequence of commands on the shell prompt adds users pankaj, veena and akriti and roles admin and user. Further, veena is assigned role admin and everyone, including veena, is assigned role user. We use these users and roles in subsequent examples.

Login Module JSTKLoginModule

Writing a login module requires implementing the LoginModule interface of package javax.security.auth.spi. This interface is shown in Listing 5-7.

Listing 5-7. Interface LoginModule
package javax.security.auth.spi;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import java.util.Map;

public interface LoginModule {
    void initialize(Subject subject, CallbackHandler callbackHandler,
   Map sharedState, Map options);
    boolean login() throws LoginException;
    boolean commit() throws LoginException;
    boolean abort() throws LoginException;
    boolean logout() throws LoginException;
}

Also, look at the portion of code from DisplayFileLauncher.java, a source file from an earlier example, calling the methods login() and logout():

LoginContext lc = new LoginContext("DF", new DFCallbackHandler());
lc.login();
// initialize action.
Subject.doAs(lc.getSubject(), action);
lc.logout();

What we see is that a program doesn't call methods on a login module directly, but works with a LoginContext object. The constructor of LoginContext reads the login configuration file specified by system property java.security.auth.login.config and instantiates the modules listed in the entry corresponding to the first argument of the constructor. But how does this code know what classes to instantiate? Recall that the login modules in a login configuration entry are fully qualified class names of the classes implementing LoginModule interface. The LoginContext constructor uses reflection to instantiate the corresponding classes.

After instantiation, the LoginContext constructor code invokes initialize() method of LoginModule with following arguments:

subject: A data member of LoginContext, of type Subject. The login module adds Principals of the validated user to this during authentication process.

callbackHandler: Received from the caller. The login module invokes handle() method on this handler to get user login name and password..

sharedState: A container to share name value pairs among different login modules. Could be used to cache values.

options: contains all the name value pairs specified as options for the particular login module in the login configuration entry.

Calling login() on a LoginContext object causes a two phase process to begin for user authentication using the stack of login modules specified in the login configuration entry. First, login() method on the top login module is invoked. Depending on the return value (a return value of true means success) and the flag associated with the module (Refer to Login Configuration section of this chapter for details), either continue with the next module or succeed or fail. If continuing, follow the same steps with the next module and so on. When the validation succeeds, traverse up the stack, calling commit() on each of the login modules. In case of validation failure, call abort() in place of commit().

A login module performs the validation of user login name against supplied password on login() and returns true on success and false on failure. No changes are made to the subject on login(). All the Principals are added to the subject on commit(). The method abort() should perform cleanup, if there is a need.

Method logout() of a LoginModule is called by logout() of LoginContext. A LoginModule should remove all the Principals added by it from the subject in the logout().

With the above explanation, we are ready to look at the source code of JSTKLoginModule. Let us start with member fields in Listing 5-8a.

Listing 5-8a. Member fields of JSTKLoginModule class
package org.jstk.uam;
// import declarations omitted.
public class JSTKLoginModule implements LoginModule {
  private UserAccountManager uam;   // to access JSTK UAM.
  private boolean initStatus;       // Did initialization succeed?

  private Subject subject;
  private CallbackHandler callbackHandler;
  private Map sharedState;
  private Map options;
  private boolean debug;            // Is debug enabled?

  // the authentication status
  private boolean succeeded = false;
  private boolean commitSucceeded = false;

  // username and password
  private String username;
  private char[] password;

  // users' Principals. To be obtained from JSTK UAM.
  private Principal userPrincipal;
  private Vector rolePrincipals;

The member fields are used to share information across different methods. Based on our previous discussion and name of member fields, you can get an idea of what they will be used for. Instead of explaining each of the fields, we look at the individual methods and see how these fields are accessed within the method body, starting with the initialize() method.

Listing 5-8b. Definition of the method initialize()
public void initialize(Subject subject, CallbackHandler
      callbackHandler, Map sharedState, Map options) {
    this.subject = subject;
    this.callbackHandler = callbackHandler;
    this.sharedState = sharedState;
    this.options = options;

    debug = "true".equalsIgnoreCase((String)options.get("debug"));
    String uamfile = (String)options.get("uamfile");
    if (uamfile != null){
   // Code to instantiate uam and error handling omitted.
      initStatus = true;
    } else {
      initStatus = false;
    }
  }

This method initializes the member fields from arguments and instantiates a UserAccountManager object. We have already talked about the UserAccountManager class in the previous section.

The next interesting method to look at is the login() method. Listing 5-8c shows its body. We have omitted some error handling and parameter checking code for brevity.

Listing 5-8c. Definition of the method login()
public boolean login() throws LoginException {
    if (!initStatus)
      throw new LoginException("Error: JSTKLoginModule init. failed ");

    // Prompt the user for login and password and process input.
    Callback[] callbacks = new Callback[2];
    callbacks[0] = new NameCallback("login: ");
    callbacks[1] = new PasswordCallback("password: ", false);
    try {
      callbackHandler.handle(callbacks);
      username = ((NameCallback)callbacks[0]).getName();
      char[] tmpPassword =
          ((PasswordCallback)callbacks[1]).getPassword();

      if (tmpPassword == null) // Treat null as empty string.
        tmpPassword = new char[0];
      password = new char[tmpPassword.length];
      System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
   ((PasswordCallback)callbacks[1]).clearPassword();

    } catch (java.io.IOException ioe) {
      throw new LoginException(ioe.toString());
    } catch (UnsupportedCallbackException uce) {
      throw new LoginException("Error: " + uce.getCallback().toString()
        + " not available to get authentication info. from the user");
    }

    // verify the username/password
    boolean usernameCorrect = true;
    boolean passwordCorrect = true;
    try {
      uam.validate(username, new String(password));
    } catch (UserAccountManager.NoSuchUserException e){
      usernameCorrect = false;
    } catch (UserAccountManager.InvalidPasswordException e){
      passwordCorrect = false;
    }
    if (!usernameCorrect || !passwordCorrect){
      succeeded = false;
      username = null;
      for (int i = 0; i < password.length; i++)
        password[i] = ' ';
      password = null;
      if (!usernameCorrect) {
        throw new FailedLoginException("User Name Incorrect");
      } else {
        throw new FailedLoginException("Password Incorrect");
      }
    }
    succeeded = true;
    return true;
  }

One of the things you can't help noticing is the way the password is treated. It is handled as a char array and not as Java String object. Also, the memory locations holding the password data are blanked out as soon as possible. In fact, this is the reason for holding the password in a char array. Java Strings are immutable and cannot be blanked out by the programmer. But why bother when we know that other Java code, even those running within the same JVM, cannot get hold of the variable references once they are not visible. Well, it can still be read by another process having read access to the memory holding the JVM process.

On most Operating Systems it is possible that portions of the JVM process memory gets swapped out to disk and can be accessed and analyzed. It helps to be paranoid when dealing with security!

All this comes at a cost. You must have noticed the complexity introduced by using char array in place of String. To avoid this complexity, we have used String to hold password and other sensitive information in most of the illustration code. Keep in mind that sensitive production code should take all the necessary precautions.

Finally, let us turn our attention to the commit() method.

Listing 5-8d. Definition of the method commit()
public boolean commit() throws LoginException {
    if (succeeded == false)   // No need to proceed.
    return false;

    // Add user and role Principals to the subject
    userPrincipal = uam.getUser(username);
    if (!subject.getPrincipals().contains(userPrincipal))
      subject.getPrincipals().add(userPrincipal);
    Iterator itr = uam.userRoles(username);
    rolePrincipals = new Vector();
    while (itr.hasNext()){
      Principal rolePrincipal = (Principal)itr.next();
      if (!subject.getPrincipals().contains(rolePrincipal)){
        subject.getPrincipals().add(rolePrincipal);
        rolePrincipals.add(rolePrincipal);
      }
    }

    // in any case, clean out state
    username = null;
    for (int i = 0; i < password.length; i++)
      password[i] = ' ';
    password = null;

    commitSucceeded = true;
    return true;
  }

The main activity within the commit() function is to retrieve user Principal and role Prinicpals from JSTK User Account Management System and add those to the subject of authentication process. A list of all the Principals added need to be maintained so that these can be removed in the logout() method.

Implementation of abort() and logout() methods is on similar lines and not shown here. Refer to the JSTK source files for body of these methods.

We are now ready to apply the newly developed login module and the understanding of JAAS to a moderately complex sample application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.114.29