User Authentication
The Java Authentication and Authorization Service (JAAS) is a part of Java SE 1.4 and beyond. 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, NT 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".
You 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()); LoginContext 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:
UnixLoginModule NTLoginModule Krb5LoginModule JndiLoginModule KeyStoreLoginModule
A login policy consists of a sequence of login modules, each of which is labeled required, sufficient, requisite, or optional. The meaning of these keywords is given by the following algorithm:
- The modules are executed in turn, until a sufficient module succeeds, a requisite module fails, or the end of the module list is reached.
- Authentication is successful if all required and requisite modules succeed, or if none of them were executed, if at least one sufficient or optional module succeeds.
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.UnixPrincipal describes the UNIX login name, and the UnixNumericGroupPrincipal 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.UnixPrincipal "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 doAsPrivileged method to start a new PrivilegedAction whose run method executes the code.
Both of those methods execute an action by calling the run method of an object that implements the PrivilegedAction interface, using the permissions of the subject's principals:
PrivilegedAction<T> action = new PrivilegedAction() { public T run() { // run with permissions of subject principals . . . } }; T result = Subject.doAs(subject, action); // or Subject.doAsPrivileged(subject, action, null)
If the actions can throw checked exceptions, then you implement the PrivilegedExceptionAction interface instead.
The difference between the doAs and doAsPrivileged methods is subtle. The doAs method starts out with the current access control context, whereas the doAsPrivileged 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.Login1"; 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 Listing 9-6 and Listing 9-7 demonstrates how to restrict permissions to certain users. The AuthTest program authenticates a user and then 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 *.java jar cvf login.jar AuthTest.class jar cvf action.jar SysPropAction.class
If you look at the policy file in Listing 9-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=AuthTest.policy -Djava.security.auth.login.config=jaas.config AuthTest
Listing 9-12 shows the login configuration.
On Windows, change Unix to NT in both AuthTest.policy and jaas.config, and 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 change the login name in the AuthTest.policy file, then a security exception should be thrown because you no longer have the required permission.
Listing 9-6. AuthTest.java
1. import java.security.*; 2. import javax.security.auth.*; 3. import javax.security.auth.login.*; 4. 5. /** 6. * This program authenticates a user via a custom login and then executes the SysPropAction 7. * with the user's privileges. 8. * @version 1.01 2007-10-06 9. * @author Cay Horstmann 10. */ 11. public class AuthTest 12. { 13. public static void main(final String[] args) 14. { 15. System.setSecurityManager(new SecurityManager()); 16. try 17. { 18. LoginContext context = new LoginContext("Login1"); 19. context.login(); 20. System.out.println("Authentication successful."); 21. Subject subject = context.getSubject(); 22. System.out.println("subject=" + subject); 23. PrivilegedAction<String> action = new SysPropAction("user.home"); 24. String result = Subject.doAsPrivileged(subject, action, null); 25. System.out.println(result); 26. context.logout(); 27. } 28. catch (LoginException e) 29. { 30. e.printStackTrace(); 31. } 32. } 33. }
Listing 9-7. SysPropAction.java
1. import java.security.*; 2. 3. /** 4. This action looks up a system property. 5. * @version 1.01 2007-10-06 6. * @author Cay Horstmann 7. */ 8. public class SysPropAction implements PrivilegedAction<String> 9. { 10. /** 11. Constructs an action for looking up a given property. 12. @param propertyName the property name (such as "user.home") 13. */ 14. public SysPropAction(String propertyName) { this.propertyName = propertyName; } 15. 16. public String run() 17. { 18. return System.getProperty(propertyName); 19. } 20. 21. private String propertyName; 22. }
Listing 9-8. AuthTest.policy
1. grant codebase "file:login.jar" 2. { 3. permission javax.security.auth.AuthPermission "createLoginContext.Login1"; 4. permission javax.security.auth.AuthPermission "doAsPrivileged"; 5. }; 6. 7. grant principal com.sun.security.auth.UnixPrincipal "harry" 8. { 9. permission java.util.PropertyPermission "user.*", "read"; 10. };
-
LoginContext(String name)
constructs a login context. The name corresponds to the login descriptor in the JAAS configuration file.
-
void login()
establishes a login or throws LoginException if the login failed. Invokes the login method on the managers in the JAAS configuration file.
-
void logout()
logs out the subject. Invokes the logout method on the managers in the JAAS configuration file.
-
Subject getSubject()
returns the authenticated subject.
-
Set<Principal> getPrincipals()
gets the principals of this subject.
- static Object doAs(Subject subject, PrivilegedAction action)
- static Object doAs(Subject subject, PrivilegedExceptionAction action)
- static Object doAsPrivileged(Subject subject, PrivilegedAction action, AccessControlContext context)
-
static Object doAsPrivileged(Subject subject, PrivilegedExceptionAction action, AccessControlContext context)
executes the privileged action on behalf of the subject. Returns the return value of the run method. The doAsPrivileged methods execute the action in the given access control context. You can supply a "context snapshot" that you obtained earlier by calling the static method AccessController.getContext(), or you can supply null to execute the code in a new context.
-
Object run()
You must define this method to execute the code that you want to have executed on behalf of a subject.
-
Object run()
You must define this method to execute the code that you want to have executed on behalf of a subject. This method may throw any checked exceptions.
-
String getName()
returns the identifying name of this principal.
JAAS Login Modules
In this section, we 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 9-9). 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 9-10. The checkLogin method checks whether the user name and password match a user 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 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,
LoginContext 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. com.sun.security.auth.callback.TextCallbackHandler gets the information from the console.
However, in our application, we have our own GUI for collecting the user name and password (see Figure 9-10). We produce a simple handler that merely stores and returns that information (see Listing 9-11).
Figure 9-10 A custom login module
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:
NameCallback nameCall = new NameCallback("username: "); PasswordCallback passCall = new PasswordCallback("password: ", false); callbackHandler.handle(new Callback[] { nameCall, passCall });
Then it retrieves the information from the callbacks.
The program in Listing 9-12 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 9-13, 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 9-14 shows the login configuration.
Listing 9-9. SimplePrincipal.java
1. import java.security.*; 2. 3. /** 4. * A principal with a named value (such as "role=HR" or "username=harry"). 5. * @version 1.0 2004-09-14 6. * @author Cay Horstmann 7. */ 8. public class SimplePrincipal implements Principal 9. { 10. /** 11. * Constructs a SimplePrincipal to hold a description and a value. 12. * @param roleName the role name 13. */ 14. public SimplePrincipal(String descr, String value) 15. { 16. this.descr = descr; 17. this.value = value; 18. } 19. 20. /** 21. * Returns the role name of this principal 22. * @return the role name 23. */ 24. public String getName() 25. { 26. return descr + "=" + value; 27. } 28. 29. public boolean equals(Object otherObject) 30. { 31. if (this == otherObject) return true; 32. if (otherObject == null) return false; 33. if (getClass() != otherObject.getClass()) return false; 34. SimplePrincipal other = (SimplePrincipal) otherObject; 35. return getName().equals(other.getName()); 36. } 37. 38. public int hashCode() 39. { 40. return getName().hashCode(); 41. } 42. 43. private String descr; 44. private String value; 45. }
Listing 9-10. SimpleLoginModule.java
1. import java.io.*; 2. import java.security.*; 3. import java.util.*; 4. import javax.security.auth.*; 5. import javax.security.auth.callback.*; 6. import javax.security.auth.login.*; 7. import javax.security.auth.spi.*; 8. 9. /** 10. * This login module authenticates users by reading usernames, passwords, and roles from a 11. * text file. 12. * @version 1.0 2004-09-14 13. * @author Cay Horstmann 14. */ 15. public class SimpleLoginModule implements LoginModule 16. { 17. public void initialize(Subject subject, CallbackHandler callbackHandler, 18. Map<String, ?> sharedState, Map<String, ?> options) 19. { 20. this.subject = subject; 21. this.callbackHandler = callbackHandler; 22. this.options = options; 23. } 24. 25. public boolean login() throws LoginException 26. { 27. if (callbackHandler == null) throw new LoginException("no handler"); 28. 29. NameCallback nameCall = new NameCallback("username: "); 30. PasswordCallback passCall = new PasswordCallback("password: ", false); 31. try 32. { 33. callbackHandler.handle(new Callback[] { nameCall, passCall }); 34. } 35. catch (UnsupportedCallbackException e) 36. { 37. LoginException e2 = new LoginException("Unsupported callback"); 38. e2.initCause(e); 39. throw e2; 40. } 41. catch (IOException e) 42. { 43. LoginException e2 = new LoginException("I/O exception in callback"); 44. e2.initCause(e); 45. throw e2; 46. } 47. 48. return checkLogin(nameCall.getName(), passCall.getPassword()); 49. } 50. 51. /** 52. * Checks whether the authentication information is valid. If it is, the subject acquires 53. * principals for the user name and role. 54. * @param username the user name 55. * @param password a character array containing the password 56. * @return true if the authentication information is valid 57. */ 58. private boolean checkLogin(String username, char[] password) throws LoginException 59. { 60. try 61. { 62. Scanner in = new Scanner(new FileReader("" + options.get("pwfile"))); 63. while (in.hasNextLine()) 64. { 65. String[] inputs = in.nextLine().split("\\|"); 66. if (inputs[0].equals(username) && Arrays.equals(inputs[1].toCharArray(), password)) 67. { 68. String role = inputs[2]; 69. Set<Principal> principals = subject.getPrincipals(); 70. principals.add(new SimplePrincipal("username", username)); 71. principals.add(new SimplePrincipal("role", role)); 72. return true; 73. } 74. } 75. in.close(); 76. return false; 77. } 78. catch (IOException e) 79. { 80. LoginException e2 = new LoginException("Can't open password file"); 81. e2.initCause(e); 82. throw e2; 83. } 84. } 85. 86. public boolean logout() 87. { 88. return true; 89. } 90. 91. public boolean abort() 92. { 93. return true; 94. } 95. 96. public boolean commit() 97. { 98. return true; 99. } 100. 101. private Subject subject; 102. private CallbackHandler callbackHandler; 103. private Map<String, ?> options; 104. }
Listing 9-11. SimpleCallbackHandler.java
1. import javax.security.auth.callback.*; 2. 3. /** 4. * This simple callback handler presents the given user name and password. 5. * @version 1.0 2004-09-14 6. * @author Cay Horstmann 7. */ 8. public class SimpleCallbackHandler implements CallbackHandler 9. { 10. /** 11. * Constructs the callback handler. 12. * @param username the user name 13. * @param password a character array containing the password 14. */ 15. public SimpleCallbackHandler(String username, char[] password) 16. { 17. this.username = username; 18. this.password = password; 19. } 20. 21. public void handle(Callback[] callbacks) 22. { 23. for (Callback callback : callbacks) 24. { 25. if (callback instanceof NameCallback) 26. { 27. ((NameCallback) callback).setName(username); 28. } 29. else if (callback instanceof PasswordCallback) 30. { 31. ((PasswordCallback) callback).setPassword(password); 32. } 33. } 34. } 35. 36. private String username; 37. private char[] password; 38. }
Listing 9-12. JAASTest.java
1. import java.awt.*; 2. import java.awt.event.*; 3. import javax.security.auth.*; 4. import javax.security.auth.login.*; 5. import javax.swing.*; 6. 7. /** 8. * This program authenticates a user via a custom login and then executes the SysPropAction 9. * with the user's privileges. 10. * @version 1.0 2004-09-14 11. * @author Cay Horstmann 12. */ 13. public class JAASTest 14. { 15. public static void main(final String[] args) 16. { 17. System.setSecurityManager(new SecurityManager()); 18. EventQueue.invokeLater(new Runnable() 19. { 20. public void run() 21. { 22. JFrame frame = new JAASFrame(); 23. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 24. frame.setVisible(true); 25. } 26. }); 27. } 28. } 29. 30. /** 31. * This frame has text fields for user name and password, a field for the name of the requested 32. * system property, and a field to show the property value. 33. */ 34. class JAASFrame extends JFrame 35. { 36. public JAASFrame() 37. { 38. setTitle("JAASTest"); 39. 40. username = new JTextField(20); 41. password = new JPasswordField(20); 42. propertyName = new JTextField(20); 43. propertyValue = new JTextField(20); 44. propertyValue.setEditable(false); 45. 46. JPanel panel = new JPanel(); 47. panel.setLayout(new GridLayout(0, 2)); 48. panel.add(new JLabel("username:")); 49. panel.add(username); 50. panel.add(new JLabel("password:")); 51. panel.add(password); 52. panel.add(propertyName); 53. panel.add(propertyValue); 54. add(panel, BorderLayout.CENTER); 55. 56. JButton getValueButton = new JButton("Get Value"); 57. getValueButton.addActionListener(new ActionListener() 58. { 59. public void actionPerformed(ActionEvent event) 60. { 61. getValue(); 62. } 63. }); 64. JPanel buttonPanel = new JPanel(); 65. buttonPanel.add(getValueButton); 66. add(buttonPanel, BorderLayout.SOUTH); 67. pack(); 68. } 69. 70. public void getValue() 71. { 72. try 73. { 74. LoginContext context = new LoginContext("Login1", new SimpleCallbackHandler(username 75. .getText(), password.getPassword())); 76. context.login(); 77. Subject subject = context.getSubject(); 78. propertyValue.setText("" 79. + Subject.doAsPrivileged(subject, new SysPropAction(propertyName.getText()), null)); 80. context.logout(); 81. } 82. catch (LoginException e) 83. { 84. JOptionPane.showMessageDialog(this, e); 85. } 86. } 87. 88. private JTextField username; 89. private JPasswordField password; 90. private JTextField propertyName; 91. private JTextField propertyValue; 92. }
Listing 9-13. JAASTest.policy
1. grant codebase "file:login.jar" 2. { 3. permission java.awt.AWTPermission "showWindowWithoutWarningBanner"; 4. permission javax.security.auth.AuthPermission "createLoginContext.Login1"; 5. permission javax.security.auth.AuthPermission "doAsPrivileged"; 6. permission javax.security.auth.AuthPermission "modifyPrincipals"; 7. permission java.io.FilePermission "password.txt", "read"; 8. }; 9. 10. grant principal SimplePrincipal "role=admin" 11. { 12. permission java.util.PropertyPermission "*", "read"; 13. };
Listing 9-14. jaas.config
1. Login1 2. { 3. SimpleLoginModule required pwfile="password.txt"; 4. };
-
void handle(Callback[] callbacks)
handles the given callbacks, interacting with the user if desired, and stores the security information in the callback objects.
- NameCallback(String prompt)
-
NameCallback(String prompt, String defaultName)
constructs a NameCallback with the given prompt and default name.
- void setName(String name)
-
String getName()
sets or gets the name gathered by this callback.
-
String getPrompt()
gets the prompt to use when querying this name.
-
String getDefaultName()
gets the default name to use when querying this name.
-
PasswordCallback(String prompt, boolean echoOn)
constructs a PasswordCallback with the given prompt and echo flag.
- void setPassword(char[] password)
-
char[] getPassword()
sets or gets the password gathered by this callback.
-
String getPrompt()
gets the prompt to use when querying this password.
-
boolean isEchoOn()
gets the echo flag to use when querying this password.
-
void initialize(Subject subject, CallbackHandler handler, Map<String,?> sharedState, Map<String,?> options)
initializes this LoginModule for authenticating the given subject. During login processing, uses the given handler to gather login information. Use the sharedState map for communication with other login modules. The options map contains the name/value pairs specified in the login configuration for this module instance.
-
boolean login()
carries out the authentication process and populates the subject's principals. Returns true if the login was successful.
-
boolean commit()
is called after all login modules were successful, for login scenarios that require a two-phase commit. Returns true if the operation was successful.
-
boolean abort()
is called if the failure of another login module caused the login process to abort. Returns true if the operation was successful.
-
boolean logout()
logs out this subject. Returns true if the operation was successful.