JAAS login module in Tomcat 7 example (Part 2)

Standard

Part 1 of this tutorial demonstrated how to implement a login module using JAAS + Tomcat 7. This next segment shows how to create a login form and call the login module.

Folder Structure:

/login.xhtml
/error.xhtml
/protected/index.html (protected via our web.xml file)

Simple JSF login page

The following is a simple form used to submit the username and password to a backing bean called loginBean. The form uses HTML5 passthrough elements, as well as built in JSF validators on the input fields. All errors are displayed using the h:messages output.

<h:form>
    <h3>Please sign in</h3>
    <h:inputText id="username" value="#{loginBean.username}" required="true" requiredMessage="Please enter your username" p:placeholder="Username" p:autofocus="true">
        <f:validateLength maximum="50" minimum="3" />
    </h:inputText>

    <h:inputSecret id="password" value="#{loginBean.password}" required="true" requiredMessage="Enter your password" p:placeholder="Password">
        <f:validateLength maximum="20" minimum="3" />
    </h:inputSecret>

    <h:messages/>

    <h:commandButton type="submit" value="Sign in" id="submit" action="#{loginBean.login()}"/>
</h:form>

Calling the login module

Once the form passes validation the login() action is called. The login action uses the submitted username / password to request a login from the servlet container. This will call the login module created in part 1 of this tutorial. If the request.login() servlet request fails, it throws a LoginException which is caught in the form of a ServletException below. If the login succeeds then the user is redirected to the protected page.

@ManagedBean(name = "loginBean")
@ViewScoped
public class LoginBean implements Serializable {
    
    private static final long serialVersionUID = 1L;

    private String username;
    private String password;

    /**
     *
     * @return
     */
    public String login() {

        try {

            // Get the current servlet request from the facesContext
            FacesContext ctx = FacesContext.getCurrentInstance();
            HttpServletRequest request = (HttpServletRequest) ctx.getExternalContext().getRequest();

            // Do login from the container (will call login module)
            request.login(username, password);

            return "/protected/index.xhtml?faces-redirect=true";

        } catch (ServletException ex) {

            FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "An Error Occured: Login failed", null));
            Logger.getLogger(LoginBean.class.getName()).log(Level.SEVERE, null, ex);
        }

        return "login.xhtml";
    }

    /**
     * @return the username
     */
    public String getUsername() {
        return username;
    }

    /**
     * @param username the username to set
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * @return the password
     */
    public String getPassword() {
        return password;
    }

    /**
     * @param password the password to set
     */
    public void setPassword(String password) {
        this.password = password;
    }

}

This concludes the configuration and implementation of JAAS container managed security. The original working copy of the complete project is available on Github.

JAAS login module in Tomcat 7 example (Part 1)

Standard

Implementing a login module using JAAS is an excellent way to secure URL’s in a web application. In this tutorial, it will walk through the steps in configuring JAAS authentication in Tomcat 7 using the form based authentication method.

Before looking at the code is it important to understand the purpose of JAAS authentication. JAAS or Java Authentication and Authorization Service is an optional package designed to work in a pluggable fashion. The JAAS component comes standard with many other servers such as Glassfish, Wildfly, and Jetty. Once the user is authorized, the JAAS component controls access to sensitive resources. In this article we will secure sensitive resources by URL patterns.

Understanding Principals

JAAS relies on users and roles to authorize the access to certain resources. In JAAS, the users and roles are separated into Principals. The separated Principals all represent the Subject identity. The Subject is an entity, such as a person or service. Lets begin by defining our user and roles Principals.

package com.sixthpoint.jaas;

import java.security.Principal;

/**
 * Holds the logged in users name
 *
 * @author sixthpoint
 */
public class UserPrincipal implements Principal {

    private String name;

    /**
     * Initializer
     *
     * @param name
     */
    public UserPrincipal(String name) {
        super();
        this.name = name;
    }

    /**
     * Set the name of the user
     *
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Get the name of the user
     *
     * @return
     */
    @Override
    public String getName() {
        return name;
    }
}
package com.sixthpoint.jaas;

import java.security.Principal;

/**
 * Holds a single role name that the user is in
 *
 * @author sixthpoint
 */
public class RolePrincipal implements Principal {

    private String name;

    /**
     * Initializer
     *
     * @param name
     */
    public RolePrincipal(String name) {
        super();
        this.name = name;
    }

    /**
     * Set the role name
     *
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Get the role name
     *
     * @return
     */
    @Override
    public String getName() {
        return name;
    }
}

Note the simplicity of the Principals. They both are similar and only require a name for referencing.

The Custom Login Module

The Login module must implement the javax.security.auth.spi.LoginModule interface. It is by inheritance that it will override the following methods:

  • initialize() – all options are loaded at this time, options can be configured in the jaas.conf file later in the tutorial
  • login() – used to perform verification logic
  • commit() – invoked after successful authentication from the login() method
  • abort() – called if something goes wrong with the login() or commit() methods
  • logout() – clears the Subject

The login module accepts the inputs from the end user and verifies they are valid through the login() method. The verification can be made in any number of ways: checking a database, a LDAP, remote API, etc. It is up to the developer to define the requirements for verification. For the purpose of this tutorial we are only verifying that the end user has entered values for the username / password. Once the verification is successful; roles are attached to the verified user. These roles are used for restricting access to resource locations in the web.xml. If an error occurs during the verification process or the assigning of roles the login() method must throw a LoginException. This will be gracefully handled by our application later in the tutorial.

The commit() method is invoked after a successful verification from the login() method. A UserPrincipal and all RolePrincipals associated with the verified user are stored in the Subject. The Subject is accessible in the javax.security.auth.Subject instance for the container managed security.

The final two methods are self explanatory. The logout() method clears all the subjects principals (user information). The abort() method nullifies the inputs (username / password), and principals if the commit failed but the user is authenticated. However, if the user is not verified or the commit() did succeed, then a standard logout() is performed.

public class CustomLoginModule implements LoginModule {

    private CallbackHandler handler;
    private Subject subject;
    private UserPrincipal userPrincipal;
    private RolePrincipal rolePrincipal;
    private List<String> userGroups;
    private Map options;
    private Map sharedState;

    // Configurable option
    private boolean debug = false;

    // Logger used to output debug information
    private static final Logger logger = Logger.getLogger(CustomLoginModule.class.getName());

    // User credentials
    private String username = null;
    private String password = null;

    private boolean isAuthenticated = false;
    private boolean commitSucceeded = false;

    /**
     * Constructor
     */
    public CustomLoginModule() {
        super();
    }

    /**
     * Initializer
     *
     * @param subject
     * @param callbackHandler
     * @param sharedState
     * @param options
     */
    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {

        // Store the handler
        this.handler = callbackHandler;

        // Subject reference holds the principals
        this.subject = subject;

        this.options = options;
        this.sharedState = sharedState;

        // Setup a logging class / state
        if ("true".equalsIgnoreCase((String) options.get("debug"))) {
            ConsoleHandler consoleHandler = new ConsoleHandler();
            logger.addHandler(consoleHandler);
            debug = true;
        }
    }

    /**
     *
     * @return @throws LoginException
     */
    @Override
    public boolean login() throws LoginException {

        // If no handler is specified throw a error
        if (handler == null) {
            throw new LoginException("Error: no CallbackHandler available to recieve authentication information from the user");
        }

        // Declare the callbacks based on the JAAS spec
        Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("login");
        callbacks[1] = new PasswordCallback("password", true);

        try {

            //Handle the callback and recieve the sent inforamation
            handler.handle(callbacks);
            username = ((NameCallback) callbacks[0]).getName();
            password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());

            // Debug the username / password
            if (debug) {
                logger.log(Level.INFO, "Username: {0}", username);
                logger.log(Level.INFO, "Password: {0}", password);
            }

            // We should never allow empty strings to be passed
            if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
                throw new LoginException("Data specified had null values");
            }

            // Validate against our database or any other options (this check is a example only)
            if (username != null && password != null) {

                // Assign the user roles
                userGroups = this.getRoles();
                isAuthenticated = true;

                return true;
            }

            throw new LoginException("Authentication failed");

        } catch (IOException | UnsupportedCallbackException e) {
            throw new LoginException(e.getMessage());
        }

    }

    /**
     * Adds the username / roles to the principal
     *
     * @return @throws LoginException
     */
    @Override
    public boolean commit() throws LoginException {

        if (!isAuthenticated) {
            return false;
        } else {

            userPrincipal = new UserPrincipal(username);
            subject.getPrincipals().add(userPrincipal);

            if (userGroups != null && userGroups.size() > 0) {
                for (String groupName : userGroups) {
                    rolePrincipal = new RolePrincipal(groupName);
                    subject.getPrincipals().add(rolePrincipal);
                }
            }

            commitSucceeded = true;

            return true;
        }
    }

    /**
     * Terminates the logged in session on error
     *
     * @return @throws LoginException
     */
    @Override
    public boolean abort() throws LoginException {
        if (!isAuthenticated) {
            return false;
        } else if (isAuthenticated && !commitSucceeded) {
            isAuthenticated = false;
            username = null;
            password = null;
            userPrincipal = null;
        } else {
            logout();
        }
        return true;
    }

    /**
     * Logs the user out
     *
     * @return @throws LoginException
     */
    @Override
    public boolean logout() throws LoginException {
        isAuthenticated = false;
        isAuthenticated = commitSucceeded;
        subject.getPrincipals().clear();
        return true;
    }

    /**
     * Returns list of roles assigned to authenticated user.
     *
     * @return
     */
    private List<String> getRoles() {

        List<String> roleList = new ArrayList<>();
        roleList.add("admin");

        return roleList;
    }

}

 

Structuring the web application

Enabling the module above requires 3 files to be configured. Once complete this will secure access to the specified folder on the Java web application.

Begin by editing the context.xml file and defining the JAAS realm. This example uses the default JAASRealm for defining the user and role Principal classes which we defined above as UserPrincipal and RolePrincipal. The appName attribute is used to define which security module Tomcat will look to use.

<Context antiJARLocking="true" path="" reloadable="true">
    <Realm appName="CustomLogin" className="org.apache.catalina.realm.JAASRealm" roleClassNames="com.sixthpoint.jaas.RolePrincipal" userClassNames="com.sixthpoint.jaas.UserPrincipal"/>
</Context>

Finally, for the web application to load the security configuration a jaas.config file must be defined. This file will inform Tomcat what class file to load when processing login requests. Note that CustomLogin mirrors our appName in the context.xml. The CustomLoginModule is referenced from our java class above.

CustomLogin {
    com.sixthpoint.jaas.CustomLoginModule required debug=true;
};

Tomcat needs to know where the application security configuration file is located (jaas.config). A JVM argument needs to be added to the catalina.sh file. On server startup the server will load the custom login module.

JAVA_OPTS="-Djava.security.auth.login.config==$CATALINA_BASE/conf/jaas.config"

Securing Resource Locations

To prevent access to certain resource location the web.xml needs modification. Any number of security constraints can be added to map to resource locations. The auth-constraint can have multiple roles as shown.

<!-- URL constraints -->
<security-constraint>
    <web-resource-collection>
        <web-resource-name>Admin</web-resource-name>
        <url-pattern>/protected/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>admin</role-name>
        <role-name>staff</role-name>
    </auth-constraint>
    <user-data-constraint>
        <!-- transport-guarantee can be CONFIDENTIAL, INTEGRAL, or NONE -->
        <transport-guarantee>NONE</transport-guarantee>
    </user-data-constraint>
</security-constraint>
<!-- What roles are allowed -->
<security-role>
    <role-name>admin</role-name>
    <role-name>staff</role-name>
</security-role>
<!-- Force login by form -->
<login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
        <form-login-page>/login.xhtml</form-login-page>
        <form-error-page>/error.xhtml</form-error-page>
    </form-login-config>
</login-config>

This concludes the setup / configuration of JAAS on a java web application. The next tutorial demonstrates how to implement a JSF login page with JAAS.

A full copy of the complete project is available on Github.