Embedded LDAP server LdapAuth3: support hashed passwords (sha1, sha-256)

Note that none of the currently supported hash are safe against
brute force attacks.

If ldapFilter property does not contain any occurrence of "${password}",
the user DN found via the filter expression will be authenticated
via a regular LDAP bind operation, using the entered password. In this
case, with embedded LDAP server, but probably also with others, hashed
passwords are supported and make sense.

Change-Id: I725832a2faa484623edcebeeeb727b23b6186de2
diff --git a/README.md b/README.md
index 6769627..54ad3bd 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,9 @@
 ldap.config = path-to-ldap-config
 ```
 
-To authenticate and authorize users, the ldap filter expression specified in `searchFilter` is used. Note that within this expression all occurrences of the placeholders `${login}` and `${password}` are replaced with the name and password the user has entered for logging in.
+To find, authenticate and authorize users, the ldap filter expression specified in `searchFilter` is used. Within this expression all occurrences of the placeholders `${login}` and `${password}` are replaced with the name and password the user has entered for logging in.
+
+If `searchFilter` does not contain any occurrence of `${password}` the user DN found via the filter expression will be authenticated via a regular LDAP bind operation, using the entered password. In this case, depending on the LDAP server, also hashed passwords are supported.
 
 ###### Example ldap config file
 ```properties
@@ -102,6 +104,16 @@
 
 Note that currently the embedded server ignores the `ldapHost` and `ldapS` settings, and only listens on the `localhost` interface. The `ldapPort` setting, on the other hand, is used.
 
+The embedded server currently supports the following password encodings:
+
+* clear passwords – prefix `{CLEAR}` or empty
+* hex – prefix `{HEX}`
+* base64 – prefix  `{BASE64}`
+* SHA1 – prefix `{SHA}`
+* SHA-256 – prefix `{SHA256}`
+
+Note that none of these are safe against brute force attacks.
+
 ###### Example users.ldif
 
 ```ldif
@@ -120,6 +132,12 @@
 uid: user
 mail: user@example.com
 userPassword: cGFzc3dvcmQ=
+
+dn: uid=user3,ou=people,dc=example,dc=com
+cn: user3
+uid: user3
+mail: user3@example.com
+userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
 ```
 
 ### Setting BasicAuthentication for Testing
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
index bdfb2ad..9975fcc 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
@@ -83,16 +83,16 @@
             }
         }
 
-        SearchResult srchRes = search(login, password, ldapConfig);
+        SearchResult srchRes = search(login, password, ldapConfig, !ldapConfig.searchFilter.contains("${password}"));
 
         if (srchRes == null || srchRes.getEntryCount() == 0) {
             if (DEBUGLOG) System.out.printf("Finding '%s': no entry found!\n", login);
             return LDAP_AUTH_RNAUTH;
         }
+
         return LDAP_AUTH_ROK;
     }
-
-    public static SearchResult search(String login, String password, LDAPConfig ldapConfig) throws LDAPException {
+    public static SearchResult search(String login, String password, LDAPConfig ldapConfig, boolean bindWithFoundDN) throws LDAPException {
         Map<String, String> valuesMap = new HashMap<>();
         valuesMap.put("login", login);
         valuesMap.put("password", password);
@@ -174,16 +174,32 @@
 
             if (DEBUGLOG) System.out.printf("Finding '%s': %d entries.\n", login, srchRes.getEntryCount());
         } catch (LDAPSearchException e) {
-            System.err.printf("Error: login: Search for User failed: '%s'!\n", e);
+            System.err.printf("Error: Search for User failed: '%s'!\n", e);
             ldapTerminate(lc);
             return null;
         }
 
-        if (srchRes.getEntryCount() == 0) {
+        if (srchRes == null || srchRes.getEntryCount() == 0) {
             if (DEBUGLOG) System.out.printf("Finding '%s': no entry found!\n", login);
+            ldapTerminate(lc);
             return null;
         }
 
+        if (bindWithFoundDN) {
+            String matchedDN = srchRes.getSearchEntries().get(0).getDN();
+            if (DEBUGLOG) System.out.printf("Requested bind for found user %s' failed.\n", matchedDN);
+            try {
+                // bind to server:
+                if (DEBUGLOG) System.out.printf("Binding with '%s' ...\n", matchedDN);
+                lc.bind(matchedDN, password);
+                if (DEBUGLOG) System.out.print("Binding: OK.\n");
+            } catch (LDAPException e) {
+                System.err.printf("Error: login: Binding failed: '%s'!\n", e);
+                ldapTerminate(lc);
+                return null;
+            }
+        }
+
         ldapTerminate(lc);
         return srchRes;
     }
@@ -193,7 +209,7 @@
         LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
         final String emailAttribute = ldapConfig.emailAttribute;
 
-        SearchResult searchResult = search(sUserDN, sUserPwd, ldapConfig);
+        SearchResult searchResult = search(sUserDN, sUserPwd, ldapConfig, false);
 
         if (searchResult == null) {
             return null;
diff --git a/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java b/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
index 76f6494..88dfe2e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
+++ b/full/src/main/java/de/ids_mannheim/korap/server/EmbeddedLdapServer.java
@@ -1,9 +1,8 @@
 package de.ids_mannheim.korap.server;
 
-import com.unboundid.ldap.listener.InMemoryDirectoryServer;
-import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
-import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.listener.*;
 import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.util.CryptoHelper;
 import com.unboundid.util.ssl.SSLUtil;
 import com.unboundid.util.ssl.TrustAllTrustManager;
 import de.ids_mannheim.korap.authentication.LDAPConfig;
@@ -11,6 +10,7 @@
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
 
 public class EmbeddedLdapServer {
     static InMemoryDirectoryServer server;
@@ -21,6 +21,9 @@
         }
 
         InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(ldapConfig.searchBase);
+        final MessageDigest sha1Digest = CryptoHelper.getMessageDigest("SHA1");
+        final MessageDigest sha256Digest = CryptoHelper.getMessageDigest("SHA-256");
+        config.setPasswordEncoders(new ClearInMemoryPasswordEncoder("{CLEAR}", null), new ClearInMemoryPasswordEncoder("{HEX}", HexPasswordEncoderOutputFormatter.getLowercaseInstance()), new ClearInMemoryPasswordEncoder("{BASE64}", Base64PasswordEncoderOutputFormatter.getInstance()), new UnsaltedMessageDigestInMemoryPasswordEncoder("{SHA}", Base64PasswordEncoderOutputFormatter.getInstance(), sha1Digest), new UnsaltedMessageDigestInMemoryPasswordEncoder("{SHA256}", Base64PasswordEncoderOutputFormatter.getInstance(), sha256Digest));
         config.addAdditionalBindCredentials(ldapConfig.sLoginDN, ldapConfig.sPwd);
         config.setSchema(null);
 
diff --git a/full/src/main/resources/embedded-ldap-default.conf b/full/src/main/resources/embedded-ldap-default.conf
index becf6e0..00cd2a2 100644
--- a/full/src/main/resources/embedded-ldap-default.conf
+++ b/full/src/main/resources/embedded-ldap-default.conf
@@ -5,6 +5,6 @@
 searchBase=dc=example,dc=com
 sLoginDN=cn=admin,dc=example,dc=com
 pwd=admin
-searchFilter=(&(uid=${login})(userPassword=${password}))
+searchFilter=(uid=${login})
 useEmbeddedServer=true
 ldifFile=src/main/resources/korap-users.ldif
diff --git a/full/src/main/resources/korap-users.ldif b/full/src/main/resources/korap-users.ldif
index 4a3e69c..8760df9 100644
--- a/full/src/main/resources/korap-users.ldif
+++ b/full/src/main/resources/korap-users.ldif
@@ -12,16 +12,35 @@
 cn: user
 uid: user
 mail: user@example.com
-userPassword: cGFzc3dvcmQ=
+userPassword: {BASE64}cGFzc3dvcmQ=
 
 dn: uid=user1,ou=people,dc=example,dc=com
 cn: user1
 uid: user1
 mail: user1@example.com
-userPassword: password1
+userPassword: {CLEAR}password1
 
 dn: uid=user2,ou=people,dc=example,dc=com
 cn: user2
 uid: user2
 mail: user2@example.com
 userPassword: password2
+
+dn: uid=user3,ou=people,dc=example,dc=com
+cn: user3
+uid: user3
+mail: user3@example.com
+userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
+
+dn: uid=user4,ou=people,dc=example,dc=com
+cn: user4
+uid: user4
+mail: user4@example.com
+userPassword: {SHA256}uXhzpA9zq+3Y1oWnzV5fheSpz7g+rCaIZkCggThQEis=
+
+dn: uid=user5,ou=people,dc=example,dc=com
+cn: user5
+uid: user5
+mail: user5@example.com
+userPassword: {PBKDF2-SHA256}26PFrg++/nI8YOiHum5MyAMp0HdqKMNOcLpY5RuO2bY=
+
diff --git a/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java b/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java
index 7783274..196451e 100644
--- a/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/server/EmbeddedLdapServerTest.java
@@ -2,13 +2,17 @@
 
 import com.unboundid.ldap.sdk.LDAPException;
 import com.unboundid.util.Base64;
-import com.unboundid.util.StaticUtils;
 import de.ids_mannheim.korap.authentication.LdapAuth3;
 import org.junit.AfterClass;
 import org.junit.Test;
 
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
 import java.net.UnknownHostException;
 import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
 
 import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_RNAUTH;
 import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_ROK;
@@ -16,7 +20,7 @@
 
 public class EmbeddedLdapServerTest {
 
-    public static final String EMBEDDED_LDAP_DEFAULT_CONF = "src/main/resources/embedded-ldap-default.conf";
+    public static final String TEST_EMBEDDED_LDAP_CONF = "src/test/resources/test-embedded-ldap.conf";
 
     @AfterClass
     public static void shutdownEmbeddedLdapServer() {
@@ -25,36 +29,61 @@
 
     @Test
     public void embeddedServerStartsAutomaticallyAndUsersCanLogin() throws LDAPException {
-        final byte[] passwordBytes = StaticUtils.getBytes("password");
-        String pw = Base64.encode(passwordBytes);
-
-        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user", pw, EMBEDDED_LDAP_DEFAULT_CONF));
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user", "password", TEST_EMBEDDED_LDAP_CONF));
     }
 
     @Test
-    public void usersWithUnencodedPasswowrdCanLogin() throws LDAPException {
-        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user1", "password1", EMBEDDED_LDAP_DEFAULT_CONF));
+    public void usersWithClearPasswordCanLogin() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user1", "password1", TEST_EMBEDDED_LDAP_CONF));
+    }
+
+    @Test
+    public void usersWithSHA1PasswordCanLogin() throws LDAPException, NoSuchAlgorithmException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user3", "password3", TEST_EMBEDDED_LDAP_CONF));
+    }
+
+    @Test
+    public void usersWithSHA256PasswordCanLogin() throws LDAPException, NoSuchAlgorithmException, InvalidKeySpecException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user4", "password4", TEST_EMBEDDED_LDAP_CONF));
     }
 
     @Test
     public void asteriskPasswordsFail() throws LDAPException {
-        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("user1", "*", EMBEDDED_LDAP_DEFAULT_CONF));
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("user1", "*", TEST_EMBEDDED_LDAP_CONF));
+    }
+
+    @Test
+    public void loginWithPreencodedPBKDF2Password() throws LDAPException, NoSuchAlgorithmException, InvalidKeySpecException {
+        byte[] salt = new byte[32];
+        KeySpec spec = new PBEKeySpec("password5".toCharArray(), salt, 65536, 256);
+        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2withHmacSHA256");
+        byte[] hash = f.generateSecret(spec).getEncoded();
+
+        final String pbkdf2sha256Password = "{PBKDF2-SHA256}" + Base64.encode(hash);
+        System.out.println(pbkdf2sha256Password);
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("user5", pbkdf2sha256Password, TEST_EMBEDDED_LDAP_CONF));
+    }
+
+    @Test
+    public void loginWithUnEncodedPBKDF2PasswordFails() throws LDAPException, NoSuchAlgorithmException, InvalidKeySpecException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("user5", "password5", TEST_EMBEDDED_LDAP_CONF));
     }
 
     @Test
     public void unauthorizedUsersAreNotAllowed() throws LDAPException {
-        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("yuser", "password", EMBEDDED_LDAP_DEFAULT_CONF));
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("yuser", "password", TEST_EMBEDDED_LDAP_CONF));
     }
 
     @Test
     public void gettingMailForUser() throws LDAPException, UnknownHostException, GeneralSecurityException {
-        EmbeddedLdapServer.startIfNotRunning(EMBEDDED_LDAP_DEFAULT_CONF);
-        assertEquals("user2@example.com", LdapAuth3.getEmail("user2", EMBEDDED_LDAP_DEFAULT_CONF));
+        EmbeddedLdapServer.startIfNotRunning(TEST_EMBEDDED_LDAP_CONF);
+        assertEquals("user2@example.com", LdapAuth3.getEmail("user2", TEST_EMBEDDED_LDAP_CONF));
     }
 
     @Test
-    public void gettingMailForUnknownUserIsNull() throws LDAPException, UnknownHostException, GeneralSecurityException {
-        EmbeddedLdapServer.startIfNotRunning(EMBEDDED_LDAP_DEFAULT_CONF);
-        assertEquals(null, LdapAuth3.getEmail("user1000", EMBEDDED_LDAP_DEFAULT_CONF));
+    public void gettingMailFoRNAUTHUserIsNull() throws LDAPException, UnknownHostException, GeneralSecurityException {
+        EmbeddedLdapServer.startIfNotRunning(TEST_EMBEDDED_LDAP_CONF);
+        assertEquals(null, LdapAuth3.getEmail("user1000", TEST_EMBEDDED_LDAP_CONF));
     }
+
 }
diff --git a/full/src/test/resources/test-embedded-ldap-users.ldif b/full/src/test/resources/test-embedded-ldap-users.ldif
new file mode 100644
index 0000000..8760df9
--- /dev/null
+++ b/full/src/test/resources/test-embedded-ldap-users.ldif
@@ -0,0 +1,46 @@
+dn: dc=example,dc=com
+dc: example
+ou: people
+objectClass: dcObject
+objectClass: organizationalUnit
+
+dn: ou=people,dc=example,dc=com
+ou: people
+objectClass: organizationalUnit
+
+dn: uid=user,ou=people,dc=example,dc=com
+cn: user
+uid: user
+mail: user@example.com
+userPassword: {BASE64}cGFzc3dvcmQ=
+
+dn: uid=user1,ou=people,dc=example,dc=com
+cn: user1
+uid: user1
+mail: user1@example.com
+userPassword: {CLEAR}password1
+
+dn: uid=user2,ou=people,dc=example,dc=com
+cn: user2
+uid: user2
+mail: user2@example.com
+userPassword: password2
+
+dn: uid=user3,ou=people,dc=example,dc=com
+cn: user3
+uid: user3
+mail: user3@example.com
+userPassword: {SHA}ERnP037iRzV+A0oI2ETuol9v0g8=
+
+dn: uid=user4,ou=people,dc=example,dc=com
+cn: user4
+uid: user4
+mail: user4@example.com
+userPassword: {SHA256}uXhzpA9zq+3Y1oWnzV5fheSpz7g+rCaIZkCggThQEis=
+
+dn: uid=user5,ou=people,dc=example,dc=com
+cn: user5
+uid: user5
+mail: user5@example.com
+userPassword: {PBKDF2-SHA256}26PFrg++/nI8YOiHum5MyAMp0HdqKMNOcLpY5RuO2bY=
+
diff --git a/full/src/test/resources/test-embedded-ldap.conf b/full/src/test/resources/test-embedded-ldap.conf
new file mode 100644
index 0000000..fb9e079
--- /dev/null
+++ b/full/src/test/resources/test-embedded-ldap.conf
@@ -0,0 +1,10 @@
+# default and sample configuration for an automatically starting
+# embedded LDAP server
+host=localhost
+port=3267
+searchBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=admin
+searchFilter=(uid=${login})
+useEmbeddedServer=true
+ldifFile=src/test/resources/test-embedded-ldap-users.ldif