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/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