Understanding Java Security and JAAS (part 3 a Custom Login Module)

2011-05-13

In this third and last part on JAAS (part1 part2) we will look into writing your own login module to a particular security domain. Writing your own login module may be necessary when you are trying to login to a security domain for which no existing JAAS login module exists (for example to integrate some Java EE servers up with that security domain). Java EE Servers such as Red Hat JBoss and Oracle WebLogic leveraged the JAAS architecture and used it for their connection with the security domain (LDAP, Database etc)

This paper includes a custom challenge/response security domain to have a "rational" for defining the custom JAAS login module. This example security domain is based on a username and answer combination. The question belonging to the answer is stored as part of the user. This example domain is created for the purpose of explaining JAAS. It is not a realistic example, because relying only on answers is too dangerous. An answer to "favorite color" can easily be guessed by trial and error.

The example security realm

The realm is stored in an xml file. When users are added to the security domain using the API (addUser), the answer is digested using a SHA1 algorithm (see AnswerDigester for implementation details). Adding users can also be done by editing the file (not when the system is running!). The answer can then be written in plain text, using a answer element. When the system writes the XML file (JVM shutdown) it will write a digested answer to the XML file.

  <user>
    <username>john</username>
    <question>Pet's name</question>
    <answer
    -digest>8a4d0664....79bc</answer>
  </user>
  <user>
    <username>jennifer</username>
    <question>favorite color</question>
    <!-- plain answer is digested on first run -->
    <answer>blue</answer>
  </user>
</realm>

After the first run, the answer element for Jennifer, is replaced by an answer-digest element containing a SHA1 digested value of the answer.

Below is the API to this security domain:

public class XMLSecurityDomain {
    public static XMLSecurityDomain getInstance(String realmName) { … }
    public void addUser(String userName, String question, String answer)  
                                      throws UsernameInUseException  {  … }
    public void authenticate(String userName, String answer)
                                      throws InvalidCredentials, UnknownUserException { ... }
    public String getQuestion(String userName)
                                       throws UnknownUserException { … }
    private Credentials getCredentials(String userName)
                                       throws UnknownUserException { … }
}

The getInstance method requires a realmName argument. This value is the the name of the XML file containing the username, questions and answers. This file needs to be located in the directory specified using the security.securitydomain.realmdir system property. Hopefully the rest of the method-semantics speak for themselves so we can dive into the JAAS part.

Building the JAAS login module

To create a custom JAAS login module you need to implement the LoginModule. The interface with the login life-cycle methods is shown below:

public interface LoginModule {
  void initialize(Subject subject, CallbackHandler callbackHandler,
                  Map<String, ?> sharedState,
                  Map<String, ?> options);

  boolean login() throws LoginException;

  boolean commit() throws LoginException;

  boolean abort() throws LoginException;

  boolean logout() throws LoginException;
}

The behavior of these methods is best explained by the two sequence diagrams below. The first diagram shows the sequence when logging in, and the second diagram underneath it, shows the sequence when logging out.

jaas login sequence

the logout sequence

~jaas logout sequence

Please observe that a single instance is used to login and that the methods are used a login-life-cycle methods.

The initialize method

Let's first implement the initialze method.

private Subject subject;
private CallbackHandler callbackHandler;
private XMLSecurityDomain domain;

public void initialize(Subject subject, CallbackHandler callbackHandler,
                                   Map<string, ?> sharedState,Map<string , ?> options) {

  this.subject = subject;
  this.callbackHandler = callbackHandler;
  String realmName = (String) options.get("realm");
  this.domain = XMLSecurityDomain.getInstance(realmName);
}

Notice how the initialize method obtains the realm value that will be specified in the JAAS policy file. The initialize method uses this value to obtain a reference to our example's security domain API (XMLSecurityDomain), recall it uses this for the name of the XML file containing the usernames, questions and answers.

The login method

private boolean authenticated;
private String username;

public boolean login() throws LoginException {
  authenticated = false;
  Callback[] cb = new Callback[1];
  NameCallback nameCallback = new NameCallback("name: ");
  cb[0] = nameCallback;

  try {
    callbackHandler.handle(cb);
    username = nameCallback.getName();
    String question = null;
    try {
      question = domain.getQuestion(username);
    } catch (UnknownUserException e) {
       // ask dummy question
       question = "What is your mother's maiden name";
    }
    TextInputCallback answerCallBack = new TextInputCallback("answer this question: "+ question);
    cb[0] = answerCallBack;
    callbackHandler.handle(cb);
    String answer = answerCallBack.getText();

    domain.authenticate(username,answer);
    authenticated = true;

    } catch (IOException e) {
      throw new LoginException(e.getMessage());
    } catch (UnsupportedCallbackException e) {
      throw new LoginException(e.getMessage());
    } catch (UnknownUserException e) {
      throw new LoginException(e.getMessage());
    } catch (InvalidCredentials invalidCredentials) {
        authenticated = false;
    }
  return authenticated;
}

The XML Domain example is a bit more interesting, because it can only ask the question, when it obtained the username from the domain. That's why there are two calls to the CallbackHandler, instead of one, with two Callback objects in it.

The reason why the code above asks a dummy question when it catches a UnknownUserException when obtaining the question, is not to let people know if a certain username is known to the system. If we would stop the login sequence due to the UnknownUserException, than someone would know that a certain username is valid, if it pops up the question. The dummy question should come from a list of dummy questions, as someone will now be able to extract valid name, when another question than "What is your mother's maiden name?" is asked. If the login fails the domain.authenticate will throw an InvalidCredentials exception, which is caught in the code above. In that catch block the authenticated flag is set to false.

Notice that the code sets a authenticated flag and stores the username in an instance field. These will be used by the commit method.

The commit method

This method is called when the LoginContext's overall authentication succeeded. It adds the different principals for this module to the subject.

private XMLUserPrincipal namePrincipal;

public boolean commit() throws LoginException {
  if (authenticated){
    namePrincipal = new XMLUserPrincipal(username);
    subject.getPrincipals().add(namePrincipal);
   }
   this.username = null;
   return authenticated;
 }

Thecommit method checks if we did successfully authenticate. This has to be done, because as described below, this commit could be invoked even though we did not successfully go through the login sequence.

The namePrincipal is stored so that thelogout method later, can remove it from the list of principals. Below is the implementation of the XMLUserPrincipal implementation.

public class XMLUserPrincipal implements Principal {
  private String username;

  public XMLUserPrincipal(String username) {
    this.username = username;
  }

  @Overrride
  public String getName() {
    return username;
  }
}  

The logout method

The purpose of this method is to remove the assigned principals. Is always called when logout is requested, regardless of this module's login success.

public boolean logout() throws LoginException {
  if (authenticated){
    subject.getPrincipals().remove(namePrincipal);
    authenticated = false;
  }
  username=null;
  return true;
}

The example project can be downloaded here

This article does not necessarily reflect the technical opinion of EDC4IT, but purely of the writer. If you want to discuss about this content, please use thecontact ussection of the site