You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by jb...@apache.org on 2021/10/29 18:35:34 UTC

[activemq-artemis] branch main updated: ARTEMIS-3140 Extra options in LDAP login module

This is an automated email from the ASF dual-hosted git repository.

jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis.git


The following commit(s) were added to refs/heads/main by this push:
     new a0c4cba  ARTEMIS-3140 Extra options in LDAP login module
     new 72f9087  This closes #3826
a0c4cba is described below

commit a0c4cba7e13ea34b2a0c3e2114664e7f41e3060c
Author: pahamala <>
AuthorDate: Thu Oct 28 14:19:52 2021 +0300

    ARTEMIS-3140 Extra options in LDAP login module
    
    Adds support for extra configuration options to LDAP login module to
    prepare for supporting any future/custom string configuration in LDAP
    directory context creation.
    
    Details:
    
     - Changed LDAPLoginModule to pass any string configuration not
    recognized by the module itself to the InitialDirContext contruction
    environment.
     - Changed the static LDAPLoginModule configuration key fields to an
    enum to be able to loop through the specified keys (e.g. to filter out
    the internal LDAPLoginModule configuration keys from the keys passed to
    InitialDirContext).
     - Few fixes for issues reported by static analysis tools.
     - Tested that LDAP authentication with TLS+GSSAPI works against a
    recent Windows AD server with Java
    OpenJDK11U-jdk_x64_windows_hotspot_11.0.13_8 by setting the property
    com.sun.jndi.ldap.tls.cbtype (see ARTEMIS-3140) in JAAS login.conf.
     - Moved LDAPLoginModuleTest to the correct package to be able to
    access LDAPLoginModule package privates from the test code.
     - Added a test to LDAPLoginModuleTest for the task changes.
     - Updated documentation to reflect the changes.
---
 .../spi/core/security/jaas/LDAPLoginModule.java    | 268 +++++++++++----------
 .../core/security/jaas/LDAPLoginModuleTest.java    |  85 ++++++-
 docs/user-manual/en/security.md                    |   3 +
 3 files changed, 217 insertions(+), 139 deletions(-)

diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
index 212dbb3..ea8456b 100644
--- a/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
+++ b/artemis-server/src/main/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModule.java
@@ -17,7 +17,6 @@
 package org.apache.activemq.artemis.spi.core.security.jaas;
 
 import javax.naming.AuthenticationException;
-import javax.naming.CommunicationException;
 import javax.naming.Context;
 import javax.naming.Name;
 import javax.naming.NameParser;
@@ -65,36 +64,59 @@ public class LDAPLoginModule implements AuditLoginModule {
 
    private static final Logger logger = Logger.getLogger(LDAPLoginModule.class);
 
-   private static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
-   private static final String CONNECTION_URL = "connectionURL";
-   private static final String CONNECTION_USERNAME = "connectionUsername";
-   private static final String CONNECTION_PASSWORD = "connectionPassword";
-   private static final String CONNECTION_PROTOCOL = "connectionProtocol";
-   private static final String AUTHENTICATION = "authentication";
-   private static final String USER_BASE = "userBase";
-   private static final String USER_SEARCH_MATCHING = "userSearchMatching";
-   private static final String USER_SEARCH_SUBTREE = "userSearchSubtree";
-   private static final String ROLE_BASE = "roleBase";
-   private static final String ROLE_NAME = "roleName";
-   private static final String ROLE_SEARCH_MATCHING = "roleSearchMatching";
-   private static final String ROLE_SEARCH_SUBTREE = "roleSearchSubtree";
-   private static final String USER_ROLE_NAME = "userRoleName";
-   private static final String EXPAND_ROLES = "expandRoles";
-   private static final String EXPAND_ROLES_MATCHING = "expandRolesMatching";
-   private static final String SASL_LOGIN_CONFIG_SCOPE = "saslLoginConfigScope";
-   private static final String AUTHENTICATE_USER = "authenticateUser";
-   private static final String REFERRAL = "referral";
-   private static final String IGNORE_PARTIAL_RESULT_EXCEPTION = "ignorePartialResultException";
-   private static final String PASSWORD_CODEC = "passwordCodec";
-   private static final String CONNECTION_POOL = "connectionPool";
-   private static final String CONNECTION_TIMEOUT = "connectionTimeout";
-   private static final String READ_TIMEOUT = "readTimeout";
+   enum ConfigKey {
+
+      DEBUG("debug"),
+      INITIAL_CONTEXT_FACTORY("initialContextFactory"),
+      CONNECTION_URL("connectionURL"),
+      CONNECTION_USERNAME("connectionUsername"),
+      CONNECTION_PASSWORD("connectionPassword"),
+      CONNECTION_PROTOCOL("connectionProtocol"),
+      AUTHENTICATION("authentication"),
+      USER_BASE("userBase"),
+      USER_SEARCH_MATCHING("userSearchMatching"),
+      USER_SEARCH_SUBTREE("userSearchSubtree"),
+      ROLE_BASE("roleBase"),
+      ROLE_NAME("roleName"),
+      ROLE_SEARCH_MATCHING("roleSearchMatching"),
+      ROLE_SEARCH_SUBTREE("roleSearchSubtree"),
+      USER_ROLE_NAME("userRoleName"),
+      EXPAND_ROLES("expandRoles"),
+      EXPAND_ROLES_MATCHING("expandRolesMatching"),
+      SASL_LOGIN_CONFIG_SCOPE("saslLoginConfigScope"),
+      AUTHENTICATE_USER("authenticateUser"),
+      REFERRAL("referral"),
+      IGNORE_PARTIAL_RESULT_EXCEPTION("ignorePartialResultException"),
+      PASSWORD_CODEC("passwordCodec"),
+      CONNECTION_POOL("connectionPool"),
+      CONNECTION_TIMEOUT("connectionTimeout"),
+      READ_TIMEOUT("readTimeout");
+
+      private final String name;
+
+      ConfigKey(String name) {
+         this.name = name;
+      }
+
+      String getName() {
+         return name;
+      }
+
+      static boolean contains(String key) {
+         for (ConfigKey k: values()) {
+            if (k.name.equals(key)) {
+               return true;
+            }
+         }
+         return false;
+      }
+   }
 
    protected DirContext context;
 
    private Subject subject;
    private CallbackHandler handler;
-   private LDAPLoginProperty[] config;
+   private final Set<LDAPLoginProperty> config = new HashSet<>();
    private String username;
    private final Set<RolePrincipal> groups = new HashSet<>();
    private boolean userAuthenticated = false;
@@ -113,37 +135,20 @@ public class LDAPLoginModule implements AuditLoginModule {
       this.subject = subject;
       this.handler = callbackHandler;
 
-      config = new LDAPLoginProperty[]{new LDAPLoginProperty(INITIAL_CONTEXT_FACTORY, (String) options.get(INITIAL_CONTEXT_FACTORY)),
-                                       new LDAPLoginProperty(CONNECTION_URL, (String) options.get(CONNECTION_URL)),
-                                       new LDAPLoginProperty(CONNECTION_USERNAME, (String) options.get(CONNECTION_USERNAME)),
-                                       new LDAPLoginProperty(CONNECTION_PASSWORD, (String) options.get(CONNECTION_PASSWORD)),
-                                       new LDAPLoginProperty(CONNECTION_PROTOCOL, (String) options.get(CONNECTION_PROTOCOL)),
-                                       new LDAPLoginProperty(AUTHENTICATION, (String) options.get(AUTHENTICATION)),
-                                       new LDAPLoginProperty(USER_BASE, (String) options.get(USER_BASE)),
-                                       new LDAPLoginProperty(USER_SEARCH_MATCHING, (String) options.get(USER_SEARCH_MATCHING)),
-                                       new LDAPLoginProperty(USER_SEARCH_SUBTREE, (String) options.get(USER_SEARCH_SUBTREE)),
-                                       new LDAPLoginProperty(ROLE_BASE, (String) options.get(ROLE_BASE)),
-                                       new LDAPLoginProperty(ROLE_NAME, (String) options.get(ROLE_NAME)),
-                                       new LDAPLoginProperty(ROLE_SEARCH_MATCHING, (String) options.get(ROLE_SEARCH_MATCHING)),
-                                       new LDAPLoginProperty(ROLE_SEARCH_SUBTREE, (String) options.get(ROLE_SEARCH_SUBTREE)),
-                                       new LDAPLoginProperty(USER_ROLE_NAME, (String) options.get(USER_ROLE_NAME)),
-                                       new LDAPLoginProperty(EXPAND_ROLES, (String) options.get(EXPAND_ROLES)),
-                                       new LDAPLoginProperty(EXPAND_ROLES_MATCHING, (String) options.get(EXPAND_ROLES_MATCHING)),
-                                       new LDAPLoginProperty(PASSWORD_CODEC, (String) options.get(PASSWORD_CODEC)),
-                                       new LDAPLoginProperty(SASL_LOGIN_CONFIG_SCOPE, (String) options.get(SASL_LOGIN_CONFIG_SCOPE)),
-                                       new LDAPLoginProperty(AUTHENTICATE_USER, (String) options.get(AUTHENTICATE_USER)),
-                                       new LDAPLoginProperty(REFERRAL, (String) options.get(REFERRAL)),
-                                       new LDAPLoginProperty(IGNORE_PARTIAL_RESULT_EXCEPTION, (String) options.get(IGNORE_PARTIAL_RESULT_EXCEPTION)),
-                                       new LDAPLoginProperty(CONNECTION_POOL, (String) options.get(CONNECTION_POOL)),
-                                       new LDAPLoginProperty(CONNECTION_TIMEOUT, (String) options.get(CONNECTION_TIMEOUT)),
-                                       new LDAPLoginProperty(READ_TIMEOUT, (String) options.get(READ_TIMEOUT))};
-
-      if (isLoginPropertySet(AUTHENTICATE_USER)) {
-         authenticateUser = Boolean.valueOf(getLDAPPropertyValue(AUTHENTICATE_USER));
-      }
-      isRoleAttributeSet = isLoginPropertySet(ROLE_NAME);
-      roleAttributeName = getLDAPPropertyValue(ROLE_NAME);
-      codecClass = getLDAPPropertyValue(PASSWORD_CODEC);
+      // copy all options to config, ignoring non-string entries
+      config.clear();
+      for (Map.Entry<String, ?> entry : options.entrySet()) {
+         if (entry.getValue() instanceof String) {
+            config.add(new LDAPLoginProperty(entry.getKey(), (String) entry.getValue()));
+         }
+      }
+
+      if (isLoginPropertySet(ConfigKey.AUTHENTICATE_USER)) {
+         authenticateUser = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.AUTHENTICATE_USER));
+      }
+      isRoleAttributeSet = isLoginPropertySet(ConfigKey.ROLE_NAME);
+      roleAttributeName = getLDAPPropertyValue(ConfigKey.ROLE_NAME);
+      codecClass = getLDAPPropertyValue(ConfigKey.PASSWORD_CODEC);
    }
 
    private String getPlainPassword(String password) {
@@ -174,13 +179,15 @@ public class LDAPLoginModule implements AuditLoginModule {
       String password = null;
 
       username = ((NameCallback) callbacks[0]).getName();
-      if (username == null)
+      if (username == null) {
          return false;
+      }
 
-      if (((PasswordCallback) callbacks[1]).getPassword() != null)
+      if (((PasswordCallback) callbacks[1]).getPassword() != null) {
          password = new String(((PasswordCallback) callbacks[1]).getPassword());
+      }
 
-      /**
+      /*
        * https://tools.ietf.org/html/rfc4513#section-6.3.1
        *
        * Clients that use the results from a simple Bind operation to make
@@ -188,8 +195,9 @@ public class LDAPLoginModule implements AuditLoginModule {
        * requests (by verifying that the supplied password is not empty) and
        * react appropriately.
        */
-      if (password == null || password.length() == 0)
+      if (password == null || password.length() == 0) {
          throw new FailedLoginException("Password cannot be null or empty");
+      }
 
       // authenticate will throw LoginException
       // in case of failed authentication
@@ -227,9 +235,8 @@ public class LDAPLoginModule implements AuditLoginModule {
          }
       }
 
-      for (RolePrincipal gp : groups) {
-         principals.add(gp);
-      }
+      principals.addAll(groups);
+
       clear();
       return result;
    }
@@ -259,7 +266,6 @@ public class LDAPLoginModule implements AuditLoginModule {
    }
 
    protected boolean authenticate(String username, String password) throws LoginException {
-
       List<String> roles = new ArrayList<>();
       try {
          String dn = resolveDN(username, roles);
@@ -271,11 +277,6 @@ public class LDAPLoginModule implements AuditLoginModule {
          } else {
             throw new FailedLoginException("Password does not match for user: " + username);
          }
-      } catch (CommunicationException e) {
-         closeContext();
-         FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
-         ex.initCause(e);
-         throw ex;
       } catch (NamingException e) {
          closeContext();
          FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
@@ -299,10 +300,6 @@ public class LDAPLoginModule implements AuditLoginModule {
    private String resolveDN(String username, List<String> roles) throws FailedLoginException {
       String dn = null;
 
-      MessageFormat userSearchMatchingFormat;
-      boolean userSearchSubtreeBool;
-      boolean ignorePartialResultExceptionBool;
-
       if (logger.isDebugEnabled()) {
          logger.debug("Create the LDAP initial context.");
       }
@@ -314,13 +311,13 @@ public class LDAPLoginModule implements AuditLoginModule {
          throw ex;
       }
 
-      if (!isLoginPropertySet(USER_SEARCH_MATCHING)) {
+      if (!isLoginPropertySet(ConfigKey.USER_SEARCH_MATCHING)) {
          return username;
       }
 
-      userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(USER_SEARCH_MATCHING));
-      userSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(USER_SEARCH_SUBTREE)).booleanValue();
-      ignorePartialResultExceptionBool = Boolean.valueOf(getLDAPPropertyValue(IGNORE_PARTIAL_RESULT_EXCEPTION)).booleanValue();
+      MessageFormat userSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.USER_SEARCH_MATCHING));
+      boolean userSearchSubtreeBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.USER_SEARCH_SUBTREE));
+      boolean ignorePartialResultExceptionBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.IGNORE_PARTIAL_RESULT_EXCEPTION));
 
       try {
 
@@ -334,8 +331,8 @@ public class LDAPLoginModule implements AuditLoginModule {
 
          // setup attributes
          List<String> list = new ArrayList<>();
-         if (isLoginPropertySet(USER_ROLE_NAME)) {
-            list.add(getLDAPPropertyValue(USER_ROLE_NAME));
+         if (isLoginPropertySet(ConfigKey.USER_ROLE_NAME)) {
+            list.add(getLDAPPropertyValue(ConfigKey.USER_ROLE_NAME));
          }
          String[] attribs = new String[list.size()];
          list.toArray(attribs);
@@ -344,13 +341,13 @@ public class LDAPLoginModule implements AuditLoginModule {
          if (logger.isDebugEnabled()) {
             logger.debug("Get the user DN.");
             logger.debug("Looking for the user in LDAP with ");
-            logger.debug("  base DN: " + getLDAPPropertyValue(USER_BASE));
+            logger.debug("  base DN: " + getLDAPPropertyValue(ConfigKey.USER_BASE));
             logger.debug("  filter: " + filter);
          }
 
          NamingEnumeration<SearchResult> results = null;
          try {
-            results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(USER_BASE), filter, constraints));
+            results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(ConfigKey.USER_BASE), filter, constraints));
          } catch (PrivilegedActionException e) {
             Exception cause = e.getException();
             FailedLoginException ex = new FailedLoginException("Error executing search query to resolve DN");
@@ -382,7 +379,7 @@ public class LDAPLoginModule implements AuditLoginModule {
 
             NameParser parser = context.getNameParser("");
             Name contextName = parser.parse(context.getNameInNamespace());
-            Name baseName = parser.parse(getLDAPPropertyValue(USER_BASE));
+            Name baseName = parser.parse(getLDAPPropertyValue(ConfigKey.USER_BASE));
             Name entryName = parser.parse(result.getName());
             Name name = contextName.addAll(baseName);
             name = name.addAll(entryName);
@@ -415,8 +412,8 @@ public class LDAPLoginModule implements AuditLoginModule {
          if (attrs == null) {
             throw new FailedLoginException("User found, but LDAP entry malformed: " + username);
          }
-         if (isLoginPropertySet(USER_ROLE_NAME)) {
-            Attribute roleNames = attrs.get(getLDAPPropertyValue(USER_ROLE_NAME));
+         if (isLoginPropertySet(ConfigKey.USER_ROLE_NAME)) {
+            Attribute roleNames = attrs.get(getLDAPPropertyValue(ConfigKey.USER_ROLE_NAME));
             if (roleNames != null) {
                NamingEnumeration<?> e = roleNames.getAll();
                while (e.hasMore()) {
@@ -436,11 +433,6 @@ public class LDAPLoginModule implements AuditLoginModule {
                }
             }
          }
-      } catch (CommunicationException e) {
-         closeContext();
-         FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
-         ex.initCause(e);
-         throw ex;
       } catch (NamingException e) {
          closeContext();
          FailedLoginException ex = new FailedLoginException("Error contacting LDAP");
@@ -456,18 +448,14 @@ public class LDAPLoginModule implements AuditLoginModule {
                                    String username,
                                    List<String> currentRoles) throws NamingException {
 
-      if (!isLoginPropertySet(ROLE_SEARCH_MATCHING)) {
+      if (!isLoginPropertySet(ConfigKey.ROLE_SEARCH_MATCHING)) {
          return;
       }
 
-      MessageFormat roleSearchMatchingFormat;
-      boolean roleSearchSubtreeBool;
-      boolean expandRolesBool;
-      boolean ignorePartialResultExceptionBool;
-      roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ROLE_SEARCH_MATCHING));
-      roleSearchSubtreeBool = Boolean.valueOf(getLDAPPropertyValue(ROLE_SEARCH_SUBTREE)).booleanValue();
-      expandRolesBool = Boolean.valueOf(getLDAPPropertyValue(EXPAND_ROLES)).booleanValue();
-      ignorePartialResultExceptionBool = Boolean.valueOf(getLDAPPropertyValue(IGNORE_PARTIAL_RESULT_EXCEPTION)).booleanValue();
+      MessageFormat roleSearchMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.ROLE_SEARCH_MATCHING));
+      boolean roleSearchSubtreeBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.ROLE_SEARCH_SUBTREE));
+      boolean expandRolesBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.EXPAND_ROLES));
+      boolean ignorePartialResultExceptionBool = Boolean.parseBoolean(getLDAPPropertyValue(ConfigKey.IGNORE_PARTIAL_RESULT_EXCEPTION));
 
       final String filter = roleSearchMatchingFormat.format(new String[]{doRFC2254Encoding(dn), doRFC2254Encoding(username)});
 
@@ -480,14 +468,14 @@ public class LDAPLoginModule implements AuditLoginModule {
       if (logger.isDebugEnabled()) {
          logger.debug("Get user roles.");
          logger.debug("Looking for the user roles in LDAP with ");
-         logger.debug("  base DN: " + getLDAPPropertyValue(ROLE_BASE));
+         logger.debug("  base DN: " + getLDAPPropertyValue(ConfigKey.ROLE_BASE));
          logger.debug("  filter: " + filter);
       }
       HashSet<String> haveSeenNames = new HashSet<>();
       Queue<String> pendingNameExpansion = new LinkedList<>();
       NamingEnumeration<SearchResult> results = null;
       try {
-         results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), filter, constraints));
+         results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(ConfigKey.ROLE_BASE), filter, constraints));
       } catch (PrivilegedActionException e) {
          Exception cause = e.getException();
          NamingException ex = new NamingException("Error executing search query to resolve roles");
@@ -513,18 +501,18 @@ public class LDAPLoginModule implements AuditLoginModule {
          }
       }
       if (expandRolesBool) {
-         MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(EXPAND_ROLES_MATCHING));
+         MessageFormat expandRolesMatchingFormat = new MessageFormat(getLDAPPropertyValue(ConfigKey.EXPAND_ROLES_MATCHING));
          while (!pendingNameExpansion.isEmpty()) {
             String name = pendingNameExpansion.remove();
             final String expandFilter = expandRolesMatchingFormat.format(new String[]{name});
             if (logger.isDebugEnabled()) {
                logger.debug("Get 'expanded' user roles.");
                logger.debug("Looking for the 'expanded' user roles in LDAP with ");
-               logger.debug("  base DN: " + getLDAPPropertyValue(ROLE_BASE));
+               logger.debug("  base DN: " + getLDAPPropertyValue(ConfigKey.ROLE_BASE));
                logger.debug("  filter: " + expandFilter);
             }
             try {
-               results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(ROLE_BASE), expandFilter, constraints));
+               results = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction< NamingEnumeration<SearchResult>>) () -> context.search(getLDAPPropertyValue(ConfigKey.ROLE_BASE), expandFilter, constraints));
             } catch (PrivilegedActionException e) {
                Exception cause = e.getException();
                NamingException ex = new NamingException("Error executing search query to expand roles");
@@ -554,7 +542,7 @@ public class LDAPLoginModule implements AuditLoginModule {
    }
 
    protected String doRFC2254Encoding(String inputString) {
-      StringBuffer buf = new StringBuffer(inputString.length());
+      StringBuilder buf = new StringBuilder(inputString.length());
       for (int i = 0; i < inputString.length(); i++) {
          char c = inputString.charAt(i);
          switch (c) {
@@ -603,17 +591,17 @@ public class LDAPLoginModule implements AuditLoginModule {
          }
       }
 
-      if (isLoginPropertySet(CONNECTION_USERNAME)) {
-         context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
+      if (isLoginPropertySet(ConfigKey.CONNECTION_USERNAME)) {
+         context.addToEnvironment(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(ConfigKey.CONNECTION_USERNAME));
       } else {
          context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
       }
-      if (isLoginPropertySet(CONNECTION_PASSWORD)) {
-         context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD)));
+      if (isLoginPropertySet(ConfigKey.CONNECTION_PASSWORD)) {
+         context.addToEnvironment(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(ConfigKey.CONNECTION_PASSWORD)));
       } else {
          context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
       }
-      context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
+      context.addToEnvironment(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(ConfigKey.AUTHENTICATION));
 
       return isValid;
    }
@@ -634,25 +622,25 @@ public class LDAPLoginModule implements AuditLoginModule {
       if (context == null) {
          try {
             Hashtable<String, String> env = new Hashtable<>();
-            env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(INITIAL_CONTEXT_FACTORY));
-            env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(CONNECTION_PROTOCOL));
-            env.put(Context.PROVIDER_URL, getLDAPPropertyValue(CONNECTION_URL));
-            env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(AUTHENTICATION));
-            if (isLoginPropertySet(CONNECTION_POOL)) {
-               env.put("com.sun.jndi.ldap.connect.pool", getLDAPPropertyValue(CONNECTION_POOL));
+            env.put(Context.INITIAL_CONTEXT_FACTORY, getLDAPPropertyValue(ConfigKey.INITIAL_CONTEXT_FACTORY));
+            env.put(Context.SECURITY_PROTOCOL, getLDAPPropertyValue(ConfigKey.CONNECTION_PROTOCOL));
+            env.put(Context.PROVIDER_URL, getLDAPPropertyValue(ConfigKey.CONNECTION_URL));
+            env.put(Context.SECURITY_AUTHENTICATION, getLDAPPropertyValue(ConfigKey.AUTHENTICATION));
+            if (isLoginPropertySet(ConfigKey.CONNECTION_POOL)) {
+               env.put("com.sun.jndi.ldap.connect.pool", getLDAPPropertyValue(ConfigKey.CONNECTION_POOL));
             }
-            if (isLoginPropertySet(CONNECTION_TIMEOUT)) {
-               env.put("com.sun.jndi.ldap.connect.timeout", getLDAPPropertyValue(CONNECTION_TIMEOUT));
+            if (isLoginPropertySet(ConfigKey.CONNECTION_TIMEOUT)) {
+               env.put("com.sun.jndi.ldap.connect.timeout", getLDAPPropertyValue(ConfigKey.CONNECTION_TIMEOUT));
             }
-            if (isLoginPropertySet(READ_TIMEOUT)) {
-               env.put("com.sun.jndi.ldap.read.timeout", getLDAPPropertyValue(READ_TIMEOUT));
+            if (isLoginPropertySet(ConfigKey.READ_TIMEOUT)) {
+               env.put("com.sun.jndi.ldap.read.timeout", getLDAPPropertyValue(ConfigKey.READ_TIMEOUT));
             }
 
             // handle LDAP referrals
             // valid values are "throw", "ignore" and "follow"
             String referral = "ignore";
-            if (getLDAPPropertyValue(REFERRAL) != null) {
-               referral = getLDAPPropertyValue(REFERRAL);
+            if (getLDAPPropertyValue(ConfigKey.REFERRAL) != null) {
+               referral = getLDAPPropertyValue(ConfigKey.REFERRAL);
             }
 
             env.put(Context.REFERRAL, referral);
@@ -660,9 +648,9 @@ public class LDAPLoginModule implements AuditLoginModule {
                logger.debug("Referral handling: " + referral);
             }
 
-            if ("GSSAPI".equalsIgnoreCase(getLDAPPropertyValue(AUTHENTICATION))) {
+            if ("GSSAPI".equalsIgnoreCase(getLDAPPropertyValue(ConfigKey.AUTHENTICATION))) {
 
-               final String configScope = isLoginPropertySet(SASL_LOGIN_CONFIG_SCOPE) ? getLDAPPropertyValue(SASL_LOGIN_CONFIG_SCOPE) : "broker-sasl-gssapi";
+               final String configScope = isLoginPropertySet(ConfigKey.SASL_LOGIN_CONFIG_SCOPE) ? getLDAPPropertyValue(ConfigKey.SASL_LOGIN_CONFIG_SCOPE) : "broker-sasl-gssapi";
                try {
                   LoginContext loginContext = new LoginContext(configScope);
                   loginContext.login();
@@ -676,19 +664,21 @@ public class LDAPLoginModule implements AuditLoginModule {
 
             } else {
 
-               if (isLoginPropertySet(CONNECTION_USERNAME)) {
-                  env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(CONNECTION_USERNAME));
+               if (isLoginPropertySet(ConfigKey.CONNECTION_USERNAME)) {
+                  env.put(Context.SECURITY_PRINCIPAL, getLDAPPropertyValue(ConfigKey.CONNECTION_USERNAME));
                } else {
                   throw new NamingException("Empty username is not allowed");
                }
 
-               if (isLoginPropertySet(CONNECTION_PASSWORD)) {
-                  env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(CONNECTION_PASSWORD)));
+               if (isLoginPropertySet(ConfigKey.CONNECTION_PASSWORD)) {
+                  env.put(Context.SECURITY_CREDENTIALS, getPlainPassword(getLDAPPropertyValue(ConfigKey.CONNECTION_PASSWORD)));
                } else {
                   throw new NamingException("Empty password is not allowed");
                }
             }
 
+            extendInitialEnvironment(config, env);
+
             try {
                context = Subject.doAs(brokerGssapiIdentity, (PrivilegedExceptionAction<DirContext>) () -> new InitialDirContext(env));
             } catch (PrivilegedActionException e) {
@@ -703,17 +693,31 @@ public class LDAPLoginModule implements AuditLoginModule {
       }
    }
 
-   private String getLDAPPropertyValue(String propertyName) {
+   protected void extendInitialEnvironment(Set<LDAPLoginProperty> moduleConfig, Hashtable<String, String> initialContextEnv) {
+      // sub-classes may override the method if the default implementation is not sufficient:
+      // add all non-module configs to initial DirContext environment to support passing
+      // any custom/future property to InitialDirContext construction
+      for (LDAPLoginProperty prop: moduleConfig) {
+         String propName = prop.getPropertyName();
+         if (initialContextEnv.get(propName) == null && !ConfigKey.contains(propName)) {
+            initialContextEnv.put(propName, prop.getPropertyValue());
+         }
+      }
+   }
+
+   private String getLDAPPropertyValue(ConfigKey key) {
       for (LDAPLoginProperty conf : config)
-         if (conf.getPropertyName().equals(propertyName))
+         if (conf.getPropertyName().equals(key.getName())) {
             return conf.getPropertyValue();
+         }
       return null;
    }
 
-   private boolean isLoginPropertySet(String propertyName) {
+   private boolean isLoginPropertySet(ConfigKey key) {
       for (LDAPLoginProperty conf : config) {
-         if (conf.getPropertyName().equals(propertyName) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue())))
+         if (conf.getPropertyName().equals(key.getName()) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue()))) {
             return true;
+         }
       }
       return false;
    }
diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
similarity index 75%
rename from artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java
rename to artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
index 3842dbf..b4cdefe 100644
--- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/security/jaas/LDAPLoginModuleTest.java
+++ b/artemis-server/src/test/java/org/apache/activemq/artemis/spi/core/security/jaas/LDAPLoginModuleTest.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.activemq.artemis.core.security.jaas;
+package org.apache.activemq.artemis.spi.core.security.jaas;
 
 import javax.naming.Context;
 import javax.naming.NameClassPair;
@@ -37,14 +37,12 @@ import java.lang.reflect.Modifier;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
-import org.apache.activemq.artemis.spi.core.security.jaas.JaasCallbackHandler;
-import org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule;
-import org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginProperty;
 import org.apache.directory.server.annotations.CreateLdapServer;
 import org.apache.directory.server.annotations.CreateTransport;
 import org.apache.directory.server.core.annotations.ApplyLdifFiles;
@@ -309,9 +307,9 @@ public class LDAPLoginModuleTest extends AbstractLdapTestUnit {
       }
       loginModule.initialize(new Subject(), callbackHandler, null, options);
 
-      LDAPLoginProperty[] ldapProps = (LDAPLoginProperty[]) configMap.get(loginModule);
+      Set<LDAPLoginProperty> ldapProps = (Set<LDAPLoginProperty>) configMap.get(loginModule);
       for (String key: options.keySet()) {
-         assertTrue("val set: " + key, presentInArray(ldapProps, key));
+         assertTrue("val set: " + key, presentIn(ldapProps, key));
       }
    }
 
@@ -364,8 +362,81 @@ public class LDAPLoginModuleTest extends AbstractLdapTestUnit {
       }
       context.logout();
    }
+   @Test
+   public void testEnvironmentProperties() throws Exception {
+      HashMap<String, Object> options = new HashMap<>();
+
+      // set module configs
+      for (LDAPLoginModule.ConfigKey configKey: LDAPLoginModule.ConfigKey.values()) {
+         if (configKey.getName().equals("initialContextFactory")) {
+            options.put(configKey.getName(), "com.sun.jndi.ldap.LdapCtxFactory");
+         } else if (configKey.getName().equals("connectionURL")) {
+            options.put(configKey.getName(), "ldap://localhost:1024");
+         } else if (configKey.getName().equals("referral")) {
+            options.put(configKey.getName(), "ignore");
+         } else if (configKey.getName().equals("connectionTimeout")) {
+            options.put(configKey.getName(), "10000");
+         } else if (configKey.getName().equals("readTimeout")) {
+            options.put(configKey.getName(), "11000");
+         } else if (configKey.getName().equals("authentication")) {
+            options.put(configKey.getName(), "simple");
+         } else if (configKey.getName().equals("connectionUsername")) {
+            options.put(configKey.getName(), PRINCIPAL);
+         } else if (configKey.getName().equals("connectionPassword")) {
+            options.put(configKey.getName(), CREDENTIALS);
+         } else if (configKey.getName().equals("connectionProtocol")) {
+            options.put(configKey.getName(), "s");
+         } else if (configKey.getName().equals("debug")) {
+            options.put(configKey.getName(), "true");
+         } else {
+            options.put(configKey.getName(), configKey.getName() + "_value_set");
+         }
+      }
+
+      // add extra configs
+      options.put("com.sun.jndi.ldap.tls.cbtype", "tls-server-end-point");
+      options.put("randomConfig", "some-value");
+
+      // add non-strings configs
+      options.put("non.string.1", new Object());
+      options.put("non.string.2", 1);
+
+      // create context
+      LDAPLoginModule loginModule = new LDAPLoginModule();
+      loginModule.initialize(new Subject(), null, null, options);
+      loginModule.openContext();
+
+      // get created environment
+      Hashtable<?, ?> environment = loginModule.context.getEnvironment();
+      // cleanup
+      loginModule.closeContext();
+
+      // module config keys should not be passed to environment
+      for (LDAPLoginModule.ConfigKey configKey: LDAPLoginModule.ConfigKey.values()) {
+         assertEquals("value should not be set for key: " + configKey.getName(), null, environment.get(configKey.getName()));
+      }
+
+      // extra, non-module configs should be passed to environment
+      assertEquals("value should be set for key: " + "com.sun.jndi.ldap.tls.cbtype", "tls-server-end-point", environment.get("com.sun.jndi.ldap.tls.cbtype"));
+      assertEquals("value should be set for key: " + "randomConfig", "some-value", environment.get("randomConfig"));
+
+      // non-string configs should not be passed to environment
+      assertEquals("value should not be set for key: " + "non.string.1", null, environment.get("non.string.1"));
+      assertEquals("value should not be set for key: " + "non.string.2", null, environment.get("non.string.2"));
+
+      // environment configs should be set
+      assertEquals("value should be set for key: " + Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory", environment.get(Context.INITIAL_CONTEXT_FACTORY));
+      assertEquals("value should be set for key: " + Context.PROVIDER_URL, "ldap://localhost:1024", environment.get(Context.PROVIDER_URL));
+      assertEquals("value should be set for key: " + Context.REFERRAL, "ignore", environment.get(Context.REFERRAL));
+      assertEquals("value should be set for key: " + "com.sun.jndi.ldap.connect.timeout", "10000", environment.get("com.sun.jndi.ldap.connect.timeout"));
+      assertEquals("value should be set for key: " + "com.sun.jndi.ldap.read.timeout", "11000", environment.get("com.sun.jndi.ldap.read.timeout"));
+      assertEquals("value should be set for key: " + Context.SECURITY_AUTHENTICATION, "simple", environment.get(Context.SECURITY_AUTHENTICATION));
+      assertEquals("value should be set for key: " + Context.SECURITY_PRINCIPAL, PRINCIPAL, environment.get(Context.SECURITY_PRINCIPAL));
+      assertEquals("value should be set for key: " + Context.SECURITY_CREDENTIALS, CREDENTIALS, environment.get(Context.SECURITY_CREDENTIALS));
+      assertEquals("value should be set for key: " + Context.SECURITY_PROTOCOL, "s", environment.get(Context.SECURITY_PROTOCOL));
+   }
 
-   private boolean presentInArray(LDAPLoginProperty[] ldapProps, String propertyName) {
+   private boolean presentIn(Set<LDAPLoginProperty> ldapProps, String propertyName) {
       for (LDAPLoginProperty conf : ldapProps) {
          if (conf.getPropertyName().equals(propertyName) && (conf.getPropertyValue() != null && !"".equals(conf.getPropertyValue())))
             return true;
diff --git a/docs/user-manual/en/security.md b/docs/user-manual/en/security.md
index 88478ad..55f3ed8 100644
--- a/docs/user-manual/en/security.md
+++ b/docs/user-manual/en/security.md
@@ -819,6 +819,9 @@ system. It is implemented by
   testing or debugging; normally, it should be set to `false`, or omitted;
   default is `false`
 
+Any additional configuration option not recognized by the LDAP login module itself 
+is passed as-is to the underlying LDAP connection logic.
+
 Add user entries under the node specified by the `userBase` option. When
 creating a new user entry in the directory, choose an object class that
 supports the `userPassword` attribute (for example, the `person` or