users@glassfish.java.net

Custom Security Realms

From: <glassfish_at_javadesktop.org>
Date: Fri, 03 Aug 2007 13:17:45 PDT

I'm trying to write my own security realm, however I get a very generic

WARNING: Web login failed: Login failed: javax.security.auth.login.LoginException: Security Exception

Thats all. Anyone have suggestions.

/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can obtain
* a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
* or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
* Sun designates this particular file as subject to the "Classpath" exception
* as provided by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the License
* Header, with the fields enclosed by brackets [] replaced by your own
* identifying information: "Portions Copyrighted [year]
* [name of copyright owner]"
*
* Contributor(s):
*
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/


package com.stryker.cmf.advjdbc;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.util.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Level;

import javax.sql.DataSource;

import sun.misc.BASE64Encoder;

import com.sun.enterprise.connectors.ConnectorRuntime;
import com.sun.enterprise.security.LoginException;
import com.sun.enterprise.security.auth.realm.BadRealmException;
import com.sun.enterprise.security.auth.realm.IASRealm;
import com.sun.enterprise.security.auth.realm.InvalidOperationException;
import com.sun.enterprise.security.auth.realm.NoSuchRealmException;
import com.sun.enterprise.security.auth.realm.NoSuchUserException;


/**
* Realm for supporting JDBC authentication.
*
* <P>The JDBC realm needs the following properties in its configuration:
* <ul>
* <li>jaas-context : JAAS context name used to access LoginModule for
* authentication (for example JDBCRealm).
* <li>datasource-jndi : jndi name of datasource
* <li>db-user : user name to access the datasource
* <li>db-password : password to access the datasource
* <li>digest: digest mechanism
* <li>charset: charset encoding
* <li>user-table: table containing user name and password
* <li>group-table: table containing user name and group name
* <li>user-name-column: column corresponding to user name in user-table and group-table
* <li>password-column : column corresponding to password in user-table
* <li>group-name-column : column corresponding to group in group-table
* <li>failures-column : column corresponding to the number of failure attempts to throttle logins
* <li>last-fail-column : time of last failure
* <li>last-success-column : time of last success
* </ul>
*
*
* @see com.sun.enterprise.security.auth.login.SolarisLoginModule
*
*/
public final class JDBCRealm extends IASRealm {
   // Descriptive string of the authentication type of this realm.
   public static final String AUTH_TYPE = "jdbc";

   public static final String PARAM_DATASOURCE_JNDI = "datasource-jndi";
   public static final String PARAM_DB_USER = "db-user";
   public static final String PARAM_DB_PASSWORD = "db-password";

   public static final String PARAM_DIGEST_ALGORITHM = "digest-algorithm";
   public static final String DEFAULT_DIGEST_ALGORITHM = "MD5";
   public static final String NONE = "none";

   public static final String PARAM_ENCODING = "encoding";
   public static final String HEX = "hex";
   public static final String BASE64 = "base64";
   public static final String DEFAULT_ENCODING = HEX; // for digest only

   public static final String PARAM_CHARSET = "charset";
   public static final String PARAM_USER_TABLE = "user-table";
   public static final String PARAM_USER_NAME_COLUMN = "user-name-column";
   public static final String PARAM_PASSWORD_COLUMN = "password-column";
   public static final String PARAM_GROUP_NAME_COLUMN = "group-name-column";
   
   public static final String PARAM_ACCOUNT_DISABLED_COLUMN = "disabled-column";
   public static final String PARAM_NUMBER_FAILURES_COLUMN = "failures-column";
   public static final String PARAM_LAST_FAILED_COLUMN = "last-fail-column";
   public static final String PARAM_LAST_SUCCESS_COLUMN = "last-success-column";

   private static final char[] HEXADECIMAL = { '0', '1', '2', '3',
       '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

   @SuppressWarnings("unchecked")
   private Map<String, Vector> groupCache;
   private Vector<String> emptyVector;
   private String passwordQuery = null;
   private String groupQuery = null;
   private String disabledQuery = null;
   private String throttleQuery = null;
   private String lastFailedUpdate = null;
   private String lastSuccessUpdate = null;
   private String failureUpdate = null;
   private MessageDigest md = null;
   
   /**
    * Initialize a realm with some properties. This can be used
    * when instantiating realms from their descriptions. This
    * method may only be called a single time.
    *
    * @param props Initialization parameters used by this realm.
    * @exception BadRealmException If the configuration parameters
    * identify a corrupt realm.
    * @exception NoSuchRealmException If the configuration parameters
    * specify a realm which doesn't exist.
    */
   @SuppressWarnings("unchecked")
public synchronized void init(Properties props)
           throws BadRealmException, NoSuchRealmException{
       super.init(props);
       String jaasCtx = props.getProperty(IASRealm.JAAS_CONTEXT_PARAM);
       String dbUser = props.getProperty(PARAM_DB_USER);
       String dbPassword = props.getProperty(PARAM_DB_PASSWORD);
       String dsJndi = props.getProperty(PARAM_DATASOURCE_JNDI);
       String digestAlgorithm = props.getProperty(PARAM_DIGEST_ALGORITHM,
           DEFAULT_DIGEST_ALGORITHM);
       String encoding = props.getProperty(PARAM_ENCODING);
       String charset = props.getProperty(PARAM_CHARSET);
       String userTable = props.getProperty(PARAM_USER_TABLE);
       String userNameColumn = props.getProperty(PARAM_USER_NAME_COLUMN);
       String passwordColumn = props.getProperty(PARAM_PASSWORD_COLUMN);
       String groupNameColumn = props.getProperty(PARAM_GROUP_NAME_COLUMN);
       String failuresColumn = props.getProperty(PARAM_NUMBER_FAILURES_COLUMN);
       String lastFailedColumn = props.getProperty(PARAM_LAST_FAILED_COLUMN);
       String lastSuccessColumn = props.getProperty(PARAM_LAST_SUCCESS_COLUMN);
       String disabledColumn = props.getProperty(PARAM_ACCOUNT_DISABLED_COLUMN);
       
       
       if (jaasCtx == null) {
           String msg = sm.getString(
               "realm.missingprop", IASRealm.JAAS_CONTEXT_PARAM, "JDBCRealm");
           throw new BadRealmException(msg);
       }

       if (dsJndi == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_DATASOURCE_JNDI, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       if (userTable == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_USER_TABLE, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       if (userNameColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_USER_NAME_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       if (passwordColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_PASSWORD_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       if (groupNameColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_GROUP_NAME_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       
       if (failuresColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_NUMBER_FAILURES_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       
       if (lastFailedColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_LAST_FAILED_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       
       if (lastSuccessColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_LAST_SUCCESS_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }
       
       if (disabledColumn == null) {
           String msg = sm.getString(
               "realm.missingprop", PARAM_ACCOUNT_DISABLED_COLUMN, "JDBCRealm");
           throw new BadRealmException(msg);
       }

       passwordQuery = "SELECT " + passwordColumn + " FROM " + userTable +
           " WHERE " + userNameColumn + " = ?";
       
       groupQuery = "SELECT " + groupNameColumn + " FROM " + userTable +
           " WHERE " + userNameColumn + " = ? ";
       
       disabledQuery = "SELECT " + disabledColumn + " FROM " + userTable +
                  " WHERE " + userNameColumn + " = ? ";
       
       throttleQuery = "SELECT " + lastFailedColumn + ", " + failuresColumn + " FROM " + userTable +
           " WHERE " + userNameColumn + " = ? ";
     
       lastSuccessUpdate = "UPDATE " + userTable + " SET " + lastSuccessColumn + " = ?" +
       " WHERE " + userNameColumn + " = ? ";
       
       lastFailedUpdate = "UPDATE " + userTable + " SET " + lastFailedColumn + " = ?" +
       " WHERE " + userNameColumn + " = ? ";
       
       failureUpdate = "UPDATE " + userTable + " SET " + failuresColumn + " = ?" +
       " WHERE " + userNameColumn + " = ? ";

       if (!NONE.equalsIgnoreCase(digestAlgorithm)) {
           try {
               md = MessageDigest.getInstance(digestAlgorithm);
           } catch(NoSuchAlgorithmException e) {
               String msg = sm.getString("jdbcrealm.notsupportdigestalg",
                   digestAlgorithm);
               throw new BadRealmException(msg);
           }
       }
       if (md != null && encoding == null) {
           encoding = DEFAULT_ENCODING;
       }

       this.setProperty(IASRealm.JAAS_CONTEXT_PARAM, jaasCtx);
       if (dbUser != null && dbPassword != null) {
           this.setProperty(PARAM_DB_USER, dbUser);
           this.setProperty(PARAM_DB_PASSWORD, dbPassword);
       }
       this.setProperty(PARAM_DATASOURCE_JNDI, dsJndi);
       this.setProperty(PARAM_DIGEST_ALGORITHM, digestAlgorithm);
       if (encoding != null) {
           this.setProperty(PARAM_ENCODING, encoding);
       }
       if (charset != null) {
           this.setProperty(PARAM_CHARSET, charset);
       }

       if (_logger.isLoggable(Level.FINEST)) {
           _logger.finest("JDBCRealm : " +
               IASRealm.JAAS_CONTEXT_PARAM + "= " + jaasCtx + ", " +
               PARAM_DATASOURCE_JNDI + " = " + dsJndi + ", " +
               PARAM_DB_USER + " = " + dbUser + ", " +
               PARAM_DIGEST_ALGORITHM + " = " + digestAlgorithm + ", " +
               PARAM_ENCODING + " = " + encoding + ", " +
               PARAM_CHARSET + " = " + charset);
       }

       groupCache = new HashMap<String, Vector>();
       emptyVector = new Vector<String>();
   }

   /**
    * Returns a short (preferably less than fifteen characters) description
    * of the kind of authentication which is supported by this realm.
    *
    * @return Description of the kind of authentication that is directly
    * supported by this realm.
    */
   public String getAuthType(){
       return AUTH_TYPE;
   }

   /**
    * Returns the name of all the groups that this user belongs to.
    * It loads the result from groupCache first.
    * This is called from web path group verification, though
    * it should not be.
    *
    * @param username Name of the user in this realm whose group listing
    * is needed.
    * @return Enumeration of group names (strings).
    * @exception InvalidOperationException thrown if the realm does not
    * support this operation - e.g. Certificate realm does not support
    * this operation.
    */
   
@SuppressWarnings("unchecked")
public Enumeration getGroupNames(String username)
           throws InvalidOperationException, NoSuchUserException {
       Vector vector = groupCache.get(username);
       if (vector == null) {
           String[] grps = findGroups(username);
           setGroupNames(username, grps);
           vector = groupCache.get(username);
       }
       return vector.elements();
   }

   private void setGroupNames(String username, String[] groups) {
       Vector<String> v = null;
       
       if (groups == null) {
           v = emptyVector;

       } else {
           v = new Vector<String>(groups.length + 1);
           for (int i=0; i<groups.length; i++) {
               v.add(groups[i]);
           }
       }
       
       synchronized (this) {
           groupCache.put(username, v);
       }
   }


   /**
    * Invoke the native authentication call.
    *
    * @param username User to authenticate.
    * @param password Given password.
    * @returns true of false, indicating authentication status.
    *
    */
   public String[] authenticate(String username, String password) {
           System.out.print("1");
       String[] groups = null;
       System.out.print("2");
       int throttle;
       System.out.print("3");
       throttle = numThrottled(username);
       System.out.print("4");
       if (isUserValid(username, password) && (throttle == 0) && isNotDisabled(username)) {
               System.out.print("5");
           groups = findGroups(username);
           System.out.print("6");
           groups = addAssignGroups(groups);
           System.out.print("7");
           setGroupNames(username, groups);
           System.out.print("8");
           updateSuccess(username);
           System.out.print("9");
           setFailure(username, throttle);
       } else {
               System.out.print("10");
               updateFailure(username);
       }
       return groups;
   }

   /**
    * Algorithm to throttle users from multiple failed login attempts.
    * @param user
    * @return
    */
   private int numThrottled(String user) {
           Connection connection = null;
           PreparedStatement statement = null;
           ResultSet rs = null;
           int throttled = 0;
           try {
                   connection = getConnection();
                   statement = connection.prepareStatement(throttleQuery);
                   statement.setString(1, user);
                   rs = statement.executeQuery();
                   Timestamp lastFailed = null;
                   int numberFailures = 0;
                   if (rs.next()) {
                           lastFailed = rs.getTimestamp(1);
                           numberFailures = rs.getInt(2);
                           if (lastFailed.getTime()+(10^numberFailures) > (new Date().getTime()))
                                   throttled = numberFailures+1;
                   }
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
           return throttled;
   }
   
   /**
    * Prevents the user from loggin in if their account is disabled.
    * @param user
    * @return
    */
   private boolean isNotDisabled(String user) {
           Connection connection = null;
           PreparedStatement statement = null;
           ResultSet rs = null;
           boolean disabled = false;
           try {
                   connection = getConnection();
                   statement = connection.prepareStatement(disabledQuery);
                   statement.setString(1, user);
                   rs = statement.executeQuery();
                   int isDisabled = 0;
                   if (rs.next()) {
                           isDisabled = rs.getInt(1);
                           if (isDisabled==1)
                                   disabled = true;
                   }
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
           return disabled;
   }
   private void updateSuccess(String user) {
           Connection connection = null;
           PreparedStatement statement = null;
           ResultSet rs = null;
           try {
                   connection = getConnection();
                   statement = connection.prepareStatement(lastSuccessUpdate);
                   statement.setTimestamp(1, new Timestamp(new Date().getTime()));
                   statement.setString(2, user);
                   statement.execute();
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
   }
   private void updateFailure(String user) {
           Connection connection = null;
           PreparedStatement statement = null;
           ResultSet rs = null;
           try {
                   connection = getConnection();
                   statement = connection.prepareStatement(lastFailedUpdate);
                   statement.setTimestamp(1, new Timestamp(new Date().getTime()));
                   statement.setString(2, user);
                   statement.execute();
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
   }
   
   private void setFailure(String user, int failures) {
           Connection connection = null;
           PreparedStatement statement = null;
           ResultSet rs = null;
           try {
                   connection = getConnection();
                   statement = connection.prepareStatement(failureUpdate);
                   statement.setInt(1, failures);
                   statement.setString(2, user);
                   statement.execute();
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
   }
   
   /**
    * Test if a user is valid
    * @param user user's identifier
    * @param password user's password
    * @return true if valid
    */
   private boolean isUserValid(String user, String password) {
       Connection connection = null;
       PreparedStatement statement = null;
       ResultSet rs = null;
       boolean valid = false;

       try {
           String hpwd = hashPassword(password);
           connection = getConnection();
           statement = connection.prepareStatement(passwordQuery);
           statement.setString(1, user);
           rs = statement.executeQuery();
           String pwd = null;
           if (rs.next()) {
               pwd = rs.getString(1);
               if (HEX.equalsIgnoreCase(getProperty(PARAM_ENCODING))) {
                   valid = pwd.equalsIgnoreCase(hpwd);
               } else {
                   valid = pwd.equals(hpwd);
               }
           }
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.invaliduser", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot validate user", ex);
           }
       } finally {
           close(connection, statement, rs);
       }
       return valid;
   }

   private String hashPassword(String password)
           throws UnsupportedEncodingException{
       String result = null;
       byte[] bytes = null;
       String charSet = getProperty(PARAM_CHARSET);
       if (charSet != null) {
           bytes = password.getBytes(charSet);
       } else {
           bytes = password.getBytes();
       }
       if (md != null) {
           synchronized(md) {
               md.reset();
               bytes = md.digest(bytes);
           }
       }

       String encoding = getProperty(PARAM_ENCODING);
       if (HEX.equalsIgnoreCase(encoding)) {
           result = hexEncode(bytes);
       } else if (BASE64.equalsIgnoreCase(encoding)) {
           result = base64Encode(bytes);
       } else { // no encoding specified
           result = new String(bytes);
       }
       return result;
   }

   private String hexEncode(byte[] bytes) {
       StringBuilder sb = new StringBuilder(2 * bytes.length);
       for (int i = 0; i < bytes.length; i++) {
           int low = (int)(bytes[i] & 0x0f);
           int high = (int)((bytes[i] & 0xf0) >> 4);
           sb.append(HEXADECIMAL[high]);
           sb.append(HEXADECIMAL[low]);
       }
       return sb.toString();
   }

   private String base64Encode(byte[] bytes) {
       BASE64Encoder encoder = new BASE64Encoder();
       return encoder.encode(bytes);
   }

   /**
    * Delegate method for retreiving users groups
    * @param user user's identifier
    * @return array of group key
    */
   private String[] findGroups(String user) {
       Connection connection = null;
       PreparedStatement statement = null;
       ResultSet rs = null;
       try{
           connection = getConnection();
           statement = connection.prepareStatement(groupQuery);
           statement.setString(1, user);
           rs = statement.executeQuery();
           final List<String> groups = new ArrayList<String>();
           while (rs.next()) {
               groups.add(rs.getString(1));
           }
           final String[] groupArray = new String[groups.size()];
           return groups.toArray(groupArray);
       } catch(Exception ex) {
           _logger.log(Level.SEVERE, "jdbcrealm.grouperror", user);
           if (_logger.isLoggable(Level.FINE)) {
               _logger.log(Level.FINE, "Cannot load group", ex);
           }
           return null;
       } finally {
           close(connection, statement, rs);
       }
   }

   private void close(Connection conn, PreparedStatement stmt,
           ResultSet rs) {
       if (rs != null) {
           try {
               rs.close();
           } catch(Exception ex) {
           }
       }
           
       if (stmt != null) {
           try {
               stmt.close();
           } catch(Exception ex) {
           }
       }
           
       if (conn != null) {
           try {
               conn.close();
           } catch(Exception ex) {
           }
       }
   }

   /**
    * Return a connection from the properties configured
    * @return a connection
    */
   private Connection getConnection() throws LoginException {

       final String dsJndi = this.getProperty(PARAM_DATASOURCE_JNDI);
       final String dbUser = this.getProperty(PARAM_DB_USER);
       final String dbPassword = this.getProperty(PARAM_DB_PASSWORD);
       try{
           final DataSource dataSource =
               (DataSource)ConnectorRuntime.getRuntime().lookupNonTxResource(dsJndi,false);
           
           Connection connection = null;
           if (dbUser != null && dbPassword != null) {
               connection = dataSource.getConnection(dbUser, dbPassword);
           } else {
               connection = dataSource.getConnection();
           }
           return connection;
       } catch(Exception ex) {
           String msg = sm.getString("jdbcrealm.cantconnect", dsJndi, dbUser);
           LoginException loginEx = new LoginException(msg);
           loginEx.initCause(ex);
           throw loginEx;
       }
   }
}
[Message sent by forum member 'amattas' (amattas)]

http://forums.java.net/jive/thread.jspa?messageID=229431