Search code examples
jsptomcatauthenticationshibbolethi2b2

Tomcat 7 JSP page: Avoid 500 error when authenticating in Tomcat using Shibboleth authentication session


Tomcat 7 Exception:

Apache Tomcat/7.0.63 - HTTP Status 500 - An exception occurred processing JSP page

Reference:

I integrated this code with our i2b2 application so we could authenticate against our Shibboleth IdP.

https://github.com/HSSC/i2b2-web-integration/blob/master/doc/INSTALL.md

application (i2b2):

http://www.i2b2.org

testshib:

(we followed this to build out Shibboleth service provider)

http://www.testshib.org/

Shibboleth IdP:

http://shibboleth.net/

Problem:

The page is failing on a JSP page with a status code of 500, on the line that sets "id" because the line above it has an "auth" variable (of type List<string> in Java) that is null. Notice that this page has a combination of JSP tags and Javascript.

Snippet of code failing on login.jsp page:

function initI2B2()
{

    // alert('initI2B2');
    <%
        shibboleth.ConnectDatabase auth = (shibboleth.ConnectDatabase) session.getAttribute("connectDatabase");
        String id = org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().identifier);  // fails here because the List<string> object is null
        String sessionId = org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().session);
        Calendar cal = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateTime = sdf.format(cal.getTime());
        String token = DigestUtils.sha512Hex(dateTime + sessionId);
    %>

    var passedInUserKey = "<%= org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().identifier)%>";
    var passedInPassKey = "<%= org.apache.commons.lang.StringEscapeUtils.escapeHtml(auth.getUser().session)%>";
    var passedInDomainK = "demo";
    var passedInInstitutionid = "1";
    var tokenV = "<%= token%>";

Workflow causing this?

The code normally works great if we have a good session. Our workaround right now is to make the session timeout in 24 hours.

Here is the workflow causing the error...

Hit i2b2 page > automatically redirects to Shibboleth IdP > log in using IdP credentials > redirected back to i2b2 page > this code is taking the user and session id (password) out of the Shibboleth session and using it to automatically authorize into the i2b2 application.

The login.jsp page is hacked to take data from the Shibboleth session, so the user doesn't have to type in credentials.

The problem is that when the shibboleth session times out, or when the Tomcat session expires or the cache is cleared in the browser, this error occurs, because this entire page needs those two variables from the session (id and password).

Clearing the cache is the quickest way to get this error. Another way is to change the Tomcat default session timeout from 30 minutes to 1 minute.

<session-config>
    <session-timeout>1</session-timeout>
</session-config>

Ideas that might give possible answers for this question:

  1. In ASP.NET, if the session is not there, you can tell it to automatically redirect to another page (using configuration). Does Tomcat offer that functionality by giving it some condition (with code)?

  2. How can we forcefully kill the Tomcat session (without processing this page), and redirect as though we had a brand new request to i2b2? I know in ASP.NET, there is code that you can call if you receive an application error. Does Tomcat offer something?

  3. If we want to hack this page even more, how can we simply avoid the error with a condition, but still have the page process, and then redirect?

Got any other ideas?

package edu.tmc.uth.i2b2shibboleth;

import java.security.*;
import java.sql.*;
import java.util.*;
import javax.faces.bean.*;
import javax.faces.context.*;
import javax.servlet.ServletContext;
import javax.servlet.http.*;

/**Manages the retrieval of Shibboleth attributes and the connection to the
 * i2b2 hive database.
 *
 * @author JRussell
 */
@ManagedBean(name = "connectDatabase")
@SessionScoped
public class ConnectDatabase implements HttpSessionBindingListener {

    private User user;
    private Connection connection;
    private List<String> test = new ArrayList<String>();
    String propertyPath = null;
    private ResourceBundle properties;

    public User getUser() {
        return user;
    }

    /*Main function for retrieving Shibboleth information and updating 
     * database records.  This is called from the Facelets page (index.xhtml).
     */
    public List<String> getUserInfo() {
        try {
            connection = connectToDatabase();
            setShibbolethAttributes();
            updateI2b2UserDatabase();

            connection.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return test;
    }

    /*Returns a connection to the i2b2 hive database.
     * Uses the properties in the database.properties file.
     */
    public Connection connectToDatabase() {
        try {            
            properties = ResourceBundle.getBundle("edu/tmc/uth/i2b2shibboleth/database");
            String url = properties.getString("I2B2_PM.connectionURL");
            String userName = properties.getString("I2B2_PM.userName");
            String password = properties.getString("I2B2_PM.password");

            Class.forName(properties.getString("I2B2_PM.databaseClass"));
            connection = DriverManager.getConnection(url, userName, password);
            if (connection != null) {
                System.out.println("connected to DB");
                test.add("Connected to i2b2 hive database.");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return connection;
    }

    /*Populates the Shibboleth attributes into the User object.
     * The request header values are based on the attribute-map.xml configuration
     * file on the Shibboleth Service Provider.  The attribute identifiers and 
     * the attributes released are configured on an institutional basis.
     */
    public void setShibbolethAttributes() {
        HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext().getRequest();        
        String firstName = request.getHeader("Shib-InetOrgPerson-givenName");
        String lastName = request.getHeader("Shib-Person-surname");
        String email = request.getHeader("Shib-InetOrgPerson-mail");
        String identifier = request.getHeader("Shib-iamPerson-subjectUniqueId");
        String session = request.getHeader("Shib-Session-ID"); 
        user = new User(firstName, lastName, email, identifier, session);
        test.add("Basic User attributes");
        test.add(user.toString());
        test.add("All Shibboleth headers");
        //Outputs all of the headers from the Shibboleth request
        Enumeration enumer = request.getHeaderNames();
        while(enumer.hasMoreElements()){
            String headerName = enumer.nextElement().toString();
            if(headerName.startsWith("Shib")){
            test.add(headerName+" - "+request.getHeader(headerName));
            }
        }
    }

    /*Add a new user to the i2b2 hive database if they don't have an existing record. 
     * Update the password and enable the user if the i2b2 user already exists. 
     */
    private void updateI2b2UserDatabase() throws SQLException {
        //Try to find current user in database
        String stmt = "SELECT user_id FROM pm_user_data WHERE user_id=?";
        PreparedStatement pst = connection.prepareStatement(stmt);
        pst.setString(1, user.identifier);
        System.out.println(stmt+" "+user.identifier);
        ResultSet result = pst.executeQuery();
        //user record found in database so update password
        if (result.next()) {
            System.out.println("user record found");
            test.add("Subject identifier already in database -  " + result.getString("user_id"));
            stmt = "UPDATE pm_user_data SET password=?, status_cd=? WHERE user_id=?";
            pst = connection.prepareStatement(stmt);
            pst.setString(1, encryptMD5(user.session));
            pst.setString(2, "A");
            pst.setString(3, user.identifier);
            pst.executeUpdate();
            connection.commit();
        } 
        else { //new user so add record to database
            test.add("New user - " + user.identifier + " " + user.first + " " + user.last);
            stmt = "INSERT INTO pm_user_data (user_id, full_name, password, email, status_cd) \n"
                    + "VALUES (?,?,?,?,?)";
            pst = connection.prepareStatement(stmt);
            pst.setString(1, user.identifier);
            pst.setString(2, user.first + " " + user.last);
            pst.setString(3, encryptMD5(user.session));
            pst.setString(4, user.email);
            pst.setString(5, "A");
            pst.executeUpdate();

            //assign user roles to i2b2 project
            String project = properties.getString("I2B2_PM.projectName");
            pst = connection.prepareStatement("INSERT INTO pm_project_user_roles (project_id, user_id, user_role_cd, status_cd) \n"
                    + "VALUES (?,?,?,?)");
            pst.setString(1, project);
            pst.setString(2, user.identifier);
            pst.setString(3, "DATA_OBFSC");
            pst.setString(4, "A");
            pst.executeUpdate();
            pst.setString(3, "DATA_AGG");
            pst.executeUpdate();
            pst.setString(3, "USER");
            pst.executeUpdate();
            connection.commit();
        }
    }

    /* The i2b2 applications expect passwords to be encrypted.
    *This function encrypts passwords with MD5 before inserting them
    *into the database.
     */
    private String encryptMD5(String text) {
        String encrypted = "";

        byte[] defaultBytes = text.getBytes();
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(defaultBytes);
            byte messageDigest[] = algorithm.digest();

            StringBuilder hexString = new StringBuilder();
            for (int i = 0; i < messageDigest.length; i++) {
                hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
            }
            encrypted = hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return encrypted;
    }

    /*Function needed to implement HttpSessionBindingListener.
     * Don't need to modify the function itself. 
     */
    public void valueBound(HttpSessionBindingEvent event) {
        //do nothing
    }

    //This function deactivates a user in the i2b2 hive database when 
    //the user session times out.  Session timeout is set in web.xml.
    public void valueUnbound(HttpSessionBindingEvent event) {
        connection = connectToDatabase();
        try {
            //Try to find current user in database
            String stmt = "SELECT user_id FROM pm_user_data WHERE user_id=?";
            PreparedStatement pst = connection.prepareStatement(stmt);
            pst.setString(1, user.identifier);
            ResultSet result = pst.executeQuery();
            //user record found in database so deactivate them since they have logged off
            if (result.next()) {
                System.out.println("in results.next");
                stmt = "UPDATE pm_user_data SET status_cd=? WHERE user_id=?";
                pst = connection.prepareStatement(stmt);
                pst.setString(1, "D");
                pst.setString(2, user.identifier);
                pst.executeUpdate();
                connection.commit();
            }
            connection.close();
            ServletContext context = event.getSession().getServletContext();
            FacesContext.getCurrentInstance().getExternalContext().redirect("logout.xhtml");
        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }
}

Solution

  • Tomcat does in fact have a way to catch unhandled application exceptions. It's sort of equivalent to the ASP.NET Application_Error event. This is probably not an elegant solution, but it works for now. Whenever we get any JSP exception, we should get redirected to this error.jsp page, and subsequently to the Shibboleth IdP login page by forcing a redirect in the error.jsp page code.

    Set this in the deployment web.xml file. This is relative to the i2b2 deploy folder.

    <error-page>
            <exception-type>java.lang.Throwable</exception-type>
            <location>/error/error.jsp</location>
    </error-page>
    

    Then I threw this into the error.jsp page:

    <%@ page isErrorPage="true" import="java.io.*" contentType="text/plain"%>
    
    Message:
    <%=exception.getMessage()%>
    
    StackTrace:
    <%
        // redirect to Shibboleth IdP login page
        String redirectURL = "https://redirecturl/";
        response.sendRedirect(redirectURL);
    %>
    

    Then restarted the Linux services. It must be done in this order, since there is a dependency with the shibd and tomcat services.

    service httpd stop
    service tomcat stop
    service jboss stop
    service shibd stop
    service shibd start
    service jboss start
    service tomcat start
    service httpd start