Java Authentication and Authorization Service (JAAS)in Java 2, Standard Edition (J2SE) 1.4

   
   

Articles Index


Traditional Java security mechanisms didn't provide the infrastructure required to support traditional authentication and authorization; security in J2SE is based on public key cryptography and code signing. In other words, authentication was based on the idea that code is executing in the JVM and not a principal making a request for a resource. And authorization was based on the notion that the code attempts to use a computing resource. The Java Authentication and Authorization Service (JAAS) was designed to address these shortcomings.

The JAAS augments the existing code-based access controls with user-based access controls and authentication capabilities. This enables you to grant permissions not just based on what code is running but who is running it. This article:

 

  • Discusses the shortcomings of code-based authentication
  • Provides an overview of JAAS
  • Presents a code-intensive example of authentication
  • Presents a code-intensive example of authorization
  • Offers a flavor of the effort involved in developing authentication and authorizations services
  • Provides sample code that you can adapt for your own applications
  • Provides a tutorial to get started with JAAS

Motivation

Traditional operating systems (such as UNIX) perform authentication on a principal or entity through some sort of challenge-response mechanism. A user ID and password combination is the most salient of these. This technique is also used for protected web resources using the HTTP basic authentication scheme. The challenge, however, can be made more complex: for example, the information can be encrypted or rely on possession of specific information such as mother's maiden name or an answer to a user-selected question. The response then must be valid based on the type of the challenge.

In addition, most operating systems base authorization on an entity or principal and a list of resources authorized for use by that entity or principal. For example, when a user tries to read/write a file, the authorization mechanism verifies that the current executing principal is authorized for the resource.

Overview of JAAS

JAAS consists of two parts: authentication and authorization. In other words, it can be used for both, authentication and authorization:

  • For authenticating users to securely determine who is executing Java code, regardless of whether the code is a stand-alone Java application, an applet, an enterprise JavaBean, or a servlet.
  • For authorization of users to make sure they have the right permissions required to perform their actions.

Authentication is based on the Pluggable Authentication Modules (PAMs) with a framework to be used for both clients and servers. The authorization part is an extension of the existing authorization schemes in J2SE using policy files. Performing authentication in a pluggable fashion enables Java applications to be independent of the underlying authentication mechanism. This has the advantage that new or revised authentication mechanisms can be plugged in without modifying the application itself.

Authorization is an extension of the existing policy-based mechanism which is used to specify what permissions an application (or executing code) can and cannot do. It is based on protection domains. In other words, this mechanism grants permissions based on where the code is coming from and not based on who is executing the code. With JAAS permissions or access control can be based not only on what code is running but who is running it.

Note: JAAS was previously an optional package to the J2SE 1.3.x, but now it has been integrated into J2SE 1.4. The J2SE java.security.Policy API has been upgraded to support Principal-based queries and Principal-based grant entries in policy files as you will see later.

Using JAAS for Authentication

Clients interact with JAAS through a LoginContext object, which provides a way to develop applications independent of the underlying authentication technology. The LoginContext class, which is part of the javax.security.auth.login package, describes the methods used to authenticate subjects. A subject is an identity in a system that you want to authenticate and assign access rights to. A subject can be a human user, a process, or a machine and it is represented by the javax.security.auth.Subject class. Since a subject may interact with multiple authorities (one password for online banking and another for an email system), a java.security.Principal is used to represent the identity in those interactions. In other words, the Principal interface is an abstract notion that can be used to represent an entity, a company, or a login ID. A Subject may contain multiple Principles. An example class implementing the Principal interface will be shown later.

The LoginContext object invokes the LoginModules that are responsible for implementing and performing the authentication. The LoginModule interface, which is part of the javax.security.auth.spi package, must be implemented by authentication technology providers and can be specified by applications to provide a specific type of authentication. The LoginContext reads the Configuration and instantiates the specified LoginModules.

A Configuration is used to specify the authentication technology, or LoginModule, to be used with a particular application. As a result, different LoginModules can be plugged in under an application without any modifications to the application's code itself.

Code Sample 1 shows a sample JAAS client. I have used the LoginContext that will invoke the LoginModules specified in the configuration to perform the authentication, and have initialized the LoginContext with a name "WeatherLogin", and callback handler "MyCallbackHandler" whose implementation is shown in Code Sample 2. The name will be used as the index in the Configuration file to determine which LoginModule should be used. This will become clearer when you see the configuration file (shown in Code Sample 5) later. The callback handler is passed to the underlying LoginModule so that they can communicate and interact with the user to prompt for a username/password, for example, through a textual or graphical user interface. Once the LoginContext has been initialized, the login method is called to login.

Code Sample 1: MyClient.java

import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

public class MyClient {
  public static void main(String argv[]) {
    LoginContext ctx = null;
    try {
      ctx = new LoginContext("WeatherLogin", new MyCallbackHandler());
    } catch(LoginException le) {
      System.err.println("LoginContext cannot be created. "+ le.getMessage());
      System.exit(-1);
    } catch(SecurityException se) {
      System.err.println("LoginContext cannot be created. "+ se.getMessage());
    }
    try {
      ctx.login();
    } catch(LoginException le) {
     System.out.println("Authentication failed. " + le.getMessage());
     System.exit(-1);
    }
    System.out.println("Authentication succeeded.");
    System.exit(-1);
  }
}

A JAAS-based application implements the CallbackHandler interface so that it can interact with users to enter specific authentication data, such as user names or passwords, or to display error and warning messages. The underlying security service may request different types of information via passing individual callbacks to the callback handler. Based on the callbacks passed, the callback handler decides how to retrieve and display information. For example, if the underlying service needs a username and a password to authenticate a user, it uses a NameCallback and PasswordCallback. Other callbacks, which are part of the javax.security.auth.callback class, include:

  • ChoiceCallback (display a list of choices)
  • ConfirmationCallback (ask for YES/NO, OK/CANCEL)
  • LanguageCallback (the Locale used for localizing text
  • TextInputCallback (retrieve generic text information)
  • TextOutputCallback (display information, warning, and error messages)

Implementing the CallbackHandler interface means that you need to provide implementation to the handle method to retrieve or display the information requested in the provided callbacks. A Sample implementation is shown in Code Sample 2. Note that here I am using the NameCallback to interact with the user.

Code Sample 2: MyCallbackHandler.java

import java.io.*;
import javax.security.auth.*;
import javax.security.auth.callback.*;

public class MyCallbackHandler implements CallbackHandler {

  public void handle(Callback callbacks[]) throws IOException, UnsupportedCallbackException {
    for(int i=0;i<callbacks.length;i++) {
      if(callbacks[i] instanceof NameCallback) {
        NameCallback nc = (NameCallback) callbacks[0];
        System.err.print(nc.getPrompt());
        System.err.flush();
        String name = (new BufferedReader(new InputStreamReader(System.in))).readLine();
        nc.setName(name);
      } else {
        throw(new UnsupportedCallbackException(callbacks[i], "Callback handler not support"));
      }
    }
  }
}

Now, let's see a sample implementation of a LoginModule. Note that in reality application developers do not need to implement LoginModules themselves; they can use login modules and plug them into their applications. For example, Sun Microsystems ships several LoginModules including: JndiLoginModule, KeyStoreLoginModule, Krb5LoginModule, NTLoginModule, UNIXLoginModule. If you'd like to learn how to use any of these login modules, please refer to the documentation in the For More Information section at the end of this article.

For the curious, however, Code Sample 3 shows a sample implementation of a LoginModule. This example is very simple as it recognizes only one authentication string and one Principal "SunnyDay", both of which are hard-coded. To login, the system displays "What is the weather like today?", if the answer is "Sunny", the user is logged in. Note how the MyCallbackHandler is being used in the implementation of the login method. In addition to the login method, you must provide implementation for four methods: initialize, commit, abort, and logout. These methods will be used by the LoginContext in the following order:

 

  • initialize: The purpose of this method is to initialize this LoginModule with the relevant information. The Subject passed in this method is used to store the Principals and Credentials if login succeeds. Note that this method takes a CallbackHanlder that can be used for entering authentication information. In this example, I do not use the CallbackHandler. The CallbackHandler is useful as it decouples the services provider from the specific input device being used.
  • login: Asks the LoginModule to authenticate the Subject. Note that the Principal has not been assigned yet.
  • commit: This method is called if the LoginContext's overall authentication succeeded.
  • abort: Informs the LoginModule that some other providers or modules have failed to authenticate the subject. The whole login should fail.
  • logout: Logout the Subject by removing the Principals and Credentials from the Subject.


Code Sample 3: WeatherLoginModule.java

import java.io.*;
import java.util.*;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.spi.LoginModule;
import javax.security.auth.login.LoginException;


public class WeatherLoginModule implements LoginModule {
  private Subject subject;
  private ExamplePrincipal entity;
  private CallbackHandler callbackhandler;
  private static final int NOT = 0;
  private static final int OK = 1;
  private static final int COMMIT = 2;
  private int status;

  public void initialize(Subject subject, CallbackHandler//
   callbackhandler, Map state, Map options) {
    status = NOT;
    entity = null;
    this.subject = subject;
    this.callbackhandler = callbackhandler;
  }

  public boolean login() throws LoginException {

    if(callbackhandler == null) {
      throw new LoginException("No callback handler is available");
    }
    Callback callbacks[] = new Callback[1];
    callbacks[0] = new NameCallback("What is the weather like today?");
    String name = null;
    try {
      callbackhandler.handle(callbacks);
      name = ((NameCallback)callbacks[0]).getName();
    } catch(java.io.IOException ioe) {
      throw new LoginException(ioe.toString());
    } catch(UnsupportedCallbackException ce) {
      throw new LoginException("Error: "+ce.getCallback().toString());
    }
    if(name.equals("Sunny")) {
      entity = new ExamplePrincipal("SunnyDay");
      status = OK;
      return true;
    } else {
      return false;
    }
  }

  public boolean commit() throws LoginException {
    if(status == NOT) {
      return false;
    }
    if(subject == null) {
      return false;
    }
    Set entities = subject.getPrincipals();
    if(!entities.contains(entity)) {
      entities.add(entity);
    }
    status = COMMIT;
    return true;
  }

  public boolean abort() throws LoginException {
    if((subject != null) && (entity != null)) {
      Set entities = subject.getPrincipals();
      if(entities.contains(entity)) {
        entities.remove(entity);
      }
    }
    subject = null;
    entity = null;
    status = NOT;
    return true;
  }

  public boolean logout() throws LoginException {
    subject.getPrincipals().remove(entity);
    status = NOT;
    subject = null;
    return true;
  }
}

As you can see from Code Sample 3, an ExamplePrincipal class is being used. This class an implementation of the Principal interface. Code Sample 4 shows an implementation of the Principal interface.

Code Sample 4: ExamplePrincipal.java

import java.security.Principal;

public class ExamplePrincipal implements Principal {
  private final String name;

  public ExamplePrincipal(String name) {
    if(name == null) {
      throw new IllegalArgumentException("Null name");
    }
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public String toString() {
    return "ExamplePrinciapl: "+name;
  }

  public boolean equals(Object obj) {
    if(obj == null) return false;
    if(obj == this) return true;
    if(!(obj instanceof ExamplePrincipal)) return false;
    ExamplePrincipal another = (ExamplePrincipal) obj;
    return name.equals(another.getName());
  }

  public int hasCode() {
    return name.hashCode();
  }
}

As I mentioned earlier, the LoginContext reads a Configuration to determine which LoginModule(s) to use. The source of login configuration can be a file or a database. The current default implementation from Sun Microsystems is a file. A login configuration file consists or one or more entries specifying which authentication technology should be used for an application. Code Sample 5 shows a login configuration file.

Code Sample 5: example.conf

 

              WeatherLogin {   WeatherLoginModule required; }; 
          

In this configuration file, the entry is named "WeatherLogin" which is the name that MyClient.java uses to refer to this entry. The entry specifies that the WeatherLoginModule should be used to perform the authentication. This module is required in order for the authentication to be considered successful. It is successful if the user enters the right information.

To run the example

  1. Create a directory, let's call it "auth" in your home directory
  2. Copy MyClient.java, WeatherLoginModule.java, ExamplePrincipal.java, example.conf to that directory
  3. Compile all java files: javac *.java
  4. Run the client using the following command specifying the login configuration file:

    prompt> java -Djava.security.auth.login.config=example.conf MyClient

    You should see the following output. Text in bold is entered by the user.

     

    What is the weather like today?  
                     gloomy
    Authentication failed. Login Failure: all modules ignored
                  

    Run the client again with the right input:

     

    What is the weather like today?  
                     Sunny
    Authentication succeeded
                  

Security Managers

In the above example, by default, the application is not running within a security manager and therefore all operations are permissible. To protect resources, run it with a security manager using the following command:

C:\sun\code\auth>java -Djava.security.manager//
 -Djava.security.auth.login.config=example.conf MyClient
LoginContext cannot be created. access denied//
(javax.security.auth.AuthPermission createLoginContext.WeatherLogin)
Exception in thread "main" java.lang.NullPointerException
        at MyClient.main(MyClient.java:17)

As you can see, an exception is thrown. The default security manager doesn't allow any operations so the login context couldn't be created. To allow this, you must create a security policy, which is a text file with grant permissions that specifies what code can and cannot do. Code Sample 6 shows a sample security policy. The createLoginContext target enables MyClient to instantiate a login context. The modifyPrincipals target allow the WeatherLoginModule to populate a Subject with a Principal

Code Sample 6: policy.txt

 

              grant codebase "file:./*" {   permission javax.security.auth.AuthPermission "createLoginContext";   permission javax.security.auth.AuthPermission "modifyPrincipals"; }; 
          

Now, you can run the client using the following command. Note that double == are used to override any default security policy.

 

              prompt>java -Djava.security.manager -Djava.security.policy==policy.txt  -Djava.security.auth.login.config==example.conf MyClient What is the weather like today?                  Sunny Authentication succeeded             
          
Note: the LoginModule implementation and application code can be placed in a JAR file if you like. For more information on this, please see the LoginModule Developer's Guide.

Using JAAS for Authorization

The JAAS authorization extends the code-centric Java security architecture that uses a security policy to specify what access rights are granted to executing code. For example, in the security policy in Code Sample 6, permissions are granted to all the code in the current directory; it does not matter whether the code is signed or unsigned, or who is running the code. JAAS extends this with user-centric access controls. Permissions can be granted based not only on what code is running but based on who is running it. Permissions can be granted in the policy file to specific principals as you will see later

In order to use JAAS authorization:

  1. The user must first be authenticated as I have done above.
  2. The doAs (or doAsPrivileged) method of the Subject class must be called, which in turn invokes the run method that contains the code to be executed as the specified subject.

Based on this, the files ExamplePrincipal.java, WeatherLoginModule.java, and example.conf remain the same.

In order to enable the client to authorize users, once the login is successful (user has been authenticated), the authenticated subject is obtained using Subject subject = ctx.getSubject(). The Subject.doAsPrivileged() is then called passing it the authenticated subject and a privileged action, and a null AccessControlContext. These changes to MyClient.java are highlighted in Code Sample 7. Note that once the authorized action is performed, I log the user out by calling ctx.logout()

Code Sample 7: MyClient.java

import javax.security.auth.Subject;
import java.security.PrivilegedAction;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

public class MyClient {
  public static void main(String argv[]) {
    LoginContext ctx = null;
    try {
      ctx = new LoginContext("WeatherLogin", new MyCallbackHandler());
    } catch(LoginException le) {
      System.err.println("LoginContext cannot be created. "+ le.getMessage());
      System.exit(-1);
    } catch(SecurityException se) {
      System.err.println("LoginContext cannot be created. "+ se.getMessage());
    }
    try {
      ctx.login();
    } catch(LoginException le) {
     System.out.println("Authentication failed");
     System.exit(-1);
    }
    System.out.println("Authentication succeeded");
  
                      Subject subject = ctx.getSubject();     PrivilegedAction action = new MyAction();     Subject.doAsPrivileged(subject, action, null);     try {       ctx.logout();     } catch(LoginException le) {       System.out.println("Logout: " + le.getMessage());     } 
  }
}
                

As you can see, the doAsPrivileged is passed an action, which is the action that the user is authorized to perform. Code Sample 8 shows MyAction.java, which implements the PrivilegedAction interface by providing code for the run method, which contains all the code that will be executed with Principal-based authorization checks. Now, when the doAsprivileged method is invoked, it will in turn invoke the run method in the PrivilegedAction action ( MyAction) to initiate execution of the rest of the code on behalf of the subject.

In this example, the action is to check that the file "max.txt" exists in the current directory.

Code Sample 8: MyAction.java

import java.io.File;
import java.security.PrivilegedAction;

public class MyAction implements PrivilegedAction {
   public Object run() {
     File file = new File("max.txt");
     if(file.exists()) {
       System.out.println("The file exists in the current working directory");
     } else {
       System.out.println("The file does not exist in the current working directory");
     }
     return null;
   }
}

Now, I need to update the policy.txt file. Code Sample 9 shows the new changes:

 

  1. In order to call the doAsPrivileged method of the Subject class, you need to have a javax.security.auth.AuthPermission with a target "doAsPrivileged".
  2. A principal-based grant has been added; it states that a principal "SunnyDay" is allowed to "read" a file "max.txt".

Code Sample 9: policy.txt

grant codebase "file:./*" {
  permission javax.security.auth.AuthPermission "createLoginContext";
  permission javax.security.auth.AuthPermission "modifyPrincipals";
   
                   permission javax.security.auth.AuthPermission "doAsPrivileged";
};

                    grant codebase "file:./*", Principal ExamplePrincipal "SunnyDay" {    permission java.io.FilePermission "max.txt", "read"; }; 
                

Now, when you run the application it will perform the authentication first. If the authentication fails the application stops, and if it succeeds the application goes on to check for the file as shown in the following sample runs:

 

              prompt&gtjava -Djava.security.manager -Djava.security.policy==policy.txt// -Djava.security.auth.login.config==example.conf MyClient What is the weather like today?                  sunny Authentication failed             
          


prompt>java -Djava.security.manager -Djava.security.policy==policy.txt//
-Djava.security.auth.login.config==example.conf MyClient
What is the weather like today?  
                   Sunny
Authentication succeeded
The file does not exist in the current working directory
                



Create the "max.txt" file and run the client again.

 

Conclusion

The JAAS is a set of APIs that enable services to authenticate and enforce access controls upon users. JAAS authentication is performed in a pluggable fashion enabling Java applications to remain independent of the underlying authentication mechanism, and JAAS authorization augments the existing code-centric Java security architecture with user-based access controls and authentication capabilities.

This article presented a fast track tutorial on JAAS. The examples shown throughout demonstrate how to use JAAS for authentication and authorization. Feel free to adapt the code for your own applications. Note that in this example the application and the action are deployed in the same codebase, as an exercise revise the security policy to place the action in its own codebase.

Acknowledgments

Special thanks to Ram Marti of Sun Microsystems, whose feedback helped me improve this article.

See Also

Security enhancements for J2SE 1.4
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/index.html)

JAAS for J2SE 1.4

JAAS White Paper (User Authentication and Authorization in the Java Platform)
(http://docs.oracle.com/javase/6/docs/technotes/guides/security/jaas/acsac.html)

LoginModule Developer's Guide
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/jaas/JAASLMDevGuide.html)

Developing a JAAS Authentication for user/passwd
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/jaas/tutorials/GeneralAcnOnly.html)

Developing JAAS Authorization for user/passwd
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/jaas/tutorials/GeneralAcnAndAzn.html)

JAAS Authentication using the Kerberos LogingModule
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/jgss/tutorials/AcnOnly.html)

JAAS Authorization using the Kerberos LoginModule
(http://docs.oracle.com/javase/1.4.2/docs/guide/security/jgss/tutorials/AcnAndAzn.html)