Chapter 7. Using JAAS

Table of Contents

7.1. Overview
7.2. Implementing LoginModule to Integrate JEUS with LDAP
7.3. Configuring LDAP JAAS LoginModule Service
7.4. Implementing LoginModule to Integrate with Database
7.5. Configuring LoginModule Service

This chapter introduces Java Authentication and Authorization Service (JAAS) and how to integrate SunOne Directory Server with the JEUS security system.

JAAS stands for the Java Authentication and Authorization Service. It implements the Java version of standard Pluggable Authentication Module (PAM), and authenticates and performs access control by supporting user based authentication.

In JavaTM 2 SDK, and Standard Edition (J2SDK) v 1.3, JAAS (JavaTM Authentication and Authorization Service) was an optional package (extension). It has been integrated since the J2SDK v 1.4.

The main purposes of JAAS are as follows.

  • Authenticates users.

    Uses secure method to authenticate the user who is executing the Java code, regardless of the type of code (application, applet, bean, or servlet).

  • Approves users.

    Confirms that the user maintains the access control required to execute the action.

Java 2 has been providing the access control (based on the source of the code and the signature) of the code source base. However, since the additional function of performing the access control based on the executor is not enough, JAAS supports the extended Java2 security architecture.

JAAS authentication is executed using the plug-in availability method. Thus, an application works separately from the authentication-based technology. A new or updated authentication technology can be used as a plug-in within the application, which eliminates the need to modify the application.

The application validates the authentication process by instantiating the LoginContext object. The LoginContext object decides on the authentication technology or LoginModule for use based on the configurations. The basic LoginModule verifies a user with the entered username and password, and some modules can verify a user through a voice recognition, fingerprints, or object.

If the user is authenticated, the approval component of JAAS protects the access to the resources by working with the core Java access control model. In J2SDK v 1.3, access control was determined only by the location of the code and CodeSource. However, in J2SDK v 1.4, it is determined by the source of the execution code and the user or service which can be represented by a Subject object that executes the code. If the authentication is successful, the LoginModule updates the subject by using the relevant principal and qualifications.

For basic integration between LDAP, to which the JAAS mechanism has been applied, a database, and the JEUS security system, you must extend and implement the LoginModule. The JEUS security system provides the services related to login and approval.

The following sections show examples to explain how LDAP and database work together by applying core JAAS related classes and interfaces to the JEUS security system.

  • Example of integrating LDAP to LoginModule

  • Example of integrating DBRealm to LoginModule

The javax.security.auth.login.LoginContext interface class provides the basic method used for authentication. This class enables a user to develop an application, which provides a particular authentication type, without depending on the login authentication technology.

As shown in the following, the JAAS authentication mechanism, which is extended from the LoginModule, is provided to work with LDAP. The authentication mechanism is performed through the LdapLoginModule that inherits the LoginModule interface.

LdapLoginModule has been extended to enable integration with the JEUS security system.

[Example 7.1] <<jeus.security.impl.login.LdapLoginModule>>

package jeus.security.impl.login;

import jeus.security.base.Domain;
import jeus.security.resource.Password;
import jeus.security.resource.PrincipalImpl;
import jeus.security.resource.RolePrincipalImpl;
import jeus.util.logging.JeusLogger;

import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.security.Principal;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;

public class LdapLoginModule implements LoginModule {
    protected static final JeusLogger logger =
        (JeusLogger) JeusLogger.getLogger("jeus.security.loginmodule");

    // initial state
    private Subject subject;
    private CallbackHandler callbackHandler;

    private boolean succeeded = false;
    private boolean commitSucceeded = false;

    private String username;
    private String password;
    private String domain;

    private Principal userPrincipal;
    private Password userCredential;

    private SunOneLdapAuthenticator authenticator;

    private static final String INITIAL_CONTEXT_FACTORY =
        "initialContextFactory";  // optional
    private static final String PROVIDER_URL = "providerURL";
    private static final String CONNECTION_USERNAME = "connectionUsername";
    private static final String CONNECTION_PASSWORD = "connectionPassword";
    private static final String USER_BASE = "userBase";
    private static final String USER_SEARCH_MAPPING = "userSearchMapping";
    private static final String USER_PASSWORD_ATTR = "userPasswordAttr";
    private static final String USER_ROLE_ATTR = "userRoleAttr";
    private static final String ROLE_BASE = "roleBase";
    private static final String ROLE_NAME_ATTR = "roleNameAttr";
    private static final String ROLE_SEARCH_MAPPING = "roleSearchMapping";

    private final String SUN_JDK_LDAP_CONTEXT_FACTORY = 
          "com.sun.jndi.ldap.LdapCtxFactory";

    public void initialize(Subject subject, CallbackHandler callbackHandler,
                           Map sharedState, Map options) {

        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.domain = Domain.SYSTEM_DOMAIN_NAME;

        authenticator = new SunOneLdapAuthenticator();
        initAuthenticator(authenticator, options);
    }

    private void initAuthenticator(SunOneLdapAuthenticator authenticator,
                                   Map options) {
        // INIITIAL_CONTEXT_FACTORY
        String value = (String) options.get(INITIAL_CONTEXT_FACTORY);
        if (value == null) {
            authenticator.setContextFactory(SUN_JDK_LDAP_CONTEXT_FACTORY);
        } else {
            authenticator.setContextFactory(value);
        }

        authenticator.setProviderUrl((String) options.get(PROVIDER_URL));
        authenticator.setConnectionUsername(
                (String) options.get(CONNECTION_USERNAME));
        authenticator.setConnectionPassword(
                (String) options.get(CONNECTION_PASSWORD));

        value = (String) options.get(USER_BASE);
        if (value != null) {
            authenticator.setUserBase(value);
        } else {
            String msg = "LoginMoulde initialization failed. " +
                    "userBase option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }

        authenticator.setUserPasswordAttr((String) options.get(USER_PASSWORD_ATTR));

        value = (String) options.get(USER_SEARCH_MAPPING);
        if (value != null) {
            authenticator.setUserSearchMapping(new MessageFormat(value));
        } else {
            String msg = "LoginMoulde initialization failed. " +
                    "userSearchMapping option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }

        authenticator.setUserRoleAttr((String) options.get(USER_ROLE_ATTR));

        value = (String) options.get(ROLE_BASE);
        if (value != null) {
            authenticator.setRoleBase(value);
        } else {
            String msg = "LoginMoulde initialization failed. " +
                    "roleBase option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }

        value = (String) options.get(ROLE_NAME_ATTR);
        if (value != null) {
            authenticator.setRoleNameAttr(value);
        } else {
            String msg = "LoginMoulde initialization failed. " +
                    "roleNameAttr option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }

        value = (String) options.get(ROLE_SEARCH_MAPPING);
        if (value != null) {
            authenticator.setRoleSearchMapping(new MessageFormat(value));
        } else {
            String msg = "LoginMoulde initialization failed. " +
                    "roleSearchMapping option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }
    }

    public boolean commit() throws LoginException {
        if (succeeded == false) {
            return false;
        } else {
            userPrincipal = new PrincipalImpl(username);
            if (!subject.getPrincipals().contains(userPrincipal))
                subject.getPrincipals().add(userPrincipal);

            ArrayList roles = authenticator.getRoles();
            for (Iterator i = roles.iterator(); i.hasNext(); ) {
                String roleName = (String) i.next();
                logger.fine("Adding role to subject : username = " + username + 
                            ", roleName = " + roleName);
                subject.getPrincipals().add(new RolePrincipalImpl(roleName));
            }

            userCredential = new Password(password);
            subject.getPrivateCredentials().add(userCredential);

            username = null;
            password = null;
            domain = null;
            commitSucceeded = true;
            return true;
        }
    }

    public boolean abort() throws LoginException {
        if (succeeded == false) {
            return false;
        } else if (succeeded == true && commitSucceeded == false) {
            succeeded = false;
            username = null;
            password = null;
            domain = null;
            userPrincipal = null;
            userCredential = null;
        } else {
            logout();
        }
        return true;
    }

    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(userPrincipal);
        subject.getPrivateCredentials().remove(userCredential);
        succeeded = false;
        succeeded = commitSucceeded;
        username = null;
        password = null;
        domain = null;
        userPrincipal = null;
        userCredential = null;
        return true;
    }

    public boolean login() throws LoginException {
        Callback[] callbacks = null;

        // prompt for a user name and password
        if (callbackHandler == null)
            throw new LoginException("Error: no CallbackHandler available " +
                    "to garner authentication information from the user");

        callbacks = new Callback[3];
        callbacks[0] = new NameCallback("user name: ");
        callbacks[1] = new PasswordCallback("password: ", false);
        callbacks[2] = new TextInputCallback("domain: ");
        try {
            callbackHandler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
            if (tmpPassword == null) {
                tmpPassword = new char[0];
            }
            password = new String(tmpPassword);
            ((PasswordCallback) callbacks[1]).clearPassword();
            domain = ((TextInputCallback) callbacks[2]).getText();

            if (!authenticator.authenticate(username, password)) {
                throw new LoginException("LDAP authentication failed.");
            }
            succeeded = true;
        } catch (UnsupportedCallbackException uce) {
            uce.printStackTrace();
            LoginException le = new LoginException(
                    "Error: " + uce.getCallback().toString() +
                    " not available to garner authentication information " +
                    "from the user");
            le.initCause(uce);
            throw le;
        } catch (Exception e) {
            e.printStackTrace();    // todo. logging
            if (e instanceof LoginException) {
                throw (LoginException) e;
            } else {
                LoginException le = new LoginException(e.toString());
                le.initCause(e);
                throw le;
            }
        }
        return succeeded;
    }
}

Caution

The RolePrincipalImpl information needs to be converted to the RolePrincipalImpl type, because the user and role information, which is added during runtime, must be applied to the JEUS security system.

To provide the login service by applying the JAAS LoginModule to a particular domain in the JEUS security system, specify the <jaas-login-config> element when configuring the domain security service. For more information, refer to the 'authentication' section in "2.3. Configuring Security Domain Components".

To provide the LDAP LoginModule security service to DEFAULT_APPLICATION_DOMAIN, configure the login service and approval service as follows.

[Example 7.2] Domain Service Configuration: <<domain.xml>>

<?xml version="1.0" encoding="UTF-8"?>
<domain xmlns="http://www.tmaxsoft.com/xml/ns/jeus">
. . .
    <security-domain>
        <name>DEFAULT_APPLICATION_DOMAIN</name>
        <authentication>
            <jaas-login-config>
                <login-module>
                    <login-module-classname>
                         jeus.security.impl.login.LdapLoginModule
                    </login-module-classname>
                    <control-flag>required</control-flag>
                    <option>
                        <name>initialContextFactory</name>
                        <value>com.sun.jndi.ldap.LdapCtxFactory</value>
                    </option>
                    <option>
                        <name>providerURL</name>
                        <value>ldap://192.168.1.63:389</value>
                    </option>
                    <option>
                        <name>connectionUsername</name>
                        <value>cn=Directory Manager</value>
                    </option>
                    <option>
                        <name>connectionPassword</name>
                        <value>adminadmin</value>
                    </option>
                    <option>
                        <name>userBase</name>
                        <value>ou=People,dc=sample,dc=com</value>
                    </option>
                    <option>
                        <name>userSearchMapping</name>
                        <value>(uid={0})</value>
                    </option>
                    <option>
                        <name>roleBase</name>
                        <value>ou=Groups,dc=sample,dc=com</value>
                    </option>
                    <option>
                        <name>roleNameAttr</name>
                        <value>cn</value>
                    </option>
                    <option>
                        <name>roleSearchMapping</name>
                        <value>(uniqueMember={0})</value>
                    </option>
                </login-module>
            </jaas-login-config>
        </authentication>
        <authorization>
            <repository-service>
                <custom-repository>
                    <classname> 
    jeus.security.impl.aznrep.CustomPolicyFileRealmAuthorizationRepositoryService
                    </classname>
                    <property>
                        <name>PolicyClassName</name>
                        <value>jeus.security.base.CustomJeusPolicy</value>
                    </property>
                    <property>
                        <name>UserPrincipalClassName</name>
                        <value>jeus.security.resource.PrincipalImpl</value>
                    </property>
                    <property>
                        <name>RolePrincipalClassName</name>
                        <value>jeus.security.resource.RolePrincipalImpl</value>
                    </property>
                 </custom-repository>
            </repository-service>
        </authorization>
    <security-domain>
. . .
</domain> 


The following is the description for each class.

  • jeus.security.impl.callback.JAASUsernamePasswordCallbackHandler

    Provides the basic mechanism for obtaining authentication information (username and password) for LoginModule of the JEUS security system.

  • jeus.security.impl.login.LdapLoginModule

    Supports LoginModule based on the LDAP Attribute value that is defined as an option value in the JEUS security system.

  • jeus.security.impl.aznrep.CustomPolicyFileRealmAuthorizationRepositoryService

    Provides Authorization service by applying the UserPrincipalClassName/RolePrincipalClassName type, defined by the user, in the JEUS security system.

  • jeus.security.base.CustomJeusPolicy

    Extends and implements the jeus.security.base.Policy class. This class supports principal-to-role mapping even when there is no jeus-web-dd.xml descriptor at runtime in the JEUS security system and when the LoginModule defines the RolePrincipalImpl class for the Principal.

When you are finished with the configurations, start JEUS, and then start the login service for the application deployed to DEFAULT_APPLICATION_DOMAIN.

In order to integrate with the database, extend the LoginModule to support the JAAS authentication mechanism as follows.

The authentication mechanism is performed through the DBRealmLoginModule that inherits the LoginModule interface. To integrate with the JEUS security system, extend and implement the DBRealmLoginModule as shown in the following.

[Example 7.3] <<jeus.security.impl.login.DBRealmLoginModule>>

package jeus.security.impl.login;

import jeus.security.base.Domain;
import jeus.security.base.ServiceException;
import jeus.security.resource.Password;
import jeus.security.resource.PrincipalImpl;
import jeus.security.resource.RolePrincipalImpl;
import jeus.util.logging.JeusLogger;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.sql.DataSource;
import java.security.Principal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * User: choco
 * Date: 2007. 4. 13
 * Time: 오후 4:35:57
 */
public class DBRealmLoginModule implements LoginModule {
    protected static final JeusLogger logger =
        (JeusLogger) JeusLogger.getLogger("jeus.security.login");
    protected String dsExportName;
    protected String principalsQuery =
        "select password from jeus_users where username=?";
    protected String rolesQuery = "select role from jeus_roles where username=?";

    private String username;
    private String password;
    private String domain;

    private Subject subject;
    private CallbackHandler callbackHandler;
    private boolean succeeded = false;
    private boolean commitSucceeded = false;

    protected Map options;
    private Principal userPrincipal;
    private Password userCredential;

    public void initialize(Subject subject,
            CallbackHandler callbackHandler, Map sharedState, Map options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.options = options;

        try {
            domain = Domain.getCurrentDomain().getName();
        } catch (ServiceException e) {
            domain = Domain.SYSTEM_DOMAIN_NAME;
        }

        dsExportName = (String) options.get("exportName");
        if (dsExportName == null) {
            String msg = "LoginMoulde initialization failed. " +
                    "exportName option missing.";
            logger.warning(msg);
            throw new IllegalArgumentException(msg);
        }

        Object tmp = options.get("principalsQuery");
        if (tmp != null)
            principalsQuery = tmp.toString();
        tmp = options.get("rolesQuery");
        if (tmp != null)
            rolesQuery = tmp.toString();
        logger.debug("DBRealmLoginModule, export name : " + dsExportName);
        logger.debug("principalsQuery=" + principalsQuery);
        logger.debug("rolesQuery=" + rolesQuery);
        logger.debug("initialize successfully");
    }

    public boolean login() throws LoginException {
        Callback[] callbacks = null;

        // prompt for a user name and password
        if (callbackHandler == null)
            throw new LoginException("Error: no CallbackHandler available " +
                    "to garner authentication information from the user");

        callbacks = new Callback[3];
        callbacks[0] = new NameCallback("user name: ");
        callbacks[1] = new PasswordCallback("password: ", false);
        callbacks[2] = new TextInputCallback("domain: ");
        try {
            callbackHandler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
            if (tmpPassword == null) {
                tmpPassword = new char[0];
            }
            password = new String(tmpPassword);
            ((PasswordCallback) callbacks[1]).clearPassword();
            domain = ((TextInputCallback) callbacks[2]).getText();

            String expectedPassword = getUsersPassword();
            if (validatePassword(password, expectedPassword) == false) {
                throw new LoginException("DBRealm authentication failed.");
            }
            succeeded = true;
        } catch (UnsupportedCallbackException uce) {
            uce.printStackTrace();
            LoginException le = new LoginException(
                    "Error: " + uce.getCallback().toString() +
                    " not available to garner authentication information " +
                    "from the user");
            le.initCause(uce);
            throw le;
        } catch (Exception e) {
            e.printStackTrace();    // todo. logging
            if (e instanceof LoginException) {
                throw (LoginException) e;
            } else {
                LoginException le = new LoginException(e.toString());
                le.initCause(e);
                throw le;
            }
        }
        return succeeded;
    }

    private String getUsersPassword() throws LoginException {
        String password = null;
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            InitialContext ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup(dsExportName);
            conn = ds.getConnection();
            ps = conn.prepareStatement(principalsQuery);
            ps.setString(1, username);
            rs = ps.executeQuery();
            if (rs.next() == false)
                throw new FailedLoginException("No matching username found");

            password = rs.getString(1);
        }
        catch (NamingException ex) {
            throw new LoginException(ex.toString(true));
        }
        catch (SQLException ex) {
            logger.debug("Query failed", ex);
            throw new LoginException(ex.toString());
        }
        finally {
            if (rs != null) {
                try {
                    rs.close();
                }
                catch (SQLException e) {
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                }
                catch (SQLException e) {
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                }
                catch (SQLException ex) {
                }
            }
        }
        return password;
    }

    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(userPrincipal);
        subject.getPrivateCredentials().remove(userCredential);
        succeeded = false;
        succeeded = commitSucceeded;
        username = null;
        password = null;
        domain = null;
        // trusted = false;
        userPrincipal = null;
        userCredential = null;
        return true;
    }

    public boolean commit() throws LoginException {
        if (succeeded == false) {
            return false;
        } else {
            userPrincipal = new PrincipalImpl(username);
            if (!subject.getPrincipals().contains(userPrincipal))
                subject.getPrincipals().add(userPrincipal);

            ArrayList roles = getRoleSets();
            for (Iterator i = roles.iterator(); i.hasNext();) {
                String roleName = (String) i.next();
                logger.debug("Adding role to subject : username = " + username +
                        ", roleName = " + roleName);
                subject.getPrincipals().add(new RolePrincipalImpl(roleName));
            }

            userCredential = new Password(password);
            subject.getPrivateCredentials().add(userCredential);

            username = null;
            password = null;
            domain = null;
            commitSucceeded = true;
            return true;
        }
    }

    public boolean abort() throws LoginException {
        if (succeeded == false) {
            return false;
        } else if (succeeded == true && commitSucceeded == false) {
            succeeded = false;
            username = null;
            password = null;
            domain = null;
            userPrincipal = null;
            userCredential = null;
        } else {
            logout();
        }
        return true;
    }

    protected ArrayList getRoleSets() throws LoginException {
        Connection conn = null;
        HashMap setsMap = new HashMap();
        PreparedStatement ps = null;
        ResultSet rs = null;
        ArrayList roles = new ArrayList();
        try {
            InitialContext ctx = new InitialContext();
            DataSource ds = (DataSource) ctx.lookup(dsExportName);
            conn = ds.getConnection();
            ps = conn.prepareStatement(rolesQuery);
            try {
                ps.setString(1, username);
            }
            catch (ArrayIndexOutOfBoundsException ignore) {
            }
            rs = ps.executeQuery();

            while (rs.next()) {
                String rolename = rs.getString(1);
                roles.add(rolename);
            }
        }
        catch (NamingException ex) {
            throw new LoginException(ex.toString(true));
        }
        catch (SQLException ex) {
            logger.debug("SQL failure", ex);
            throw new LoginException(ex.toString());
        }
        finally {
            if (rs != null) {
                try {
                    rs.close();
                }
                catch (SQLException e) {
                }
            }
            if (ps != null) {
                try {
                    ps.close();
                }
                catch (SQLException e) {
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                }
                catch (Exception ex) {
                }
            }
        }

        return roles;
    }

    protected boolean validatePassword(String inputPassword, String expectedPassword) {
        if (inputPassword == null || expectedPassword == null)
            return false;
        return inputPassword.equals(expectedPassword);
    }
}

Caution

In order to apply the user and role information, available at runtime, to the JEUS security system, RolePrincipalImpl information needs to be converted to the JEUS RolePrincipalImpl type.

To provide the login service by applying the JAAS LoginModule to a particular domain in the JEUS security system, configure the <jaas-login-config> element for the domain security service. For the details, refer to 'authentication' section in "2.3. Configuring Security Domain Components".

To provide the Database LoginModule security service to DEFAULT_APPLICATION_DOMAIN, configure the login service and authentication service as follows:


The following is the description for each class.

  • jeus.security.impl.login.DBRealmLoginModule

    Supports LoginModule based on the exportname, principal, and role database query values. Optional for the JEUS security system.

    The following are the list of options.

    OptionDescription
    exportNameSets the export-name of the database, defined in domain.xml.
    principalsQueryDefines a query, with one primary key, that obtains a password for the principal.
    rolesQueryDefines a query, with one primary key, that obtains roles of the principal.
  • jeus.security.impl.aznrep.CustomPolicyFileRealmAuthorizationRepositoryService

    Provides the authorization service by applying the UserPrincipalClassName/RolePrincipalClassName type, defined by the user, in the JEUS security system.

  • jeus.security.base.CustomJeusPolicy

    Extends and implements the jeus.security.base.Policy class. This class supports principal-to-role mapping even when there is no jeus-web-dd.xml descriptor at runtime in the JEUS security system and when the LoginModule defines the RolePrincipalImpl class for the Principal.

When you are finished with the configurations, start JEUS, and then start the login service for the application deployed to DEFAULT_APPLICATION_DOMAIN.