You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by or...@apache.org on 2019/09/29 01:07:10 UTC

[qpid-broker-j] branch master updated (5856e5b -> e4687a4)

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

orudyy pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git.


    from 5856e5b  QPID-8361: [Broker-J] Create a developer guide for Qpid Broker-J
     new 4c7aeb2  QPID-8363: [Broker-J] Add support for GSSAPI authentication into SimpleLDAP authentication provider
     new e4687a4  QPID-8364: [Broker-J] Add support for SPNEGO authentication

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 broker-core/pom.xml                                |  11 +
 ...urityToken.java => TokenCarryingPrincipal.java} |   8 +-
 .../manager/KerberosAuthenticationManager.java     |  82 +++-
 .../auth/manager/LdapAuthenticationMethod.java     |  23 +-
 .../manager/SimpleLDAPAuthenticationManager.java   |  20 +-
 .../SimpleLDAPAuthenticationManagerImpl.java       | 207 ++++++---
 .../security/auth/manager/SpnegoAuthenticator.java | 293 ++++++++++++
 .../manager/KerberosAuthenticationManagerTest.java | 262 +++++++++++
 .../SimpleLDAPAuthenticationManagerTest.java       | 495 +++++++++++++++++++++
 .../auth/manager/SpnegoAuthenticatorTest.java      | 175 ++++++++
 .../qpid/server/test/EmbeddedKdcResource.java      | 196 ++++++++
 .../apache/qpid/server/test/KerberosUtilities.java | 189 ++++++++
 broker-core/src/test/resources/login.config        |  80 ++++
 .../src/test/resources/users.ldif                  |  38 +-
 .../server/management/plugin/HttpManagement.java   |   1 +
 .../auth/SpnegoInteractiveAuthenticator.java       | 102 +++++
 ...tor.java => SpnegoPreemptiveAuthenticator.java} |  39 +-
 .../plugin/filter/AuthenticationCheckFilter.java   |   4 +
 .../authenticationprovider/simpleldap/add.html     |  30 +-
 .../authenticationprovider/simpleldap/show.html    |   8 +
 .../authenticationprovider/simpleldap/add.js       |  36 +-
 .../authenticationprovider/simpleldap/show.js      |   7 +-
 ...-Security-Authentication-Providers-Kerberos.xml |  46 +-
 ...oker-Security-Authentication-Providers-LDAP.xml |  10 +
 pom.xml                                            |  26 ++
 .../qpid/test/utils/SystemPropertySetter.java      |  38 +-
 .../org/apache/qpid/test/utils/UnitTestBase.java   |  38 +-
 27 files changed, 2282 insertions(+), 182 deletions(-)
 copy broker-core/src/main/java/org/apache/qpid/server/security/{SecurityToken.java => TokenCarryingPrincipal.java} (87%)
 copy broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/codec/VariableWidthTypeConstructor.java => broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/LdapAuthenticationMethod.java (73%)
 create mode 100644 broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticator.java
 create mode 100644 broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManagerTest.java
 create mode 100644 broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerTest.java
 create mode 100644 broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticatorTest.java
 create mode 100644 broker-core/src/test/java/org/apache/qpid/server/test/EmbeddedKdcResource.java
 create mode 100644 broker-core/src/test/java/org/apache/qpid/server/test/KerberosUtilities.java
 create mode 100644 broker-core/src/test/resources/login.config
 copy joramtests/src/test/resources/jms-client/jndi.properties => broker-core/src/test/resources/users.ldif (55%)
 create mode 100644 broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoInteractiveAuthenticator.java
 copy broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/{OAuth2PreemptiveAuthenticator.java => SpnegoPreemptiveAuthenticator.java} (58%)
 copy broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQTypeMap.java => qpid-test-utils/src/main/java/org/apache/qpid/test/utils/SystemPropertySetter.java (56%)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org


[qpid-broker-j] 02/02: QPID-8364: [Broker-J] Add support for SPNEGO authentication

Posted by or...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

orudyy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git

commit e4687a493b97c3bb4d1101f00139bc09c0618fda
Author: Alex Rudyy <or...@apache.org>
AuthorDate: Wed Sep 25 17:42:10 2019 +0100

    QPID-8364: [Broker-J] Add support for SPNEGO authentication
---
 broker-core/pom.xml                                |   6 +
 .../server/security/TokenCarryingPrincipal.java    |  27 ++
 .../manager/KerberosAuthenticationManager.java     |  82 +++++-
 .../security/auth/manager/SpnegoAuthenticator.java | 293 +++++++++++++++++++++
 .../manager/KerberosAuthenticationManagerTest.java | 262 ++++++++++++++++++
 .../auth/manager/SpnegoAuthenticatorTest.java      | 175 ++++++++++++
 .../qpid/server/test/EmbeddedKdcResource.java      | 196 ++++++++++++++
 .../apache/qpid/server/test/KerberosUtilities.java | 189 +++++++++++++
 broker-core/src/test/resources/login.config        |  25 ++
 .../server/management/plugin/HttpManagement.java   |   1 +
 .../auth/SpnegoInteractiveAuthenticator.java       | 102 +++++++
 .../plugin/auth/SpnegoPreemptiveAuthenticator.java |  63 +++++
 .../plugin/filter/AuthenticationCheckFilter.java   |   4 +
 ...-Security-Authentication-Providers-Kerberos.xml |  46 +++-
 pom.xml                                            |   8 +
 .../qpid/test/utils/SystemPropertySetter.java      |  54 ++++
 .../org/apache/qpid/test/utils/UnitTestBase.java   |  38 +--
 17 files changed, 1535 insertions(+), 36 deletions(-)

diff --git a/broker-core/pom.xml b/broker-core/pom.xml
index d06f629..f69bf29 100644
--- a/broker-core/pom.xml
+++ b/broker-core/pom.xml
@@ -97,6 +97,12 @@
       <artifactId>apacheds-all</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.kerby</groupId>
+      <artifactId>kerb-simplekdc</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
    
   <build>
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/TokenCarryingPrincipal.java b/broker-core/src/main/java/org/apache/qpid/server/security/TokenCarryingPrincipal.java
new file mode 100644
index 0000000..87d47a2
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/TokenCarryingPrincipal.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.security;
+
+import java.util.Map;
+
+public interface TokenCarryingPrincipal extends QpidPrincipal
+{
+    Map<String, String> getTokens();
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManager.java b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManager.java
index 9cf4674..69a9776 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManager.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManager.java
@@ -18,14 +18,20 @@
  */
 package org.apache.qpid.server.security.auth.manager;
 
+import java.io.File;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.ConfiguredObject;
 import org.apache.qpid.server.model.Container;
 import org.apache.qpid.server.model.ManagedObject;
 import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
 import org.apache.qpid.server.model.NamedAddressSpace;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
 import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
 import org.apache.qpid.server.security.auth.sasl.SaslSettings;
 import org.apache.qpid.server.security.auth.sasl.kerberos.KerberosNegotiator;
@@ -33,13 +39,38 @@ import org.apache.qpid.server.security.auth.sasl.kerberos.KerberosNegotiator;
 @ManagedObject( category = false, type = "Kerberos" )
 public class KerberosAuthenticationManager extends AbstractAuthenticationManager<KerberosAuthenticationManager>
 {
+    private static final String JAAS_CONFIG_PROPERTY = "java.security.auth.login.config";
+    private static final String GSSAPI_SERVER_NAME = "qpid.auth.gssapi.serverName";
+    private static final String GSSAPI_SPNEGO_STRIP_REALM = "qpid.auth.gssapi.spnegoStripRealmFromPrincipalName";
+    static final String GSSAPI_SPNEGO_CONFIG = "qpid.auth.gssapi.spnegoConfigScope";
     public static final String PROVIDER_TYPE = "Kerberos";
     public static final String GSSAPI_MECHANISM = "GSSAPI";
 
+    private final Container<?> _container;
+    private final SpnegoAuthenticator _authenticator;
+    private volatile String _serverName;
+    private volatile String _spnegoConfig;
+    private volatile boolean _stripRealmFromPrincipalName;
+
     @ManagedObjectFactoryConstructor
     protected KerberosAuthenticationManager(final Map<String, Object> attributes, final Container<?> container)
     {
         super(attributes, container);
+        _container = container;
+        _authenticator = new SpnegoAuthenticator(this);
+    }
+
+    @Override
+    protected void onOpen()
+    {
+        super.onOpen();
+        final Set<String> contextKeys = getContextKeys(false);
+        _serverName = contextKeys.contains(GSSAPI_SERVER_NAME) ?
+                getContextValue(String.class, GSSAPI_SERVER_NAME) : null;
+        _spnegoConfig = contextKeys.contains(GSSAPI_SPNEGO_CONFIG) ?
+                getContextValue(String.class, GSSAPI_SPNEGO_CONFIG) : null;
+        _stripRealmFromPrincipalName = contextKeys.contains(GSSAPI_SPNEGO_STRIP_REALM) ?
+                getContextValue(Boolean.class, GSSAPI_SPNEGO_STRIP_REALM) : false;
     }
 
     @Override
@@ -55,11 +86,60 @@ public class KerberosAuthenticationManager extends AbstractAuthenticationManager
     {
         if(GSSAPI_MECHANISM.equals(mechanism))
         {
-            return new KerberosNegotiator(this, saslSettings.getLocalFQDN());
+            final String serverName = _serverName == null ? saslSettings.getLocalFQDN(): _serverName;
+            return new KerberosNegotiator(this, serverName);
         }
         else
         {
             return null;
         }
     }
+
+    public AuthenticationResult authenticate(String authorizationHeader)
+    {
+        return _authenticator.authenticate(authorizationHeader);
+    }
+
+    public String getSpnegoLoginConfigScope()
+    {
+        return _spnegoConfig;
+    }
+
+    public boolean isStripRealmFromPrincipalName()
+    {
+        return _stripRealmFromPrincipalName;
+    }
+
+    @Override
+    protected void validateOnCreate()
+    {
+        super.validateOnCreate();
+        validate(this);
+    }
+
+    @Override
+    protected void validateChange(final ConfiguredObject<?> proxyForValidation, final Set<String> changedAttributes)
+    {
+        super.validateChange(proxyForValidation, changedAttributes);
+        validate(proxyForValidation);
+    }
+
+    private void validate(final ConfiguredObject<?> authenticationProvider)
+    {
+        final String config = System.getProperty(JAAS_CONFIG_PROPERTY);
+        if (config != null && !new File(config).exists())
+        {
+            throw new IllegalConfigurationException(String.format(
+                    "A path to non-existing file is specified in JVM system property '%s'", JAAS_CONFIG_PROPERTY));
+        }
+
+        if (_container.getChildren(AuthenticationProvider.class)
+                      .stream()
+                      .anyMatch(p -> p instanceof KerberosAuthenticationManager
+                                     && p != authenticationProvider))
+        {
+            throw new IllegalConfigurationException("Another Kerberos authentication provider already exists."
+                                                    + " Only one Kerberos authentication provider can be created.");
+        }
+    }
 }
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticator.java b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticator.java
new file mode 100644
index 0000000..48cf4f9
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticator.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.security.auth.manager;
+
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import com.google.common.base.StandardSystemProperty;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.security.TokenCarryingPrincipal;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+
+public class SpnegoAuthenticator
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoAuthenticator.class);
+    public static final String REQUEST_AUTH_HEADER_NAME = "Authorization";
+    public static final String RESPONSE_AUTH_HEADER_NAME = "WWW-Authenticate";
+    public static final String RESPONSE_AUTH_HEADER_VALUE_NEGOTIATE = "Negotiate";
+    public static final String AUTH_TYPE = "SPNEGO";
+    static final String NEGOTIATE_PREFIX = "Negotiate ";
+    private final KerberosAuthenticationManager _kerberosProvider;
+
+    SpnegoAuthenticator(final KerberosAuthenticationManager kerberosProvider)
+    {
+        _kerberosProvider = kerberosProvider;
+    }
+
+    public AuthenticationResult authenticate(String authorizationHeader)
+    {
+        if (authorizationHeader == null)
+        {
+            if (LOGGER.isDebugEnabled())
+            {
+                LOGGER.debug("'Authorization' header is not set");
+            }
+            return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+        }
+        else
+        {
+            if (!hasNegotiatePrefix(authorizationHeader))
+            {
+                if (LOGGER.isDebugEnabled())
+                {
+                    LOGGER.debug("'Authorization' header value does not start with '{}'", NEGOTIATE_PREFIX);
+                }
+                return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+            }
+            else
+            {
+                final String negotiateToken = authorizationHeader.substring(NEGOTIATE_PREFIX.length());
+                final byte[] decodedNegotiateHeader;
+                try
+                {
+                    decodedNegotiateHeader = Base64.getDecoder().decode(negotiateToken);
+                }
+                catch (RuntimeException e)
+                {
+                    if (LOGGER.isDebugEnabled())
+                    {
+                        LOGGER.debug("Ticket decoding failed", e);
+                    }
+                    return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+                }
+
+                if (decodedNegotiateHeader.length == 0)
+                {
+                    if (LOGGER.isDebugEnabled())
+                    {
+                        LOGGER.debug("Empty ticket");
+                    }
+                    return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+                }
+                else
+                {
+                    return authenticate(decodedNegotiateHeader);
+                }
+            }
+        }
+    }
+
+    private boolean hasNegotiatePrefix(final String authorizationHeader)
+    {
+        if (authorizationHeader.length() > NEGOTIATE_PREFIX.length() )
+        {
+            return NEGOTIATE_PREFIX.equalsIgnoreCase(authorizationHeader.substring(0, NEGOTIATE_PREFIX.length()));
+        }
+        return false;
+    }
+
+    public AuthenticationResult authenticate(byte[] negotiateToken)
+    {
+        LoginContext loginContext = null;
+        try
+        {
+            loginContext = new LoginContext(_kerberosProvider.getSpnegoLoginConfigScope());
+            loginContext.login();
+            Subject subject = loginContext.getSubject();
+
+            return doAuthenticate(subject, negotiateToken);
+        }
+        catch (LoginException e)
+        {
+            LOGGER.error("JASS login failed", e);
+            return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
+        }
+        finally
+        {
+            if (loginContext != null)
+            {
+                try
+                {
+                    loginContext.logout();
+                }
+                catch (LoginException e)
+                {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    private AuthenticationResult doAuthenticate(final Subject subject, final byte[] negotiateToken)
+    {
+        GSSContext context = null;
+        try
+        {
+
+            final int credentialLifetime;
+            if (String.valueOf(System.getProperty(StandardSystemProperty.JAVA_VENDOR.key()))
+                      .toUpperCase()
+                      .contains("IBM"))
+            {
+                credentialLifetime = GSSCredential.INDEFINITE_LIFETIME;
+            }
+            else
+            {
+                credentialLifetime = GSSCredential.DEFAULT_LIFETIME;
+            }
+
+            final GSSManager manager = GSSManager.getInstance();
+            final PrivilegedExceptionAction<GSSCredential> credentialsAction =
+                    () -> manager.createCredential(null,
+                                                   credentialLifetime,
+                                                   new Oid("1.3.6.1.5.5.2"),
+                                                   GSSCredential.ACCEPT_ONLY);
+            final GSSContext gssContext = manager.createContext(Subject.doAs(subject, credentialsAction));
+            context = gssContext;
+
+            final PrivilegedExceptionAction<byte[]> acceptAction =
+                    () -> gssContext.acceptSecContext(negotiateToken, 0, negotiateToken.length);
+            final byte[] outToken = Subject.doAs(subject, acceptAction);
+
+            if (outToken == null)
+            {
+                LOGGER.debug("Ticket validation failed");
+                return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+            }
+
+            final PrivilegedAction<String> authenticationAction = () -> {
+                if (gssContext.isEstablished())
+                {
+                    GSSName gssName = null;
+                    try
+                    {
+                        gssName = gssContext.getSrcName();
+                    }
+                    catch (final GSSException e)
+                    {
+                        LOGGER.error("Unable to get src name from gss context", e);
+                    }
+
+                    if (gssName != null)
+                    {
+                        return stripRealmNameIfRequired(gssName.toString());
+                    }
+                }
+                return null;
+            };
+            final String principalName = Subject.doAs(subject, authenticationAction);
+            if (principalName != null)
+            {
+                TokenCarryingPrincipal principal = new TokenCarryingPrincipal()
+                {
+                    @Override
+                    public Map<String, String> getTokens()
+                    {
+                        return Collections.singletonMap(RESPONSE_AUTH_HEADER_NAME,
+                                                        NEGOTIATE_PREFIX + Base64.getEncoder().encodeToString(outToken));
+                    }
+
+                    @Override
+                    public ConfiguredObject<?> getOrigin()
+                    {
+                        return _kerberosProvider;
+                    }
+
+                    @Override
+                    public String getName()
+                    {
+                        return principalName;
+                    }
+                };
+                return new AuthenticationResult(principal);
+            }
+            return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+        }
+        catch (GSSException e)
+        {
+            if (LOGGER.isDebugEnabled())
+            {
+                LOGGER.debug("Ticket validation failed", e);
+            }
+            return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
+        }
+        catch (PrivilegedActionException e)
+        {
+            final Exception cause = e.getException();
+            if (cause instanceof GSSException)
+            {
+                if (LOGGER.isDebugEnabled())
+                {
+                    LOGGER.debug("Service login failed", e);
+                }
+            }
+            else
+            {
+                LOGGER.error("Service login failed", e);
+            }
+            return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
+        }
+        finally
+        {
+            if (context != null)
+            {
+                try
+                {
+                    context.dispose();
+                }
+                catch (GSSException e)
+                {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    private String stripRealmNameIfRequired(String name)
+    {
+        if (_kerberosProvider.isStripRealmFromPrincipalName() && name != null)
+        {
+            final int i = name.indexOf('@');
+            if (i > 0)
+            {
+                name = name.substring(0, i);
+            }
+        }
+        return name;
+    }
+}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManagerTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManagerTest.java
new file mode 100644
index 0000000..4fb6293
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/KerberosAuthenticationManagerTest.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.security.auth.manager;
+
+import static org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManager.GSSAPI_MECHANISM;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.net.URL;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.LoginContext;
+import javax.security.sasl.Sasl;
+import javax.security.sasl.SaslClient;
+
+import org.ietf.jgss.GSSException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.server.test.EmbeddedKdcResource;
+import org.apache.qpid.server.test.KerberosUtilities;
+import org.apache.qpid.test.utils.JvmVendor;
+import org.apache.qpid.test.utils.SystemPropertySetter;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+public class KerberosAuthenticationManagerTest extends UnitTestBase
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(KerberosAuthenticationManagerTest.class);
+    private static final String LOGIN_CONFIG = "login.config";
+    private static final String REALM = "QPID.ORG";
+    private static final String SERVER_NAME = "localhost";
+    private static final String SERVER_PROTOCOL = "AMQP";
+    private static final String SERVICE_PRINCIPAL_NAME = SERVER_PROTOCOL + "/" + SERVER_NAME;
+    private static final String SERVER_PRINCIPAL_FULL_NAME = SERVICE_PRINCIPAL_NAME + "@" + REALM;
+    private static final String CLIENT_PRINCIPAL_NAME = "client";
+    private static final String CLIENT_PRINCIPAL_FULL_NAME = CLIENT_PRINCIPAL_NAME + "@" + REALM;
+
+    private static final KerberosUtilities UTILS = new KerberosUtilities();
+
+    @ClassRule
+    public static final EmbeddedKdcResource KDC = new EmbeddedKdcResource(REALM);
+
+    @ClassRule
+    public static final SystemPropertySetter SYSTEM_PROPERTY_SETTER = new SystemPropertySetter();
+
+    private static File _clientKeyTabFile;
+
+    private KerberosAuthenticationManager _kerberosAuthenticationProvider;
+    private Broker<?> _broker;
+
+    @BeforeClass
+    public static void createKeyTabs() throws Exception
+    {
+        assumeThat(getJvmVendor(), not(JvmVendor.IBM));
+        KDC.createPrincipal("broker.keytab", SERVER_PRINCIPAL_FULL_NAME);
+        _clientKeyTabFile = KDC.createPrincipal("client.keytab", CLIENT_PRINCIPAL_FULL_NAME);
+        final URL resource = KerberosAuthenticationManagerTest.class.getClassLoader().getResource(LOGIN_CONFIG);
+        LOGGER.debug("JAAS config:" + resource);
+        assertNotNull(resource);
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("java.security.auth.login.config", resource.getPath());
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("javax.security.auth.useSubjectCredsOnly", "false");
+    }
+
+    @Before
+    public void setUp() throws Exception
+    {
+        Map<String, String> context = Collections.singletonMap(KerberosAuthenticationManager.GSSAPI_SPNEGO_CONFIG,
+                                                               "com.sun.security.jgss.accept");
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(AuthenticationProvider.NAME, getTestName());
+        attributes.put(AuthenticationProvider.CONTEXT, context);
+        _broker = BrokerTestHelper.createBrokerMock();
+        _kerberosAuthenticationProvider = new KerberosAuthenticationManager(attributes, _broker);
+        _kerberosAuthenticationProvider.create();
+        when(_broker.getChildren(AuthenticationProvider.class))
+                .thenReturn(Collections.singleton(_kerberosAuthenticationProvider));
+    }
+
+    @Test
+    public void testCreateSaslNegotiator() throws Exception
+    {
+        final SaslSettings saslSettings = mock(SaslSettings.class);
+        when(saslSettings.getLocalFQDN()).thenReturn(SERVER_NAME);
+        final SaslNegotiator negotiator = _kerberosAuthenticationProvider.createSaslNegotiator(GSSAPI_MECHANISM,
+                                                                                               saslSettings,
+                                                                                               null);
+        assertNotNull("Could not create SASL negotiator", negotiator);
+        try
+        {
+            final AuthenticationResult result = authenticate(negotiator);
+            assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+            assertEquals(new KerberosPrincipal(CLIENT_PRINCIPAL_FULL_NAME).getName(),
+                         result.getMainPrincipal().getName());
+        }
+        finally
+        {
+            negotiator.dispose();
+        }
+    }
+
+    @Test
+    public void testSeveralKerberosAuthenticationProviders()
+    {
+        final Map<String, Object> attributes =
+                Collections.singletonMap(AuthenticationProvider.NAME, getTestName() + "2");
+        final KerberosAuthenticationManager kerberosAuthenticationProvider =
+                new KerberosAuthenticationManager(attributes, _broker);
+        try
+        {
+            kerberosAuthenticationProvider.create();
+            fail("Exception expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            // pass
+        }
+    }
+
+    @Test
+    public void testCreateKerberosAuthenticationProvidersWithNonExistingJaasLoginModule()
+    {
+        when(_broker.getChildren(AuthenticationProvider.class)).thenReturn(Collections.emptySet());
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("java.security.auth.login.config",
+                                                 "config.module." + System.nanoTime());
+        final Map<String, Object> attributes = Collections.singletonMap(AuthenticationProvider.NAME, getTestName());
+        final KerberosAuthenticationManager kerberosAuthenticationProvider =
+                new KerberosAuthenticationManager(attributes, _broker);
+        try
+        {
+            kerberosAuthenticationProvider.create();
+            fail("Exception expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            // pass
+        }
+    }
+
+    @Test
+    public void testAuthenticateUsingNegotiationToken() throws GSSException
+    {
+        final String token =
+                Base64.getEncoder().encodeToString(UTILS.buildToken(CLIENT_PRINCIPAL_NAME, SERVICE_PRINCIPAL_NAME));
+        final String authenticationHeader = SpnegoAuthenticator.NEGOTIATE_PREFIX + token;
+
+        final AuthenticationResult result = _kerberosAuthenticationProvider.authenticate(authenticationHeader);
+
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+    }
+
+    private AuthenticationResult authenticate(final SaslNegotiator negotiator) throws Exception
+    {
+        final LoginContext lc = UTILS.createKerberosKeyTabLoginContext(getTestName(),
+                                                                       CLIENT_PRINCIPAL_FULL_NAME,
+                                                                       _clientKeyTabFile);
+        try
+        {
+            lc.login();
+            final Subject clientSubject = lc.getSubject();
+            final SaslClient saslClient = createSaslClient(clientSubject);
+            return performNegotiation(clientSubject, saslClient, negotiator);
+        }
+        finally
+        {
+            lc.logout();
+        }
+    }
+
+    private AuthenticationResult performNegotiation(final Subject clientSubject,
+                                                    final SaslClient saslClient,
+                                                    final SaslNegotiator negotiator)
+            throws PrivilegedActionException
+    {
+        AuthenticationResult result;
+        byte[] response = null;
+        boolean initiated = false;
+        do
+        {
+            if (!initiated)
+            {
+                initiated = true;
+                response = Subject.doAs(clientSubject, (PrivilegedExceptionAction<byte[]>) () -> {
+                    if (saslClient.hasInitialResponse())
+                    {
+                        return saslClient.evaluateChallenge(new byte[0]);
+                    }
+                    return null;
+                });
+            }
+
+            result = negotiator.handleResponse(response);
+
+            byte[] challenge = result.getChallenge();
+            if (challenge != null)
+            {
+                response = Subject.doAs(clientSubject,
+                                        (PrivilegedExceptionAction<byte[]>) () -> saslClient.evaluateChallenge(
+                                                challenge));
+            }
+        }
+        while (result.getStatus() == AuthenticationResult.AuthenticationStatus.CONTINUE);
+        return result;
+    }
+
+    private SaslClient createSaslClient(final Subject clientSubject) throws PrivilegedActionException
+    {
+        return Subject.doAs(clientSubject, (PrivilegedExceptionAction<SaslClient>) () -> {
+
+            final Map<String, String> props =
+                    Collections.singletonMap("javax.security.sasl.server.authentication", "true");
+
+            return Sasl.createSaslClient(new String[]{GSSAPI_MECHANISM},
+                                         null,
+                                         SERVER_PROTOCOL,
+                                         SERVER_NAME,
+                                         props,
+                                         null);
+        });
+    }
+}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticatorTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticatorTest.java
new file mode 100644
index 0000000..5b34736
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SpnegoAuthenticatorTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.security.auth.manager;
+
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.URL;
+import java.security.Principal;
+import java.util.Base64;
+import java.util.Map;
+
+import org.ietf.jgss.GSSException;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.security.TokenCarryingPrincipal;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.test.EmbeddedKdcResource;
+import org.apache.qpid.server.test.KerberosUtilities;
+import org.apache.qpid.test.utils.JvmVendor;
+import org.apache.qpid.test.utils.SystemPropertySetter;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+public class SpnegoAuthenticatorTest extends UnitTestBase
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(SpnegoAuthenticatorTest.class);
+    private static final String CLIENT_NAME = "client";
+    private static final String SERVER_NAME = "AMQP/localhost";
+    private static final String ANOTHER_SERVICE = "foo/localhost";
+    private static final String REALM = "QPID.ORG";
+    private static final String LOGIN_CONFIG = "login.config";
+    private static final KerberosUtilities UTILS = new KerberosUtilities();;
+
+    @ClassRule
+    public static final EmbeddedKdcResource KDC = new EmbeddedKdcResource(REALM);
+
+    @ClassRule
+    public static final SystemPropertySetter SYSTEM_PROPERTY_SETTER = new SystemPropertySetter();
+
+    private SpnegoAuthenticator _spnegoAuthenticator;
+    private KerberosAuthenticationManager _kerberosAuthenticationManager;
+
+    @BeforeClass
+    public static void createKeyTabs() throws Exception
+    {
+        assumeThat(getJvmVendor(), not(JvmVendor.IBM));
+        KDC.createPrincipal("broker.keytab", SERVER_NAME + "@" + REALM);
+        KDC.createPrincipal("client.keytab", CLIENT_NAME + "@" + REALM);
+        KDC.createPrincipal("another.keytab", ANOTHER_SERVICE + "@" + REALM);
+        final URL resource = KerberosAuthenticationManagerTest.class.getClassLoader().getResource(LOGIN_CONFIG);
+        LOGGER.debug("JAAS config:" + resource);
+        assertNotNull(resource);
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("java.security.auth.login.config", resource.getPath());
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("javax.security.auth.useSubjectCredsOnly", "false");
+    }
+
+    @Before
+    public void setUp()
+    {
+        _kerberosAuthenticationManager = mock(KerberosAuthenticationManager.class);
+        when(_kerberosAuthenticationManager.getSpnegoLoginConfigScope()).thenReturn("com.sun.security.jgss.accept");
+        when(_kerberosAuthenticationManager.isStripRealmFromPrincipalName()).thenReturn(true);
+
+        _spnegoAuthenticator = new SpnegoAuthenticator(_kerberosAuthenticationManager);
+    }
+
+    @Test
+    public void testAuthenticate() throws GSSException
+    {
+        final String token = Base64.getEncoder().encodeToString(buildToken(SERVER_NAME));
+        final String authenticationHeader = SpnegoAuthenticator.NEGOTIATE_PREFIX + token;
+
+        final AuthenticationResult result = _spnegoAuthenticator.authenticate(authenticationHeader);
+
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        final Principal principal = result.getMainPrincipal();
+        assertTrue(principal instanceof TokenCarryingPrincipal);
+        assertEquals(CLIENT_NAME, principal.getName());
+
+        final Map<String, String> tokens = ((TokenCarryingPrincipal)principal).getTokens();
+        assertNotNull(tokens);
+        assertTrue(tokens.containsKey(SpnegoAuthenticator.RESPONSE_AUTH_HEADER_NAME));
+    }
+
+    @Test
+    public void testAuthenticateNoAuthenticationHeader()
+    {
+        final AuthenticationResult result = _spnegoAuthenticator.authenticate((String) null);
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testAuthenticateNoNegotiatePrefix() throws GSSException
+    {
+        final String token = Base64.getEncoder().encodeToString(buildToken(SERVER_NAME));
+        final AuthenticationResult result = _spnegoAuthenticator.authenticate(token);
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testAuthenticateEmptyToken()
+    {
+        final AuthenticationResult result =
+                _spnegoAuthenticator.authenticate(SpnegoAuthenticator.NEGOTIATE_PREFIX + "");
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testAuthenticateInvalidToken()
+    {
+        final AuthenticationResult result =
+                _spnegoAuthenticator.authenticate(SpnegoAuthenticator.NEGOTIATE_PREFIX + "Zm9v");
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testAuthenticateWrongConfigName() throws GSSException
+    {
+        when(_kerberosAuthenticationManager.getSpnegoLoginConfigScope()).thenReturn("foo");
+        final String token = Base64.getEncoder().encodeToString(buildToken(SERVER_NAME));
+        final String authenticationHeader = SpnegoAuthenticator.NEGOTIATE_PREFIX + token;
+
+        final AuthenticationResult result = _spnegoAuthenticator.authenticate(authenticationHeader);
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testAuthenticateWrongServer() throws GSSException
+    {
+        final String token = Base64.getEncoder().encodeToString(buildToken(ANOTHER_SERVICE));
+        final String authenticationHeader = SpnegoAuthenticator.NEGOTIATE_PREFIX + token;
+
+        final AuthenticationResult result = _spnegoAuthenticator.authenticate(authenticationHeader);
+        assertNotNull(result);
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    private byte[] buildToken(final String anotherService) throws GSSException
+    {
+        return UTILS.buildToken(CLIENT_NAME, anotherService);
+    }
+}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/test/EmbeddedKdcResource.java b/broker-core/src/test/java/org/apache/qpid/server/test/EmbeddedKdcResource.java
new file mode 100644
index 0000000..cda31a8
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/test/EmbeddedKdcResource.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.nio.file.FileSystems;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.kerby.kerberos.kerb.KrbException;
+import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
+import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer;
+import org.junit.rules.ExternalResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EmbeddedKdcResource extends ExternalResource
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedKdcResource.class);
+    private final SimpleKdcServer _simpleKdcServer;
+    private final String _realm;
+    private final List<File> _createdFiles = new ArrayList<>();
+    private volatile File _kdcDirectory;
+
+    public EmbeddedKdcResource(final String realm)
+    {
+        this(InetAddress.getLoopbackAddress().getCanonicalHostName(), 0, "QpidTestKerberosServer", realm);
+    }
+
+    public EmbeddedKdcResource(final String host, final int port, final String serviceName, final String realm)
+    {
+        _realm = realm;
+        try
+        {
+            _simpleKdcServer = new SimpleKdcServer();
+            _simpleKdcServer.setKdcHost(host);
+            if (port > 0)
+            {
+                _simpleKdcServer.setKdcTcpPort(port);
+            }
+            _simpleKdcServer.setAllowUdp(false);
+            _simpleKdcServer.setKdcRealm(realm);
+            _simpleKdcServer.getKdcConfig().setString(KdcConfigKey.KDC_SERVICE_NAME, serviceName);
+        }
+        catch (KrbException e)
+        {
+            throw new AssertionError(String.format("Unable to create SimpleKdcServer': %s", e.getMessage()), e);
+        }
+    }
+
+    @Override
+    public void before() throws Exception
+    {
+        final Path targetDir = FileSystems.getDefault().getPath("target");
+        _kdcDirectory = Files.createTempDirectory(targetDir, "simple-kdc-").toFile();
+        _simpleKdcServer.setWorkDir(_kdcDirectory);
+        _simpleKdcServer.init();
+        _simpleKdcServer.start();
+    }
+
+    @Override
+    public void after()
+    {
+        try
+        {
+            _simpleKdcServer.stop();
+        }
+        catch (KrbException e)
+        {
+            LOGGER.warn("Failure to stop KDC server", e);
+        }
+        finally
+        {
+            try
+            {
+                delete(_kdcDirectory);
+            }
+            catch (IOException e)
+            {
+                LOGGER.warn("Failure to delete KDC directory", e);
+            }
+            for (File f: _createdFiles)
+            {
+                if (!f.delete())
+                {
+                    LOGGER.warn("Failure to delete file {}", f.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    public String getRealm()
+    {
+        return _realm;
+    }
+
+    private void delete(File f) throws IOException
+    {
+        Files.walkFileTree(f.toPath(),
+                           new SimpleFileVisitor<Path>()
+                           {
+                               @Override
+                               public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
+                                       throws IOException
+                               {
+                                   Files.delete(file);
+                                   return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult postVisitDirectory(final Path dir, final IOException exc)
+                                       throws IOException
+                               {
+                                   Files.delete(dir);
+                                   return FileVisitResult.CONTINUE;
+                               }
+                           });
+    }
+
+    public int getPort()
+    {
+        return _simpleKdcServer.getKdcTcpPort();
+    }
+
+    public File createPrincipal(String keyTabFileName, String... principals)
+            throws Exception
+    {
+        final File ketTabFile = createFile(keyTabFileName);
+        _createdFiles.add(ketTabFile);
+        createPrincipal(ketTabFile, principals);
+        return ketTabFile;
+    }
+
+    public void createPasswordPrincipal(String name, String password)
+            throws Exception
+    {
+        _simpleKdcServer.createPrincipal(name, password);
+    }
+
+    private void createPrincipal(File keyTabFile, String... principals)
+            throws Exception
+    {
+        _simpleKdcServer.createPrincipals(principals);
+        if (keyTabFile.exists() && !keyTabFile.delete())
+        {
+            LOGGER.error("Failed to delete keytab file: " + keyTabFile);
+        }
+        for (String principal : principals)
+        {
+            _simpleKdcServer.getKadmin().exportKeytab(keyTabFile, principal);
+        }
+    }
+
+    private static File createFile(final String keyTabFile) throws IOException
+    {
+        final File target = FileSystems.getDefault().getPath("target").toFile();
+        final File file = new File(target, keyTabFile);
+        if (file.exists())
+        {
+            if (!file.delete())
+            {
+                throw new IOException(String.format("Cannot delete existing file '%s'", keyTabFile));
+            }
+        }
+        if (!file.createNewFile())
+        {
+            throw new IOException(String.format("Cannot create file '%s'", keyTabFile));
+        }
+        return file;
+    }
+
+}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/test/KerberosUtilities.java b/broker-core/src/test/java/org/apache/qpid/server/test/KerberosUtilities.java
new file mode 100644
index 0000000..182df06
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/test/KerberosUtilities.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.test;
+
+import static java.lang.Boolean.TRUE;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.DestroyFailedException;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.TextOutputCallback;
+import javax.security.auth.kerberos.KerberosKey;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.kerberos.KeyTab;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KerberosUtilities
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(KerberosUtilities.class);
+    private static final String IBM_LOGIN_MODULE_CLASS = "com.ibm.security.auth.module.Krb5LoginModule";
+    private static final String SUN_LOGIN_MODULE_CLASS = "com.sun.security.auth.module.Krb5LoginModule";
+    public static final String KERBEROS_LOGIN_MODULE_CLASS =
+            System.getProperty("java.vendor").contains("IBM") ? IBM_LOGIN_MODULE_CLASS : SUN_LOGIN_MODULE_CLASS;
+
+    public byte[] buildToken(String clientPrincipalName, String targetServerPrincipalName) throws GSSException
+    {
+        final GSSManager manager = GSSManager.getInstance();
+        final GSSName clientName = manager.createName(clientPrincipalName, GSSName.NT_USER_NAME);
+        final GSSCredential credential = manager.createCredential(clientName,
+                                                                  GSSCredential.DEFAULT_LIFETIME,
+                                                                  new Oid("1.2.840.113554.1.2.2"),
+                                                                  GSSCredential.INITIATE_ONLY);
+
+        final GSSName serverName = manager.createName(targetServerPrincipalName, GSSName.NT_USER_NAME);
+        final Oid spnegoMechOid = new Oid("1.3.6.1.5.5.2");
+        final GSSContext clientContext = manager.createContext(serverName.canonicalize(spnegoMechOid),
+                                                               spnegoMechOid,
+                                                               credential,
+                                                               GSSContext.DEFAULT_LIFETIME);
+        try
+        {
+            clientContext.requestCredDeleg(true);
+            return clientContext.initSecContext(new byte[]{}, 0, 0);
+        }
+        finally
+        {
+            clientContext.dispose();
+        }
+    }
+
+    public LoginContext createKerberosKeyTabLoginContext(final String scopeName,
+                                                         final String principalName,
+                                                         final File keyTabFile)
+            throws LoginException
+    {
+        final KerberosPrincipal principal = new KerberosPrincipal(principalName);
+        final KeyTab keyTab = getKeyTab(principal, keyTabFile);
+        final Subject subject = new Subject(false,
+                                            Collections.singleton(principal),
+                                            Collections.emptySet(),
+                                            Collections.singleton(keyTab));
+
+        return createLoginContext(scopeName,
+                                  subject,
+                                  createKeyTabConfiguration(scopeName, keyTabFile, principal.getName()));
+    }
+
+    public KerberosKeyTabLoginConfiguration createKeyTabConfiguration(final String scopeName,
+                                                                      final File keyTabFile,
+                                                                      final String name)
+    {
+        return new KerberosKeyTabLoginConfiguration(scopeName, name, keyTabFile);
+    }
+
+
+    private LoginContext createLoginContext(final String serviceName, final Subject subject, final Configuration config)
+            throws LoginException
+    {
+        return new LoginContext(serviceName, subject, callbacks -> {
+            for (Callback callback : callbacks)
+            {
+                if (callback instanceof TextOutputCallback)
+                {
+                    LOGGER.error(((TextOutputCallback) callback).getMessage());
+                }
+            }
+        }, config);
+    }
+
+
+    private KeyTab getKeyTab(final KerberosPrincipal principal, final File keyTabFile)
+    {
+        if (!keyTabFile.exists() || !keyTabFile.canRead())
+        {
+            throw new IllegalArgumentException("Specified file does not exist or is not readable.");
+        }
+
+        final KeyTab keytab = KeyTab.getInstance(principal, keyTabFile);
+        if (!keytab.exists())
+        {
+            throw new IllegalArgumentException("Specified file is not a keyTab file.");
+        }
+
+        final KerberosKey[] keys = keytab.getKeys(principal);
+        if (keys.length == 0)
+        {
+            throw new IllegalArgumentException("Specified file does not contain at least one key for this principal.");
+        }
+
+        for (final KerberosKey key : keys)
+        {
+            try
+            {
+                key.destroy();
+            }
+            catch (DestroyFailedException e)
+            {
+                LOGGER.debug("Unable to destroy key", e);
+            }
+        }
+
+        return keytab;
+    }
+
+    public static class KerberosKeyTabLoginConfiguration extends Configuration
+    {
+        private final String _scopeName;
+        private final AppConfigurationEntry _entry;
+
+        KerberosKeyTabLoginConfiguration(final String scopeName,
+                                         final String principalName,
+                                         final File keyTabFile)
+        {
+            final Map<String, String> options = new HashMap<>();
+            options.put("principal", principalName);
+            options.put("useKeyTab", TRUE.toString());
+            options.put("keyTab", keyTabFile.getAbsolutePath());
+            options.put("refreshKrb5Config", TRUE.toString());
+            options.put("doNotPrompt", TRUE.toString());
+            _entry = new AppConfigurationEntry(KERBEROS_LOGIN_MODULE_CLASS,
+                                               AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                                               options);
+            _scopeName = scopeName;
+        }
+
+        @Override
+        public AppConfigurationEntry[] getAppConfigurationEntry(String name)
+        {
+            if (_scopeName.equals(name))
+            {
+                return new AppConfigurationEntry[]{_entry};
+            }
+            return new AppConfigurationEntry[0];
+        }
+    }
+}
diff --git a/broker-core/src/test/resources/login.config b/broker-core/src/test/resources/login.config
index d458516..4103d07 100644
--- a/broker-core/src/test/resources/login.config
+++ b/broker-core/src/test/resources/login.config
@@ -53,3 +53,28 @@ qpid-broker-j {
     principal="service/localhost"
     keyTab="target/kerberos.keytab";
 };
+
+com.sun.security.jgss.accept {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    storeKey=true
+    doNotPrompt=true
+    isInitiator=false
+    debug=true
+    refreshKrb5Config=true
+    realm="QPID.ORG"
+    principal="AMQP/localhost"
+    keyTab="target/broker.keytab";
+};
+
+com.sun.security.jgss.initiate {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    storeKey=true
+    doNotPrompt=true
+    debug=true
+    refreshKrb5Config=true
+    realm="QPID.ORG"
+    principal="client"
+    keyTab="target/client.keytab";
+};
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
index 4fa87ed..b97b6ca 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
@@ -140,6 +140,7 @@ public class HttpManagement extends AbstractPluginAdapter<HttpManagement> implem
     public static final String PLUGIN_TYPE = "MANAGEMENT-HTTP";
 
     public static final String DEFAULT_LOGOUT_URL = "/logout.html";
+    public static final String DEFAULT_LOGIN_URL = "/index.html";
 
     private static final String OPERATIONAL_LOGGING_NAME = "Web";
 
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoInteractiveAuthenticator.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoInteractiveAuthenticator.java
new file mode 100644
index 0000000..004a8f4
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoInteractiveAuthenticator.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.management.plugin.auth;
+
+import java.security.Principal;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.qpid.server.management.plugin.HttpManagement;
+import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
+import org.apache.qpid.server.management.plugin.HttpManagementUtil;
+import org.apache.qpid.server.management.plugin.HttpRequestInteractiveAuthenticator;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.Port;
+import org.apache.qpid.server.plugin.PluggableService;
+import org.apache.qpid.server.security.SubjectCreator;
+import org.apache.qpid.server.security.TokenCarryingPrincipal;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
+import org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManager;
+import org.apache.qpid.server.security.auth.manager.SpnegoAuthenticator;
+
+@PluggableService
+public class SpnegoInteractiveAuthenticator implements HttpRequestInteractiveAuthenticator
+{
+
+    @Override
+    public AuthenticationHandler getAuthenticationHandler(final HttpServletRequest request,
+                                                          final HttpManagementConfiguration configuration)
+    {
+        final AuthenticationProvider authenticationProvider = configuration.getAuthenticationProvider(request);
+        if (authenticationProvider instanceof KerberosAuthenticationManager)
+        {
+            final KerberosAuthenticationManager kerberosProvider =
+                    (KerberosAuthenticationManager) authenticationProvider;
+            return response -> {
+                final String authorizationHeader = request.getHeader(SpnegoAuthenticator.REQUEST_AUTH_HEADER_NAME);
+                final AuthenticationResult authenticationResult = kerberosProvider.authenticate(authorizationHeader);
+                if (authenticationResult == null
+                    || authenticationResult.getStatus() == AuthenticationResult.AuthenticationStatus.ERROR)
+                {
+                    response.setHeader(SpnegoAuthenticator.RESPONSE_AUTH_HEADER_NAME,
+                                       SpnegoAuthenticator.RESPONSE_AUTH_HEADER_VALUE_NEGOTIATE);
+                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+                }
+                else
+                {
+                    final Principal principal = authenticationResult.getMainPrincipal();
+                    if (principal instanceof TokenCarryingPrincipal)
+                    {
+                        ((TokenCarryingPrincipal) principal).getTokens().forEach(response::setHeader);
+                    }
+
+                    final Port<?> port = configuration.getPort(request);
+                    final SubjectCreator subjectCreator = port.getSubjectCreator(request.isSecure(), request.getServerName());
+                    final SubjectAuthenticationResult result = subjectCreator.createResultWithGroups(authenticationResult);
+                    final Subject subject = HttpManagementUtil.createServletConnectionSubject(request, result.getSubject());
+
+                    final Broker broker = (Broker) kerberosProvider.getParent();
+                    HttpManagementUtil.assertManagementAccess(broker, subject);
+                    HttpManagementUtil.saveAuthorisedSubject(request, subject);
+                    request.getRequestDispatcher(HttpManagement.DEFAULT_LOGIN_URL).forward(request, response);
+                }
+            };
+        }
+        return null;
+    }
+
+
+    @Override
+    public LogoutHandler getLogoutHandler(final HttpServletRequest request,
+                                          final HttpManagementConfiguration configuration)
+    {
+        return response -> response.sendRedirect(HttpManagement.DEFAULT_LOGOUT_URL);
+    }
+
+    @Override
+    public String getType()
+    {
+        return SpnegoAuthenticator.AUTH_TYPE;
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoPreemptiveAuthenticator.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoPreemptiveAuthenticator.java
new file mode 100644
index 0000000..35c070e
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/auth/SpnegoPreemptiveAuthenticator.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.server.management.plugin.auth;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
+import org.apache.qpid.server.management.plugin.HttpRequestPreemptiveAuthenticator;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Port;
+import org.apache.qpid.server.plugin.PluggableService;
+import org.apache.qpid.server.security.SubjectCreator;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.manager.SpnegoAuthenticator;
+import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
+import org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManager;
+
+@PluggableService
+public class SpnegoPreemptiveAuthenticator implements HttpRequestPreemptiveAuthenticator
+{
+
+    @Override
+    public Subject attemptAuthentication(final HttpServletRequest request,
+                                         final HttpManagementConfiguration configuration)
+    {
+        final AuthenticationProvider<?> authenticationProvider = configuration.getAuthenticationProvider(request);
+        if (authenticationProvider instanceof KerberosAuthenticationManager)
+        {
+            final KerberosAuthenticationManager kerberosProvider = (KerberosAuthenticationManager) authenticationProvider;
+            final String authorizationHeader = request.getHeader(SpnegoAuthenticator.REQUEST_AUTH_HEADER_NAME);
+            final AuthenticationResult authenticationResult = kerberosProvider.authenticate(authorizationHeader);
+            final Port<?> port = configuration.getPort(request);
+            final SubjectCreator subjectCreator = port.getSubjectCreator(request.isSecure(), request.getServerName());
+            final SubjectAuthenticationResult result = subjectCreator.createResultWithGroups(authenticationResult);
+            return result.getSubject();
+        }
+        return null;
+    }
+
+    @Override
+    public String getType()
+    {
+        return SpnegoAuthenticator.AUTH_TYPE;
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/filter/AuthenticationCheckFilter.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/filter/AuthenticationCheckFilter.java
index b8f89e9..5357232 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/filter/AuthenticationCheckFilter.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/filter/AuthenticationCheckFilter.java
@@ -42,6 +42,7 @@ import javax.servlet.http.HttpSession;
 
 import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
 import org.apache.qpid.server.management.plugin.HttpManagementUtil;
+import org.apache.qpid.server.security.TokenCarryingPrincipal;
 import org.apache.qpid.server.management.plugin.servlet.ServletConnectionPrincipal;
 import org.apache.qpid.server.model.Broker;
 import org.apache.qpid.server.security.auth.ManagementConnectionPrincipal;
@@ -98,6 +99,9 @@ public class AuthenticationCheckFilter implements Filter
                 else
                 {
                     subject = tryPreemptiveAuthentication(httpRequest);
+
+                    subject.getPrincipals(TokenCarryingPrincipal.class)
+                           .forEach(p -> p.getTokens().forEach(((HttpServletResponse) response)::setHeader));
                     isPreemptiveAuthentication = true;
                 }
             }
diff --git a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Kerberos.xml b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Kerberos.xml
index f710f1b..c69e5b2 100644
--- a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Kerberos.xml
+++ b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Kerberos.xml
@@ -53,7 +53,49 @@ com.sun.security.jgss.accept {
         environment where you are running (see the existing documentation for the C++ broker about
         creating a keytab file). </para>
 
-    <para> Note: You may need to install the "Java Cryptography Extension (JCE) Unlimited Strength
-        Jurisdiction Policy Files" appropriate for your JDK in order to get Kerberos support working. </para>
+    <section xml:id="Java-Broker-Security-Kerberos-Provider-Spnego">
+        <title>SPNEGO Authentication</title>
+        <para>
+            SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) based authentication can be configured
+            for Web Management Console and REST API.
+        </para>
+        <para>A special JAAS login configuration needs to be provided for
+            Service Principal Name (SPN) <emphasis>HTTP/{FQDN}@REALM</emphasis> in addition to configuration
+            provided for broker service principal in scope <emphasis>com.sun.security.jgss.accept</emphasis>.
+            An example of such SPNEGO configuration is provided below, </para>
+        <programlisting>
+spnego {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    storeKey=true
+    doNotPrompt=true
+    realm="EXAMPLE.COM"
+    useSubjectCredsOnly=false
+    kdc="kerberos.example.com"
+    keyTab="/path/to/keytab-file-for-HTTP-principal"
+    principal="HTTP/broker.org";
+};</programlisting>
+
+        <important>
+            <para>Please, note that in the example above a principal name is specified as
+            <emphasis>HTTP/broker.org</emphasis> where <emphasis>broker.org</emphasis> is supposed to be
+            a fully qualified name of the host where broker is running. The FQDN used to access the Broker
+            must match the host name in the SPN exactly otherwise the authentication will fail.</para>
+        </important>
+        <para>
+            A name of configuration module in the example above is <emphasis>spnego</emphasis>. It can be
+            communicated to the Kerberos authentication provider via context variable or JVM system property
+            <emphasis>qpid.auth.gssapi.spnegoConfigScope</emphasis>. For example,
+
+    <programlisting>
+        export QPID_OPTS=-Dqpid.auth.gssapi.spnegoConfigScope=spnego -Djavax.security.auth.useSubjectCredsOnly=false -Djava.security.auth.login.config=qpid.conf
+    </programlisting>
+        </para>
+        <para>
+            The RELM part in name of authenticated principal logged with SPNEGO mechanism can be stripped by
+            setting context variable <emphasis>qpid.auth.gssapi.spnegoStripRealmFromPrincipalName</emphasis> to
+            <emphasis>true</emphasis>.
+        </para>
+    </section>
 
 </section>
diff --git a/pom.xml b/pom.xml
index 2f17c3b..0d2cecb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,6 +153,7 @@
     <maven-surefire-report-plugin-version>2.22.0</maven-surefire-report-plugin-version>
     <h2.version>1.4.199</h2.version>
     <apache-directory-version>2.0.0-M23</apache-directory-version>
+    <kerby-version>1.0.1</kerby-version>
   </properties>
 
   <modules>
@@ -742,6 +743,13 @@
           </exclusion>
         </exclusions>
       </dependency>
+      <!-- kerby test dependency -->
+      <dependency>
+        <groupId>org.apache.kerby</groupId>
+        <artifactId>kerb-simplekdc</artifactId>
+        <scope>test</scope>
+        <version>${kerby-version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/SystemPropertySetter.java b/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/SystemPropertySetter.java
new file mode 100644
index 0000000..8f9f0d1
--- /dev/null
+++ b/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/SystemPropertySetter.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.qpid.test.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.rules.ExternalResource;
+
+public class SystemPropertySetter extends ExternalResource
+{
+    private Map<String, String> _storedProperties = new HashMap<>();
+
+    @Override
+    public synchronized void after()
+    {
+        _storedProperties.forEach(this::setProperty);
+    }
+
+    public synchronized void setSystemProperty(final String name, final String value)
+    {
+        _storedProperties.putIfAbsent(name, System.getProperty(name));
+        setProperty(name, value);
+    }
+
+    private void setProperty(final String name, final String value)
+    {
+        if (value == null)
+        {
+            System.clearProperty(name);
+        }
+        else
+        {
+            System.setProperty(name, value);
+        }
+    }
+}
diff --git a/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/UnitTestBase.java b/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/UnitTestBase.java
index 752885b..4dcfab0 100644
--- a/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/UnitTestBase.java
+++ b/qpid-test-utils/src/main/java/org/apache/qpid/test/utils/UnitTestBase.java
@@ -20,9 +20,7 @@
 
 package org.apache.qpid.test.utils;
 
-import java.util.HashMap;
 import java.util.LinkedHashSet;
-import java.util.Map;
 import java.util.Set;
 
 import com.google.common.base.StandardSystemProperty;
@@ -40,31 +38,17 @@ public class UnitTestBase
     @Rule
     public final TestName _testName = new TestName();
 
-    private final Map<String, String> _propertiesSetForTest = new HashMap<>();
+    @Rule
+    public final SystemPropertySetter _systemPropertySetter = new SystemPropertySetter();
+
     private final Set<Runnable> _tearDownRegistry = new LinkedHashSet<>();
 
     @After
     public void cleanupPostTest()
     {
-        revertSysProps();
-
         _tearDownRegistry.forEach(Runnable::run);
     }
 
-    private void revertSysProps()
-    {
-        _propertiesSetForTest.forEach((key, value) -> {
-            if (value != null)
-            {
-                System.setProperty(key, value);
-            }
-            else
-            {
-                System.clearProperty(key);
-            }
-        });
-    }
-
     public String getTestName()
     {
         return _testName.getMethodName();
@@ -72,19 +56,7 @@ public class UnitTestBase
 
     public void setTestSystemProperty(final String property, final String value)
     {
-        if (!_propertiesSetForTest.containsKey(property))
-        {
-            _propertiesSetForTest.put(property, System.getProperty(property));
-        }
-
-        if (value == null)
-        {
-            System.clearProperty(property);
-        }
-        else
-        {
-            System.setProperty(property, value);
-        }
+        _systemPropertySetter.setSystemProperty(property, value);
     }
 
     public int findFreePort()
@@ -103,7 +75,7 @@ public class UnitTestBase
         _tearDownRegistry.add(runnable);
     }
 
-    public JvmVendor getJvmVendor()
+    public static JvmVendor getJvmVendor()
     {
         final String property = String.valueOf(System.getProperty(StandardSystemProperty.JAVA_VENDOR.key())).toUpperCase();
         if (property.contains("IBM"))


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org


[qpid-broker-j] 01/02: QPID-8363: [Broker-J] Add support for GSSAPI authentication into SimpleLDAP authentication provider

Posted by or...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

orudyy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git

commit 4c7aeb273736baebd49cf5c0807359ca3f15ed7e
Author: Alex Rudyy <or...@apache.org>
AuthorDate: Sun Sep 15 00:14:19 2019 +0100

    QPID-8363: [Broker-J] Add support for GSSAPI authentication into SimpleLDAP authentication provider
---
 broker-core/pom.xml                                |   5 +
 .../auth/manager/LdapAuthenticationMethod.java     |  27 +-
 .../manager/SimpleLDAPAuthenticationManager.java   |  20 +-
 .../SimpleLDAPAuthenticationManagerImpl.java       | 207 ++++++---
 .../SimpleLDAPAuthenticationManagerTest.java       | 495 +++++++++++++++++++++
 broker-core/src/test/resources/login.config        |  55 +++
 broker-core/src/test/resources/users.ldif          |  50 +++
 .../authenticationprovider/simpleldap/add.html     |  30 +-
 .../authenticationprovider/simpleldap/show.html    |   8 +
 .../authenticationprovider/simpleldap/add.js       |  36 +-
 .../authenticationprovider/simpleldap/show.js      |   7 +-
 ...oker-Security-Authentication-Providers-LDAP.xml |  10 +
 pom.xml                                            |  18 +
 13 files changed, 878 insertions(+), 90 deletions(-)

diff --git a/broker-core/pom.xml b/broker-core/pom.xml
index e846559..d06f629 100644
--- a/broker-core/pom.xml
+++ b/broker-core/pom.xml
@@ -92,6 +92,11 @@
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.directory.server</groupId>
+      <artifactId>apacheds-all</artifactId>
+    </dependency>
+
   </dependencies>
    
   <build>
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/LdapAuthenticationMethod.java
similarity index 56%
copy from broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js
copy to broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/LdapAuthenticationMethod.java
index 9e13956..50335e5 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/LdapAuthenticationMethod.java
@@ -17,24 +17,23 @@
  * under the License.
  */
 
-define(["qpid/common/util", "dojo/domReady!"], function (util)
+package org.apache.qpid.server.security.auth.manager;
+
+public enum LdapAuthenticationMethod
 {
+    NONE("none"),
+    SIMPLE("simple"),
+    GSSAPI("GSSAPI");
+
+    private final String _method;
 
-    function SimpleLdapAuthenticationProvider(data)
+    LdapAuthenticationMethod(String method)
     {
-        this.fields = [];
-        var attributes = data.parent.management.metadata.getMetaData("AuthenticationProvider", "SimpleLDAP").attributes;
-        for (var name in attributes)
-        {
-            this.fields.push(name);
-        }
-        util.buildUI(data.containerNode, data.parent, "authenticationprovider/simpleldap/show.html", this.fields, this);
+        _method = method;
     }
 
-    SimpleLdapAuthenticationProvider.prototype.update = function (data)
+    public String getMethod()
     {
-        util.updateUI(data, this.fields, this);
+        return _method;
     }
-
-    return SimpleLdapAuthenticationProvider;
-});
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManager.java b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManager.java
index 4820675..fe650f7 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManager.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManager.java
@@ -43,10 +43,15 @@ public interface SimpleLDAPAuthenticationManager<X extends SimpleLDAPAuthenticat
     String PROVIDER_AUTH_URL = "providerAuthUrl";
     String SEARCH_CONTEXT = "searchContext";
     String LDAP_CONTEXT_FACTORY = "ldapContextFactory";
-    String SEARCH_USERNAME = "getSearchUsername";
-    String SEARCH_PASSWORD = "getSearchPassword";
+    String SEARCH_USERNAME = "searchUsername";
+    String SEARCH_PASSWORD = "searchPassword";
     String TRUST_STORE = "trustStore";
-
+    String SEARCH_FILTER = "searchFilter";
+    String GROUP_SEARCH_CONTEXT = "groupSearchContext";
+    String GROUP_SEARCH_FILTER = "groupSearchFilter";
+    String AUTHENTICATION_METHOD ="authenticationMethod";
+    String LOGIN_CONFIG_SCOPE = "loginConfigScope";
+    String LOGIN_CONFIG_SCOPE_DEFAULT = "qpid-broker-j";
 
     @ManagedAttribute( description = "LDAP server URL", mandatory = true)
     String getProviderUrl();
@@ -90,6 +95,15 @@ public interface SimpleLDAPAuthenticationManager<X extends SimpleLDAPAuthenticat
     @ManagedAttribute( description = "Defines the group search scope. If true the search for group entries is performed in the entire subtree of the group search context")
     boolean isGroupSubtreeSearchScope();
 
+    @ManagedAttribute(description = "Method of authentication to use when binding into LDAP. Supported methods: NONE, SIMPLE, GSSAPI.",
+            defaultValue = "NONE")
+    LdapAuthenticationMethod getAuthenticationMethod();
+
+    @ManagedAttribute(description = "The scope of JAAS configuration from login module to use to obtain Kerberos"
+                                    + " initiator credentials when the authentication method is GSSAPI",
+            defaultValue = LOGIN_CONFIG_SCOPE_DEFAULT)
+    String getLoginConfigScope();
+
     @DerivedAttribute
     List<String> getTlsProtocolWhiteList();
 
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerImpl.java b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerImpl.java
index 863f421..7a18c6c 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerImpl.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerImpl.java
@@ -25,6 +25,8 @@ import static java.util.Collections.unmodifiableList;
 
 import java.security.GeneralSecurityException;
 import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -47,6 +49,9 @@ import javax.naming.directory.SearchResult;
 import javax.net.SocketFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
 
 import com.google.common.util.concurrent.ListenableFuture;
 import org.slf4j.Logger;
@@ -69,23 +74,27 @@ import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
 import org.apache.qpid.server.security.auth.sasl.SaslSettings;
 import org.apache.qpid.server.security.auth.sasl.plain.PlainNegotiator;
 import org.apache.qpid.server.security.group.GroupPrincipal;
+import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
 import org.apache.qpid.server.util.CipherSuiteAndProtocolRestrictingSSLSocketFactory;
 import org.apache.qpid.server.util.ParameterizedTypes;
+import org.apache.qpid.server.util.ServerScopedRuntimeException;
 import org.apache.qpid.server.util.StringUtil;
-import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
 
-public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationManager<SimpleLDAPAuthenticationManagerImpl>
+public class SimpleLDAPAuthenticationManagerImpl
+        extends AbstractAuthenticationManager<SimpleLDAPAuthenticationManagerImpl>
         implements SimpleLDAPAuthenticationManager<SimpleLDAPAuthenticationManagerImpl>
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(SimpleLDAPAuthenticationManagerImpl.class);
 
     private static final List<String> CONNECTIVITY_ATTRS = unmodifiableList(Arrays.asList(PROVIDER_URL,
-                                                                             PROVIDER_AUTH_URL,
-                                                                             SEARCH_CONTEXT,
-                                                                             LDAP_CONTEXT_FACTORY,
-                                                                             SEARCH_USERNAME,
-                                                                             SEARCH_PASSWORD,
-                                                                             TRUST_STORE));
+                                                                                          PROVIDER_AUTH_URL,
+                                                                                          SEARCH_CONTEXT,
+                                                                                          LDAP_CONTEXT_FACTORY,
+                                                                                          SEARCH_USERNAME,
+                                                                                          SEARCH_PASSWORD,
+                                                                                          TRUST_STORE,
+                                                                                          LOGIN_CONFIG_SCOPE,
+                                                                                          AUTHENTICATION_METHOD));
 
     /**
      * Environment key to instruct {@link InitialDirContext} to override the socket factory.
@@ -131,6 +140,12 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
     @ManagedAttributeField
     private boolean _groupSubtreeSearchScope;
 
+    @ManagedAttributeField
+    private LdapAuthenticationMethod _authenticationMethod;
+
+    @ManagedAttributeField
+    private String _loginConfigScope;
+
     private List<String> _tlsProtocolWhiteList;
     private List<String>  _tlsProtocolBlackList;
 
@@ -154,9 +169,7 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
     protected void validateOnCreate()
     {
         super.validateOnCreate();
-
-        Class<? extends SocketFactory> sslSocketFactoryOverrideClass = createSslSocketFactoryOverrideClass(_trustStore);
-        validateInitialDirContext(sslSocketFactoryOverrideClass, _providerUrl, _searchUsername, _searchPassword);
+        validateInitialDirContext(this);
     }
 
     @Override
@@ -166,11 +179,8 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
 
         if (!disjoint(changedAttributes, CONNECTIVITY_ATTRS))
         {
-            SimpleLDAPAuthenticationManager changed = (SimpleLDAPAuthenticationManager)proxyForValidation;
-            TrustStore changedTruststore = changed.getTrustStore();
-            Class<? extends SocketFactory> sslSocketFactoryOverrideClass = createSslSocketFactoryOverrideClass(changedTruststore);
-            validateInitialDirContext(sslSocketFactoryOverrideClass, changed.getProviderUrl(), changed.getSearchUsername(),
-                                      changed.getSearchPassword());
+            SimpleLDAPAuthenticationManager changed = (SimpleLDAPAuthenticationManager) proxyForValidation;
+            validateInitialDirContext(changed);
         }
     }
 
@@ -279,6 +289,18 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
     }
 
     @Override
+    public LdapAuthenticationMethod getAuthenticationMethod()
+    {
+        return _authenticationMethod;
+    }
+
+    @Override
+    public String getLoginConfigScope()
+    {
+        return _loginConfigScope;
+    }
+
+    @Override
     public List<String> getMechanisms()
     {
         return singletonList(PlainNegotiator.MECHANISM);
@@ -307,10 +329,24 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
 
     private AuthenticationResult doLDAPNameAuthentication(String userId, String password)
     {
+        Subject gssapiIdentity = null;
+        if (LdapAuthenticationMethod.GSSAPI.equals(getAuthenticationMethod()))
+        {
+            try
+            {
+                gssapiIdentity = doGssApiLogin(getLoginConfigScope());
+            }
+            catch (LoginException e)
+            {
+                LOGGER.warn("JAAS Login failed", e);
+                return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
+            }
+        }
+
         final String name;
         try
         {
-            name = getNameFromId(userId);
+            name = getNameFromId(userId, gssapiIdentity);
         }
         catch (NamingException e)
         {
@@ -334,7 +370,7 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         InitialDirContext ctx = null;
         try
         {
-            ctx = createInitialDirContext(env, _sslSocketFactoryOverrideClass);
+            ctx = createInitialDirContext(env, _sslSocketFactoryOverrideClass, gssapiIdentity);
 
             Set<Principal> groups = Collections.emptySet();
             if (isGroupSearchRequired())
@@ -342,9 +378,9 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
                 if (!providerAuthUrl.equals(getProviderUrl()))
                 {
                     closeSafely(ctx);
-                    ctx = createSearchInitialDirContext();
+                    ctx = createSearchInitialDirContext(gssapiIdentity);
                 }
-                groups = findGroups(ctx, name);
+                groups = findGroups(ctx, name, gssapiIdentity);
             }
 
             //Authentication succeeded
@@ -402,7 +438,8 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         return value != null && !"".equals(value);
     }
 
-    private Set<Principal> findGroups(DirContext context, String userDN) throws NamingException
+    private Set<Principal> findGroups(DirContext context, String userDN, final Subject gssapiIdentity)
+            throws NamingException
     {
         Set<Principal> groupPrincipals = new HashSet<>();
         if (getGroupAttributeName() != null && !"".equals(getGroupAttributeName()))
@@ -436,10 +473,11 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
             searchControls.setSearchScope(isGroupSubtreeSearchScope()
                                                   ? SearchControls.SUBTREE_SCOPE
                                                   : SearchControls.ONELEVEL_SCOPE);
-            NamingEnumeration<?> groupEnumeration = context.search(getGroupSearchContext(),
-                                                                   getGroupSearchFilter(),
-                                                                   new String[]{encode(userDN)},
-                                                                   searchControls);
+            PrivilegedExceptionAction<NamingEnumeration<?>> search = () -> context.search(getGroupSearchContext(),
+                                                                                          getGroupSearchFilter(),
+                                                                                          new String[]{encode(userDN)},
+                                                                                          searchControls);
+            NamingEnumeration<?> groupEnumeration = invokeContextOperationAs(gssapiIdentity, search);
             while (groupEnumeration.hasMore())
             {
                 SearchResult result = (SearchResult) groupEnumeration.next();
@@ -491,8 +529,9 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         return env;
     }
 
-    private InitialDirContext createInitialDirContext(Hashtable<String, Object> env,
-                                                      Class<? extends SocketFactory> sslSocketFactoryOverrideClass) throws NamingException
+    private InitialDirContext createInitialDirContext(final Hashtable<String, Object> env,
+                                                      final Class<? extends SocketFactory> sslSocketFactoryOverrideClass,
+                                                      final Subject gssapiIdentity) throws NamingException
     {
         ClassLoader existingContextClassLoader = null;
 
@@ -508,7 +547,7 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
                 Thread.currentThread().setContextClassLoader(sslSocketFactoryOverrideClass.getClassLoader());
                 revertContentClassLoader = true;
             }
-            return new InitialDirContext(env);
+            return invokeContextOperationAs(gssapiIdentity, () -> new InitialDirContext(env));
         }
         finally
         {
@@ -564,27 +603,45 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
                ", providerUrl=" + _providerUrl + ", providerAuthUrl=" + _providerAuthUrl +
                ", searchContext=" + _searchContext + ", state=" + getState() +
                ", searchFilter=" + _searchFilter + ", ldapContextFactory=" + _ldapContextFactory +
-               ", bindWithoutSearch=" + _bindWithoutSearch  + ", trustStore=" + _trustStore  +
-               ", searchUsername=" + _searchUsername + "]";
+               ", bindWithoutSearch=" + _bindWithoutSearch + ", trustStore=" + _trustStore +
+               ", searchUsername=" + _searchUsername + ", loginConfigScope=" + _loginConfigScope +
+               ", authenticationMethod=" + _authenticationMethod + "]";
     }
 
-    private void validateInitialDirContext(Class<? extends SocketFactory> sslSocketFactoryOverrideClass,
-                                           final String providerUrl,
-                                           final String searchUsername, final String searchPassword)
+    private void validateInitialDirContext(final SimpleLDAPAuthenticationManager<?> authenticationProvider)
     {
-        Hashtable<String,Object> env = createInitialDirContextEnvironment(providerUrl);
+        final TrustStore truststore = authenticationProvider.getTrustStore();
+        final Class<? extends SocketFactory> sslSocketFactoryOverrideClass =
+                createSslSocketFactoryOverrideClass(truststore);
 
-        setupSearchContext(env, searchUsername, searchPassword);
+        final Hashtable<String, Object> env =
+                createInitialDirContextEnvironment(authenticationProvider.getProviderUrl());
+        setAuthenticationProperties(env,
+                                    authenticationProvider.getSearchUsername(),
+                                    authenticationProvider.getSearchPassword(),
+                                    authenticationProvider.getAuthenticationMethod());
 
         InitialDirContext ctx = null;
         try
         {
-            ctx = createInitialDirContext(env, sslSocketFactoryOverrideClass);
+            Subject gssapiIdentity = null;
+            if (LdapAuthenticationMethod.GSSAPI.equals(authenticationProvider.getAuthenticationMethod()))
+            {
+                gssapiIdentity = doGssApiLogin(authenticationProvider.getLoginConfigScope());
+            }
+            ctx = createInitialDirContext(env, sslSocketFactoryOverrideClass, gssapiIdentity);
         }
         catch (NamingException e)
         {
-            LOGGER.error("Failed to establish connectivity to the ldap server for '{}'", providerUrl, e);
-            throw new IllegalConfigurationException("Failed to establish connectivity to the ldap server." , e);
+            LOGGER.debug("Failed to establish connectivity to the ldap server for '{}'",
+                         authenticationProvider.getProviderUrl(),
+                         e);
+            throw new IllegalConfigurationException("Failed to establish connectivity to the ldap server.", e);
+        }
+        catch (LoginException e)
+        {
+            LOGGER.debug("JAAS login failed ", e);
+            throw new IllegalConfigurationException("JAAS login failed.", e);
         }
         finally
         {
@@ -592,14 +649,26 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         }
     }
 
-    private void setupSearchContext(final Hashtable<String, Object> env,
-                                    final String searchUsername, final String searchPassword)
+    private void setAuthenticationProperties(final Hashtable<String, Object> env,
+                                             final String userName,
+                                             final String password,
+                                             final LdapAuthenticationMethod authenticationMethod)
     {
-        if(_searchUsername != null && _searchUsername.trim().length()>0)
+        if (LdapAuthenticationMethod.GSSAPI.equals(authenticationMethod))
+        {
+            env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+        }
+        else if (LdapAuthenticationMethod.SIMPLE.equals(authenticationMethod))
         {
             env.put(Context.SECURITY_AUTHENTICATION, "simple");
-            env.put(Context.SECURITY_PRINCIPAL, searchUsername);
-            env.put(Context.SECURITY_CREDENTIALS, searchPassword);
+            if (userName != null)
+            {
+                env.put(Context.SECURITY_PRINCIPAL, userName);
+            }
+            if (password != null)
+            {
+                env.put(Context.SECURITY_CREDENTIALS, password);
+            }
         }
         else
         {
@@ -607,11 +676,12 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         }
     }
 
-    private String getNameFromId(String id) throws NamingException
+    private String getNameFromId(final String id, final Subject gssapiIdentity)
+            throws NamingException
     {
-        if(!isBindWithoutSearch())
+        if (!isBindWithoutSearch())
         {
-            InitialDirContext ctx = createSearchInitialDirContext();
+            InitialDirContext ctx = createSearchInitialDirContext(gssapiIdentity);
 
             try
             {
@@ -622,7 +692,13 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
                 NamingEnumeration<?> namingEnum = null;
 
                 LOGGER.debug("Searching for '{}'", id);
-                namingEnum = ctx.search(_searchContext, _searchFilter, new String[]{id}, searchControls);
+                namingEnum = invokeContextOperationAs(gssapiIdentity,
+                                                      (PrivilegedExceptionAction<NamingEnumeration<?>>) () -> ctx.search(
+                                                              _searchContext,
+                                                              _searchFilter,
+                                                              new String[]{id},
+                                                              searchControls));
+
                 if (namingEnum.hasMore())
                 {
                     SearchResult result = (SearchResult) namingEnum.next();
@@ -645,14 +721,45 @@ public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationM
         {
             return id;
         }
+    }
 
+    private <T> T invokeContextOperationAs(final Subject identity, final PrivilegedExceptionAction<T> action)
+            throws NamingException
+    {
+        try
+        {
+            return Subject.doAs(identity, action);
+        }
+        catch (PrivilegedActionException e)
+        {
+            final Exception exception = e.getException();
+            if (exception instanceof NamingException)
+            {
+                throw (NamingException) exception;
+            }
+            else if (exception instanceof RuntimeException)
+            {
+                throw (RuntimeException) exception;
+            }
+            else
+            {
+                throw new ServerScopedRuntimeException(exception);
+            }
+        }
+    }
+
+    private Subject doGssApiLogin(final String configScope) throws LoginException
+    {
+        LoginContext loginContext = new LoginContext(configScope);
+        loginContext.login();
+        return loginContext.getSubject();
     }
 
-    private InitialDirContext createSearchInitialDirContext() throws NamingException
+    private InitialDirContext createSearchInitialDirContext(final Subject gssapiIdentity) throws NamingException
     {
         Hashtable<String, Object> env = createInitialDirContextEnvironment(_providerUrl);
-        setupSearchContext(env, _searchUsername, _searchPassword);
-        return createInitialDirContext(env, _sslSocketFactoryOverrideClass);
+        setAuthenticationProperties(env, _searchUsername, _searchPassword, _authenticationMethod);
+        return createInitialDirContext(env, _sslSocketFactoryOverrideClass, gssapiIdentity);
     }
 
 
diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerTest.java
new file mode 100644
index 0000000..b5dd2a1
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/SimpleLDAPAuthenticationManagerTest.java
@@ -0,0 +1,495 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.auth.manager;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.qpid.server.security.auth.manager.CachingAuthenticationProvider.AUTHENTICATION_CACHE_MAX_SIZE;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosPrincipal;
+
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.util.Strings;
+import org.apache.directory.server.annotations.CreateKdcServer;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.annotations.SaslMechanism;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.integ.CreateLdapServerRule;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import org.apache.directory.server.factory.ServerAnnotationProcessor;
+import org.apache.directory.server.kerberos.kdc.KdcServer;
+import org.apache.directory.server.kerberos.shared.crypto.encryption.KerberosKeyFactory;
+import org.apache.directory.server.kerberos.shared.keytab.Keytab;
+import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
+import org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.apache.directory.shared.kerberos.KerberosTime;
+import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
+import org.apache.directory.shared.kerberos.components.EncryptionKey;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.SocketConnectionPrincipal;
+import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.test.utils.JvmVendor;
+import org.apache.qpid.test.utils.SystemPropertySetter;
+import org.apache.qpid.test.utils.TestFileUtils;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+@CreateDS(
+        name = "testDS",
+        partitions =
+                {
+                        @CreatePartition(name = "test", suffix = "dc=qpid,dc=org")
+                },
+        additionalInterceptors =
+                {
+                        KeyDerivationInterceptor.class
+                }
+)
+@CreateLdapServer(
+        transports =
+                {
+                        @CreateTransport(protocol = "LDAP")
+                },
+        allowAnonymousAccess = true,
+        saslHost = "localhost",
+        saslPrincipal = "ldap/localhost@QPID.ORG",
+        saslMechanisms =
+                {
+                        @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = PlainMechanismHandler.class),
+                        @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = GssapiMechanismHandler.class)
+                }
+)
+@CreateKdcServer(
+        transports =
+                {
+                        @CreateTransport(protocol = "TCP", port = 0)
+                },
+        searchBaseDn = "ou=users,dc=qpid,dc=org")
+@ApplyLdifFiles("users.ldif")
+public class SimpleLDAPAuthenticationManagerTest extends UnitTestBase
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleLDAPAuthenticationManagerTest.class);
+    private static final String ROOT = "dc=qpid,dc=org";
+    private static final String USERS_DN = "ou=users," + ROOT;
+    private static final String SEARCH_CONTEXT_VALUE = USERS_DN;
+    private static final String SEARCH_FILTER_VALUE = "(uid={0})";
+    private static final String LDAP_URL_TEMPLATE = "ldap://localhost:%d";
+    private static final String USER_1_NAME = "test1";
+    private static final String USER_1_PASSWORD = "password1";
+    private static final String USER_1_DN = "cn=integration-test1,ou=users,dc=qpid,dc=org";
+    private static final String GROUP_SEARCH_CONTEXT_VALUE = "ou=groups,dc=qpid,dc=org";
+    private static final String GROUP_SEARCH_FILTER_VALUE = "(member={0})";
+    private static final String LDAP_SERVICE_NAME = "ldap";
+    private static final String REALM = "QPID.ORG";
+    private static final String HOSTNAME = "localhost";
+    private static final String BROKER_PRINCIPAL = "service/" + HOSTNAME;
+    private static final String LINE_SEPARATOR = System.lineSeparator();
+    private static final String LOGIN_CONFIG = "login.config";
+    private static final String LOGIN_SCOPE = "ldap-gssapi-bind";
+    private static final AtomicBoolean KERBEROS_SETUP = new AtomicBoolean();
+
+    @ClassRule
+    public static CreateLdapServerRule LDAP = new CreateLdapServerRule();
+
+    @ClassRule
+    public static final SystemPropertySetter SYSTEM_PROPERTY_SETTER = new SystemPropertySetter();
+
+    private SimpleLDAPAuthenticationManager _authenticationProvider;
+
+    @Before
+    public void setUp()
+    {
+        _authenticationProvider = createAuthenticationProvider();
+    }
+
+    @After
+    public void tearDown()
+    {
+        if (_authenticationProvider != null)
+        {
+            _authenticationProvider.close();
+        }
+    }
+
+    @Test
+    public void testAuthenticateSuccess()
+    {
+        final AuthenticationResult result = _authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+    }
+
+    @Test
+    public void testAuthenticateFailure()
+    {
+        final AuthenticationResult result = _authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD + "_");
+        assertEquals(AuthenticationResult.AuthenticationStatus.ERROR, result.getStatus());
+    }
+
+    @Test
+    public void testSaslPlainNegotiatorPlain()
+    {
+        final SaslSettings saslSettings = mock(SaslSettings.class);
+        when(saslSettings.getLocalFQDN()).thenReturn(HOSTNAME);
+
+        final SaslNegotiator negotiator = _authenticationProvider.createSaslNegotiator("PLAIN", saslSettings, null);
+        assertNotNull("Could not create SASL negotiator for mechanism 'PLAIN'", negotiator);
+
+        final AuthenticationResult result = negotiator.handleResponse(new byte[0]);
+        assertEquals("Unexpected authentication status",
+                     AuthenticationResult.AuthenticationStatus.CONTINUE,
+                     result.getStatus());
+
+        final AuthenticationResult result2 =
+                negotiator.handleResponse(String.format("\0%s\0%s", USER_1_NAME, USER_1_PASSWORD).getBytes(UTF_8));
+
+        assertEquals("Unexpected authentication status",
+                     AuthenticationResult.AuthenticationStatus.SUCCESS,
+                     result2.getStatus());
+    }
+
+    @Test
+    public void testGroups()
+    {
+        _authenticationProvider.close();
+        final Map<String, Object> groupSetUp = new HashMap<>();
+        groupSetUp.put(SimpleLDAPAuthenticationManager.GROUP_SEARCH_CONTEXT, GROUP_SEARCH_CONTEXT_VALUE);
+        groupSetUp.put(SimpleLDAPAuthenticationManager.GROUP_SEARCH_FILTER, GROUP_SEARCH_FILTER_VALUE);
+        _authenticationProvider = createAuthenticationProvider(groupSetUp);
+
+        final AuthenticationResult result = _authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+
+        final Set<Principal> principals = result.getPrincipals();
+        assertNotNull(principals);
+
+        final Principal groupPrincipal = principals.stream()
+                                                   .filter(p -> "cn=group1,ou=groups,dc=qpid,dc=org".equalsIgnoreCase(p.getName()))
+                                                   .findFirst()
+                                                   .orElse(null);
+        assertNotNull(groupPrincipal);
+    }
+
+    @Test
+    public void testAuthenticateSuccessWhenCachingEnabled()
+    {
+        _authenticationProvider.close();
+        _authenticationProvider = createCachingAuthenticationProvider();
+
+        final SocketConnectionPrincipal principal = mock(SocketConnectionPrincipal.class);
+        when(principal.getRemoteAddress()).thenReturn(new InetSocketAddress(HOSTNAME, 5672));
+        final Subject subject =
+                new Subject(true, Collections.singleton(principal), Collections.emptySet(), Collections.emptySet());
+        final AuthenticationResult result = Subject.doAs(subject,
+                                                         (PrivilegedAction<AuthenticationResult>) () -> _authenticationProvider
+                                                                 .authenticate(USER_1_NAME, USER_1_PASSWORD));
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+    }
+
+    @Test
+    public void testGssapiBindWithKeyTab() throws Exception
+    {
+        setUpKerberosAndJaas();
+
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.AUTHENTICATION_METHOD, LdapAuthenticationMethod.GSSAPI.name());
+        attributes.put(SimpleLDAPAuthenticationManager.LOGIN_CONFIG_SCOPE, LOGIN_SCOPE);
+        final SimpleLDAPAuthenticationManagerImpl authenticationProvider = createAuthenticationProvider(attributes);
+        final AuthenticationResult result = authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+    }
+
+    @Test
+    public void testChangeAuthenticationToGssapi() throws Exception
+    {
+        setUpKerberosAndJaas();
+
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.AUTHENTICATION_METHOD, LdapAuthenticationMethod.GSSAPI.name());
+        attributes.put(SimpleLDAPAuthenticationManager.LOGIN_CONFIG_SCOPE, LOGIN_SCOPE);
+        _authenticationProvider.setAttributes(attributes);
+
+        final AuthenticationResult result = _authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+    }
+
+    @Test
+    public void testChangeAuthenticationToGssapiWithInvalidScope() throws Exception
+    {
+        setUpKerberosAndJaas();
+
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.AUTHENTICATION_METHOD, LdapAuthenticationMethod.GSSAPI.name());
+        attributes.put(SimpleLDAPAuthenticationManager.LOGIN_CONFIG_SCOPE, "non-existing");
+        try
+        {
+            _authenticationProvider.setAttributes(attributes);
+            fail("Exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            // pass
+        }
+    }
+
+    @Test
+    public void testChangeAuthenticationToGssapiWhenConfigIsBroken() throws Exception
+    {
+        setUpKerberosAndJaas();
+
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.AUTHENTICATION_METHOD, LdapAuthenticationMethod.GSSAPI.name());
+        attributes.put(SimpleLDAPAuthenticationManager.LOGIN_CONFIG_SCOPE, "ldap-gssapi-bind-broken");
+        try
+        {
+            _authenticationProvider.setAttributes(attributes);
+            fail("Exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            // pass
+        }
+    }
+
+    @Test
+    public void testChangeAuthenticationToGssapiNoScopeProvided() throws Exception
+    {
+        setUpKerberosAndJaas();
+
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.AUTHENTICATION_METHOD, LdapAuthenticationMethod.GSSAPI.name());
+        _authenticationProvider.setAttributes(attributes);
+
+        final AuthenticationResult result = _authenticationProvider.authenticate(USER_1_NAME, USER_1_PASSWORD);
+        assertEquals(AuthenticationResult.AuthenticationStatus.SUCCESS, result.getStatus());
+        assertEquals(USER_1_DN, result.getMainPrincipal().getName());
+    }
+
+    private SimpleLDAPAuthenticationManagerImpl createAuthenticationProvider()
+    {
+        return createAuthenticationProvider(Collections.emptyMap());
+    }
+
+    private SimpleLDAPAuthenticationManagerImpl createCachingAuthenticationProvider()
+    {
+        final Map<String, String> context = Collections.singletonMap(AUTHENTICATION_CACHE_MAX_SIZE, "1");
+        final Map<String, Object> attributes =
+                Collections.singletonMap(SimpleLDAPAuthenticationManager.CONTEXT, context);
+        return createAuthenticationProvider(attributes);
+    }
+
+    private SimpleLDAPAuthenticationManagerImpl createAuthenticationProvider(final Map<String, Object> settings)
+    {
+        final Broker<?> broker = BrokerTestHelper.createBrokerMock();
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(SimpleLDAPAuthenticationManager.NAME, getTestName());
+        attributes.put(SimpleLDAPAuthenticationManager.SEARCH_CONTEXT, SEARCH_CONTEXT_VALUE);
+        attributes.put(SimpleLDAPAuthenticationManager.PROVIDER_URL,
+                       String.format(LDAP_URL_TEMPLATE, LDAP.getLdapServer().getPort()));
+        attributes.put(SimpleLDAPAuthenticationManager.SEARCH_FILTER, SEARCH_FILTER_VALUE);
+        attributes.put(SimpleLDAPAuthenticationManager.CONTEXT,
+                       Collections.singletonMap(AUTHENTICATION_CACHE_MAX_SIZE, "0"));
+        attributes.putAll(settings);
+        final SimpleLDAPAuthenticationManagerImpl authenticationProvider =
+                new SimpleLDAPAuthenticationManagerImpl(attributes, broker);
+        authenticationProvider.open();
+        return authenticationProvider;
+    }
+
+
+    private void setUpKerberosAndJaas() throws Exception
+    {
+        assumeThat(getJvmVendor(), not(JvmVendor.IBM));
+        if (KERBEROS_SETUP.compareAndSet(false, true))
+        {
+            setUpKerberos();
+            setUpJaas();
+        }
+    }
+
+    private void setUpKerberos() throws Exception
+    {
+        final LdapServer ldapServer = LDAP.getLdapServer();
+        final KdcServer kdcServer =
+                ServerAnnotationProcessor.getKdcServer(LDAP.getDirectoryService(), ldapServer.getPort() + 1);
+        kdcServer.getConfig().setPaEncTimestampRequired(false);
+        final KerberosPrincipal servicePrincipal =
+                new KerberosPrincipal(LDAP_SERVICE_NAME + "/" + HOSTNAME + "@" + REALM,
+                                      KerberosPrincipal.KRB_NT_SRV_HST);
+        final String servicePrincipalName = servicePrincipal.getName();
+        ldapServer.setSaslHost(servicePrincipalName.substring(servicePrincipalName.indexOf("/") + 1,
+                                                              servicePrincipalName.indexOf("@")));
+        ldapServer.setSaslPrincipal(servicePrincipalName);
+        ldapServer.setSearchBaseDn(USERS_DN);
+
+        final String krb5confPath = createKrb5Conf(kdcServer.getTransports()[0].getPort());
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("java.security.krb5.conf", krb5confPath);
+
+        createPrincipal("KDC", "KDC", "krbtgt", UUID.randomUUID().toString(), "krbtgt/" + REALM + "@" + REALM);
+        createPrincipal("Service", "LDAP Service", "ldap", UUID.randomUUID().toString(), servicePrincipalName);
+    }
+
+    private void setUpJaas() throws LdapException, IOException
+    {
+        createKeyTab(BROKER_PRINCIPAL);
+        final URL resource = SimpleLDAPAuthenticationManagerTest.class.getClassLoader().getResource(LOGIN_CONFIG);
+        LOGGER.debug("JAAS config:" + resource);
+        assertNotNull(resource);
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("java.security.auth.login.config", resource.getPath());
+        SYSTEM_PROPERTY_SETTER.setSystemProperty("sun.security.krb5.debug", "true");
+    }
+
+    private String createKrb5Conf(final int port) throws IOException
+    {
+        final File file = createFile("krb5", ".conf");
+        final String config = String.format("[libdefaults]%1$s"
+                                            + "    default_realm = %2$s%1$s"
+                                            + "    udp_preference_limit = 1%1$s"
+                                            + "[realms]%1$s"
+                                            + "    %2$s = {%1$s"
+                                            + "    kdc = %3$s%1$s"
+                                            + "    }%1$s"
+                                            + "[domain_realm]%1$s"
+                                            + "    .%4$s = %2$s%1$s"
+                                            + "    %4$s = %2$s%1$s",
+                                            LINE_SEPARATOR,
+                                            REALM,
+                                            HOSTNAME + ":" + port,
+                                            Strings.toLowerCaseAscii(REALM));
+        LOGGER.debug("krb5.conf:" + config);
+        TestFileUtils.saveTextContentInFile(config, file);
+        return file.getAbsolutePath();
+    }
+
+    private void createPrincipal(final String sn,
+                                 final String cn,
+                                 final String uid,
+                                 final String userPassword,
+                                 final String kerberosPrincipalName) throws LdapException
+    {
+        final DirectoryService directoryService = LDAP.getDirectoryService();
+        final Entry entry = new DefaultEntry(directoryService.getSchemaManager());
+        entry.setDn(String.format("uid=%s,%s", uid, USERS_DN));
+        entry.add("objectClass", "top", "person", "inetOrgPerson", "krb5principal", "krb5kdcentry");
+        entry.add("cn", cn);
+        entry.add("sn", sn);
+        entry.add("uid", uid);
+        entry.add("userPassword", userPassword);
+        entry.add("krb5PrincipalName", kerberosPrincipalName);
+        entry.add("krb5KeyVersionNumber", "0");
+        directoryService.getAdminSession().add(entry);
+    }
+
+    private void createPrincipal(String uid, String userPassword) throws LdapException
+    {
+        createPrincipal(uid, uid, uid, userPassword, uid + "@" + REALM);
+    }
+
+    private void createPrincipal(final File keyTabFile, final String... principals) throws LdapException, IOException
+    {
+        final Keytab keytab = new Keytab();
+        final List<KeytabEntry> entries = new ArrayList<>();
+        final String password = UUID.randomUUID().toString();
+        for (final String principal : principals)
+        {
+            createPrincipal(principal, password);
+            final String principalName = principal + "@" + REALM;
+            final KerberosTime timestamp = new KerberosTime();
+            final Map<EncryptionType, EncryptionKey> keys = KerberosKeyFactory.getKerberosKeys(principalName, password);
+            keys.forEach((type, key) -> entries.add(new KeytabEntry(principalName,
+                                                                    1,
+                                                                    timestamp,
+                                                                    (byte) key.getKeyVersion(),
+                                                                    key)));
+        }
+        keytab.setEntries(entries);
+        keytab.write(keyTabFile);
+    }
+
+    private void createKeyTab(String... principals) throws LdapException, IOException
+    {
+        final File keyTabFile = createFile("kerberos", ".keytab");
+        createPrincipal(keyTabFile, principals);
+    }
+
+    private File createFile(final String prefix, final String suffix) throws IOException
+    {
+        final Path targetDir = FileSystems.getDefault().getPath("target");
+        final File file = new File(targetDir.toFile(), prefix + suffix);
+        if (file.exists())
+        {
+            if (!file.delete())
+            {
+                throw new IOException(String.format("Cannot delete existing file '%s'", file.getAbsolutePath()));
+            }
+        }
+        if (!file.createNewFile())
+        {
+            throw new IOException(String.format("Cannot create file '%s'", file.getAbsolutePath()));
+        }
+        return file;
+    }
+}
diff --git a/broker-core/src/test/resources/login.config b/broker-core/src/test/resources/login.config
new file mode 100644
index 0000000..d458516
--- /dev/null
+++ b/broker-core/src/test/resources/login.config
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ldap-gssapi-bind {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    debug=true
+    storeKey=true
+    doNotPrompt=true
+    isInitiator=true
+    refreshKrb5Config=true
+    realm="QPID.ORG"
+    principal="service/localhost"
+    keyTab="target/kerberos.keytab";
+};
+
+ldap-gssapi-bind-broken {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    debug=true
+    storeKey=true
+    doNotPrompt=true
+    isInitiator=true
+    refreshKrb5Config=true
+    realm="QPID.ORG"
+    principal="service/localhost"
+    keyTab="target/kerberos-non-existing.keytab";
+};
+
+qpid-broker-j {
+    com.sun.security.auth.module.Krb5LoginModule required
+    useKeyTab=true
+    debug=true
+    storeKey=true
+    doNotPrompt=true
+    isInitiator=true
+    refreshKrb5Config=true
+    realm="QPID.ORG"
+    principal="service/localhost"
+    keyTab="target/kerberos.keytab";
+};
diff --git a/broker-core/src/test/resources/users.ldif b/broker-core/src/test/resources/users.ldif
new file mode 100644
index 0000000..0d76baa
--- /dev/null
+++ b/broker-core/src/test/resources/users.ldif
@@ -0,0 +1,50 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+version: 1
+dn: dc=qpid,dc=org
+objectClass: domain
+objectClass: top
+dc: tests
+
+dn: ou=users,dc=qpid,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: Users
+
+dn: ou=groups,dc=qpid,dc=org
+objectClass: organizationalUnit
+objectClass: top
+ou: Groups
+
+dn: cn=integration-test1,ou=users,dc=qpid,dc=org
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: integration-test1
+sn: ldap-integration-test1
+uid: test1
+userPassword: password1
+
+dn: cn=group1,ou=groups,dc=qpid,dc=org
+cn: Group1
+member: cn=integration-test1,ou=Users,dc=qpid,dc=org
+objectClass: groupOfNames
+objectClass: top
diff --git a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/add.html b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/add.html
index 5b88a3c..3601540 100644
--- a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/add.html
+++ b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/add.html
@@ -68,11 +68,38 @@
                               promptMessage: 'Fully qualified class name for LDAP Context Factory'"/>
         </div>
     </div>
-
+    <div class="clear">
+        <div class="formLabel-labelCell tableContainer-labelCell">Authentication Method:</div>
+        <div class="tableContainer-valueCell formLabel-controlCell">
+            <select class="authenticationMethod" data-dojo-type="dijit/form/FilteringSelect"
+                    id="ldapAuthenticationMethod"
+                    data-dojo-props="
+                                    required: false,
+                                    name: 'authenticationMethod',
+                                    placeHolder: 'Select authentication method',
+                                    promptMessage: 'Select authentication method to bind into LDAP server',
+                                    title: 'Select authentication method to bind into LDAP server',
+                                    searchAttr: 'name'">
+            </select>
+        </div>
+    </div>
+    <div class="clear">
+        <div class="formLabel-labelCell tableContainer-labelCell">Login Config Scope:</div>
+        <div class="formLabel-controlCell tableContainer-valueCell">
+            <input type="text" class="loginConfigScope"
+                   data-dojo-type="dijit/form/ValidationTextBox"
+                   data-dojo-props="
+                              name: 'loginConfigScope',
+                              placeHolder: 'loginConfigScope',
+                              title: 'Login configuration name for GSSAPI authentication',
+                              promptMessage: 'Login configuration name for GSSAPI authentication'"/>
+        </div>
+    </div>
     <div class="clear">
         <div class="formLabel-labelCell tableContainer-labelCell">Search username:</div>
         <div class="formLabel-controlCell tableContainer-valueCell">
             <input type="text" class="searchUsername"
+                   id="ldapSearchUsername"
                    data-dojo-type="dijit/form/ValidationTextBox"
                    data-dojo-props="
                               name: 'searchUsername',
@@ -85,6 +112,7 @@
         <div class="formLabel-labelCell tableContainer-labelCell">Search password:</div>
         <div class="formLabel-controlCell tableContainer-valueCell">
             <input type="password" class="searchPassword"
+                   id="ldapSearchPassword"
                    data-dojo-type="dijit/form/ValidationTextBox"
                    data-dojo-props="
                               name: 'searchPassword',
diff --git a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/show.html b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/show.html
index d71e01f..94bf306 100644
--- a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/show.html
+++ b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/simpleldap/show.html
@@ -41,6 +41,14 @@
         <div class="formLabel-labelCell">Search password:</div>
         <div><span class="searchPassword" ></span></div>
     </div>
+    <div class="clear">
+        <div class="formLabel-labelCell">Authentication method:</div>
+        <div><span class="authenticationMethod" ></span></div>
+    </div>
+    <div class="clear">
+        <div class="formLabel-labelCell">Login config scope:</div>
+        <div><span class="loginConfigScope" ></span></div>
+    </div>
     <div class="clear"></div>
 
     <div class="formBox">
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/add.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/add.js
index 06bd2d5..edc8243 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/add.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/add.js
@@ -81,6 +81,7 @@ define(["dojo/query",
                 .then(function (trustStores)
                 {
                     that._initTrustStores(trustStores, data.containerNode);
+                    that._initAuthenticationMethods(data.parent.management.metadata, data.containerNode);
                     if (data.data)
                     {
                         that._initFields(data.data, data.containerNode, data.parent.management.metadata);
@@ -137,28 +138,23 @@ define(["dojo/query",
             var trustStore = registry.byNode(query(".trustStore", containerNode)[0]);
             trustStore.set("store", trustStoresStore);
         },
-        _initFields: function (data, containerNode, metadata)
+        _initAuthenticationMethods: function (metadata, containerNode)
         {
             var attributes = metadata.getMetaData("AuthenticationProvider", "SimpleLDAP").attributes;
-            for (var name in attributes)
-            {
-                var domNode = query("." + name, containerNode)[0];
-                if (domNode)
-                {
-                    var widget = registry.byNode(domNode);
-                    if (widget)
-                    {
-                        if (widget instanceof dijit.form.CheckBox)
-                        {
-                            widget.set("checked", data[name]);
-                        }
-                        else
-                        {
-                            widget.set("value", data[name]);
-                        }
-                    }
-                }
-            }
+            var store = util.makeTypeStore(attributes.authenticationMethod.validValues);
+            var authenticationMethod = registry.byId("ldapAuthenticationMethod");
+            authenticationMethod.set("store", store);
+            authenticationMethod.on("change", function (value) {
+                registry.byId('ldapSearchUsername').set("required", (value === "SIMPLE"));
+            });
+        },
+        _initFields: function (data, containerNode, metadata)
+        {
+            util.applyToWidgets(containerNode,
+                "AuthenticationProvider",
+                "SimpleLDAP",
+                data,
+                metadata);
         }
     };
 });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js
index 9e13956..096aa36 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/simpleldap/show.js
@@ -26,7 +26,10 @@ define(["qpid/common/util", "dojo/domReady!"], function (util)
         var attributes = data.parent.management.metadata.getMetaData("AuthenticationProvider", "SimpleLDAP").attributes;
         for (var name in attributes)
         {
-            this.fields.push(name);
+            if (attributes.hasOwnProperty(name))
+            {
+                this.fields.push(name);
+            }
         }
         util.buildUI(data.containerNode, data.parent, "authenticationprovider/simpleldap/show.html", this.fields, this);
     }
@@ -34,7 +37,7 @@ define(["qpid/common/util", "dojo/domReady!"], function (util)
     SimpleLdapAuthenticationProvider.prototype.update = function (data)
     {
         util.updateUI(data, this.fields, this);
-    }
+    };
 
     return SimpleLdapAuthenticationProvider;
 });
diff --git a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-LDAP.xml b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-LDAP.xml
index ed81c09..91ce9dc 100644
--- a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-LDAP.xml
+++ b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-LDAP.xml
@@ -57,6 +57,16 @@
                 certificate).</para>
         </listitem>
         <listitem>
+            <para><emphasis>Authentication method</emphasis> is a method of authentication to use on binding into LDAP
+                when <literal>bind without search</literal> mode is not selected.
+                Supported methods are NONE, SIMPLE, GSSAPI. The latter requires setting of <emphasis>Login Config Scope</emphasis>
+                which is a name of JAAS login module from JASS login configuration file specified using JVM system
+                property <emphasis>java.security.auth.login.config</emphasis> or Java security properties file. If
+                <emphasis>Login Config Scope</emphasis> is not specified with <literal>GSSAPI</literal>
+                <emphasis>Authentication method</emphasis>, the scope <emphasis>qpid-broker-j</emphasis> will be used.
+            </para>
+        </listitem>
+        <listitem>
             <para>Additional group information can be obtained from LDAP.
                 There are two common ways of representing group membership in LDAP.
                 <itemizedlist>
diff --git a/pom.xml b/pom.xml
index 6b8fcda..2f17c3b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,6 +152,7 @@
     <maven-jar-plugin-version>3.1.0</maven-jar-plugin-version>
     <maven-surefire-report-plugin-version>2.22.0</maven-surefire-report-plugin-version>
     <h2.version>1.4.199</h2.version>
+    <apache-directory-version>2.0.0-M23</apache-directory-version>
   </properties>
 
   <modules>
@@ -724,6 +725,23 @@
         <artifactId>maven-resolver-transport-http</artifactId>
         <version>${maven-resolver-version}</version>
       </dependency>
+      <!-- apacheds test dependency -->
+      <dependency>
+        <groupId>org.apache.directory.server</groupId>
+        <artifactId>apacheds-all</artifactId>
+        <version>${apache-directory-version}</version>
+        <scope>test</scope>
+        <exclusions>
+           <exclusion>
+            <groupId>org.apache.directory.shared</groupId>
+            <artifactId>shared-ldap-schema</artifactId>
+          </exclusion>
+          <exclusion>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-ldap-schema-data</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org