diff --git a/glassfish-realm/nb-configuration.xml b/glassfish-realm/nb-configuration.xml new file mode 100644 index 0000000..eacb475 --- /dev/null +++ b/glassfish-realm/nb-configuration.xml @@ -0,0 +1,17 @@ + + + + + all + + diff --git a/glassfish-realm/pom.xml b/glassfish-realm/pom.xml new file mode 100644 index 0000000..f4d4151 --- /dev/null +++ b/glassfish-realm/pom.xml @@ -0,0 +1,110 @@ + + 4.0.0 + + net.eisele.security + glassfish-realm + 1.0-SNAPSHOT + jar + + glassfish-realm + http://maven.apache.org + + + + UTF-8 + com.mysql.jdbc.Driver + jdbc:mysql://localhost:3306/jdbcrealmdb + root + root + + + + + + org.codehaus.mojo + sql-maven-plugin + 1.3 + + + mysql + mysql-connector-java + 5.1.18 + + + + ${driver} + ${url} + ${username} + ${password} + ${maven.test.skip} + + src/test/data/drop-and-create-table.sql + + + + + create-table + test-compile + + execute + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.4.2 + + + + user + ${username} + + + password + ${password} + + + + + + + + + + + + junit + junit + 4.10 + test + + + org.glassfish.extras + glassfish-embedded-all + 3.1.1 + provided + + + mysql + mysql-connector-java + 5.1.18 + test + + + diff --git a/glassfish-realm/src/main/java/net/eisele/security/glassfishrealm/UserRealm.java b/glassfish-realm/src/main/java/net/eisele/security/glassfishrealm/UserRealm.java new file mode 100644 index 0000000..bde820a --- /dev/null +++ b/glassfish-realm/src/main/java/net/eisele/security/glassfishrealm/UserRealm.java @@ -0,0 +1,99 @@ +package net.eisele.security.glassfishrealm; + +import com.sun.appserv.security.AppservRealm; +import com.sun.enterprise.security.auth.realm.BadRealmException; +import com.sun.enterprise.security.auth.realm.InvalidOperationException; +import com.sun.enterprise.security.auth.realm.NoSuchRealmException; +import com.sun.enterprise.security.auth.realm.NoSuchUserException; +import java.util.Enumeration; +import java.util.Properties; +import java.util.logging.Level; +import net.eisele.security.util.Password; +import net.eisele.security.util.SecurityStore; + +/** + * High Security UserRealm for GlassFish Server. Implementing password salting. + * + * @author eiselem + */ +public class UserRealm extends AppservRealm { + + private String jaasCtxName; + private String dataSource; + + /** + * Init realm from properties + * + * @param props + * @throws BadRealmException + * @throws NoSuchRealmException + */ + @Override + protected void init(Properties props) throws BadRealmException, NoSuchRealmException { + _logger.fine("init()"); + jaasCtxName = props.getProperty("jaas-context", "UserRealm"); + dataSource = props.getProperty("dataSource", "jdbc/userdb"); + } + + /** + * {@inheritDoc } + * + * @return + */ + @Override + public String getJAASContext() { + return jaasCtxName; + } + + /** + * {@inheritDoc } + * + * @return + */ + @Override + public String getAuthType() { + return "High Security UserRealm"; + } + + /** + * Authenticates a user against GlassFish + * + * @param uid The User ID + * @param givenPwd The Password to check + * @return String[] of the groups a user belongs to. + * @throws Exception + */ + public String[] authenticate(String name, String givenPwd) throws Exception { + SecurityStore store = new SecurityStore(dataSource); + // attempting to read the users-salt + String salt = store.getSaltForUser(name); + + // Defaulting to a failed login by setting null + String[] result = null; + + if (salt != null) { + Password pwd = new Password(); + // get the byte[] from the salt + byte[] saltBytes = pwd.bytesFrombase64(salt); + // hash password and salt + byte[] passwordBytes = pwd.hashWithSalt(givenPwd, saltBytes); + // Base64 encode to String + String password = pwd.base64FromBytes(passwordBytes); + _logger.log(Level.FINE, "PWD Generated {0}", password); + // validate password with the db + if (store.validateUser(name, password)) { + result[0] = "ValidUser"; + } + } + return result; + } + + /** + * {@inheritDoc } + */ + @Override + public Enumeration getGroupNames(String string) throws InvalidOperationException, NoSuchUserException { + //never called. Only here to make compiler happy. + return null; + } +} diff --git a/glassfish-realm/src/main/java/net/eisele/security/util/Password.java b/glassfish-realm/src/main/java/net/eisele/security/util/Password.java new file mode 100644 index 0000000..a8834c4 --- /dev/null +++ b/glassfish-realm/src/main/java/net/eisele/security/util/Password.java @@ -0,0 +1,113 @@ +package net.eisele.security.util; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import sun.misc.BASE64Decoder; +import sun.misc.BASE64Encoder; + +/** + * A utility library for creating secure hashes with salts. + * + * @author eiselem + */ +public class Password { + + private SecureRandom random; + private static final String CHARSET = "UTF-8"; + private static final String ENCRYPTION_ALGORITHM = "SHA-512"; + private BASE64Decoder decoder = new BASE64Decoder(); + private BASE64Encoder encoder = new BASE64Encoder(); + + /** + * Generate a secure salt from SecureRandom with a given length + * + * @param length + * @return + */ + public byte[] getSalt(int length) { + random = new SecureRandom(); + byte bytes[] = new byte[length]; + random.nextBytes(bytes); + return bytes; + } + + /** + * Hash a password with a salt. + * + * @param password + * @param salt + * @return + */ + public byte[] hashWithSalt(String password, byte[] salt) { + byte[] hash = null; + try { + byte[] bytesOfMessage = password.getBytes(CHARSET); + MessageDigest md; + md = MessageDigest.getInstance(ENCRYPTION_ALGORITHM); + md.reset(); + md.update(salt); + md.update(bytesOfMessage); + hash = md.digest(); + + } catch (UnsupportedEncodingException | NoSuchAlgorithmException ex) { + Logger.getLogger(Password.class.getName()).log(Level.SEVERE, "Encoding Problem", ex); + } + return hash; + } + + /** + * Hash with a slow salt. PBKDF2WithHmacSHA1 + * + * @param password + * @param salt + * @return + */ + public byte[] hashWithSlowsalt(String password, byte[] salt) { + SecretKeyFactory factory; + Key key = null; + try { + factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keyspec = new PBEKeySpec(password.toCharArray(), salt, 1000, 512); + key = factory.generateSecret(keyspec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) { + Logger.getLogger(Password.class.getName()).log(Level.SEVERE, null, ex); + } + return key.getEncoded(); + } + + /** + * Get a string from byte[] with bas64 encoding + * + * @param text + * @return + */ + public String base64FromBytes(byte[] text) { + return encoder.encode(text); + } + + /** + * Get a byte[] from a string with base64 encoding + * + * @param text + * @return + */ + public byte[] bytesFrombase64(String text) { + byte[] textBytes = null; + try { + textBytes = decoder.decodeBuffer(text); + } catch (IOException ex) { + Logger.getLogger(Password.class.getName()).log(Level.SEVERE, "Encoding failed!", ex); + } + return textBytes; + } +} diff --git a/glassfish-realm/src/main/java/net/eisele/security/util/SecurityStore.java b/glassfish-realm/src/main/java/net/eisele/security/util/SecurityStore.java new file mode 100644 index 0000000..9ea0f2b --- /dev/null +++ b/glassfish-realm/src/main/java/net/eisele/security/util/SecurityStore.java @@ -0,0 +1,138 @@ +package net.eisele.security.util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +/** + * Database abstraction for a User and a Group table to use with a GlassFish + * realm. + * + * @author eiselem + */ +public class SecurityStore { + + private Connection con; + private final static Logger LOGGER = Logger.getLogger(Password.class.getName()); + private final static String ADD_USER = "INSERT INTO users VALUES(?,?,?);"; + private final static String SALT_FOR_USER = "SELECT salt FROM users u WHERE username = ?;"; + private final static String VERIFY_USER = "SELECT username FROM users u WHERE username = ? AND password = ?;"; + + /** + * Public constructor for use with Java EE App-servers or Clients which have + * access to an InitialContext. In this case a javax.sql.DataSource is + * looked up with the Context. + * + * @param dataSource + */ + public SecurityStore(String dataSource) { + Context ctx = null; + try { + ctx = new InitialContext(); + DataSource ds = (javax.sql.DataSource) ctx.lookup(dataSource); + con = ds.getConnection(); + } catch (NamingException | SQLException e) { + LOGGER.log(Level.SEVERE, "Error getting connection!", e); + } finally { + if (ctx != null) { + try { + ctx.close(); + } catch (NamingException e) { + LOGGER.log(Level.SEVERE, "Error closing context!", e); + } + } + } + } + + /** + * Public constructor for use with standalone tests or separate databases. + * User and password have to be supplied. MySQL Database is assumed to be on + * localhost:3306 and schema called "jdbcrealmdb" + * + * @param user + * @param passwd + */ + public SecurityStore(String user, String passwd) { + try { + Class.forName("com.mysql.jdbc.Driver").newInstance(); + + con = DriverManager + .getConnection("jdbc:mysql://localhost:3306/jdbcrealmdb?user=" + user + "&password=" + passwd + ""); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | SQLException ex) { + Logger.getLogger(SecurityStore.class.getName()).log(Level.SEVERE, "Error getting connection", ex); + } + } + + /** + * Adds a User to the Database + * + * @param name The username + * @param salt The dynamic salt + * @param password The password (Hashed) + */ + public void addUser(String name, String salt, String password) { + try { + PreparedStatement pstm = con.prepareStatement(ADD_USER); + pstm.setString(1, name); + pstm.setString(2, salt); + pstm.setString(3, password); + pstm.executeUpdate(); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Create User failed!", ex); + } + } + + /** + * Get's the salt for a given user + * + * @param name User name + * @return + */ + public String getSaltForUser(String name) { + String salt = null; + try { + PreparedStatement pstm = con.prepareStatement(SALT_FOR_USER); + pstm.setString(1, name); + ResultSet rs = pstm.executeQuery(); + + if (rs.next()) { + salt = rs.getString(1); + } + + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "User not found!", ex); + } + return salt; + } + + /** + * validates a user with a given password and a username + * + * @param name the username + * @param password the password (Hashed) + * @return + */ + public boolean validateUser(String name, String password) { + + try { + PreparedStatement pstm = con.prepareStatement(VERIFY_USER); + pstm.setString(1, name); + pstm.setString(2, password); + ResultSet rs = pstm.executeQuery(); + if (rs.next()) { + return true; + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "User validation failed!", ex); + } + return false; + } +} diff --git a/glassfish-realm/src/test/data/drop-and-create-table.sql b/glassfish-realm/src/test/data/drop-and-create-table.sql new file mode 100644 index 0000000..3fef495 --- /dev/null +++ b/glassfish-realm/src/test/data/drop-and-create-table.sql @@ -0,0 +1,8 @@ +USE jdbcrealmdb; +DROP TABLE IF EXISTS `jdbcrealmdb`.`groups`; +DROP TABLE IF EXISTS `jdbcrealmdb`.`users`; + +CREATE TABLE `jdbcrealmdb`.`users` (`username` varchar(255) NOT NULL,`salt` varchar(255) NOT NULL,`password` varchar(255) DEFAULT NULL,PRIMARY KEY (`username`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `jdbcrealmdb`.`groups` (`username` varchar(255) DEFAULT NULL,`groupname` varchar(255) DEFAULT NULL)ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE INDEX groups_users_FK1 ON groups(username ASC); + diff --git a/glassfish-realm/src/test/java/net/eisele/security/util/SecurityStoreTest.java b/glassfish-realm/src/test/java/net/eisele/security/util/SecurityStoreTest.java new file mode 100644 index 0000000..15a98e6 --- /dev/null +++ b/glassfish-realm/src/test/java/net/eisele/security/util/SecurityStoreTest.java @@ -0,0 +1,77 @@ +package net.eisele.security.util; + +import static org.junit.Assert.*; +import org.junit.Test; + +/** + * A set of test-cases to veryfy that the {@link SecurityStore} is working. + * + * @author eiselem + */ +public class SecurityStoreTest { + + /** + * get username and password from the surefire system properties. + */ + private final static String USER = System.getProperty("user"); + private final static String PASSWORD = System.getProperty("password"); + + @org.junit.BeforeClass + public static void setupUsers() { + System.out.println("addUser testUser1"); + String name = "testUser1"; + String salt = "salt1"; + String password = "password1"; + SecurityStore instance = new SecurityStore(USER, PASSWORD); + instance.addUser(name, salt, password); + + System.out.println("addUser testUser1"); + String name2 = "testUser2"; + String salt2 = "salt2"; + String password2 = "password2"; + + instance.addUser(name2, salt2, password2); + + } + + /** + * Test of getSaltForUser method, of class SecurityStore. + */ + @Test + public void getSaltForUser() { + System.out.println("getSaltForUser"); + String name = "testUser1"; + SecurityStore instance = new SecurityStore(USER, PASSWORD); + String expResult = "salt1"; + String result = instance.getSaltForUser(name); + assertEquals(expResult, result); + } + + /** + * Test of validateUser method, of class SecurityStore. + */ + @Test + public void validateUser() { + System.out.println("validateUser"); + String name = "testUser1"; + String password = "password1"; + SecurityStore instance = new SecurityStore(USER, PASSWORD); + boolean expResult = true; + boolean result = instance.validateUser(name, password); + assertEquals(expResult, result); + } + + /** + * Test of validateUser method, of class SecurityStore. + */ + @Test + public void validateFalseUser() { + System.out.println("validateFalseUser"); + String name = "testUser1"; + String password = "password2"; + SecurityStore instance = new SecurityStore(USER, PASSWORD); + boolean expResult = false; + boolean result = instance.validateUser(name, password); + assertEquals(expResult, result); + } +} diff --git a/glassfish-realm/src/test/java/net/eisele/security/util/UserTest.java b/glassfish-realm/src/test/java/net/eisele/security/util/UserTest.java new file mode 100644 index 0000000..4eb1c36 --- /dev/null +++ b/glassfish-realm/src/test/java/net/eisele/security/util/UserTest.java @@ -0,0 +1,98 @@ +package net.eisele.security.util; + +import java.util.logging.Level; +import java.util.logging.Logger; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +/** + * A set of test-cases to verify that the + * + * @author eiselem + */ +public class UserTest { + + /** + * get username and password from the surefire system properties. + */ + private final static String USER = System.getProperty("user"); + private final static String PASSWORD = System.getProperty("password"); + public static final Logger LOGGER = Logger.getLogger(UserTest.class.getName()); + + /** + * Add the test-users + */ + @org.junit.BeforeClass + public static void addUsers() { + String user1 = "user1"; + String user2 = "user2"; + String passwordStr = "TestPassword"; + Password pwd = new Password(); + + byte[] saltBytes = pwd.getSalt(64); + byte[] passwordBytes = pwd.hashWithSalt(passwordStr, saltBytes); + + String password = pwd.base64FromBytes(passwordBytes); + String salt = pwd.base64FromBytes(saltBytes); + + SecurityStore store = new SecurityStore(USER, PASSWORD); + store.addUser(user1, salt, password); + store.addUser(user2, salt, password); + + LOGGER.log(Level.INFO, "Bytes {0}", passwordBytes); + LOGGER.log(Level.INFO, "String {0}", password); + + } + + /** + * Validate user 1 + */ + @Test + public void validateUser1() { + String user = "user1"; + String passwordStr = "TestPassword"; + SecurityStore store = new SecurityStore(USER, PASSWORD); + String salt = store.getSaltForUser(user); + Password pwd = new Password(); + + // get the byte[] from the salt + byte[] saltBytes = pwd.bytesFrombase64(salt); + // hash password and salt + byte[] passwordBytes = pwd.hashWithSalt(passwordStr, saltBytes); + // Base64 encode to String + String password = pwd.base64FromBytes(passwordBytes); + LOGGER.log(Level.INFO, "PWD Generated {0}", password); + // validate password with the db + boolean validated = store.validateUser(user, password); + assertTrue(validated); + + } + + /** + * Validate Fail Login User 2 + */ + @Test + public void validateFailUser2() { + String user = "user2"; + String passwordStr = "TestPassword2"; + + + SecurityStore store = new SecurityStore(USER, PASSWORD); + String salt = store.getSaltForUser(user); + + Password pwd = new Password(); + + // get the byte[] from the salt + byte[] saltBytes = pwd.bytesFrombase64(salt); + // hash password and salt + byte[] passwordBytes = pwd.hashWithSalt(passwordStr, saltBytes); + // Base64 encode to String + String password = pwd.base64FromBytes(passwordBytes); + LOGGER.log(Level.INFO, "PWD Generated {0}", password); + // validate password with the db + boolean validated = store.validateUser(user, password); + assertFalse(validated); + + } +}