All that JAAS

Scalable Java security with JAAS

1 2 3 Page 2
Page 2 of 3

JAAS comes with seven built-in Callbacks in the javax.security.auth.callback package: ChoiceCallback, ConfirmationCallback, LocaleCallback, NameCallback, PasswordCallback, TextInputCallback, TextOutputCallback. And J2SE 1.4 supplies two CallbackHandlers in the com.sun.security.auth.callback package: DialogCallbackHander and TextCallbackHandler. Many Callbacks resemble each other, and all are oriented towards client-side scenarios. We'll show you below how to develop your own CallbackHandler later.

Configuration files

As we mentioned, much of JAAS's extensibility comes from its ability to be dynamically configured. This configuration is typically specified through a text file consisting of one or more configuration blocks, called applications. Each application is a set of one or more specified LoginModules.

When your code instantiates a LoginContext, you pass it the name of one of the application blocks in the configuration file. The LoginContext will use LoginModules based on what you specify in the application entry. These specifications drive which LoginModules are invoked, in which order, and according to which rules.

The configuration file has the following structure:

      Application {
          ModuleClass  Flag    ModuleOptions;
          ModuleClass  Flag    ModuleOptions;
          ...
      };
      Application {
          ModuleClass  Flag    ModuleOptions;
          ...
      };
      ...

Here's an example of an application block named Sample:

Sample {
    com.sun.security.auth.module.NTLoginModule Required debug=true;
};

This basic application specifies that a LoginContext should use the NTLoginModule for authentication. The class name is specified in the ModuleClass entry. The Flag entry controls the login behavior when multiple modules exist for a single block. Flag's four allowable values: Required, Sufficient, Requisite, and Optional. The most common is Required, which means the module is always called, and its authentication must pass for the overall authentication to succeed. The Flag field's idiosyncrasies can grow complex, and, since we're using only one module per block in our example, we won't go into elaborate detail here (see javax.security.auth.login.Configuration's manual page for more specifics).

The ModuleOptions entry allows any number of module-specific variables to be specified in name-value pairs. For example, many prebuilt login modules let you specify a debug flag such as debug=true to see diagnostic output sent to System.out.

The configuration file can have any name and location. A running JAAS framework locates it using the java.security.auth.login.config system property. Running our sample application, JaasTest, with a configuration file called jaas.config, we would specify the location on the command line as follows: java -Djava.security.auth.login.config=jaas.config JaasTest.

Figure 2 demonstrates the relationship among these configuration elements.

Figure 2. JAAS configuration

Login: Command line

To demonstrate what you can do with JAAS, we'll walk you through the development of two examples: a simple, traditional command-line program and a more complex server-side JSP application. Both will query the user for a username/password combination and authenticate using a RDBMS via JDBC.

To authenticate against the database, we:

  1. Create an RdbmsLoginModule that can authenticate against the data
  2. Set up a configuration file that tells a LoginContext how to use RdbmsLoginModule
  3. Create a ConsoleCallbackHandler to gather user input
  4. Add the application code to use the RdbmsLoginModule, the configuration file, and the ConsoleCallbackHandler

In our RdbmsLoginModule, we must implement the five methods defined in the LoginModule interface. The first of these is the initialize() method, which we define as follows:

public void initialize(Subject subject, CallbackHandler 
callbackHandler,
        Map sharedState, Map options)
{
   this.subject          = subject;
   this.callbackHandler  = callbackHandler;
   this.sharedState      = sharedState;
   this.options          = options;
   url         = (String)options.get("url");
   driverClass = (String)options.get("driver");
   debug       = "true".equalsIgnoreCase((String)options.get("debug"));
}

LoginContext calls this method at the beginning of every call to its login() method. As with typical LoginModules, RdbmsLoginModule's first task is to keep a reference of the four arguments it has been given. The Subject, which was either supplied at the application layer or created by the LoginContext, can later be populated with Principals and credentials if we successfully authenticated. The CallbackHandler will be used in our login() method. The sharedState Map can share data across LoginModules; a nice feature, but we won't use it in this example.

Last, but certainly not least, is the Map of named options. The options Map is the way LoginModule receives the option values from its corresponding ModuleOptions field in the configuration file. When the LoginContext loads the configuration, it creates a Map from these name-value pairs and passes them through an options parameter.

Our example's configuration file looks like this:

   Example {
      RdbmsLoginModule required
   driver="org.gjt.mm.mysql.Driver"
   url="jdbc:mysql://localhost/jaasdb?user=root"
   debug="true";
   };

As you can see, the RdbmsLoginModule takes three options; driver and url are required, and debug is optional. The first two options tell us how to make our JDBC connection: through a standard JDBC driver class name and a JDBC URL. A more advanced implementation might use additional option entries to specify a portion of the actual database query, such as the table or column names. The main idea is that you can use the configuration file to parameterize the JDBC access. Our LoginModule's initialize() method keeps a copy of each option.

Earlier, we mentioned that a LoginContext's configuration file tells it which application entry to use. That information passes through the LoginContext's constructor. Here's the initial application/client-layer code that creates and invokes LoginContext (we will explain the CallbackHandler shortly):

   ConsoleCallbackHandler cbh = new ConsoleCallbackHandler();
   LoginContext lc = new LoginContext("Example", cbh);
   lc.login();

Now when the LoginContext.login() method is called, it will invoke the login() method on all LoginModules it has loaded, which for this configuration is only our RdbmsLoginModule.

Our login() method then performs the following steps:

  1. Creates two Callbacks that will receive username/password information from the user. It uses two of the built-in JAAS callbacks from the javax.security.auth.callback package: NameCallback and PasswordCallback. Both of these classes implement the Callback interface, a marker interface with no methods.
  2. Invokes the Callbacks indirectly by passing them to the handle() method of the CallbackHandler parameter specified in the call to initialize().
  3. Retrieves the username/password from the Callbacks.
  4. Uses the username/password in our own rdbmsValidate() method, which checks them against the database via a JDBC query.

This sequence is shown below in this snippet from RdbmsLoginModule's login() method:

public boolean login() throws LoginException {
   if (callbackHandler == null)
        throw new LoginException("no handler");
   NameCallback nameCb = new NameCallback("user: ");
   PasswordCallback passCb = new PasswordCallback("password: ", true);
   callbacks = new Callback[] { nameCb, passCb };
   callbackHandler.handle(callbacks);
   String username = nameCb.getName();
   String password = new String(passCb.getPassword());
   success = rdbmsValidate(username, password);
   return(true);
}

Our rdbmsValidate() method, if successful, assigns any Principals or credentials necessary for our application. You can view the rdbmsValidate() method and the rest of this LoginModule by downloading the code from Resources.

We can see how user interaction works with the Callbacks by examining the implementation of ConsoleCallbackHandler's handle() method:

public void handle(Callback[] callbacks)
              throws java.io.IOException, UnsupportedCallbackException {
    for (int i = 0; i < callbacks.length; i++) {
        if (callbacks[i] instanceof NameCallback) {
            NameCallback nameCb = (NameCallback)callbacks[i];
            System.out.print(nameCb.getPrompt());
            String user=(new BufferedReader(new 
InputStreamReader(System.in))).readLine();
            nameCb.setName(user);
        } else if (callbacks[i] instanceof PasswordCallback) {
            PasswordCallback passCb = (PasswordCallback)callbacks[i];
            System.out.print(passCb.getPrompt());
            String pass=(new BufferedReader(new 
InputStreamReader(System.in))).readLine();
            passCb.setPassword(pass.toCharArray());
        } else {
            throw(new UnsupportedCallbackException(callbacks[i],
                    "Callback class not supported"));
        }
    }
}

Login: Use JAAS plus JSP plus RDBMS

Now that we have our command-line version working, we want to reuse it in a Web application. You might be asking yourself, "Well, how the heck am I supposed to launch a command prompt for a Web user to login?"

And you're right—that login approach won't work. Because of the differences between traditional JAAS callback-based authentication and stateless HTTP, we can't use any of the provided standard Callbacks or CallbackHandlers; they just don't make sense for connectionless HTTP, and you can't very well open up a command prompt for user input.

You might also be thinking that we could instead use HTTP basic authentication, whereby we could get the input through the browser's pop-up Username/Password dialog box. But that has many complications; it requires multiple round-trip HTTP connections (hard to do from our single login() method) and is used less often than form-based Web logins. We'll follow the form-based Web approach in this article's example: we'll get the information from an HTML form and process it via our RdbmsLoginModule.

Given that we don't interact with our LoginModule directly from the application layer, only indirectly through the LoginContext, how we will get our username and password from the form into our LoginModule for authentication is not immediately apparent. Alternatives are available. One option: Prior to creating the LoginContext, we could instantiate a Subject and store the username and password in that Subject's credentials. Then we could supply this prepopulated Subject to the LoginContext constructor. Although technically that would work, it's a bit unwieldy as it adds more security-related code at the application layer, and Subjects are typically populated after authentication, not before.

But given, as we showed above, that we can create our own CallbackHandler and give that to the LoginContext, we can use a similar design here to manage our username/password data. To do that, we created a new class called PassiveCallbackHandler. Here's how we'll use it from our JSP:

   String user = request.getParameter("user");
   String pass = request.getParameter("pass");
   PassiveCallbackHandler cbh = new PassiveCallbackHandler(user, pass);
   LoginContext lc = new LoginContext("Example", cbh);

Our new CallbackHandler has a constructor that takes the username and password so it doesn't actually need to prompt anyone for information. Then later it simply sets the appropriate values in the Callbacks so the LoginModule can remove them as it did in the command-line example. Here's how the PassiveCallbackHandler's handle() method works:

public void handle(Callback[] callbacks)
    throws java.io.IOException, UnsupportedCallbackException {
        for (int i = 0; i < callbacks.length; i++) {
            if (callbacks[i] instanceof NameCallback) {
                NameCallback nameCb = (NameCallback)callbacks[i]; 
nameCb.setName(user);
            } else if(callbacks[i] instanceof PasswordCallback) {
                PasswordCallback passCb = (PasswordCallback)callbacks[i];
                passCb.setPassword(pass.toCharArray());
            } else {
                throw(new UnsupportedCallbackException(callbacks[i],
                    "Callback class not supported"));
             }
        }
    }
}

We only removed a line or two from the ConsoleCallbackHandler—the lines that prompted for user input. Thus, we decided to call our implementation passive: because it does not need to interact with the user.

A note about using this example from JSP: We need to set the system property so the LoginContext knows where to look for the "Example" login configuration. In this example, we use the Resin application server. With its configuration setup, we edit the resin.conf file, and add a node under the <caucho.com> node that looks like this:

<system-property 
java.security.auth.login.config="/resin/conf/jaas.config"/>

Here are some additional ideas for JSP-based JAAS:

  • By creating a CallbackHandler with an HttpServletRequest argument, you can authenticate using additional information from the request, including the client's IP address.
  • By having your LoginContext also implement the HttpSessionBindingListener interface, you can log users out when their sessions end (see example class SessionLoginContext).
  • Some application servers' proprietary authentication and authorization interfaces can integrate with JAAS. For example, you can build an implementation of the Jakarta Project's Tomcat Realm interface, which verifies access for users of Tomcat Web applications, around JAAS code.

Other notes

We detailed the authentication half of JAAS only. If you're interested in learning more about the authorization half, including Policies and Permissions, we list many Resources below.

1 2 3 Page 2
Page 2 of 3