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
+
+
+
+
+
+ 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);
+
+ }
+}