The Java API provides a framework, called the Java Authentication and Authorization Service (JAAS), that integrates platform-provided authentication with permission management. We’ll discuss the JAAS framework in the following sections.
1. The JAAS Framework
As you can tell from its name, the JAAS framework has two components. The “authentication” part is concerned with ascertaining the identity of a program user. The “authorization” part maps users to permissions.
JAAS is a “pluggable” API that isolates Java applications from the particular technology used to implement authentication. It supports, among others, UNIX logins, Windows logins, Kerberos authentication, and certificate-based authentication.
Once a user has been authenticated, you can attach a set of permissions. For example, here we grant Harry a particular set of permissions that other users do not have:
grant principal com.sun.security.auth.UnixPrincipal “harry”
{
permission java.util.PropertyPermission “user.*”, “read”;
…
};
The com.sun.security.auth.UnixPrincipal class checks the name of the UNIX user who is running this program. Its getName method returns the UNIX login name, and we check whether that name equals “harry”.
Use a LoginContext to allow the security manager to check such a grant statement. Here is the basic outline of the login code:
try
{
System.setSecurityManager(new SecurityManager());
var context = new LoginContext(“Login1”); // defined in JAAS configuration file context.login();
// get the authenticated Subject Subject subject = context.getSubject();
…
context.logout();
}
catch (LoginException exception) // thrown if login was not successful
{
exception.printStackTrace();
}
Now the subject denotes the individual who has been authenticated.
The string parameter “Login1” in the LoginContext constructor refers to an entry with the same name in the JAAS configuration file. Here is a sample configuration file:
Login1
{
com.sun.security.auth.module.UnixLoginModule required;
com.whizzbang.auth.module.RetinaScanModule sufficient;
};
Login2
{
…
};
Of course, the JDK contains no biometric login modules. The following modules are supplied in the com.sun.security.auth.module package:
UnixLoginModute
NTLoginModute
KrbsLoginModule
JndiLoginModute
KeyStoreLoginModute
A login policy consists of a sequence of login modules, each labeled required, sufficient, requisite, or optional. The meaning of these keywords is given by the following algorithm.
A login authenticates a subject, which can have multiple principals. A principal describes some property of the subject, such as the user name, group ID, or role. As you saw in the grant statement, principals govern permissions. The com.sun.security.auth.UnixPrincipat describes the UNIX login name, and the UnixNumericGroupPrincipat can test for membership in a UNIX group.
A grant clause can test for a principal, with the syntax
grant principalClass “principalName”
For example:
grant com.sun.security.auth.UnixPrincipat “harry”
When a user has logged in, you then run, in a separate access control context, the code that requires checking of principals. Use the static doAs or doAsPriviteged method to start a new PrivitegedAction whose run method executes the code.
Both of those methods execute an action by calling the run method of an object that implements the PrivitegedAction interface, using the permissions of the subject’s principals:
PrivitegedAction<T> action = () ->
{
// run with permissions of subject principals
…
};
T resutt
= Subject.doAs(subject, action); // or Subject.doAsPriviteged(subject, action, nutt)
If the actions can throw checked exceptions, you need to implement the PrivitegedExceptionAction interface instead.
The difference between the doAs and doAsPriviteged methods is subtle. The doAs method starts out with the current access control context, whereas the doAsPriviteged method starts out with a new context. The latter method allows you to separate the permissions for the login code and the “business logic.” In our example application, the login code has permissions
permission javax.security.auth.AuthPermission “createLoginContext.Loginl”;
permission javax.security.auth.AuthPermission “doAsPrivileged”;
The authenticated user has a permission
permission java.util.PropertyPermission “user.*”, “read”;
If we had used doAs instead of doAsPrivileged, then the login code would have also needed that permission!
The program in Listings 10.6 and 10.7 demonstrates how to restrict permissions to certain users. The AuthTest program authenticates a user and runs a simple action that retrieves a system property.
To make this example work, package the code for the login and the action into two separate JAR files:
javac auth/*.java
jar cvf login.jar auth/AuthTest.class
jar cvf action.jar auth/SysPropAction.class
If you look at the policy file in Listing 10.8, you will see that the UNIX user with the name harry has the permission to read all files. Change harry to your login name. Then run the command
java -classpath login.jar:action.jar \
-Djava.security.policy=auth/AuthTest.policy \
-Djava.security.auth.login.config=auth/jaas.config \
auth.AuthTest
Listing 10.9 shows the login configuration.
On Windows, change UnixPrincipal to NTUserPrincipal in AuthTest.policy and UnixLoginModule to NTLoginModule in jaas.config. When running the program, use a semicolon to separate the JAR files:
java -classpath login.jar;action.jar . . .
The AuthTest program should now display the value of the user.home property. However, if you log in with a different name, a security exception should be thrown because you no longer have the required permission.
2. JAAS Login Modules
In this section, we’ll look at a JAAS example that shows you
- How to implement your own login module
- How to implement role-based authentication
Supplying your own login module is useful if you store login information in a database. Even if you are happy with the default module, studying a custom module will help you understand the JAAS configuration file options.
Role-based authentication is essential if you manage a large number of users. It would be impractical to put the names of all legitimate users into a policy file. Instead, the login module should map users to roles such as “admin” or “HR”, and the permissions should be based on these roles.
One job of the login module is to populate the principal set of the subject that is being authenticated. If a login module supports roles, it adds Principal objects that describe roles. The Java library does not provide a class for this purpose, so we wrote our own (see Listing 10.10). The class simply stores a description/value pair, such as role=admin. Its getName method returns that pair, so we can add role-based permissions into a policy file:
grant principal SimplePrincipal “role=admin” { . . . }
Our login module looks up users, passwords, and roles in a text file that contains lines like this:
harry|secret|admin
carl|guessme|HR
Of course, in a realistic login module, you would store this information in a database or directory.
You can find the code for the SimpleLoginModule in Listing 10.11. The checkLogin method checks whether the user name and password match a record in the password file. If so, we add two SimplePrincipal objects to the subject’s principal set:
Set<Principal> principals = subject.getPrincipals();
principals.add(new SimplePrincipal(“username”, username));
principals.add(new SimplePrincipal(“role”, role));
The remainder of SimpleLoginModule is straightforward plumbing. The initialize method receives
- The Subject that is being authenticated
- A handler to retrieve login information
- A sharedState map that can be used for communication between login modules
- An options map that contains the name/value pairs that are set in the login configuration
For example, we configure our module as follows:
SimpleLoginModule required pwfile=”password.txt”;
The login module retrieves the pwfile settings from the options map.
The login module does not gather the user name and password; that is the job of a separate handler. This separation allows you to use the same login module without worrying whether the login information comes from a GUI dialog box, a console prompt, or a configuration file.
The handler is specified when you construct the LoginContext, for example:
var context = new LoginContext(“Login1”,
new com.sun.security.auth.callback.DialogCallbackHandler());
The DialogCallbackHandler pops up a simple GUI dialog box to retrieve the user name and password. The com.sun.security.auth.callback.TextCallbackHandler class gets the information from the console.
However, in our application, we have our own GUI for collecting the user name and password (see Figure 10.8). We produce a simple handler that merely stores and returns that information (see Listing 10.12).
The handler has a single method, handle, that processes an array of Callback objects. A number of predefined classes, such as NameCallback and PasswordCallback, implement the Callback interface. You could also add your own class, such as
RetinaScanCallback. The handler code is a bit unsightly because it needs to analyze the types of the callback objects:
public void handle(Callback[] callbacks)
{
for (Callback callback : callbacks)
{
if (callback instanceof NameCallback) . . .
else if (callback instanceof PasswordCallback) . . .
else . . .
}
}
The login module prepares an array of the callbacks that it needs for authentication:
var nameCall = new NameCallback(“username: “);
var passCall = new PasswordCallback(“password: “, false);
callbackHandler.handle(new Callback[] { nameCall, passCall });
Then it retrieves the information from the callbacks.
The program in Listing 10.13 displays a form for entering the login information and the name of a system property. If the user is authenticated, the property value is retrieved in a PrivilegedAction. As you can see from the policy file in Listing 10.14, only users with the admin role have permission to read properties.
As in the preceding section, you must separate the login and action code. Create two JAR files:
javac *.java
jar cvf login.jar JAAS*.class Simple*.class
jar cvf action.jar SysPropAction.class
Then run the program as
java -classpath login.jar:action.jar \
-Djava.security.policy=JAASTest.policy \
-Djava.security.auth.login.config=jaas.config \
JAASTest
Listing 10.15 shows the policy file.
Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.