In-Depth
All That JAAS
JAAS is based on the Pluggable Authentication Modules model and provides authentication and authorization services. Check out its many security benefits for Java applications.
The Java Authentication and Authorization Service (JAAS) was introduced during the lifetime of JDK 1.3. JAAS provides two services: authentication and authorization. The authentication service is pluggable, which means that an application can remain independent of the authentication techniques used, that authentication modules can be upgraded easily, and that an application can use multiple forms of authentication concurrently. Let's look at the authentication part of JAAS, and then we'll discuss authorization later.
JAAS is based on the Pluggable Authentication Modules (PAM) model. PAM is used widely in the Unix world. Pluggable authentication confers several benefits: it allows applications to provide for single sign-on across multiple security domains; it allows for easy upgrading of a given login module; it allows for other modules to be added as necessary without changing client code; and it allows for rules to be applied to each login module such that the result of one login may or may not effect the overall login process-that is, a module can be marked as optional or required.
In JAAS the login modules that an application uses are configured externally to that application. That configuration is made available to the application, which simply calls a single login method to perform the login (see Figure 1). Each module then gathers the necessary data to perform the login. So, how does this work?
Within an application, making use of JAAS for authentication is straightforward. The application creates a new LoginContext and calls its login method. The LoginContext is populated with a collection of LoginModules. The LoginModules to use are specified in a configuration file, which is passed to the application at startup. We'll see how this process works shortly. Each LoginModule is then executed as necessary, depending on the options in the configuration file.
JAAS Configuration
The JAAS configuration file is similar in layout to a Java security policy file. The file specifies the fully qualified classname of the login module to load along with a flag specifying the behavior as authentication proceeds. The format of this file is:
Application1 {
Class Flag Options;
Class Flag Options;
Class Flag Options;
};
Application2 {
Class Flag Options;
Class Flag Options;
};
other {
Class Flag Options;
Class Flag Options;
};
Application1 and so on is the named block referenced by the application when creating the LoginContext. Note that a given configuration file can contain more than one named block. The other block is a special application block that is consulted if the application name used in the Java code does not match any application name used in the configuration file.
Each entry in the application block consists of three values: Class is the fully qualified class name of the code implementing the LoginModule; Flag indicates the behavior of the login procedure; and Options are login-context-specific properties, although several standard properties have emerged. We'll discuss each of these as well in more detail shortly.
The name of the configuration file is arbitrary; it doesn't follow any naming convention and does not have to have a specific extension, but it has to be processed by the JAAS runtime, in particular by the javax.security.auth.login.Configuration class. How does this class get a reference to the configuration file? It turns out there are two ways. One way is by passing the name of the configuration file as a system property using the -D flag to the Java runtime, therefore, assuming the name of the file is jaas.config, the command to start the VM would be:
java -Djava.security.auth.
login.config=
jaas.config MyApp
The other option is to add the names of all the configuration files to the java.security file in <JRE_HOME>/lib/security/java.security. There are two entries in the file that can be set:
login.configuration.provider
and:
login.config.url.n
The first setting (login.configuration.provider) allows you to specify the name of the provider for the configuration file. The configuration shown previously is the reference implementation. You could write your own provider that uses another syntax, such as XML, and specify the class name of that provider here. This option is outside the scope of this discussion. The default value for this property, and the one that is used if this property is not set, is com.sun.security.auth.login.ConfigFile.
The second option (login.config.url.n) is what you use to specify the location of the configuration file, for example:
login.config.url.1=
file:c:/myapp/jaas.config
If neither the system property or the entry in the java.security file is set, then JAAS attempts to load the default property file from:
file:${user.home}/.java.login.
config
Recall that an entry in the configuration file looks like:
Class Flag Options;
Flag specifies the behavior of the authentication procedure as it applies to this module and can take one of four values: required, requisite, optional, and sufficient. If the Required value is set, it means that this login module is required to succeed. If authentication fails at this point, the other modules are still executed; however, overall authentication will have failed. If the Requisite value is set, it means that this login module is required to succeed. If authentication fails at this point, control returns to the application immediately and authentication will have failed. If the Sufficient value is set, it means that this login module is not required to succeed. If authentication succeeds at this point, control returns to the application immediately. If the Optional value is set, it means that this login module is not required to succeed. If authentication succeeds or fails at this point, the other modules are still executed.
Sound Reasoning
Note that the overall authentication succeeds only if all Required and Requisite LoginModules succeed. If a Sufficient LoginModule is configured and succeeds, then only the Required and Requisite LoginModules prior to that Sufficient LoginModule need to have succeeded for the overall authentication to succeed. If no Required or Requisite LoginModules are configured for an application, then at least one Sufficient or Optional LoginModule must succeed.
Also take note that with the Requisite value control returns to the application immediately; whereas, with the Required value all subsequent LoginModules are attempted. At first blush this seems a little odd as once a Required module fails then the login will fail. However, the reasoning is sound; there is a possibility that in failing immediately an attacker can get information about the LoginModule that has failed; whereas, if all the modules have been tried and the login attempt fails, then an attacker does not know which particular LoginModule failed.
The options passed to a login module through the configuration file are application specific. Options are white space-separated name="value" pairs. A common option to pass here is debug=true, to allow the module to output debugging data. Note the options must be understood and processed by the login module. There are also several standard options that originate in the PAM specifications. For example, try_first_past, use_first_pass, and related options allow a set of modules to use a single sign-on approach. By passing either of these options you are asking this login module to use the password passed to the first login module, which requires that each login module is able to communicate with other modules. We will see how this is achieved when we create our own login module.
At this point we have seen how to configure the login process by supplying the configuration file to JAAS and by providing various flags to the configured login modules. When the user calls LoginContext.login, the LoginContext calls each LoginModule's login method to perform the login. Each module will need some data to perform the login, so the next question is, how does the module get the data that it needs? There are several ways this data gathering can happen, but the most common way is for the LoginModule to call back into the application and get the application to specify the data that is needed to perform the login.
The application passes the login context a reference to a CallbackHandler (that implements javax.security.auth.callback.CallbackHandler), and this handler is passed to each login module. Each module then calls the handler passing an array of javax.security.auth.callback.Callbacks. The CallbackHandler then checks the type of callback and performs the appropriate action. An example will make things clearer. First, we define our handler:
class MyCallbackHandler
implements CallbackHandler
{
public void handle(
Callback[] callbacks)
throws IOException,
UnsupportedCallbackException
{
// perform work here
}
}
We then pass a reference to this handler to the login method:
LoginContext context =
new LoginContext(
"SimpleContext",
new MyCallbackHandler());
This reference is then passed to each login module, in turn, and each module calls the handler passing an array of Callbacks that gather the information the module needs to perform its authentication.
Handling Login Data
The JDK comes with a number of predefined callbacks, such as NameCallback, that can be used to gather a users' name and PasswordCallback to gather the password. How the handler gathers the data is down to the application; for example, in a Web application there may have been a login page that extracts this information. In a stand-alone application there may be user interaction through a dialog box or the command line. If our handler wanted to gather the username and password it could look something like the code shown in Listing 1. This handler gets the user name and password form the system input stream and passes those values to whichever login module is calling us.
The handler is relatively straightforward. The application simply responds to each callback and provides the data needed. Handlers can be more complicated; for example, a callback handler might want to display a dialog box to the user asking for the details. There are a couple of problems with doing this: first, what text should appear in the dialog to identify to the user which login module they are using, and second, when should the dialog appear, that is, what type of callback should trigger the dialog?
As an example, if we run the code shown in Listing 1 after configuring the application with the com.sun.security.auth.module.KeyStoreLoginModule login module-one of the sample modules that comes with the JDK-we'll get two password callbacks. The interaction with the handler looks something like:
Keystore alias:
signjar
Keystore password:
changeit
Private key password (optional):
changeit
where Keystore alias:, Keystore password:, and Private key password (optional): are the prompts, and the other data are the names and passwords. Notice that the module asks for two passwords, so if the application wants to display a dialog box it has to be after the second password. But how does the application know this?
Establishing a Dialog
To solve the problem of when to prompt the user for input, the JDK comes with another callback, the ConfirmationCallback, which is used to ask the application a Yes/No/Cancel question and can be used by an application to trigger the display of any dialog box needed to gather the data. The KeyStoreLoginModule also uses a TextOutputCallback to allow the dialog box to provide a message to the user. Sun provides an unsupported callback handler to display a dialog. The class to use is com.sun.security.auth.callback.DialogCallbackHandler, and using it produces the dialog for the KeyStoreLoginModule (see Figure 2). Although this is an unsupported class, you can use it as the basis for your own dialog box.
The JDK comes with a number of login modules, such as the NTLoginModule, JNDILoginModule, and KeyStoreLoginModule. These modules are all in the com.sun.security.auth.module package, are not a formal part of the JDK, and hence are unsupported. While you can use these modules, in all likelihood you will need to write your own.
Login modules implement the javax.security.auth.spi.LoginModule interface:
void initialize(
Subject subject,
CallbackHandler
callbackHandler,
Map sharedState, Map
options);
boolean login() throws
loginException;
boolean logout() throws
loginException;
boolean abort() throws
loginException;
boolean commit() throws
loginException;
The initialize method obviously initializes the module, login is called when the user logs in, and logout is called when the user logs out. However, what about abort and commit? The login process is a two-phase process. A login module may coexist alongside other modules within a login context, and the result of those login modules can affect ours. For example, if a required login module is called after ours and fails to authenticate, we need to abort the login, which is why the two-phase protocol is necessary.
We will write a login module to allow a user to login through JDBC. We will not show all the code here, but instead just outline the basic principles (download the sample code to view it all). Before we look at the login module it is important to understand what the call to login returns. LoginContext.login returns a javax.security.auth.Subject. The Subject is a container for java.security.Principal objects and for a collection of credentials associated with these principals. A principal represents a logged-on entity, which means that each login module creates a principal and stores the principal in the Subject container. The subject can then execute code on behalf of these principals, which is another topic for a future article.
How will the JDBC login module work? At a high level the login module will need to be initialized with the JDBC information, gather the user name and password from the user, and check in the database to see if this user exists. If all authentications succeed, then it'll create a principal and add it to the subject; if authentication fails, it'll tidy up its login data.
Getting a Helping Hand
Specifying options is easy. These options can be passed as part of the Options string in the configuration file. Our configuration entry would look like:
com.develop.kevinj.
JDBCLoginModule required
className=
"com.mysql.jdbc.Driver"
URL=
"jdbc:mysql://localhost/users"
debug="true";
This entire entry appears on a single line (the page's column width requires showing it on multiple lines here). When the module is loaded its initialize method is called, and it needs to get the driver classname and the URL from the options:
public void initialize(
Subject subject,
CallbackHandler
callbackHandler,
Map sharedState, Map
options)
{
this.subject = subject;
this.callbackHandler =
callbackHandler;
this.sharedState =
sharedState;
jdbcURL = (String)
options.get("URL");
className = (String)
options.get("className");
}
Notice that we also store references to the other parameters for later use. It's the login method that performs the login. This process has to gather the data needed for the authentication attempt and then authenticate this user. The data gathering is done by creating an array of callbacks and passing them to the callback handler's handle method. Once the data is gathered the login method then does the login, which we do by calling a helper (see Listing 2).
Notice that we pass a TextOutput callback handler, which allows the client to display an informational message, Name and Password callback handlers to get the data, and a Confirmation callback handler to allow the user to confirm the login. The helper method, doLocalLogin(), checks the database. If the login succeeds the helper method extracts the user's full name from the database and stores it in an instance variable for later use, along with the user name and password.
If all authentication succeeds, then the confirm method will be called. This method has to create the principal and credentials and store them in the subject (which was stored in the initialize method):
public boolean commit() throws
LoginException
{
if (loginSucceeded)
{
Set principals =
subject.getPrincipals();
principals.add(
new JDBCPrincipal(
fullname));
Set credentials = subject.
getPrivateCredentials();
credentials.add(password);
}
name = null;
password = null;
fullname = null;
return true;
}
The JDBCPrincipal class is defined as part of this module and simply stores a reference to the full name of the user that was gathered during the login processing. Notice also that the method cleans up after itself by deleting the data used during the login process.
The abort method is called if authentication fails; its job is to tidy up the module for further use. (This code is not shown.)
And that's it. You can include the classes on your classpath, make sure the module is configured properly, and off you go. One other thing, the module expects the database to have three columns: Username, Password, and Fullname.
Mapping the Modules
One of the benefits of JAAS is that you can use it to perform a single sign-on. Only one of the modules (the first) gathers the login credentials and then communicates the information to the other modules in the context. If you look at the initialize method, it is passed a variable called sharedState that is a map, which is the mechanism used to communicate among modules. One module stores data here, and another module can read it.
This mechanism gives rise to two questions: How does the module know if it should look in the shared state for data? And, how does a module know what the key of that data is? (Keep in mind the shared state is a map.)
The module knows that it should retrieve or set information in the sharedState by using the options in the config file. For example, the JndiLoginModule uses useFirstPass, tryFirstPass, storePass, and clearPass; these values are similar to those used by the PAM. The useFirstPass option means that the module will use the password stored in shared state and not retry if the authentication fails; while tryFirstPass causes the module to use the callback handlers if the password is invalid. The store and clear options store the password in and remove the password from shared state.
If the module supports single sign-on, then it needs to be able to access and/or store the password and username in shared state. This requirement is accomplished by using two standard property names: javax.security.auth.login.name and javax.security.auth.login.password.
Writing a LoginModule that supports single sign-on is slightly more complicated. In the login method you have to check for try/usesFirstPass and get the password from the shared state. If storePass is set, then the password has to be placed into shared state, which also has to be done in the login method. If clearPass is set, then any shared passwords must be cleared during the commit phase.
The first part of JAAS is authentication. The end product of the authentication phase is a subject. This subject can now be authorized. Authorization is the act of ensuring that the returned subject should be allowed to perform a given function.
Remember that subjects are collections of principals. A principal represents a logged in entity, and authorization checks are performed on behalf of a principal. A Java class represents principals, and they have a name. This point is important because two principals can have the same name, but if they are represented by different Java classes they are potentially different principals.
Traditionally, security in Java has been based on from where code was loaded. The default mechanism in Java is for the AccessController to read a policy file. The policy specifies the level of access for code that has been loaded from a specific location. JAAS extends the policy file so that the Principal can also be taken into account when granting permissions. Note that in earlier versions of the JDK, JAAS had its own policy file. However, since JDK 1.4 the JAAS and standard policy files have been merged.
Granting Permission
To specify permissions for a principal you would add something like this to the policy file:
grant Principal com.develop.
jsec.auth.JDBCPrincipal
"Alice"
{
permission java.io.
FilePermission
"<<ALL FILES>>", "read";
};
This grant block will grant the defined permissions to the principal with the Java class com.develop.jsec.auth.JDBCPrincipal and the name "Alice." Once the permissions have been specified in the policy file, how do they get used in code? This is the responsibility of the Subject class.
The Subject class has a static doAs() method that performs work on behalf of a given subject. This method combines the current threads AccessControlContext with an AccessControlContext as defined in the policy file for the principals that make up the Subject class. What this means in plain English is that the Subject class reads the policy file and combines the permissions associated with the principals to the permissions associated with the thread. The doAs() method is passed a reference to a PrivilegedAction object, which has a run method, and it is this run method that executes in the new, combined AccessControlContext. Whatever the run method, attempts must be granted by the policy file. If not, then an exception is thrown. Also, the principal and the thread that is executing the run method must both have appropriate privileges. For example, if a principal has permission to read a file but the current thread doesn't, then the code will not be able to read the file.
As an example of this scenario, imagine that we want to open a file on behalf of a principal. The first step is to write a PrivilegedAction class:
public class JAASAction
implements PrivilegedAction
{
final String fileName =
"somefilename";
public Object run()
{
try
{
FileInputStream fis =
new FileInputStream(
fileName);
}
catch (
FileNotFoundException e)
{
// handle exception
}
return null;
}
}
This action is very simple; it simply opens a file. Calling this code requires the client to call Subject.doAs():
context = new LoginContext(
"SimpleContext", new
DialogCallbackHandler());
context.login();
Subject s =
context.getSubject();
Subject.doAs(
s, new JAASAction());
This action will execute the code on behalf of the principals in the subject as defined by the parameter s. And remember that at least one principal and the current thread must have permission to read the file.
Something else worthy to ask is what happens if the run method wants to throw an exception (in our case a FileNotFoundException)? We may want that exception propagated back to the caller. To do this, rather than implement PrivilegedAction you could implement PrivilegedExceptionAction. The code would be very similar except now the run method is defined as throwing Exception, and this exception is made available to the calling code as a PrivilegedActionException.
Permission to Run
Suppose you want to run the code only in the AccessControlContext of the subject, or in some other AccessControlContext other than that defined by the thread, which you do by calling Subject.doAsPrivileged(). This takes an extra parameter: a reference to an AccessControlContext. If this last value is non-null, then the subject's AccessControlContext is combined with the passed AccessControlContext, rather than being combined with the thread's AccessControlContext. If the value is passed as null, then essentially no combining takes place and only the subject's AccessControlContext is used. Therefore, as long as the subject has the permissions to execute the code in the run method, the code will run. The code would be similar to the previous code:
context = new LoginContext(
"SimpleContext", new
DialogCallbackHandler());
context.login();
Subject s =
context.getSubject();
Subject.doAsPrivileged(
s, new JAASAction(), null);
If you look back at the policy file shown previously you will see that the grant block is defined for a specific principal in a specific class; however, the grant block allows for wildcards. So we could have done:
grant Principal * *
{
permission java.security.
AllPermission;
}
which means all principals, or:
grant Principal * "Alice"
{
permission java.security.
AllPermission;
}
which is the "Alice" principal from any Java class, or, finally:
grant Principal com.develop.
jsec.auth.JDBCPrincipal *
{
permission java.security.
AllPermission;
}
which means any principal whose class is com.develop.jsec.auth.JDBCPrincipal.
JAAS has two functions: authentication and authorization. We've looked at both. From the client's point of view authentication means creating a LoginContext and calling its login() method. The context is populated with LoginModules. Each module's login() method is called to perform that module's authentication procedure. If authentication succeeds, the client returns a subject that is populated with principals and credentials. If the authentication fails, then an exception is thrown. Remember you can also specify various flags and options to manage the login more accurately. You can also enable single sign-on using JAAS.
JAAS also supports authorization by extending the Java policy file syntax to provide support for principals and by allowing the subject to execute code in a specific context.