Commonize & simplify LDAP and LDAPS auth

This makes it easy for further instances and projects
outside the IDS to use Kustvakt-full with their own LDAP.

Change-Id: I710f50079348d6cff9fd33376aebda33bc9f408e
diff --git a/README.md b/README.md
index 5d03dd9..686db0a 100644
--- a/README.md
+++ b/README.md
@@ -66,9 +66,27 @@
 
 ### Setting LDAP
 
-Set the location of the LDAP configuration file for Kustvakt full version. The file should contain an admin password to access an LDAP system. Without LDAP, user authentication functions and services cannot be used. Authentication mechanism can be extended by implementing other authentication methods e.g. using a database. 
+Set the location of the LDAP configuration file for Kustvakt full version. The file must contain all necessary information to access the LDAP system and to authenticate and authorize users (see example LDAP config below).
 
-	ldap.config = path-to-ldap-password
+```properties
+ldap.config = path-to-ldap-config
+```
+
+To authenticate and authorize users, the ldap filter expression specified in `ldapFilter` is used. Note that within this expression all occurrences of the placeholders `${username}` and `${password}` are replaced with the name and password the user has entered for logging in.
+
+###### Example ldap config file
+```properties
+ldapHost=ldap.example.org
+# use LDAP over SSL (LDAPS) if the server supports it
+ldapS=true
+ldapPort=636
+# to trust all certs, leave trustStore empty
+trustStore=truststore.jks
+ldapBase=dc=example,dc=org
+sLoginDN=cn=admin,dc=example,dc=org
+pwd=adminpassword
+ldapFilter=(&(&(uid=${username})(userPassword=${password}))(signedeula=TRUE))
+```
 
 ### Setting BasicAuthentication for Testing
 
diff --git a/core/pom.xml b/core/pom.xml
index 3e24e61..2f45fef 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -494,5 +494,11 @@
 			<artifactId>javax.servlet-api</artifactId>
 			<version>4.0.1</version>
 		</dependency>
-	</dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-text</artifactId>
+      <version>1.9</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
 </project>
diff --git a/full/pom.xml b/full/pom.xml
index 4b982be..5b2ecfb 100644
--- a/full/pom.xml
+++ b/full/pom.xml
@@ -86,6 +86,9 @@
 					<include>**/*.info</include>
 					<include>**/*.properties</include>
 					<include>**/*.json</include>
+					<include>**/*.p12</include>
+					<include>**/*.jks</include>
+					<include>**/*.ldif</include>
 				</includes>
 			</testResource>
 		</testResources>
@@ -134,11 +137,16 @@
 					<argLine>-Xmx512m -XX:MaxPermSize=256m
 						-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager</argLine>
 					<excludes>
-						<exclude>de/ids_mannheim/korap/authentication/*.java</exclude>
+						<exclude>de/ids_mannheim/korap/authentication/APIAuthenticationTest.java</exclude>
+						<exclude>de/ids_mannheim/korap/authentication/AuthenticationFilterTest.java</exclude>
+						<exclude>de/ids_mannheim/korap/authentication/Kustvakt*.java</exclude>
+						<exclude>de/ids_mannheim/korap/authentication/LdapTest.java</exclude>
+						<exclude>de/ids_mannheim/korap/authentication/Random*.java</exclude>
 						<exclude>de/ids_mannheim/korap/web/controller/TokenExpiryTest.java</exclude>
 					</excludes>
 					<includes>
 						<include>de/ids_mannheim/korap/**/*.java</include>
+						<include>de/ids_mannheim/korap/authentication/LdapAuth3Test.java</include>
 					</includes>
 				</configuration>
 			</plugin>
@@ -284,5 +292,13 @@
 		    <version>5.13.2</version>
 		    <scope>test</scope>
 		</dependency>
+
+		<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-text</artifactId>
+			<version>1.9</version>
+		</dependency>
+
 	</dependencies>
 </project>
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 810d22b..53c8a1b 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
@@ -1,511 +1,221 @@
-/* - Klasse zum Implementieren einer Benutzer-Authentifikation mittels LDAP
- *   in der IDM-Datenbank (Identit�tsmanagement) von Eric Seubert, IDS.
- * - externe Bibliothek ist Novel JLDAP.
- * 27.01.17/FB
- *
- * Sourcen:
- * - https://www.novell.com/documentation/developer/samplecode/jldap_sample/VerifyPassword.java.html
- * - https://www.novell.com/documentation/developer/samplecode/jldap_sample/LDAPOIDs.java.html
- * - https://www.novell.com/documentation/developer/jldap/jldapenu/data/a90352e.html
- * WICHTIG:
- * - Novell-Bibliothek liefert 0 Treffer, wenn man nacheinander sucht!
- *   Grund daf�r nicht gefunden.
- *
- * Version von unboundID - 19.04.17/FB
- *
- * UnboundID LDAP SDK For Java � 3.2.1
- * The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for 
- * communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, 
- * encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package 
- * contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for 
- * communicating with LDAPv3 directory servers. 
- * TODO:
- * - gesichertes Login mit gesch�tztem Passwort.
- * - Passwort des Admin verschl�sseln.
+/*
+ *   user authentication via LDAP
  */
- 
+
 package de.ids_mannheim.korap.authentication;
 
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.Enumeration;
-import java.util.Properties;
-
 import com.nimbusds.jose.JOSEException;
-import com.unboundid.ldap.sdk.Attribute;
-import com.unboundid.ldap.sdk.LDAPConnection;
-import com.unboundid.ldap.sdk.LDAPException;
-import com.unboundid.ldap.sdk.LDAPSearchException;
-import com.unboundid.ldap.sdk.SearchResult;
-import com.unboundid.ldap.sdk.SearchResultEntry;
-import com.unboundid.ldap.sdk.SearchScope;
-
+import com.unboundid.ldap.sdk.*;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+import com.unboundid.util.ssl.TrustStoreTrustManager;
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.constant.TokenType;
+import org.apache.commons.text.StringSubstitutor;
+
+import javax.net.ssl.SSLSocketFactory;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
 
 
 /**
- * LDAP Login Tests
- * 
- * @author bodmer, margaretha
+ * LDAP Login
+ *
+ * @author bodmer, margaretha, kupietz
  * @see APIAuthentication
  */
 public class LdapAuth3 extends APIAuthentication {
 
-    /* For SSL Connection to LDAP, see: https://www.novell.com/documentation/developer/jldap/jldapenu/data/cchcbejj.html.
-	 * and use DEFAULT_SSL_PORT.
-     * For now, plain text connection is used.
-	 * FB
-	 */
-	final static Boolean DEBUGLOG 	= false;		// log debug output.
-	final static String attC2 		= "idsC2";		// if value == TRUE: registered for COSMAS II (KorAP) Service.
-	final static String attStatus	= "idsStatus";	// value must be 0..2, 3 = locked account.
-	final static String attEmail	= "mail";		// attribute whose value is the requested email.
-	final static int ldapPort 		= 389; 			//LDAPConnection.DEFAULT_PORT;
-	// final static int ldapVersion	= LDAPConnection.LDAP_V3;
-	final static String ldapHost 	= "ldap.ids-mannheim.de";
-	final static String ldapBase	= "dc=ids-mannheim,dc=de";
-	final static String sLoginDN 	= "cn=casaling,dc=ids-mannheim,dc=de";
-	static String sPwd 				= null;
+    public static final int LDAP_AUTH_ROK = 0;
+    public static final int LDAP_AUTH_RCONNECT = 1; // cannot connect to LDAP Server
+    public static final int LDAP_AUTH_RINTERR = 2; // internal error: cannot verify User+Pwd.
+    /* cannot be distinguished, currently
+    public static final int LDAP_AUTH_RUNKNOWN = 3; // User Account or Pwd unknown;
+    public static final int LDAP_AUTH_RLOCKED = 4; // User Account locked;
+    public static final int LDAP_AUTH_RNOTREG = 5; // User known, but has not registered to KorAP/C2 Service yet;
+     */
+    public static final int LDAP_AUTH_RNOEMAIL = 6; // cannot obtain email for sUserDN
+    public static final int LDAP_AUTH_RNAUTH = 7; // User Account or Pwd unknown, or not authorized
+    final static Boolean DEBUGLOG = false;        // log debug output.
 
-	/**
-	 * return codes for functions of this class:
-	 */
-
-	public static final int LDAP_AUTH_ROK		= 0; 
-	public static final int LDAP_AUTH_RCONNECT	= 1; // cannot connect to LDAP Server
-	public static final int LDAP_AUTH_RINTERR	= 2; // internal error: cannot verify User+Pwd.
-	public static final int LDAP_AUTH_RUNKNOWN	= 3; // User Account or Pwd unknown;
-	public static final int LDAP_AUTH_RLOCKED	= 4; // User Account locked;
-	public static final int LDAP_AUTH_RNOTREG	= 5; // User known, but has not registered to KorAP/C2 Service yet;
-	public static final int LDAP_AUTH_RNOEMAIL	= 6; // cannot obtain email for sUserDN.
-
-    public LdapAuth3 (FullConfiguration config) throws JOSEException {
+    public LdapAuth3(FullConfiguration config) throws JOSEException {
         super(config);
-    }	
-    
-	
-	@Override
-	public TokenType getTokenType () {
-	    return TokenType.API;
-	}
-	 
-	/**
-	 * getErrMessage:
-	 * returns String Message for LDAP_AUTH_Rxxx code.
-	 * @date 20.04.17/FB
-	 * @param code
-	 * @return Message in string form.
-	 */
-	public static String getErrMessage(int code)
-	
-	{
-	switch(code)
-		{
-	case LDAP_AUTH_ROK: 
-		return "LDAP Authentication successfull.";
-	case LDAP_AUTH_RCONNECT:
-		return "LDAP Authentication: connecting to LDAP Server failed!";
-	case LDAP_AUTH_RINTERR: 
-		return "LDAP Authentication failed due to an internal error!";
-	case LDAP_AUTH_RUNKNOWN:
-		return "LDAP Authentication failed due to unknown user or password!";
-	case LDAP_AUTH_RLOCKED:
-		return "LDAP Authentication: known user is locked!";
-	case LDAP_AUTH_RNOTREG:
-		return "LDAP Authentication: known user has not registered yet for COSMAS II/KorAP!";
-	case LDAP_AUTH_RNOEMAIL:
-		return "LDAP Authentication: known user, but cannot obtain email!";
-	default:
-		return "LDAP Authentication failed with unknown error code!";
-		}
-	} // getErrMessage
-	
-	/**
-	 * ldapCode2StatusCode:
-	 * - converts a LDAP_AUTH_xxx Error Code to an Error Code of StatusCode.java.
-	 * @param base : Base value inside of StatusCode.java reserved for LDAP_AUTH Error Codes.
-	 * @param ldapErrCode : the LDAP_AUTH Error code
-	 * @return the StatusCode in the range reserved for LDAP_AUTH Errors.
-	 * @date 21.04.17/FB
-	 */
-	public int ldapCode2StatusCode(int base, int ldapErrCode)
-	
-	{
-	return base + ldapErrCode;	
-	} // ldapCode2StatusCode
-	
-	/*
-	 *  load properties for LDAP Handling.
-	 *  17.02.17/FB
-	 */
-	
-	static String loadProp(String sConfFile) throws IOException
-	
-	{
-		String sPwd = null;
-		FileInputStream in;
-		Properties prop;
-		
+    }
+
+    public static String getErrMessage(int code) {
+        switch (code) {
+            case LDAP_AUTH_ROK:
+                return "LDAP Authentication successful.";
+            case LDAP_AUTH_RCONNECT:
+                return "LDAP Authentication: connecting to LDAP Server failed!";
+            case LDAP_AUTH_RINTERR:
+                return "LDAP Authentication failed due to an internal error!";
+/* cannot be distinguished, currently
+            case LDAP_AUTH_RUNKNOWN:
+                return "LDAP Authentication failed due to unknown user or password!";
+            case LDAP_AUTH_RLOCKED:
+                return "LDAP Authentication: known user is locked!";
+            case LDAP_AUTH_RNOTREG:
+                return "LDAP Authentication: known user has not registered yet!";
+*/
+            case LDAP_AUTH_RNOEMAIL:
+                return "LDAP Authentication: known user, but cannot obtain email!";
+            case LDAP_AUTH_RNAUTH:
+                return "LDAP Authentication: unknown user or password, or user is locked or not authorized!";
+            default:
+                return "LDAP Authentication failed with unknown error code!";
+        }
+    }
+
+    static HashMap<String, String> typeCastConvert(Properties prop) {
+        Map<String, String> step2 = (Map<String, String>) (Map) prop;
+        return new HashMap<>(step2);
+    }
+
+    static HashMap<String, String> loadProp(String sConfFile) throws IOException {
+        FileInputStream in;
+        Properties prop;
+
         try {
             in = new FileInputStream(sConfFile);
-            }
-        catch( IOException ex )
-            {
-        	System.err.printf("Error: LDAP.loadProp: cannot load Property file '%s'!\n", sConfFile); 
+        } catch (IOException ex) {
+            System.err.printf("Error: LDAP.loadProp: cannot load Property file '%s'!\n", sConfFile);
             ex.printStackTrace();
             return null;
-            }
+        }
 
-        if( DEBUGLOG ) System.out.println("Debug: loaded: " + sConfFile);
-	    
+        if (DEBUGLOG) System.out.println("Debug: loaded: " + sConfFile);
+
         prop = new Properties();
-        Enumeration<?> e;
-        
+
         try {
             prop.load(in);
-            e = prop.propertyNames();
+            return typeCastConvert(prop);
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
 
-            while( e.hasMoreElements() )
-                {
-                String key = (String)e.nextElement();
-                String val = prop.getProperty(key);
-                if( key.compareTo("pwd") == 0 )
-                	return val; 
-                
-                //System.out.println("Property '" + key + "' = '" + val + "'.");
+        return new HashMap<>();
+    }
+
+    public static int login(String sUserDN, String sUserPwd, String ldapConfigFilename) throws LDAPException {
+
+        sUserDN = Filter.encodeValue(sUserDN);
+        sUserPwd = Filter.encodeValue(sUserPwd);
+
+        Map<String, String> ldapConfig;
+        try {
+            ldapConfig = loadProp(ldapConfigFilename);
+        } catch (IOException e) {
+            System.out.println("Error: LDAPAuth.login: cannot load Property file!");
+            return LDAP_AUTH_RINTERR;
+        }
+
+        final Boolean ldapS = Boolean.parseBoolean(ldapConfig.getOrDefault("ldapS", "false"));
+        final String ldapHost = ldapConfig.getOrDefault("ldapHost", "localhost");
+        final int ldapPort = Integer.parseInt(ldapConfig.getOrDefault("ldapPort", (ldapS ? "636" : "389")));
+        final String ldapBase = ldapConfig.getOrDefault("ldapBase", "dc=example,dc=com");
+        final String sLoginDN = ldapConfig.getOrDefault("sLoginDN", "cn=admin,dc=example,dc=com");
+        final String ldapFilter = ldapConfig.getOrDefault("ldapFilter", "(&(|(&(mail=${username})(idsC2Password=${password}))(&(idsC2Profile=${username})(idsC2Password=${password})))(&(idsC2=TRUE)(|(idsStatus=1)(|(idsStatus=0)(xidsStatus=\00)))))");
+        final String sPwd = ldapConfig.getOrDefault("pwd", "");
+        final String trustStorePath = ldapConfig.getOrDefault("trustStore", null);
+
+        Map<String, String> valuesMap = new HashMap<>();
+        valuesMap.put("username", sUserDN);
+        valuesMap.put("password", sUserPwd);
+        StringSubstitutor sub = new StringSubstitutor(valuesMap);
+        String ldapFilterInstance = sub.replace(ldapFilter);
+
+        if (DEBUGLOG) {
+            //System.out.printf("LDAP Version      = %d.\n", LDAPConnection.LDAP_V3);
+            System.out.printf("LDAP Host & Port  = '%s':%d.\n", ldapHost, ldapPort);
+            System.out.printf("Login User = '%s'\n", sUserDN);
+        }
+
+        // LDAP Connection:
+        if (DEBUGLOG) System.out.println("LDAPS " + ldapS);
+
+        LDAPConnection lc = null;
+
+        if (ldapS) {
+            try {
+                SSLUtil sslUtil;
+                if (trustStorePath != null && !trustStorePath.isEmpty()) {
+                    sslUtil = new SSLUtil(new TrustStoreTrustManager(trustStorePath));
+                } else {
+                    sslUtil = new SSLUtil(new TrustAllTrustManager());
                 }
-             }
-          catch( IOException ex )
-             {
-             ex.printStackTrace();
-             }
+                SSLSocketFactory socketFactory = sslUtil.createSSLSocketFactory();
+                lc = new LDAPConnection(socketFactory, ldapHost, ldapPort);
+            } catch (GeneralSecurityException e) {
+                System.err.printf("Error: login: Connecting to LDAPS Server: failed: '%s'!\n", e);
+                return ldapTerminate(lc, LDAP_AUTH_RCONNECT);
+            }
+        } else {
+            lc = new LDAPConnection();
+            try {
+                lc.connect(ldapHost, ldapPort);
+                if (DEBUGLOG && ldapS) System.out.println("LDAPS Connection = OK\n");
+                if (DEBUGLOG && !ldapS) System.out.println("LDAP Connection = OK\n");
+            } catch (LDAPException e) {
+                System.err.printf("Error: login: Connecting to LDAP Server: failed: '%s'!\n", e);
+                return ldapTerminate(lc, LDAP_AUTH_RCONNECT);
+            }
+        }
 
-		return sPwd;
 
-	} // loadProp
+        if (DEBUGLOG) System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
 
-	/**
-	 * ldapLogin
-	 * Arguments:
-	 * sUserDN  : either COSMAS II specific Account Name or IDS wide (IDM) account name;
-	 * sUserPwd : either COSMAS II specific Password     or IDS wide (IDM) password;
-	 * return   : 0 = OK, User Account + Pwd are OK, no restrictions;
-	 *            1 = internal error: cannot verify User+Pwd;
-	 *            2 = User Account or Pwd unknown;
-	 *            3 = User Account locked;
-	 *            4 = User known, but has not registered to KorAP/C2 Service yet;
-	 * LDAP Attributes that are checked (definition by Eric Seubert, 02.02.17):
-	 *  idsC2 = TRUE  -> Zugang zu C2 (registriert und zugelassen)
-	 *  idsC2 = FALSE (bzw Attribut nicht vorhanden) 
-	 *		            -> kein Zugang zu C2 (nicht zugelassen, egal ob registriert oder nicht)
-	 *
-	 *	idsStatus = 0 -> Nutzerkennung OK;
-	 *	idsStatus = 1 -> Nutzer ist kein aktiver IDS-Mitarbeiter
-	 *  idsStatus = 3 -> Nutzer ist LDAP-weit gesperrt
-	 */
+        try {
+            // bind to server:
+            if (DEBUGLOG) System.out.printf("Binding with '%s' ...\n", sLoginDN);
+            lc.bind(sLoginDN, sPwd);
+            if (DEBUGLOG) System.out.printf("Binding: OK.\n");
+        } catch (LDAPException e) {
+            System.err.printf("Error: login: Binding failed: '%s'!\n", e);
+            return ldapTerminate(lc, LDAP_AUTH_RINTERR);
+        }
 
-	public static int login(String sUserDN, String sUserPwd, String ldapConfig) throws LDAPException
+        if (DEBUGLOG) System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
 
-	{
-	String sUserC2DN	= sUserDN;
-	String sUserC2Pwd	= sUserPwd;
+        if (DEBUGLOG) System.out.printf("Finding user '%s'...\n", sUserDN);
 
-	/* login with e-mail - 15.09.21/FB:
-	 */
-	String ldapFilter = String.format("(|(&(mail=%s)(userPassword=%s))(&(uid=%s)(userPassword=%s))(&(idsC2Profile=%s)(idsC2Password=%s)))",
-			sUserDN, sUserPwd, sUserDN, sUserPwd, sUserC2DN, sUserC2Pwd);
-	/* without e-mail login:
-	 * String ldapFilter = String.format("(|(&(uid=%s)(userPassword=%s))(&(idsC2Profile=%s)(idsC2Password=%s)))",
-												 sUserDN, sUserPwd, sUserC2DN, sUserC2Pwd);
-	 */
-	SearchResult srchRes = null;
+        SearchResult srchRes;
+        try {
+            // SCOPE_SUB = Scope Subtree.
+            if (DEBUGLOG) System.out.printf("Finding Filter: '%s'.\n", ldapFilterInstance);
 
-	try{
-		sPwd = loadProp(ldapConfig);
-		}
-	catch( IOException e )
-		{
-		System.out.println("Error: LDAPAuth.login: cannot load Property file!");
-		return LDAP_AUTH_RINTERR;
-		}
-															
-	if( DEBUGLOG )
-		{
-		//System.out.printf("LDAP Version      = %d.\n", LDAPConnection.LDAP_V3);
-		System.out.printf("LDAP Host & Port  = '%s':%d.\n", ldapHost, ldapPort);
-		System.out.printf("Login User & Pwd  = '%s' + '%s'\n", sUserDN, sUserPwd);
-		}
+            srchRes = lc.search(ldapBase, SearchScope.SUB, ldapFilterInstance);
 
-	// LDAP Connection:
-	if( DEBUGLOG ) System.out.println("");
+            if (DEBUGLOG) System.out.printf("Finding '%s': %d entries.\n", sUserDN, srchRes.getEntryCount());
+        } catch (LDAPSearchException e) {
+            System.err.printf("Error: login: Search for User failed: '%s'!\n", e);
+            return ldapTerminate(lc, LDAP_AUTH_RNAUTH);
+        }
 
-	LDAPConnection lc = new LDAPConnection();
-	try {
-		// connect to LDAP Server:
-		lc.connect(ldapHost, ldapPort);
-		if( DEBUGLOG ) System.out.println("LDAP Connection = OK\n");
-		}
-	catch( LDAPException e) 	
-		{
-		System.err.printf("Error: login: Connecting to LDAP Server: failed: '%s'!\n", e.toString());
-		return ldapTerminate(lc, LDAP_AUTH_RCONNECT);
-		}
+        if (srchRes.getEntryCount() == 0) {
+            if (DEBUGLOG) System.out.printf("Finding '%s': no entry found!\n", sUserDN);
+            return ldapTerminate(lc, LDAP_AUTH_RNAUTH);
+        }
 
-	if( DEBUGLOG ) 
-		System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
-	
-	try {
-		// bind to server:
-		if( DEBUGLOG ) System.out.printf("Binding with '%s' + '%s'...\n", sLoginDN, sPwd);
-		lc.bind(sLoginDN, sPwd);
-		if( DEBUGLOG ) System.out.printf("Binding: OK.\n");
-		}
-	catch( LDAPException e )
-		{
-		System.err.printf("Error: login: Binding failed: '%s'!\n", e.toString());
-		return ldapTerminate(lc, LDAP_AUTH_RINTERR);
-		}
+        return ldapTerminate(lc, LDAP_AUTH_ROK); // OK.
+    }
 
-	if( DEBUGLOG ) 
-		System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
-		
-	if( DEBUGLOG ) System.out.printf("Finding user '%s'...\n", sUserDN);
-	try{
-		// SCOPE_SUB = Scope Subtree.
-		if( DEBUGLOG ) System.out.printf("Finding Filter: '%s'.\n", ldapFilter);
+    public static int ldapTerminate(LDAPConnection lc, int ret) {
+        if (DEBUGLOG) System.out.println("Terminating...");
 
-		// hier werden alle Attribute abgefragt:
-		//srchRes = lc.search(ldapBase, SearchScope.SUB, ldapFilter, null);
-		// wir fragen nur diese Attribute ab:
-		srchRes = lc.search(ldapBase, SearchScope.SUB, ldapFilter, attStatus, attC2);
+        lc.close(null);
+        if (DEBUGLOG) System.out.println("closing connection: done.\n");
+        return ret;
+    }
 
-		if( DEBUGLOG ) System.out.printf("Finding '%s': %d entries.\n", sUserDN, srchRes.getEntryCount());
-		}
-	catch( LDAPSearchException e )
-		{
-		System.err.printf("Error: login: Search for User failed: '%s'!\n", e.toString());
-		return ldapTerminate(lc, LDAP_AUTH_RUNKNOWN);
-		}
-
-	if( srchRes.getEntryCount() == 0 )
-		{
-		if( DEBUGLOG ) System.out.printf("Finding '%s': no entry found!\n", sUserDN);
-		return ldapTerminate(lc, LDAP_AUTH_RUNKNOWN);
-		}
-
-	if( DEBUGLOG ) System.out.println("Display results:");
-
-	Boolean
-		bStatus = false,
-		bC2     = false;
-	
-	// Attribute pr�fen:
-	for (SearchResultEntry e : srchRes.getSearchEntries())
-		{
-		for( Attribute attr : e.getAttributes() )
-			{
-			Integer val;
-
-			if( DEBUGLOG ) 
-				System.out.printf(" att: '%s'='%s'.\n", attr.getName(), attr.getValue());
-
-			// checking pertinent attribut/value pairs:
-			// "idsStatus": values 0=OK, 1=inaktiv=OK, 2-3 = locked account.
-			if( attr.getName().equals(attStatus) )
-				{
-				if( (val = attr.getValueAsInteger()) == null || (val != 0 && val != 1) )
-					{
-					if( DEBUGLOG ) System.out.printf("idsStatus = '%s' -> User locked!\n", attr.getValue());
-					return ldapTerminate(lc, LDAP_AUTH_RLOCKED);
-					}
-				if( DEBUGLOG ) System.out.printf(" att: '%s'='%s': OK.\n", attr.getName(), attr.getValue());
-				bStatus = true;
-				}
-
-			// "c2IDS" must be set to "TRUE" = User known, but has not yet registered to C2 Service -> KorAP Service.
-			if( attr.getName().equals(attC2) ) 
-				{
-				if( attr.getValue().equals("FALSE") )
-					{
-					if( DEBUGLOG ) 
-						System.out.printf("idsC2 = '%s'-> User known, but has not registered C2/KorAP Service yet!\n", 
-							attr.getValue());
-					return ldapTerminate(lc, LDAP_AUTH_RNOTREG);
-					}
-				if( DEBUGLOG ) 
-					System.out.printf(" att: idsC2 = '%s'-> registered User: OK.\n", attr.getValue());
-				bC2 = true;
-				}
-			}
-
-		if( DEBUGLOG ) System.out.println();
-		}
-
-	if( bStatus == true && bC2 == true )
-		{
-		return ldapTerminate(lc, LDAP_AUTH_ROK); // OK.
-		}
-	else
-		return ldapTerminate(lc, LDAP_AUTH_RNOTREG); // Attribute konnten nicht gepr�ft werden.
-	
-	} // ldapLogin
-
-	/**
-	 *                getEmail():
-	 * 
-	 * Arguments:
-	 * sUserDN  	: either COSMAS II specific Account Name or IDS wide (IDM) account name;
-	 * ldapConfig	: path+file name of LDAP configuration file.
-	 * 
-	 * Returns		: the requested Email of sUserDN.
-	 * Notices:
-	 * - no password needed. Assuming that sUserDN is already authorized and active.
-	 * 
-	 * 
-	 * 16.09.21/FB
-	 */
-
-	public static String getEmail(String sUserDN, String ldapConfig) throws LDAPException
-
-	{
-	final String func = "LdapAuth3.getEmail";
-	
-	// sUSerDN is either C2/KorAP specific account name or the IDS wide account name:
-	String ldapFilter = String.format("(|(uid=%s)(idsC2Profile=%s))", sUserDN, sUserDN);
-
-	SearchResult srchRes = null;
-
-	try{
-		sPwd = loadProp(ldapConfig);
-		}
-	catch( IOException e )
-		{
-		System.err.printf("Error: %s: cannot load Property file '%s'!", func, ldapConfig);
-		return null;
-		}
-															
-	if( DEBUGLOG )
-		{
-		//System.out.printf("LDAP Version      = %d.\n", LDAPConnection.LDAP_V3);
-		System.out.printf("%s: LDAP Host & Port  = '%s':%d.\n", func, ldapHost, ldapPort);
-		System.out.printf("%s: User Account      = '%s'\n", func, sUserDN);
-		}
-
-	// LDAP Connection:
-	if( DEBUGLOG ) System.out.println("");
-
-	LDAPConnection 
-		lc = new LDAPConnection();
-	
-	try {
-		// connect to LDAP Server:
-		lc.connect(ldapHost, ldapPort);
-		if( DEBUGLOG ) System.out.println("LDAP Connection = OK\n");
-		}
-	catch( LDAPException e) 	
-		{
-		System.err.printf("Error: %s: Connecting to LDAP Server: failed: '%s'!\n", func, e.toString());
-		ldapTerminate(lc, LDAP_AUTH_RCONNECT);
-		return null;
-		}
-
-	if( DEBUGLOG ) 
-		System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
-	
-	try {
-		// bind to server:
-		if( DEBUGLOG ) System.out.printf("Binding with '%s' + '%s'...\n", sLoginDN, sPwd);
-		lc.bind(sLoginDN, sPwd);
-		if( DEBUGLOG ) System.out.printf("Binding: OK.\n");
-		}
-	catch( LDAPException e )
-		{
-		System.err.printf("Error: %s: Binding failed: '%s'!\n", func, e.toString());
-		ldapTerminate(lc, LDAP_AUTH_RINTERR);
-		return null;
-		}
-
-	if( DEBUGLOG ) 
-		System.out.printf("Debug: isConnected=%d\n", lc.isConnected() ? 1 : 0);
-		
-	if( DEBUGLOG ) System.out.printf("Finding user '%s'...\n", sUserDN);
-	try{
-		// SCOPE_SUB = Scope Subtree.
-		if( DEBUGLOG ) System.out.printf("Finding Filter: '%s'.\n", ldapFilter);
-
-		// requested attribute is attEmail:
-		srchRes = lc.search(ldapBase, SearchScope.SUB, ldapFilter, attEmail);
-
-		if( DEBUGLOG ) System.out.printf("Finding '%s': %d entries.\n", sUserDN, srchRes.getEntryCount());
-		}
-	catch( LDAPSearchException e )
-		{
-		System.err.printf("Error: %s: Search for User '%s' failed: '%s'!\n", func, sUserDN, e.toString());
-		ldapTerminate(lc, LDAP_AUTH_RUNKNOWN);
-		return null;
-		}
-
-	if( srchRes.getEntryCount() == 0 )
-		{
-		if( DEBUGLOG ) System.out.printf("Error: %s: account '%s': 0 entries found!\n", func, sUserDN);
-		ldapTerminate(lc, LDAP_AUTH_RUNKNOWN);
-		return null;
-		}
-
-	if( DEBUGLOG ) System.out.printf("Debug: %s: Extract email from results.\n", func);
-
-	// Now get email from result.
-	// expected: 1 result with 1 attribute value:
-	
-	SearchResultEntry 
-		e;
-	Attribute
-		attr;
-	String
-		email;
-	
-	if( (e = srchRes.getSearchEntries().get(0)) != null &&
-		(attr = e.getAttribute(attEmail)) != null && 
-		(email = attr.getValue()) != null )
-		{
-		// return email:
-		if( DEBUGLOG ) 
-			System.out.printf("Debug: %s: account '%s' has email = '%s'.\n", func, sUserDN, email);
-		ldapTerminate(lc, LDAP_AUTH_ROK); // OK.
-		return email;
-		}
-	
-	// cannot obtain email from result:
-	System.err.printf("Error: %s: account '%s': no attribute '%s' for email found!\n", func, sUserDN, attEmail);
-	
-	ldapTerminate(lc, LDAP_AUTH_RNOEMAIL); // no email found.
-	return null;
-	} // getEmail
-
-/**
- * ldapTerminate
- */
- 
-public static int ldapTerminate(LDAPConnection lc, int ret)
-
-	{
-	if( DEBUGLOG ) System.out.println("Terminating...");
-	/*
-	try{
-		lc.finalize();
-		if( DEBUGLOG ) System.out.println("Debug: finalize: OK.");
-		}
-	catch( LDAPException e )
-		{
-		System.out.printf("finalize failed: '%s'!\n", e.toString());
-		}
-	*/
-
-	lc.close(null);
-	if( DEBUGLOG ) System.out.println("closing connection: done.\n");
-	return ret;
-	} // ldapTerminate
+    @Override
+    public TokenType getTokenType() {
+        return TokenType.API;
+    }
 
 }
-
diff --git a/full/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java b/full/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java
new file mode 100644
index 0000000..dc9e3ae
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java
@@ -0,0 +1,135 @@
+package de.ids_mannheim.korap.authentication;
+
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.util.Base64;
+import com.unboundid.util.StaticUtils;
+import com.unboundid.util.ssl.KeyStoreKeyManager;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+import com.unboundid.util.ssl.TrustStoreTrustManager;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.security.GeneralSecurityException;
+
+import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_ROK;
+import static de.ids_mannheim.korap.authentication.LdapAuth3.LDAP_AUTH_RNAUTH;
+import static org.junit.Assert.assertEquals;
+
+public class LdapAuth3Test {
+    public static final String TEST_LDAP_PROPERTIES = "src/test/resources/test-ldap.properties";
+    public static final String TEST_LDAPS_PROPERTIES = "src/test/resources/test-ldaps.properties";
+    public static final String TEST_LDAPS_TS_PROPERTIES = "src/test/resources/test-ldaps-with-truststore.properties";
+    public static final String TEST_LDAP_USERS_LDIF = "src/test/resources/test-ldap-users.ldif";
+    private static final String keyStorePath = "src/test/resources/keystore.p12";
+    static InMemoryDirectoryServer server;
+
+    @BeforeClass
+    public static void startDirectoryServer() throws LDAPException, GeneralSecurityException {
+        InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
+        config.addAdditionalBindCredentials("cn=admin,dc=example,dc=com", "adminpassword");
+        config.setSchema(null);
+
+        final SSLUtil serverSSLUtil = new SSLUtil(new KeyStoreKeyManager(keyStorePath, "password".toCharArray(), "PKCS12", "server-cert"), new TrustStoreTrustManager(keyStorePath));
+
+        final SSLUtil clientSslUtil = new SSLUtil(new TrustAllTrustManager());
+
+        config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", // Listener name
+                        null, // Listen address. (null = listen on all interfaces)
+                        3268, // Listen port (0 = automatically choose an available port)
+                        clientSslUtil.createSSLSocketFactory()), // StartTLS factory
+                InMemoryListenerConfig.createLDAPSConfig("LDAPS", // Listener name
+                        null, // Listen address. (null = listen on all interfaces)
+                        3269, // Listen port (0 = automatically choose an available port)
+                        serverSSLUtil.createSSLServerSocketFactory(), clientSslUtil.createSSLSocketFactory()));
+        server = new InMemoryDirectoryServer(config);
+
+        String configPath = TEST_LDAP_USERS_LDIF;
+        server.importFromLDIF(true, configPath);
+        server.startListening();
+    }
+
+    @AfterClass
+    public static void ShutDownDirectoryServer() {
+        server.shutDown(true);
+    }
+
+    @Test
+    public void testLoginWithUsername() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("testuser", "topsecret", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testLoginWithUid() throws LDAPException {
+        final byte[] passwordBytes = StaticUtils.getBytes("password");
+        String pw = Base64.encode(passwordBytes);
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("testuser", pw, TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testLoginWithEmail() throws LDAPException {
+        final byte[] passwordBytes = StaticUtils.getBytes("password");
+        String pw = Base64.encode(passwordBytes);
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("testuser@example.com", pw, TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingLoginWithWrongEmail() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("notestuser@example.com", "topsecret", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingLoginWithEmailAndWrongPassword() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("testuser@example.com", "wrongpw", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingLoginWithUsernameAndWrongPassword() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("testuser", "wrongpw", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingLoginWithoutC2Attr() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("doe", "topsecret", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingLoginWithoutBadStatus() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("berserker", "topsecret", TEST_LDAP_PROPERTIES));
+    }
+
+    @Test
+    public void testSecureLoginWithUsername() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("testuser", "topsecret", TEST_LDAPS_PROPERTIES));
+    }
+
+    @Test
+    public void testSecureLoginWithTrustStoreAndUsername() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("testuser", "topsecret", TEST_LDAPS_TS_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingSecureLoginWithTrustStoreAndUsernameAndWrongPW() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("testuser", "topsecrets", TEST_LDAPS_TS_PROPERTIES));
+    }
+
+    @Test
+    public void testPasswordWithAsterisk() throws LDAPException {
+        assertEquals(LDAP_AUTH_ROK, LdapAuth3.login("test", "top*ecret", TEST_LDAPS_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingEscapedPW() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("testuser", "top*", TEST_LDAPS_TS_PROPERTIES));
+    }
+
+    @Test
+    public void testFailingIllegalPW() throws LDAPException {
+        assertEquals(LDAP_AUTH_RNAUTH, LdapAuth3.login("testuser", "*", TEST_LDAPS_TS_PROPERTIES));
+    }
+
+}
diff --git a/full/src/test/resources/keystore.p12 b/full/src/test/resources/keystore.p12
new file mode 100644
index 0000000..a1d7980
--- /dev/null
+++ b/full/src/test/resources/keystore.p12
Binary files differ
diff --git a/full/src/test/resources/test-ldap-users.ldif b/full/src/test/resources/test-ldap-users.ldif
new file mode 100644
index 0000000..a965181
--- /dev/null
+++ b/full/src/test/resources/test-ldap-users.ldif
@@ -0,0 +1,66 @@
+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=testuser,ou=people,dc=example,dc=com
+cn: Peter Testuser
+sn: Testuser
+givenName: Peter
+mail: testuser@example.com
+userPassword: cGFzc3dvcmQ=
+displayName: Dr. Peter Testuser
+idsC2: TRUE
+idsC2Profile: testuser
+idsC2Password: topsecret
+idsC2News: TRUE
+title: Herr
+uid: testuser
+
+dn: uid=test,ou=people,dc=example,dc=com
+cn: Peter Test
+sn: Test
+givenName: Peter
+mail: test@example.com
+userPassword: top*ecret
+displayName: Dr. Peter Test
+idsC2: TRUE
+idsStatus: 1
+idsC2Profile: test
+idsC2Password: top*ecret
+uid: test
+
+dn: uid=doe,ou=people,dc=example,dc=com
+cn: John Doe
+sn: doe
+givenName: John
+mail: doe@example.com
+userPassword: cGFzc3dvcmQ=
+displayName: Dr. John Doe
+idsStatus: 0
+idsC2: FALSE
+idsC2Profile: doe
+idsC2Password: topsecret
+idsC2News: TRUE
+title: Herr
+uid: doe
+
+dn: uid=berserker,ou=people,dc=example,dc=com
+cn: Bernd Berserker
+sn: berserker
+givenName: Joe
+mail: berserker@example.com
+userPassword: cGFzc3dvcmQ=
+displayName: berserk
+idsStatus: 2
+idsC2: TRUE
+idsC2Profile: doe
+idsC2Password: topsecret
+idsC2News: TRUE
+title: Herr
+uid: berserk
diff --git a/full/src/test/resources/test-ldap.properties b/full/src/test/resources/test-ldap.properties
new file mode 100644
index 0000000..aa27f14
--- /dev/null
+++ b/full/src/test/resources/test-ldap.properties
@@ -0,0 +1,6 @@
+ldapHost=localhost
+ldapPort=3268
+ldapBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=adminpassword
+ldapFilter=(&(|(&(|(uid=${username})(mail=${username}))(userPassword=${password}))(&(idsC2Profile=${username})(idsC2Password=${password})))(&(idsC2=TRUE)(|(idsStatus=1)(|(idsStatus=0)(!(idsStatus=*))))))
diff --git a/full/src/test/resources/test-ldaps-with-truststore.properties b/full/src/test/resources/test-ldaps-with-truststore.properties
new file mode 100644
index 0000000..d785301
--- /dev/null
+++ b/full/src/test/resources/test-ldaps-with-truststore.properties
@@ -0,0 +1,8 @@
+ldapHost=localhost
+ldapPort=3269
+ldapS=true
+trustStore=src/test/resources/truststore.jks
+ldapBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=adminpassword
+ldapFilter=(&(|(&(|(uid=${username})(mail=${username}))(userPassword=${password}))(&(idsC2Profile=${username})(idsC2Password=${password})))(&(idsC2=TRUE)(|(idsStatus=1)(|(idsStatus=0)(!(idsStatus=*))))))
diff --git a/full/src/test/resources/test-ldaps.properties b/full/src/test/resources/test-ldaps.properties
new file mode 100644
index 0000000..732076f
--- /dev/null
+++ b/full/src/test/resources/test-ldaps.properties
@@ -0,0 +1,8 @@
+ldapHost=localhost
+ldapPort=3269
+ldapS=true
+trustStore=
+ldapBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=adminpassword
+ldapFilter=(&(|(&(|(uid=${username})(mail=${username}))(userPassword=${password}))(&(idsC2Profile=${username})(idsC2Password=${password})))(&(idsC2=TRUE)(|(idsStatus=1)(|(idsStatus=0)(!(idsStatus=*))))))
diff --git a/full/src/test/resources/truststore.jks b/full/src/test/resources/truststore.jks
new file mode 100644
index 0000000..50804be
--- /dev/null
+++ b/full/src/test/resources/truststore.jks
Binary files differ