com.krminc.phr.security.PHRRealm.java Source code

Java tutorial

Introduction

Here is the source code for com.krminc.phr.security.PHRRealm.java

Source

/**
 * Copyright (C) 2012 KRM Associates, Inc. healtheme@krminc.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.krminc.phr.security;

import com.sun.appserv.security.AppservRealm;
import com.sun.enterprise.security.auth.realm.User;
import com.sun.enterprise.security.auth.realm.BadRealmException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;
import com.sun.enterprise.security.auth.realm.NoSuchRealmException;
import java.util.Enumeration;
import java.util.Vector;
import java.util.Properties;

import org.apache.commons.codec.digest.DigestUtils;

import java.util.logging.Logger;
import java.util.logging.Level;
import com.sun.logging.LogDomains;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.GregorianCalendar;

import javax.naming.InitialContext;

/**
 *
 * @author cmccall
 */
public class PHRRealm extends AppservRealm {
    //the following property variables are allocated in the following fashion:
    // private static PROPERTY => default value of PROPERTY
    // private static PROPERTY_PARAM => string used to specific PROPERTY in domain.xml
    // private propertyValue => variable containing version of PROPERTY for use in code (whether default or specific in domain.xml)

    //auth type property variables
    private static String AUTH_TYPE = "phrcustomauth";
    private static String AUTH_TYPE_PARAM = "auth-type";
    private String authType = null;

    //failed attempts property variables
    private static String FAILED_ATTEMPTS = "5";
    private static String FAILED_ATTEMPTS_PARAM = "failed-attempts";
    private Integer failedAttempts = null;

    //locked role name property variables
    private static String LOCKED_ROLE = "ROLE_LOCKED";
    private static String LOCKED_ROLE_PARAM = "locked-role";
    private String lockedRole = null;

    //reset role name property variables
    private static String RESET_ROLE = "ROLE_RESET";
    private static String RESET_ROLE_PARAM = "reset-role";
    private String resetRole = null;

    //jdbc resource name property variables
    private static String JDBC_RESOURCE = "jdbc/phr";
    private static String JDBC_RESOURCE_PARAM = "jdbc-resource";
    private String jdbcResource = null;

    //logging level property variables
    private static String LOG_LEVEL = "INFO";
    private static String LOG_LEVEL_PARAM = "logging-level";
    private Level logLevel = null;

    // end properties variables

    private Connection conn = null;
    private com.sun.appserv.jdbc.DataSource ds = null;

    private static Logger _logger = null;

    static {
        _logger = Logger.getLogger(LogDomains.SECURITY_LOGGER);
    }

    /*
     * This method is invoked during server startup when the realm is
     * initially loaded.
     * The props argument contains the properties defined
     * for this realm in domain.xml.
     * The realm can do any initialization it needs in this method.
     * If the method returns without throwing an exception,
     * J2EE Application Server assumes the realm is ready
     * to service authentication requests.
     * If an exception is thrown, the realm eis disabled,
     * check the server.log for messages.
     */
    public void init(Properties props) throws BadRealmException, NoSuchRealmException {

        super.init(props);

        /*
         * Set the jaas context, otherwise server doesn't indentify the login module.
         * jaas-context is the property specified in domain.xml and
         * is the name corresponding to LoginModule
         * config/login.conf
         */
        String jaasCtx = props.getProperty(AppservRealm.JAAS_CONTEXT_PARAM);
        this.setProperty(AppservRealm.JAAS_CONTEXT_PARAM, jaasCtx);

        /*
         * Get any other interested properties from configuration file - domain.xml
         * 
         */
        String authTypeProp = props.getProperty(AUTH_TYPE_PARAM);
        this.authType = (authTypeProp != null) ? authTypeProp : AUTH_TYPE;

        String failedAttemptsProp = props.getProperty(FAILED_ATTEMPTS_PARAM);
        this.failedAttempts = Integer.valueOf((failedAttemptsProp != null) ? failedAttemptsProp : FAILED_ATTEMPTS);

        String lockedRoleProp = props.getProperty(LOCKED_ROLE_PARAM);
        this.lockedRole = (lockedRoleProp != null) ? lockedRoleProp : LOCKED_ROLE;

        String resetRoleProp = props.getProperty(RESET_ROLE_PARAM);
        this.resetRole = (resetRoleProp != null) ? resetRoleProp : RESET_ROLE;

        String jdbcResourceProp = props.getProperty(JDBC_RESOURCE_PARAM);
        this.jdbcResource = (jdbcResourceProp != null) ? jdbcResourceProp : JDBC_RESOURCE;

        String logLevelParam = props.getProperty(LOG_LEVEL_PARAM);
        logLevelParam = (logLevelParam != null) ? logLevelParam : LOG_LEVEL;
        if (logLevelParam.equalsIgnoreCase("INFO")) {
            this.logLevel = Level.INFO;
        } else if (logLevelParam.equalsIgnoreCase("SEVERE")) {
            this.logLevel = Level.SEVERE;
        } else if (logLevelParam.equalsIgnoreCase("WARNING")) {
            this.logLevel = Level.WARNING;
        } else if (logLevelParam.equalsIgnoreCase("OFF")) {
            this.logLevel = Level.OFF;
        } else if (logLevelParam.equalsIgnoreCase("FINEST")) {
            this.logLevel = Level.FINEST;
        }

        log("Initialized PHR Custom Realm");
    }

    private boolean createDS() {
        if (ds != null)
            return true;

        try {
            InitialContext ctx = new InitialContext();

            if (ctx == null) {
                log("JNDI problem, Cannot get Initial Context.");
                return false;
            }

            ds = (com.sun.appserv.jdbc.DataSource) ctx.lookup(jdbcResource);

            if (ds == null) {
                log("Unable to lookup datasource.");
                return false;
            }
        } catch (Exception e) {
            log("Exception encountered in database connection initialization.");
            log(e.getMessage());
            return false;
        }
        return true;
    }

    protected Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException("Not supported");
    }

    /**
      * Return a short description supported authentication by this realm.
      *
      * @return Description of the kind of authentication that is directly
      *     supported by this realm.
      */
    public String getAuthType() {
        return this.authType;
    }

    public String getLockedRole() {
        return this.lockedRole;
    }

    public String getResetRole() {
        return this.resetRole;
    }

    public String getJdbcResource() {
        return this.jdbcResource;
    }

    public Integer getFailedAttempts() {
        return this.failedAttempts;
    }

    public Level getLoggingLevel() {
        return this.logLevel;
    }

    /**
     * Returns names of all the users in this particular realm.
     *
     * @return enumeration of user names
     *
     */
    public Enumeration getUserNames() throws BadRealmException {
        String query = "SELECT username FROM user_users";
        ResultSet rs = null;
        Vector usernames = new Vector();
        PreparedStatement st = null;
        try {
            createDS();
            conn = ds.getNonTxConnection();
            st = conn.prepareStatement(query);
            rs = st.executeQuery();
        } catch (Exception e) {
            log("Error getting usernames from database");
            log(e.getMessage());
            rs = null;
        } finally {
            try {
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
        if (rs != null) {
            try {
                rs.beforeFirst();
                while (rs.next()) {
                    usernames.add(rs.getString(1));
                }
            } catch (Exception e) {
                log("Error getting usernames from resultset");
                log(e.getMessage());
            } finally {
                try {
                    st.close();
                    rs.close();
                } catch (Exception e) {
                    log(e.getMessage());
                }
            }
        }
        return usernames.elements();
    }

    /**
     * Returns the information recorded about a particular named user.
     *
     * This method always throws a BadRealmException since this method
     * is not supported in this context.
     *
     * @exception BadRealmException
     *
     */
    public User getUser(String name) throws NoSuchUserException, BadRealmException {
        throw new BadRealmException("Not supported");
    }

    /**
     * Returns names of all the groups in this particular realm.
     *
     * @return enumeration of group names (strings)
     *
     */
    public Enumeration getGroupNames() throws BadRealmException {
        //check role cache before querying

        String query = "SELECT UNIQUE role FROM user_roles";
        ResultSet rs = null;
        Vector roles = new Vector();
        PreparedStatement st = null;
        try {
            createDS();
            conn = ds.getNonTxConnection();
            st = conn.prepareStatement(query);
            rs = st.executeQuery();
        } catch (Exception e) {
            log("Error getting roles from database");
            log(e.getMessage());
            rs = null;
        } finally {
            try {
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
        if (rs != null) {
            try {
                rs.beforeFirst();
                while (rs.next()) {
                    roles.add(rs.getString(1));
                }
            } catch (Exception e) {
                log("Error getting roles from resultset");
                log(e.getMessage());
            } finally {
                try {
                    st.close();
                    rs.close();
                } catch (Exception e) {
                    log(e.getMessage());
                }
            }
        }
        return roles.elements();
    }

    /**
     * Returns enumeration of groups that a particular user belongs to.
     *
     *@exception NoSuchUserException
     */
    public Enumeration getGroupNames(String user) throws NoSuchUserException {
        //check user cache before querying?

        String query = "SELECT DISTINCT role FROM user_roles WHERE username = ?";
        ResultSet rs = null;
        Vector roles = new Vector();
        PreparedStatement st = null;
        try {
            createDS();
            conn = ds.getNonTxConnection();
            st = conn.prepareStatement(query);
            st.setString(1, user);
            rs = st.executeQuery();
        } catch (Exception e) {
            log("Error getting roles from database");
            log(e.getMessage());
            rs = null;
        } finally {
            try {
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
        if (rs != null) {
            try {
                rs.beforeFirst();
                while (rs.next()) {
                    roles.add(rs.getString(1));
                }
            } catch (Exception e) {
                log("Error getting roles from resultset");
                log(e.getMessage());
            } finally {
                try {
                    st.close();
                    rs.close();
                } catch (Exception e) {
                    log(e.getMessage());
                }
            }
        } else {
            throw new NoSuchUserException("User not available.");
        }
        return roles.elements();
    }

    /**
     * Refreshes the realm data so that new users/groups are visible.
     *
     */
    public void refresh() throws BadRealmException {
        super.refresh();
    }

    /**
     * Checks the authentication of a user and returns the groups it belongs to.
     *
     * @return groups that this particular user belongs to
     */
    public String[] authenticateUser(String user, String password) {
        String query = "SELECT password, requires_reset, is_locked_out, active, failed_password_attempts FROM user_users WHERE username = ?";
        ResultSet rs = null;
        String passwordHash = new String();
        boolean requiresReset = false;
        boolean lockedOut = false;
        boolean active = false;
        int failedAttemptsVal = 0;
        PreparedStatement st = null;
        try {
            createDS();
            conn = ds.getNonTxConnection();
            st = conn.prepareStatement(query);
            st.setString(1, user);
            rs = st.executeQuery();
        } catch (Exception e) {
            log("Error getting password from database");
            log(e.getMessage());
            rs = null;
        } finally {
            try {
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
        if (rs != null) {
            try {
                if (rs.next()) {
                    passwordHash = rs.getString(1);
                    requiresReset = rs.getBoolean(2);
                    lockedOut = rs.getBoolean(3);
                    active = rs.getBoolean(4);
                    failedAttemptsVal = rs.getInt(5);
                }
            } catch (Exception e) {
                log("Error getting password from resultset");
                log(e.getMessage());
            } finally {
                try {
                    st.close();
                    rs.close();
                } catch (Exception e) {
                    log(e.getMessage());
                }
            }
        }

        //inactive users have no roles, no login attempt monitoring
        if (!active)
            return null;

        //locked users have a locked role, which is filtered by the Login class as needed
        if (lockedOut) {
            incrementFailedUpdate(user);
            String[] lock = { lockedRole };
            return lock;
        }

        if (!passwordHash.isEmpty()) {
            if (passwordHash.equals(DigestUtils.sha512Hex(password))) {
                //password is correct
                //find and return groups
                String[] retArr = null;
                try {
                    Enumeration userGroups = getGroupNames(user);
                    ArrayList retGroups = new ArrayList();

                    //populate with groups from db
                    while (userGroups.hasMoreElements()) {
                        retGroups.add(userGroups.nextElement().toString());
                    }

                    //force password reset if needed by adding role
                    if (requiresReset) {
                        retGroups.add(resetRole);
                    }

                    //formulate returnable collection
                    Object[] arr = retGroups.toArray();
                    retArr = new String[arr.length];
                    for (int i = 0; i < arr.length; i++) {
                        retArr[i] = arr[i].toString();
                    }
                } catch (NoSuchUserException e) {
                    log("Exception encountered looking up password");
                } catch (Exception e) {
                    log("Group lookup error: " + e.getClass() + ":" + e.getMessage());
                }

                //reset bad login info, if needed, now that we're successful
                // this will only happen for valid logins and pw reset logins, not locks or inactive accts
                if (failedAttemptsVal > 0)
                    doSuccessfulUpdate(user);

                return retArr;
            } else {
                log("Passwords do not match");
                try {
                    InvalidPasswordAttempt(user);
                } catch (NoSuchUserException e) {
                    //not an issue here
                }
            }
        } else {
            log("Unable to find user password");
        }
        return null;

    }

    private void InvalidPasswordAttempt(String username) throws NoSuchUserException {
        String query = "SELECT failed_password_attempts, failed_password_window_start FROM user_users WHERE username = ?";
        ResultSet rs = null;
        PreparedStatement st = null;
        Timestamp windowStart = null;
        int failedAttemptsVal = 0;
        try {
            createDS();
            conn = ds.getNonTxConnection();
            st = conn.prepareStatement(query);
            st.setString(1, username);
            rs = st.executeQuery();
        } catch (Exception e) {
            log("Error getting roles from database");
            log(e.getMessage());
            rs = null;
        } finally {
            try {
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
        if (rs != null) {
            try {
                rs.beforeFirst();
                while (rs.next()) {
                    failedAttemptsVal = rs.getInt(1);
                    windowStart = rs.getTimestamp(2);
                }
            } catch (Exception e) {
                log("Error getting invalid attempt values from resultset");
                log(e.getMessage());
            } finally {
                try {
                    st.close();
                    rs.close();
                } catch (Exception e) {
                    log(e.getMessage());
                }
            }
        } else {
            throw new NoSuchUserException("User not available.");
        }

        //take values and decide whether to lock account, increment failed attempts, or start new failure window
        if (windowStart != null) {
            //check if user has more than X previously existing failed logins
            if (Integer.valueOf(failedAttemptsVal).compareTo(failedAttempts) >= 0) {
                //lock out
                doFailedUpdate(username, null, failedAttemptsVal + 1, true);
            } else {
                //dont lock account, just increment failed attempts
                doFailedUpdate(username, windowStart, failedAttemptsVal + 1, false);
            }
        } else {
            //windowStart is null, set to now and set failed attempts to 1
            GregorianCalendar tempCal = new GregorianCalendar();
            windowStart = new java.sql.Timestamp(tempCal.getTimeInMillis());
            failedAttemptsVal = 1;
            doFailedUpdate(username, windowStart, failedAttemptsVal, false);
        }

    }

    //reset password attempts and password window
    private void doSuccessfulUpdate(String username) {
        String query = "UPDATE user_users SET failed_password_attempts = ? , failed_password_window_start = ? WHERE username = ?";
        PreparedStatement st = null;

        try {
            createDS();
            //TX for UPDATE
            conn = ds.getConnection();
            st = conn.prepareStatement(query);
            st.setInt(1, 0); //reset failed attempt count
            st.setTimestamp(2, null); //reset failed password timestamp
            st.setString(3, username);
            st.executeUpdate();
        } catch (Exception e) {
            log("Error resetting failed password values");
            log(e.getMessage());
        } finally {
            try {
                st.close();
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
    }

    //extracted to own method, not using doFailedUpdate, to avoid overhead of manipulating multiple fields
    private void incrementFailedUpdate(String username) {
        String query = "UPDATE user_users SET failed_password_attempts = failed_password_attempts + 1 WHERE username = ?";
        PreparedStatement st = null;

        try {
            createDS();
            //TX for UPDATE
            conn = ds.getConnection();
            st = conn.prepareStatement(query);
            st.setString(1, username);
            st.executeUpdate();
        } catch (Exception e) {
            log("Error incrementing failed password value");
            log(e.getMessage());
        } finally {
            try {
                st.close();
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
    }

    private void doFailedUpdate(String username, java.sql.Timestamp windowStart, int failedAttempts,
            boolean setLock) {
        String query = "UPDATE user_users SET failed_password_attempts = ? , failed_password_window_start = ? , is_locked_out = ?, lockout_begin = ? WHERE username = ?";
        PreparedStatement st = null;
        java.sql.Timestamp lockoutBegin = null;

        if (setLock) {
            GregorianCalendar tempCal = new GregorianCalendar(java.util.TimeZone.getTimeZone("GMT"));
            lockoutBegin = new java.sql.Timestamp(tempCal.getTimeInMillis());
        }

        try {
            createDS();
            //TX for UPDATE
            conn = ds.getConnection();
            st = conn.prepareStatement(query);
            st.setInt(1, failedAttempts);
            st.setTimestamp(2, windowStart);
            st.setBoolean(3, setLock);
            st.setTimestamp(4, lockoutBegin);
            st.setString(5, username);
            st.executeUpdate();
        } catch (Exception e) {
            log("Error updating failed password values");
            log(e.getMessage());
        } finally {
            try {
                st.close();
                conn.close();
            } catch (Exception e) {
                log(e.getMessage());
            }
            conn = null;
        }
    }

    public static Logger getLogger() {
        return _logger;
    }

    /*
     * Helper method
     *
     * Simple message print method used throughout the program
     */
    public void log(String message) {
        if (logLevel == null)
            logLevel = Level.INFO;
        _logger.log(logLevel, "PHRRealm:" + message);
    }
}