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

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


The following commit(s) were added to refs/heads/main by this push:
     new 58b1b3e  QPID-8566: [Broker-J] Implement composite authentication 
provider (#118)
58b1b3e is described below

commit 58b1b3e961c4259956d084bc130ba4bc23fdb468
Author: Daniil Kirilyuk <daniel.kiril...@gmail.com>
AuthorDate: Wed Mar 9 07:04:16 2022 +0100

    QPID-8566: [Broker-J] Implement composite authentication provider (#118)
    
    Co-authored-by: aw924 <daniil.kiril...@deutsche-boerse.com>
---
 ...ositeUsernamePasswordAuthenticationManager.java |  35 +
 ...eUsernamePasswordAuthenticationManagerImpl.java | 528 +++++++++++
 ...eUsernamePasswordAuthenticationManagerTest.java | 963 +++++++++++++++++++++
 .../authenticationprovider/composite/add.html      |  28 +
 .../authenticationprovider/composite/show.html     |  26 +
 .../authenticationprovider/composite/add.js        | 159 ++++
 .../authenticationprovider/composite/show.js       |  38 +
 ...Security-Authentication-Providers-Composite.xml |  76 ++
 ...va-Broker-Security-Authentication-Providers.xml |   1 +
 9 files changed, 1854 insertions(+)

diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
 
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
new file mode 100644
index 0000000..a9e1131
--- /dev/null
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util.List;
+
+import org.apache.qpid.server.model.ManagedAttribute;
+import org.apache.qpid.server.model.ManagedObject;
+
+@ManagedObject( category = false, type = "Composite" )
+public interface CompositeUsernamePasswordAuthenticationManager<T extends 
CompositeUsernamePasswordAuthenticationManager<T>>
+        extends CachingAuthenticationProvider<T>, 
UsernamePasswordAuthenticationProvider<T>
+{
+    String PROVIDER_TYPE = "Composite";
+
+    @ManagedAttribute(description = "delegate authentication providers", 
mandatory = true)
+    List<String> getDelegates();
+
+}
diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
 
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
new file mode 100644
index 0000000..3674ef5
--- /dev/null
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
@@ -0,0 +1,528 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+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.ConfiguredObject;
+import org.apache.qpid.server.model.Container;
+import org.apache.qpid.server.model.ManagedAttributeField;
+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.PasswordSource;
+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.crammd5.CramMd5Base64HashedNegotiator;
+import 
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HexNegotiator;
+import org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Negotiator;
+import org.apache.qpid.server.security.auth.sasl.plain.PlainNegotiator;
+import org.apache.qpid.server.security.auth.sasl.scram.ScramNegotiator;
+import org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServerSource;
+import 
org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServerSourceAdapter;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+
+/**
+ * Composite username / password authentication provider.
+ *
+ * Contains list of delegate authentication providers, which are assessed one 
by one during authentication process
+ * until first successful authentication or until all authentication attempts 
fail.
+ *
+ * When two delegates share same SASL mechanism (e.g. 
PlainAuthenticationProvider and ScramSHA256AuthenticationManager
+ * have in common SCRAM-SHA-256), implementation is resolved in runtime 
choosing the delegate containing username requested.
+ * If user with same username is present in both delegates, authentication 
will be performed only against the first
+ * delegate in the list.
+ *
+ */
+public class CompositeUsernamePasswordAuthenticationManagerImpl
+        extends 
AbstractAuthenticationManager<CompositeUsernamePasswordAuthenticationManagerImpl>
+        implements 
CompositeUsernamePasswordAuthenticationManager<CompositeUsernamePasswordAuthenticationManagerImpl>
+{
+
+    /**
+     * Mechanism name
+     */
+    @SuppressWarnings("unused")
+    public static final String MECHANISM_NAME = "COMPOSITE";
+
+    /**
+     * Logger
+     */
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(CompositeUsernamePasswordAuthenticationManagerImpl.class);
+
+    /**
+     * List of delegate authentication provider names
+     */
+    @SuppressWarnings("unused")
+    @ManagedAttributeField
+    private List<String> _delegates;
+
+    /**
+     * Authentication provider delegates
+     */
+    private final Set<UsernamePasswordAuthenticationProvider<?>> 
_authenticationProviders = new LinkedHashSet<>();
+
+    /**
+     * Available SASL negotiators
+     */
+    private final Map<String, Function<SaslSettings, SaslNegotiator>> 
_saslNegotiators = new HashMap<>();
+
+    /**
+     * Available scram adapters
+     */
+    private final Map<String, ScramSaslServerSourceAdapter> _scramAdapters = 
new HashMap<>();
+
+    /**
+     * HMAC names
+     */
+    private final Map<String, String> _hmacNames = new HashMap<>();
+
+    /**
+     * Digest names
+     */
+    private final Map<String, String> _digestNames = new HashMap<>();
+
+    /**
+     * List of supported SASL mechanisms
+     */
+    private List<String> _mechanisms = new ArrayList<>();
+
+    /**
+     * List of supported SASL mechanisms
+     */
+    private List<String> _secureOnlyMechanisms = new ArrayList<>();
+
+    /**
+     * List of supported SASL mechanisms
+     */
+    private List<String> _disabledMechanisms = new ArrayList<>();
+
+    /**
+     * Scram iterations count
+     */
+    final int scramIterationCount = getContextValue(
+            Integer.class,
+            
AbstractScramAuthenticationManager.QPID_AUTHMANAGER_SCRAM_ITERATION_COUNT
+    );
+
+    /**
+     * Constructor creates configured object
+     *
+     * @param attributes Attributes
+     * @param container Parent container
+     */
+    @ManagedObjectFactoryConstructor()
+    public CompositeUsernamePasswordAuthenticationManagerImpl(final 
Map<String, Object> attributes, final Container<?> container)
+    {
+        super(attributes, container);
+    }
+
+    /**
+     * Initiates SCRAM adapters, delegates
+     */
+    @Override
+    protected void postResolveChildren()
+    {
+        super.postResolveChildren();
+
+        final PasswordSource passwordSource = getPasswordSource();
+
+        _scramAdapters.put(
+            ScramSHA1AuthenticationManager.MECHANISM,
+            new ScramSaslServerSourceAdapter(
+                scramIterationCount,
+                ScramSHA1AuthenticationManager.HMAC_NAME,
+                ScramSHA1AuthenticationManager.DIGEST_NAME,
+                passwordSource
+            )
+        );
+
+        _scramAdapters.put(
+            ScramSHA256AuthenticationManager.MECHANISM,
+            new ScramSaslServerSourceAdapter(
+                scramIterationCount,
+                ScramSHA256AuthenticationManager.HMAC_NAME,
+                ScramSHA256AuthenticationManager.DIGEST_NAME,
+                passwordSource
+            )
+        );
+
+        // check for duplicates
+        if (new HashSet<>(_delegates).size() != _delegates.size())
+        {
+            throw new IllegalConfigurationException("Composite authentication 
manager shouldn't contain duplicate names");
+        }
+
+        // prepare delegate authentication providers
+        for (final String delegate: _delegates)
+        {
+            final AuthenticationProvider<?> authProvider = 
resolveDelegate(delegate);
+            
_authenticationProviders.add((UsernamePasswordAuthenticationProvider<?>) 
authProvider);
+        }
+
+        if (_authenticationProviders.isEmpty())
+        {
+            throw new IllegalConfigurationException("Composite authentication 
manager should contain at least one delegate");
+        }
+
+        // supported SASL mechanisms are prepared as intersection of delegates 
mechanisms
+        _mechanisms = new 
ArrayList<>(_authenticationProviders.stream().findFirst().get().getMechanisms());
+        _authenticationProviders.forEach(authProvider -> 
_mechanisms.retainAll(authProvider.getMechanisms()));
+        _authenticationProviders.stream()
+            .filter(authProvider -> authProvider.getDisabledMechanisms() != 
null)
+            .forEach(authProvider -> 
_mechanisms.removeAll(authProvider.getDisabledMechanisms()));
+
+        // secure only SASL mechanisms are prepared as union of delegates 
secure only mechanisms
+        _secureOnlyMechanisms = Stream.concat(
+            
Optional.ofNullable(super.getSecureOnlyMechanisms()).orElse(Collections.emptyList()).stream(),
+            _authenticationProviders.stream()
+            .filter(authProvider -> authProvider.getSecureOnlyMechanisms() != 
null)
+            .flatMap(authProvider -> 
authProvider.getSecureOnlyMechanisms().stream()))
+            .distinct().collect(Collectors.toList());
+
+        // disabled only SASL mechanisms are prepared as union of delegates 
disabled mechanisms
+        _disabledMechanisms = Stream.concat(
+            
Optional.ofNullable(super.getDisabledMechanisms()).orElse(Collections.emptyList()).stream(),
+            _authenticationProviders.stream()
+            .filter(authProvider -> authProvider.getDisabledMechanisms() != 
null)
+            .flatMap(authProvider -> 
authProvider.getDisabledMechanisms().stream()))
+            .distinct().collect(Collectors.toList());
+    }
+
+    /**
+     * Initializes SASL negotiators
+     */
+    @Override
+    protected void onOpen()
+    {
+        super.onOpen();
+
+        // initialize hmac names
+        _hmacNames.put(ScramSHA1AuthenticationManager.MECHANISM, 
ScramSHA1AuthenticationManager.HMAC_NAME);
+        _hmacNames.put(ScramSHA256AuthenticationManager.MECHANISM, 
ScramSHA256AuthenticationManager.HMAC_NAME);
+
+        // initialize digest names
+        _digestNames.put(ScramSHA1AuthenticationManager.MECHANISM, 
ScramSHA1AuthenticationManager.DIGEST_NAME);
+        _digestNames.put(ScramSHA256AuthenticationManager.MECHANISM, 
ScramSHA256AuthenticationManager.DIGEST_NAME);
+
+        // initialize available SASL negotiators
+        _saslNegotiators.put(CramMd5Negotiator.MECHANISM, (saslSettings) -> 
new CramMd5Negotiator(getAuthenticationProviderStub(), 
saslSettings.getLocalFQDN(), getPasswordSource()));
+        _saslNegotiators.put(CramMd5Base64HashedNegotiator.MECHANISM, 
(saslSettings) -> new 
CramMd5Base64HashedNegotiator(getAuthenticationProviderStub(), 
saslSettings.getLocalFQDN(), getPasswordSource()));
+        _saslNegotiators.put(CramMd5Base64HexNegotiator.MECHANISM, 
(saslSettings) -> new 
CramMd5Base64HexNegotiator(getAuthenticationProviderStub(), 
saslSettings.getLocalFQDN(), getPasswordSource()));
+        _saslNegotiators.put(PlainNegotiator.MECHANISM, (saslSettings) -> new 
PlainNegotiator(this));
+        _saslNegotiators.put(ScramSHA1AuthenticationManager.MECHANISM, 
(saslSettings) -> new ScramNegotiator(this, 
getScramSaslServerSource(ScramSHA1AuthenticationManager.MECHANISM), 
ScramSHA1AuthenticationManager.MECHANISM));
+        _saslNegotiators.put(ScramSHA256AuthenticationManager.MECHANISM, 
(saslSettings) -> new ScramNegotiator(this, 
getScramSaslServerSource(ScramSHA256AuthenticationManager.MECHANISM), 
ScramSHA256AuthenticationManager.MECHANISM));
+
+    }
+
+    /**
+     * Validate changes
+     *
+     * @param proxyForValidation ConfiguredObject
+     * @param changedAttributes Attribute names
+     */
+    @Override
+    public void validateChange(final ConfiguredObject<?> proxyForValidation, 
final Set<String> changedAttributes)
+    {
+        super.validateChange(proxyForValidation, changedAttributes);
+        final Collection<String> delegates = (Collection<String>) 
proxyForValidation.getAttribute("delegates");
+        if (delegates.isEmpty())
+        {
+            throw new IllegalConfigurationException("Composite authentication 
manager should contain at least one delegate");
+        }
+        delegates.forEach(this::resolveDelegate);
+    }
+
+    /**
+     * MD5 => ["PLAIN", "CRAM-MD5-HASHED", "CRAM-MD5-HEX"]
+     * Plain => ["PLAIN", "CRAM-MD5", "SCRAM-SHA-1", "SCRAM-SHA-256"]
+     * SCRAM-SHA-1 => ["PLAIN", "SCRAM-SHA-1"]
+     * SCRAM-SHA-256 => ["PLAIN", "SCRAM-SHA-256"]
+     * SimpleLDAP => ["PLAIN"]
+     *
+     * @return List of mechanism names
+     */
+    @Override
+    public List<String> getMechanisms()
+    {
+        return Collections.unmodifiableList(_mechanisms);
+    }
+
+    /**
+     * Returns list of available SASL mechanism names
+     *
+     * @param secure Secure flag
+     *
+     * @return List of mechanism names
+     */
+    @Override
+    public List<String> getAvailableMechanisms(boolean secure)
+    {
+        final List<String> result = _mechanisms.stream()
+            .filter(mechanism -> secure || 
!_secureOnlyMechanisms.contains(mechanism))
+            .filter(mechanism -> !_disabledMechanisms.contains(mechanism))
+            .collect(Collectors.toList());
+        return Collections.unmodifiableList(result);
+    }
+
+    /**
+     * Creates SASL negotiator based on available options
+     *
+     * @param mechanism Mechanism name
+     * @param saslSettings SaslSettings
+     * @param addressSpace NamedAddressSpace
+     *
+     * @return SaslNegotiator
+     */
+    @Override
+    public SaslNegotiator createSaslNegotiator(
+        final String mechanism,
+        final SaslSettings saslSettings,
+        final NamedAddressSpace addressSpace
+    )
+    {
+        return _saslNegotiators.getOrDefault(mechanism, (settings) -> 
null).apply(saslSettings);
+    }
+
+    /**
+     * Iterates over authentication provider delegates attempting 
authentication for each one.
+     *
+     * @param username username
+     * @param password password
+     *
+     * @return AuthenticationResult
+     */
+    @Override
+    public AuthenticationResult authenticate(String username, String password)
+    {
+        for (final UsernamePasswordAuthenticationProvider<?> 
authenticationProvider : _authenticationProviders)
+        {
+            final AuthenticationResult authResult = 
authenticationProvider.authenticate(username, password);
+            if 
(AuthenticationResult.AuthenticationStatus.ERROR.equals(authResult.getStatus()))
+            {
+                LOGGER.debug(
+                    "Authentication of user '{}' against '{}' failed",
+                    username,
+                    authenticationProvider.getClass().getSimpleName()
+                );
+                continue;
+            }
+            LOGGER.debug(
+                "Authentication of user '{}' against '{}' succeeded",
+                username,
+                authenticationProvider.getClass().getSimpleName()
+            );
+            return authResult;
+        }
+        LOGGER.debug("All authentication attempts failed");
+        return new 
AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+    }
+
+    /**
+     * Creates ScramSaslServerSource instance which will retrieve 
SaltAndPasswordKeys from the authentication
+     * provider delegate containing requested username.
+     *
+     * @param mechanism SASL mechanism
+     *
+     * @return ScramSaslServerSource
+     */
+    private ScramSaslServerSource getScramSaslServerSource(String mechanism)
+    {
+        return new ScramSaslServerSource()
+        {
+
+            @Override
+            public int getIterationCount()
+            {
+                return scramIterationCount;
+            }
+
+            @Override
+            public String getDigestName()
+            {
+                return Optional.ofNullable(_digestNames.get(mechanism))
+                    .orElseThrow(() -> new 
ConnectionScopedRuntimeException("Mechanism '" + mechanism + "' not 
supported"));
+            }
+
+            @Override
+            public String getHmacName()
+            {
+                return Optional.ofNullable(_hmacNames.get(mechanism))
+                    .orElseThrow(() -> new 
ConnectionScopedRuntimeException("Mechanism '" + mechanism + "' not 
supported"));
+            }
+
+            @Override
+            public SaltAndPasswordKeys getSaltAndPasswordKeys(final String 
username)
+            {
+                return _authenticationProviders.stream()
+                    .filter(authProvider -> authProvider instanceof 
ConfigModelPasswordManagingAuthenticationProvider<?>)
+                    .filter(authProvider -> 
((ConfigModelPasswordManagingAuthenticationProvider<?>)authProvider).getUser(username)
 != null)
+                    .findFirst().map(authProvider ->
+                    {
+                        if (authProvider instanceof 
AbstractScramAuthenticationManager<?>)
+                        {
+                            return ((AbstractScramAuthenticationManager<?>) 
authProvider).getSaltAndPasswordKeys(username);
+                        }
+                        return 
_scramAdapters.get(mechanism).getSaltAndPasswordKeys(username);
+                    
}).orElse(_scramAdapters.get(mechanism).getSaltAndPasswordKeys(username));
+            }
+        };
+    }
+
+    /**
+     * Resolves delegate authentication provider by name
+     *
+     * @param delegate Delegate name
+     *
+     * @return Delegate AuthenticationProvider
+     */
+    private AuthenticationProvider<?> resolveDelegate(final String delegate)
+    {
+        final Broker<?> broker = (Broker<?>) getParent();
+
+        final Optional<AuthenticationProvider<?>> optAuthProvider = 
broker.getAuthenticationProviders().stream()
+            .filter(provider -> 
provider.getName().equals(delegate)).findFirst();
+
+        if (!optAuthProvider.isPresent())
+        {
+            throw new IllegalConfigurationException("Authentication provider 
'" + delegate + "' not found");
+        }
+
+        final AuthenticationProvider<?> authProvider = optAuthProvider.get();
+
+        if (!(authProvider instanceof 
UsernamePasswordAuthenticationProvider<?>))
+        {
+            throw new IllegalConfigurationException("Authentication provider 
'" + delegate + "' is not UsernamePasswordAuthenticationProvider");
+        }
+
+        if (authProvider instanceof 
CompositeUsernamePasswordAuthenticationManager<?>)
+        {
+            throw new IllegalConfigurationException("Composite authentication 
providers shouldn't be nested");
+        }
+
+        return authProvider;
+    }
+
+    /**
+     * Retrieves username from the first authentication provider containing 
user with matching username
+     *
+     * @return PasswordSource
+     */
+    private PasswordSource getPasswordSource()
+    {
+        return username -> _authenticationProviders.stream()
+                .filter(authProvider -> authProvider instanceof 
ConfigModelPasswordManagingAuthenticationProvider)
+                .filter(authProvider -> 
((ConfigModelPasswordManagingAuthenticationProvider<?>) 
authProvider).getUser(username) != null)
+                .findFirst()
+                .map(authProvider -> 
(((ConfigModelPasswordManagingAuthenticationProvider<?>) authProvider)
+                    .getPasswordSource().getPassword(username)))
+                .orElse(null);
+    }
+
+    /**
+     * Creates authentication provider stub used by some SASL negotiators
+     *
+     * @return ConfigModelPasswordManagingAuthenticationProvider stub
+     */
+    private <X extends ConfigModelPasswordManagingAuthenticationProvider<X>> 
ConfigModelPasswordManagingAuthenticationProvider<X> 
getAuthenticationProviderStub()
+    {
+        final Map<String, Object> attributes = new HashMap<>();
+        attributes.put(AuthenticationProvider.NAME, 
"AuthenticationProviderStub");
+        attributes.put(AuthenticationProvider.ID, UUID.randomUUID());
+
+        final CompositeUsernamePasswordAuthenticationManagerImpl parent = this;
+        final PasswordSource passwordSource = getPasswordSource();
+
+        return new 
ConfigModelPasswordManagingAuthenticationProvider<X>(attributes, (Container<?>) 
getParent())
+        {
+
+            @Override
+            public AuthenticationResult authenticate(final String username, 
final String password)
+            {
+                return parent.authenticate(username, password);
+            }
+
+            @Override
+            public PasswordSource getPasswordSource()
+            {
+                return passwordSource;
+            }
+
+            @Override
+            protected String createStoredPassword(final String password)
+            {
+                throw new ConnectionScopedRuntimeException("SaslNegotiator 
isn't supposed to call createStoredPassword()");
+            }
+
+            @Override
+            void validateUser(final ManagedUser managedUser)
+            {
+                throw new ConnectionScopedRuntimeException("SaslNegotiator 
isn't supposed to call validateUser()");
+            }
+
+            @Override
+            public List<String> getMechanisms()
+            {
+                throw new ConnectionScopedRuntimeException("SaslNegotiator 
isn't supposed to call getMechanisms()");
+            }
+
+            @Override
+            public SaslNegotiator createSaslNegotiator(
+                final String mechanism,
+                final SaslSettings saslSettings,
+                final NamedAddressSpace addressSpace
+            )
+            {
+                throw new ConnectionScopedRuntimeException("SaslNegotiator 
isn't supposed to call createSaslNegotiator()");
+            }
+        };
+    }
+
+    @Override
+    public String toString()
+    {
+        return "CompositeAuthenticationManagerImpl {"
+           + "_authenticationProviders=" + _authenticationProviders + '}';
+    }
+
+    @Override
+    public List<String> getDelegates()
+    {
+        return _delegates;
+    }
+}
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
 
b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
new file mode 100644
index 0000000..2e5a081
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
@@ -0,0 +1,963 @@
+/*
+ * 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.US_ASCII;
+import static 
org.apache.qpid.server.security.auth.AuthenticationResult.AuthenticationStatus.SUCCESS;
+import static 
org.apache.qpid.server.security.auth.AuthenticationResult.AuthenticationStatus.ERROR;
+import static 
org.apache.qpid.server.security.auth.manager.CachingAuthenticationProvider.AUTHENTICATION_CACHE_MAX_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+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.integ.CreateLdapServerRule;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import 
org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
+import 
org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.configuration.updater.CurrentThreadTaskExecutor;
+import org.apache.qpid.server.configuration.updater.TaskExecutor;
+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.model.ConfiguredObject;
+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.SaslUtil;
+import 
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HashedNegotiator;
+import 
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HexNegotiator;
+import org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Negotiator;
+import org.apache.qpid.server.util.Strings;
+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/localh...@qpid.org",
+    saslMechanisms =
+    {
+        @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass = 
PlainMechanismHandler.class),
+        @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass = 
GssapiMechanismHandler.class)
+    }
+)
+@ApplyLdifFiles("users.ldif")
+public class CompositeUsernamePasswordAuthenticationManagerTest extends 
UnitTestBase
+{
+    @ClassRule
+    public static CreateLdapServerRule LDAP = new CreateLdapServerRule();
+    private final List<AuthenticationProvider<?>> _authenticationProviders = 
new ArrayList<>();
+    private Broker<?> _broker;
+    private TaskExecutor _executor;
+
+    private static final String USERNAME = "user1";
+    private static final String PASSWORD = "password1";
+
+    @Before
+    public void setUp() throws Exception
+    {
+        _executor = new CurrentThreadTaskExecutor();
+        _executor.start();
+        _broker = BrokerTestHelper.createBrokerMock();
+        when(_broker.getTaskExecutor()).thenReturn(_executor);
+        when(_broker.getChildExecutor()).thenReturn(_executor);
+        
when(_broker.getAuthenticationProviders()).thenReturn(_authenticationProviders);
+        SaslHelper._clientNonce = UUID.randomUUID().toString();
+    }
+
+    @After
+    public void tearDown() throws Exception
+    {
+        _executor.stop();
+        _authenticationProviders.clear();
+    }
+
+    @SuppressWarnings("unchecked")
+    private CompositeUsernamePasswordAuthenticationManager<?> 
createCompositeAuthenticationManager(
+            UsernamePasswordAuthenticationProvider<?>... 
authenticationProviders
+                                                                               
                   )
+    {
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(AuthenticationProvider.TYPE, 
CompositeUsernamePasswordAuthenticationManager.PROVIDER_TYPE);
+        attributesMap.put(AuthenticationProvider.NAME, 
"CompositeAuthenticationProvider");
+        attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+        if (authenticationProviders.length > 0)
+        {
+            attributesMap.put(
+                    "delegates",
+                    
Arrays.stream(authenticationProviders).map(ConfiguredObject::getName).collect(Collectors.toList())
+            );
+        }
+
+        AuthenticationProvider<?> authProvider =
+                
_broker.getObjectFactory().create(AuthenticationProvider.class, attributesMap, 
_broker);
+        _authenticationProviders.add(authProvider);
+        return (CompositeUsernamePasswordAuthenticationManager<?>) 
authProvider;
+    }
+
+    @SuppressWarnings("unchecked")
+    private MD5AuthenticationProvider createMD5AuthenticationProvider()
+    {
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(AuthenticationProvider.NAME, 
"MD5AuthenticationProvider");
+        attributesMap.put(AuthenticationProvider.TYPE, "MD5");
+        attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+        AuthenticationProvider<?> authProvider =
+                
_broker.getObjectFactory().create(AuthenticationProvider.class, attributesMap, 
_broker);
+        _authenticationProviders.add(authProvider);
+        return (MD5AuthenticationProvider) authProvider;
+    }
+
+    @SuppressWarnings("unchecked")
+    private PlainAuthenticationProvider 
createPlainAuthenticationProvider(String... names)
+    {
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(AuthenticationProvider.NAME, names.length == 0 ? 
"PlainAuthenticationProvider" : names[0]);
+        attributesMap.put(AuthenticationProvider.TYPE, "Plain");
+        attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+        PlainAuthenticationProvider authProvider = 
(PlainAuthenticationProvider) _broker.getObjectFactory()
+            .create(AuthenticationProvider.class, attributesMap, _broker);
+        _authenticationProviders.add(authProvider);
+        return authProvider;
+    }
+
+    @SuppressWarnings("unchecked")
+    private ScramSHA256AuthenticationManager 
createScramSHA256AuthenticationManager(String... names)
+    {
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(AuthenticationProvider.NAME,
+                          names.length == 0 ? 
"ScramSHA256AuthenticationManager" : names[0]);
+        attributesMap.put(AuthenticationProvider.TYPE, "SCRAM-SHA-256");
+        attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+        ScramSHA256AuthenticationManager authProvider = 
(ScramSHA256AuthenticationManager) _broker.getObjectFactory()
+            .create(AuthenticationProvider.class, attributesMap, _broker);
+        _authenticationProviders.add(authProvider);
+        return authProvider;
+    }
+
+    @SuppressWarnings("unchecked")
+    private ScramSHA1AuthenticationManager 
createScramSHA1AuthenticationManager(String... names)
+    {
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(AuthenticationProvider.NAME,
+                          names.length == 0 ? "ScramSHA1AuthenticationManager" 
: names[0]);
+        attributesMap.put(AuthenticationProvider.TYPE, "SCRAM-SHA-1");
+        attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+        ScramSHA1AuthenticationManager authProvider = 
(ScramSHA1AuthenticationManager) _broker.getObjectFactory()
+            .create(AuthenticationProvider.class, attributesMap, _broker);
+        _authenticationProviders.add(authProvider);
+        return authProvider;
+    }
+
+    @SuppressWarnings("unchecked")
+    private SimpleLDAPAuthenticationManager<?> 
createSimpleLDAPAuthenticationManager()
+    {
+        final String LDAP_URL_TEMPLATE = "ldap://localhost:%d";;
+        final String ROOT = "dc=qpid,dc=org";
+        final String SEARCH_CONTEXT_VALUE = "ou=users," + ROOT;
+        final String SEARCH_FILTER_VALUE = "(uid={0})";
+
+        final Map<String, Object> attributesMap = new HashMap<>();
+        attributesMap.put(SimpleLDAPAuthenticationManager.NAME, 
"SimpleLDAPAuthenticationManager");
+        attributesMap.put(SimpleLDAPAuthenticationManager.ID, 
UUID.randomUUID());
+        attributesMap.put(SimpleLDAPAuthenticationManager.TYPE, 
SimpleLDAPAuthenticationManager.PROVIDER_TYPE);
+        attributesMap.put(SimpleLDAPAuthenticationManager.SEARCH_CONTEXT, 
SEARCH_CONTEXT_VALUE);
+        attributesMap.put(SimpleLDAPAuthenticationManager.PROVIDER_URL,
+                          String.format(LDAP_URL_TEMPLATE, 
LDAP.getLdapServer().getPort()));
+        attributesMap.put(SimpleLDAPAuthenticationManager.SEARCH_FILTER, 
SEARCH_FILTER_VALUE);
+        attributesMap.put(SimpleLDAPAuthenticationManager.CONTEXT,
+                          
Collections.singletonMap(AUTHENTICATION_CACHE_MAX_SIZE, "0"));
+        final SimpleLDAPAuthenticationManager<?> authProvider =
+                (SimpleLDAPAuthenticationManager<?>) _broker.getObjectFactory()
+                .create(AuthenticationProvider.class, attributesMap, _broker);
+        _authenticationProviders.add(authProvider);
+        return authProvider;
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void failToCreateCompositeAuthenticationManager()
+    {
+        createCompositeAuthenticationManager();
+    }
+
+    @Test()
+    public void authenticateAgainstPlainAuthenticationProvider() throws 
Exception
+    {
+
+        final PlainAuthenticationProvider plainAuthenticationProvider = 
createPlainAuthenticationProvider();
+        plainAuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                plainAuthenticationProvider
+        );
+
+        final AuthenticationResult result = authManager.authenticate(USERNAME, 
PASSWORD);
+        assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+        assertEquals("Unexpected result principal", USERNAME, 
result.getMainPrincipal().getName());
+
+        // authenticate via SASL PLAIN
+        final String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+        final SaslNegotiator plainSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult plainAuthResult = 
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status",
+                     SUCCESS,
+                     plainAuthResult.getStatus());
+
+        // authenticate via SASL CRAM-MD5
+        saslCramMd(
+            CramMd5Negotiator.MECHANISM,
+            authManager.createSaslNegotiator(
+                CramMd5Negotiator.MECHANISM,
+                CRAM_MD_SASL_SETTINGS,
+                null
+            ),
+            USERNAME,
+            PASSWORD
+        );
+
+        // authenticate via SASL SCRAM-SHA-1
+        saslScramSha(
+            ScramSHA1AuthenticationManager.MECHANISM,
+            
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM, 
null, null),
+            USERNAME,
+            PASSWORD
+        );
+
+        // authenticate via SASL SCRAM-SHA-256
+        saslScramSha(
+            ScramSHA256AuthenticationManager.MECHANISM,
+            
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+            USERNAME,
+            PASSWORD
+        );
+    }
+
+    @Test()
+    public void authenticateAgainstMD5AuthenticationProvider() throws Exception
+    {
+
+        final PlainAuthenticationProvider plainAuthenticationProvider = 
createPlainAuthenticationProvider();
+        final MD5AuthenticationProvider md5AuthenticationProvider = 
createMD5AuthenticationProvider();
+        md5AuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                plainAuthenticationProvider, md5AuthenticationProvider
+        );
+        AuthenticationResult result = authManager.authenticate(USERNAME, 
PASSWORD);
+        assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+        assertEquals("Unexpected result principal", USERNAME, 
result.getMainPrincipal().getName());
+
+        // authenticate via SASL PLAIN
+        final String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+        final SaslNegotiator plainSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult plainAuthResult = 
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
plainAuthResult.getStatus());
+
+        // authenticate via SASL CRAM-MD5-HASHED
+        saslCramMd(
+            CramMd5Base64HashedNegotiator.MECHANISM,
+            authManager.createSaslNegotiator(
+                CramMd5Base64HashedNegotiator.MECHANISM,
+                CRAM_MD_SASL_SETTINGS,
+               null
+            ),
+            USERNAME,
+            PASSWORD
+        );
+
+        // authenticate via SASL CRAM-MD5-HEX
+        saslCramMd(
+            CramMd5Base64HexNegotiator.MECHANISM,
+            authManager.createSaslNegotiator(
+                CramMd5Base64HexNegotiator.MECHANISM,
+                CRAM_MD_SASL_SETTINGS,
+               null
+            ),
+            USERNAME,
+            PASSWORD
+        );
+    }
+
+    @Test()
+    public void authenticateAgainstScramSHA1AuthenticationManager() throws 
Exception
+    {
+
+        final ScramSHA1AuthenticationManager scramSHA1AuthenticationManager =
+                createScramSHA1AuthenticationManager();
+        scramSHA1AuthenticationManager.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                scramSHA1AuthenticationManager
+        );
+        AuthenticationResult result = authManager.authenticate(USERNAME, 
PASSWORD);
+        assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+        assertEquals("Unexpected result principal", USERNAME, 
result.getMainPrincipal().getName());
+
+        saslScramSha(
+                ScramSHA1AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME,
+                PASSWORD
+        );
+    }
+
+    @Test()
+    public void authenticateAgainstScramSHA256AuthenticationManager() throws 
Exception
+    {
+
+        final ScramSHA256AuthenticationManager 
scramSHA256AuthenticationManager =
+                createScramSHA256AuthenticationManager();
+        scramSHA256AuthenticationManager.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                scramSHA256AuthenticationManager
+                                                                               
                                   );
+        AuthenticationResult result = authManager.authenticate(USERNAME, 
PASSWORD);
+        assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+        assertEquals("Unexpected result principal", USERNAME, 
result.getMainPrincipal().getName());
+
+        saslScramSha(
+            ScramSHA256AuthenticationManager.MECHANISM,
+            
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+            USERNAME,
+            PASSWORD
+        );
+    }
+
+    @Test()
+    public void authenticateAgainstSimpleLDAPAuthenticationManager()
+    {
+        final String LDAP_USERNAME = "test1";
+
+        final SimpleLDAPAuthenticationManager<?> 
simpleLDAPAuthenticationManager =
+                createSimpleLDAPAuthenticationManager();
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                simpleLDAPAuthenticationManager
+        );
+
+        AuthenticationResult result = authManager.authenticate(LDAP_USERNAME, 
PASSWORD);
+        assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+        assertEquals("Unexpected result principal", 
"cn=integration-test1,ou=users,dc=qpid,dc=org", 
result.getMainPrincipal().getName());
+
+        // authenticate via SASL PLAIN
+        final String RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME, 
PASSWORD);
+        final SaslNegotiator plainSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult plainAuthResult = 
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
plainAuthResult.getStatus());
+    }
+
+    @Test()
+    public void authenticateAgainstPlainAndMd5AndSimpleLdap() throws Exception
+    {
+        final String MD5_USERNAME = "user2";
+        final String MD5_PASSWORD = "password2";
+        final String LDAP_USERNAME = "test1";
+        final String LDAP_PASSWORD = "password1";
+
+        final PlainAuthenticationProvider plainAuthenticationProvider = 
createPlainAuthenticationProvider();
+        plainAuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+
+        final MD5AuthenticationProvider md5AuthenticationProvider = 
createMD5AuthenticationProvider();
+        md5AuthenticationProvider.createUser(MD5_USERNAME, MD5_PASSWORD, 
Collections.emptyMap());
+
+        final SimpleLDAPAuthenticationManager<?> 
simpleLDAPAuthenticationManager =
+                createSimpleLDAPAuthenticationManager();
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                plainAuthenticationProvider, md5AuthenticationProvider, 
simpleLDAPAuthenticationManager
+        );
+
+        // authenticate against PlainAuthenticationProvider via SASL PLAIN
+        String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+        final SaslNegotiator plainSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult plainAuthResult = 
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
plainAuthResult.getStatus());
+        assertEquals(
+                "Unexpected result principal",
+                USERNAME,
+                plainAuthResult.getMainPrincipal().getName()
+        );
+
+        // authenticate against PlainAuthenticationProvider via SASL CRAM-MD5
+        saslCramMd(
+                CramMd5Negotiator.MECHANISM,
+                authManager.createSaslNegotiator(CramMd5Negotiator.MECHANISM, 
CRAM_MD_SASL_SETTINGS, null),
+                USERNAME,
+                PASSWORD
+        );
+
+        // authenticate against PlainAuthenticationProvider via SASL 
SCRAM-SHA-1
+        saslScramSha(
+                ScramSHA1AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME,
+                PASSWORD
+        );
+
+        // authenticate against PlainAuthenticationProvider via SASL 
SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME,
+                PASSWORD
+        );
+
+        // authenticate against MD5AuthenticationProvider via SASL PLAIN
+        RESPONSE = String.format("\0%s\0%s", MD5_USERNAME, MD5_PASSWORD);
+        final SaslNegotiator md5SaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult md5AuthResult = 
md5SaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
md5AuthResult.getStatus());
+        assertEquals(
+                "Unexpected result principal",
+                MD5_USERNAME,
+                md5AuthResult.getMainPrincipal().getName()
+        );
+
+        // authenticate against MD5AuthenticationProvider via SASL 
CRAM-MD5-HASHED
+        saslCramMd(
+                CramMd5Base64HashedNegotiator.MECHANISM,
+                
authManager.createSaslNegotiator(CramMd5Base64HashedNegotiator.MECHANISM, 
CRAM_MD_SASL_SETTINGS, null),
+                MD5_USERNAME,
+                MD5_PASSWORD
+        );
+
+        // authenticate against MD5AuthenticationProvider via SASL CRAM-MD5-HEX
+        saslCramMd(
+                CramMd5Base64HexNegotiator.MECHANISM,
+                
authManager.createSaslNegotiator(CramMd5Base64HexNegotiator.MECHANISM, 
CRAM_MD_SASL_SETTINGS,null),
+                MD5_USERNAME,
+                MD5_PASSWORD
+        );
+
+        // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+        RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME, LDAP_PASSWORD);
+        final SaslNegotiator ldapSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult ldapAuthResult = 
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
ldapAuthResult.getStatus());
+        assertEquals(
+                "Unexpected result principal",
+                "cn=integration-test1,ou=users,dc=qpid,dc=org",
+                ldapAuthResult.getMainPrincipal().getName()
+        );
+    }
+
+    @Test()
+    public void authenticateAgainstPlainAndSha256AndSimpleLdap() throws 
Exception
+    {
+        final String SHA256_USERNAME = "user2";
+        final String SHA256_PASSWORD = "password2";
+        final String LDAP_USERNAME = "test1";
+        final String LDAP_PASSWORD = "password1";
+
+        final PlainAuthenticationProvider plainAuthenticationProvider = 
createPlainAuthenticationProvider();
+        plainAuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider = 
createScramSHA256AuthenticationManager();
+        sha256AuthenticationProvider.createUser(SHA256_USERNAME, 
SHA256_PASSWORD, Collections.emptyMap());
+
+        final SimpleLDAPAuthenticationManager<?> 
simpleLDAPAuthenticationManager =
+                createSimpleLDAPAuthenticationManager();
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                plainAuthenticationProvider, sha256AuthenticationProvider, 
simpleLDAPAuthenticationManager
+        );
+
+        // authenticate against PlainAuthenticationProvider via SASL PLAIN
+        String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+        final SaslNegotiator plainSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult plainAuthResult = 
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
plainAuthResult.getStatus());
+        assertEquals("Unexpected result principal", USERNAME, 
plainAuthResult.getMainPrincipal().getName());
+
+        // authenticate against PlainAuthenticationProvider via SASL CRAM-MD5
+        saslCramMd(
+                CramMd5Negotiator.MECHANISM,
+                authManager.createSaslNegotiator(CramMd5Negotiator.MECHANISM, 
CRAM_MD_SASL_SETTINGS, null),
+                USERNAME, PASSWORD);
+
+        // authenticate against PlainAuthenticationProvider via SASL 
SCRAM-SHA-1
+        saslScramSha(
+                ScramSHA1AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME, PASSWORD);
+
+        // authenticate against PlainAuthenticationProvider via SASL 
SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME, PASSWORD);
+
+        // authenticate against ScramSHA256AuthenticationManager via SASL 
SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                SHA256_USERNAME, SHA256_PASSWORD);
+
+        // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+        RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME, LDAP_PASSWORD);
+        final SaslNegotiator ldapSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult ldapAuthResult = 
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", SUCCESS, 
ldapAuthResult.getStatus());
+        assertEquals(
+                "Unexpected result principal",
+                "cn=integration-test1,ou=users,dc=qpid,dc=org",
+                ldapAuthResult.getMainPrincipal().getName()
+        );
+    }
+
+    @Test()
+    public void usernameCollision() throws Exception
+    {
+        final String PLAIN_PASSWORD = "password1";
+        final String SHA256_PASSWORD = "password2";
+
+        final PlainAuthenticationProvider plainAuthenticationProvider = 
createPlainAuthenticationProvider();
+        plainAuthenticationProvider.createUser(USERNAME, PLAIN_PASSWORD, 
Collections.emptyMap());
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider = 
createScramSHA256AuthenticationManager();
+        sha256AuthenticationProvider.createUser(USERNAME, SHA256_PASSWORD, 
Collections.emptyMap());
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                plainAuthenticationProvider, sha256AuthenticationProvider);
+
+        // authenticate against PlainAuthenticationProvider via SASL 
SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME, PLAIN_PASSWORD);
+
+        // authenticate against ScramSHA256AuthenticationManager via SASL 
SCRAM-SHA-256 (fails due username collision)
+        saslScramShaInvalidCredentials(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME, SHA256_PASSWORD);
+    }
+
+    @Test()
+    public void differentUsersInScramSHA256AuthenticationManagers() throws 
Exception
+    {
+        final String SHA256_USERNAME2 = "user2";
+        final String SHA256_PASSWORD2 = "password2";
+        final String SHA256_USERNAME3 = "user3";
+        final String SHA256_PASSWORD3 = "password4";
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider1 = 
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager1");
+        sha256AuthenticationProvider1.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider2 = 
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager2");
+        sha256AuthenticationProvider2.createUser(SHA256_USERNAME2, 
SHA256_PASSWORD2, Collections.emptyMap());
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider3 = 
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager3");
+        sha256AuthenticationProvider3.createUser(SHA256_USERNAME3, 
SHA256_PASSWORD3, Collections.emptyMap());
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                sha256AuthenticationProvider1, sha256AuthenticationProvider2, 
sha256AuthenticationProvider3);
+
+        // authenticate against first ScramSHA256AuthenticationManager via 
SASL SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                USERNAME, PASSWORD);
+
+        // authenticate against second ScramSHA256AuthenticationManager via 
SASL SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                SHA256_USERNAME2, SHA256_PASSWORD2);
+
+        // authenticate against third ScramSHA256AuthenticationManager via 
SASL SCRAM-SHA-256
+        saslScramSha(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                SHA256_USERNAME3, SHA256_PASSWORD3);
+
+    }
+
+    @Test()
+    public void userNotFound() throws Exception
+    {
+        final String SHA1_USERNAME = "user2";
+        final String SHA1_PASSWORD = "password2";
+        final String NON_EXISTING_USERNAME = "test99";
+
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider = 
createScramSHA256AuthenticationManager();
+        sha256AuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+
+        final ScramSHA1AuthenticationManager sha1AuthenticationProvider = 
createScramSHA1AuthenticationManager();
+        sha1AuthenticationProvider.createUser(SHA1_USERNAME, SHA1_PASSWORD, 
Collections.emptyMap());
+
+        final SimpleLDAPAuthenticationManager<?> 
simpleLDAPAuthenticationManager =
+                createSimpleLDAPAuthenticationManager();
+
+        final CompositeUsernamePasswordAuthenticationManager<?> authManager = 
createCompositeAuthenticationManager(
+                sha256AuthenticationProvider, sha1AuthenticationProvider, 
simpleLDAPAuthenticationManager);
+
+        // authenticate against ScramSHA256AuthenticationManager via SASL 
SCRAM-SHA-256
+        saslScramShaInvalidCredentials(
+                ScramSHA256AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM, 
null, null),
+                NON_EXISTING_USERNAME, PASSWORD);
+
+        // authenticate against ScramSHA256AuthenticationManager via SASL 
SCRAM-SHA-1
+        saslScramShaInvalidCredentials(
+                ScramSHA1AuthenticationManager.MECHANISM,
+                
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM, 
null, null),
+                NON_EXISTING_USERNAME, SHA1_PASSWORD);
+
+        // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+        String RESPONSE = String.format("\0%s\0%s", NON_EXISTING_USERNAME, 
PASSWORD);
+        final SaslNegotiator ldapSaslNegotiator = 
authManager.createSaslNegotiator("PLAIN", null, null);
+        final AuthenticationResult ldapAuthResult = 
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+        assertEquals("Unexpected result status", ERROR, 
ldapAuthResult.getStatus());
+    }
+
+    @Test(expected = IllegalConfigurationException.class)
+    public void nestedComposteUsernamePasswordAuthenticationManager()
+    {
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider = 
createScramSHA256AuthenticationManager();
+        sha256AuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        final CompositeUsernamePasswordAuthenticationManager<?> composite1 = 
createCompositeAuthenticationManager(sha256AuthenticationProvider);
+        createCompositeAuthenticationManager(composite1);
+    }
+
+    @Test(expected = IllegalConfigurationException.class)
+    public void duplicateDelegates()
+    {
+        final ScramSHA256AuthenticationManager sha256AuthenticationProvider = 
createScramSHA256AuthenticationManager();
+        sha256AuthenticationProvider.createUser(USERNAME, PASSWORD, 
Collections.emptyMap());
+        createCompositeAuthenticationManager(sha256AuthenticationProvider, 
sha256AuthenticationProvider);
+    }
+
+    private void saslCramMd(
+        String mechanism,
+        SaslNegotiator saslNegotiator,
+        String username,
+        String password
+    ) throws Exception
+    {
+
+        final AuthenticationResult firstResult = 
saslNegotiator.handleResponse(new byte[0]);
+        assertEquals(
+                "Unexpected first result status",
+                AuthenticationResult.AuthenticationStatus.CONTINUE,
+                firstResult.getStatus()
+        );
+
+        byte[] responseBytes = SaslUtil.generateCramMD5ClientResponse(
+                mechanism, username, password, firstResult.getChallenge()
+        );
+
+        final AuthenticationResult secondResult = 
saslNegotiator.handleResponse(responseBytes);
+
+        assertEquals("Unexpected second result status",
+                     SUCCESS,
+                     secondResult.getStatus());
+        assertNull("Unexpected second result challenge", 
secondResult.getChallenge());
+        assertEquals("Unexpected second result main principal",
+                     username,
+                     secondResult.getMainPrincipal().getName());
+
+        final AuthenticationResult thirdResult = 
saslNegotiator.handleResponse(new byte[0]);
+        assertEquals("Unexpected third result status",
+                     AuthenticationResult.AuthenticationStatus.ERROR,
+                     thirdResult.getStatus());
+    }
+
+    private void saslScramSha(
+        String mechanism,
+        SaslNegotiator saslNegotiator,
+        String username,
+        String password
+    ) throws Exception
+    {
+
+        final byte[] initialResponse = 
SaslHelper.createInitialResponse(username);
+
+        final AuthenticationResult firstResult = 
saslNegotiator.handleResponse(initialResponse);
+        assertEquals(
+                "Unexpected first result status",
+                AuthenticationResult.AuthenticationStatus.CONTINUE,
+                firstResult.getStatus()
+         );
+        assertNotNull("Unexpected first result challenge", 
firstResult.getChallenge());
+
+        final byte[] response = SaslHelper.calculateClientProof(
+                firstResult.getChallenge(),
+                ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+                    ? ScramSHA256AuthenticationManager.HMAC_NAME : 
ScramSHA1AuthenticationManager.HMAC_NAME,
+                ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+                    ? ScramSHA256AuthenticationManager.DIGEST_NAME : 
ScramSHA1AuthenticationManager.DIGEST_NAME,
+                password
+        );
+
+        final AuthenticationResult secondResult = 
saslNegotiator.handleResponse(response);
+        assertEquals(
+                "Unexpected second result status",
+                SUCCESS,
+                secondResult.getStatus()
+        );
+        assertNotNull("Unexpected second result challenge", 
secondResult.getChallenge());
+        assertEquals(
+                "Unexpected second result principal",
+                username,
+                secondResult.getMainPrincipal().getName()
+        );
+
+        final String serverFinalMessage = new 
String(secondResult.getChallenge(), SaslHelper.ASCII);
+        final String[] parts = serverFinalMessage.split(",");
+        if (!parts[0].startsWith("v="))
+        {
+            fail("Server final message did not contain verifier");
+        }
+        final byte[] serverSignature = 
Strings.decodeBase64(parts[0].substring(2));
+        if (!Arrays.equals(SaslHelper._serverSignature, serverSignature))
+        {
+            fail("Server signature did not match");
+        }
+
+        final AuthenticationResult thirdResult = 
saslNegotiator.handleResponse(initialResponse);
+        assertEquals("Unexpected result status after completion of 
negotiation",
+                     AuthenticationResult.AuthenticationStatus.ERROR,
+                     thirdResult.getStatus());
+        assertNull("Unexpected principal after completion of negotiation", 
thirdResult.getMainPrincipal());
+    }
+
+    private void saslScramShaInvalidCredentials(
+            String mechanism,
+            SaslNegotiator saslNegotiator,
+            String username,
+            String password) throws Exception
+    {
+
+        final byte[] initialResponse = 
SaslHelper.createInitialResponse(username);
+
+        final AuthenticationResult firstResult = 
saslNegotiator.handleResponse(initialResponse);
+        assertEquals(
+                "Unexpected first result status",
+                AuthenticationResult.AuthenticationStatus.CONTINUE,
+                firstResult.getStatus()
+                    );
+        assertNotNull("Unexpected first result challenge", 
firstResult.getChallenge());
+
+        final byte[] response = SaslHelper.calculateClientProof(
+                firstResult.getChallenge(),
+                ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+                        ? ScramSHA256AuthenticationManager.HMAC_NAME : 
ScramSHA1AuthenticationManager.HMAC_NAME,
+                ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+                        ? ScramSHA256AuthenticationManager.DIGEST_NAME : 
ScramSHA1AuthenticationManager.DIGEST_NAME,
+                password
+                                                               );
+
+        final AuthenticationResult secondResult = 
saslNegotiator.handleResponse(response);
+        assertEquals(
+                "Unexpected second result status",
+                ERROR,
+                secondResult.getStatus());
+        assertNull("Unexpected second result challenge", 
secondResult.getChallenge());
+
+    }
+
+    private static class SaslHelper
+    {
+
+        private static final String GS2_HEADER = "n,,";
+        private static final Charset ASCII = US_ASCII;
+        private static String _clientFirstMessageBare;
+        private static String _clientNonce;
+        private static byte[] _serverSignature;
+
+        private static byte[] calculateClientProof(
+                final byte[] challenge,
+                String hmacName,
+                String digestName,
+                String userPassword
+        ) throws Exception
+        {
+
+            final String serverFirstMessage = new String(challenge, ASCII);
+            final String[] parts = serverFirstMessage.split(",");
+            if (parts.length < 3)
+            {
+                fail("Server challenge '" + serverFirstMessage + "' cannot be 
parsed");
+            }
+            else if (parts[0].startsWith("m="))
+            {
+                fail("Server requires mandatory extension which is not 
supported: " + parts[0]);
+            }
+            else if (!parts[0].startsWith("r="))
+            {
+                fail("Server challenge '" + serverFirstMessage + "' cannot be 
parsed, cannot find nonce");
+            }
+            final String nonce = parts[0].substring(2);
+            if (!nonce.startsWith(_clientNonce))
+            {
+                fail("Server challenge did not use correct client nonce");
+            }
+            if (!parts[1].startsWith("s="))
+            {
+                fail("Server challenge '" + serverFirstMessage + "' cannot be 
parsed, cannot find salt");
+            }
+            final byte[] salt = Strings.decodeBase64(parts[1].substring(2));
+            if (!parts[2].startsWith("i="))
+            {
+                fail("Server challenge '" + serverFirstMessage + "' cannot be 
parsed, cannot find iteration count");
+            }
+            final int _iterationCount = 
Integer.parseInt(parts[2].substring(2));
+            if (_iterationCount <= 0)
+            {
+                fail("Iteration count " + _iterationCount + " is not a 
positive integer");
+            }
+            final byte[] passwordBytes = 
saslPrep(userPassword).getBytes(StandardCharsets.UTF_8);
+            final byte[] saltedPassword = 
generateSaltedPassword(passwordBytes, hmacName, _iterationCount, salt);
+
+            final String clientFinalMessageWithoutProof =
+                    "c=" + 
Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII))
+                    + ",r=" + nonce;
+
+            final String authMessage =
+                    _clientFirstMessageBare + "," + serverFirstMessage + "," + 
clientFinalMessageWithoutProof;
+            final byte[] clientKey = computeHmac(saltedPassword, "Client Key", 
hmacName);
+            final byte[] storedKey = 
MessageDigest.getInstance(digestName).digest(clientKey);
+            final byte[] clientSignature = computeHmac(storedKey, authMessage, 
hmacName);
+            final byte[] clientProof = clientKey.clone();
+            for (int i = 0; i < clientProof.length; i++)
+            {
+                clientProof[i] ^= clientSignature[i];
+            }
+            final byte[] serverKey = computeHmac(saltedPassword, "Server Key", 
hmacName);
+            _serverSignature = computeHmac(serverKey, authMessage, hmacName);
+            final String finalMessageWithProof = clientFinalMessageWithoutProof
+                                                 + ",p=" + 
Base64.getEncoder().encodeToString(clientProof);
+            return finalMessageWithProof.getBytes();
+        }
+
+        private static byte[] computeHmac(final byte[] key, final String 
string, String hmacName)
+                throws Exception
+        {
+            final Mac mac = createHmac(key, hmacName);
+            mac.update(string.getBytes(ASCII));
+            return mac.doFinal();
+        }
+
+        private static byte[] generateSaltedPassword(
+                final byte[] passwordBytes,
+                String hmacName,
+                final int iterationCount,
+                final byte[] salt
+        ) throws Exception
+        {
+            final Mac mac = createHmac(passwordBytes, hmacName);
+            mac.update(salt);
+            mac.update(new byte[]{0, 0, 0, 1});
+            final byte[] result = mac.doFinal();
+
+            byte[] previous = null;
+            for (int i = 1; i < iterationCount; i++)
+            {
+                mac.update(previous != null ? previous : result);
+                previous = mac.doFinal();
+                for (int x = 0; x < result.length; x++)
+                {
+                    result[x] ^= previous[x];
+                }
+            }
+
+            return result;
+        }
+
+        private static Mac createHmac(final byte[] keyBytes, String hmacName) 
throws Exception
+        {
+            final SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
+            final Mac mac = Mac.getInstance(hmacName);
+            mac.init(key);
+            return mac;
+        }
+
+        private static String saslPrep(String name)
+        {
+            name = name.replace("=", "=3D");
+            name = name.replace(",", "=2C");
+            return name;
+        }
+
+        private static byte[] createInitialResponse(final String userName)
+        {
+            _clientFirstMessageBare = "n=" + saslPrep(userName) + ",r=" + 
_clientNonce;
+            return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII);
+        }
+    }
+
+    private static final SaslSettings CRAM_MD_SASL_SETTINGS = new 
SaslSettings()
+    {
+        @Override
+        public String getLocalFQDN()
+        {
+            return "example.com";
+        }
+
+        @Override
+        public Principal getExternalPrincipal()
+        {
+            return null;
+        }
+    };
+}
diff --git 
a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
 
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
new file mode 100644
index 0000000..b20ead9
--- /dev/null
+++ 
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
@@ -0,0 +1,28 @@
+<!--
+  ~ 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.
+  -->
+<div class="clear">
+    <div class="formLabel-labelCell tableContainer-labelCell">Delegates:</div>
+    <div class="formLabel-controlCell tableContainer-valueCell">
+        <div id="composite.delegates.container"/>
+        <input type="hidden" id="composite.delegates"/>
+    </div>
+    <div data-dojo-type="dijit/Tooltip"
+         data-dojo-props="connectId: ['composite.delegates'],
+                                      label: 'List of authentication provider 
delegate names'"/>
+</div>
diff --git 
a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
 
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
new file mode 100644
index 0000000..1f40b1f
--- /dev/null
+++ 
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
@@ -0,0 +1,26 @@
+<!--
+  ~ 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.
+  -->
+<div>
+    <div class="clear">
+        <div class="formLabel-labelCell">Delegates:</div>
+        <div ><span class="delegates" ></span></div>
+    </div>
+    <div class="clear"></div>
+</div>
+
diff --git 
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
new file mode 100644
index 0000000..5490ec6
--- /dev/null
+++ 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
@@ -0,0 +1,159 @@
+/*
+ *
+ * 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.
+ *
+ */
+define([
+    'dojo/dom',
+    'dojo/query',
+    'dijit/registry',
+    'qpid/common/util'
+], function (
+    dom,
+    query,
+    registry,
+    util
+)
+{
+    return {
+        show: function (data)
+        {
+            util.parseHtmlIntoDiv(data.containerNode, 
'authenticationprovider/composite/add.html', function ()
+            {
+                if (!!data.data)
+                {
+                    util.applyToWidgets(data.containerNode,
+                        'AuthenticationProvider',
+                        'Composite',
+                        data.data,
+                        data.metadata);
+                }
+                this.data = data;
+                const that = this;
+                this.management.load({type: 'authenticationprovider'}, 
{excludeInheritedContext: true, depth: 1})
+                    .then(function (data)
+                    {
+                        const allowed = ['MD5', 'Plain', 'SCRAM-SHA-1', 
'SCRAM-SHA-256', 'SimpleLDAP'];
+                        const names = data
+                            .filter(authProvider => 
allowed.includes(authProvider.type))
+                            .map(authProvider => authProvider.name);
+
+                        const authProviderNames = that.data?.data?.delegates
+                            ? that.data?.data?.delegates.slice() : [];
+                        for (const name of names)
+                        {
+                            if (!authProviderNames.includes(name))
+                            {
+                                authProviderNames.push(name);
+                            }
+                        }
+                        const authProvidersMultiSelect = 
dom.byId('composite.delegates.container');
+                        authProvidersMultiSelect.style = 'border: 1px solid 
lightgray; padding: .5em; width: 14em;';
+
+                        for (const name of authProviderNames)
+                        {
+                            const row = document.createElement('div');
+                            row.style = 'display: flex; justify-content: 
space-between; width: 15em; margin-bottom: .5em;';
+
+                            const checkboxContainer = 
document.createElement('span');
+                            checkboxContainer.style = 'display: flex;';
+
+                            const checkbox = document.createElement('input');
+                            checkbox.setAttribute('type', 'checkbox');
+                            checkbox.setAttribute('data-dojo-type', 
'dijit/form/CheckBox');
+                            checkbox.setAttribute('id', name + '-checkbox');
+                            checkbox.setAttribute('name', name + '-checkbox');
+                            checkbox.style = 'cursor: pointer; margin-right: 
.5em;';
+                            checkboxContainer.appendChild(checkbox);
+
+                            const label = document.createElement('label');
+                            label.setAttribute('for', name + '-checkbox');
+                            label.style = 'cursor: pointer;';
+                            label.innerHTML = name;
+                            checkboxContainer.appendChild(label);
+
+                            const buttonContainer = 
document.createElement('span');
+                            buttonContainer.style = 'display: flex; 
padding-right: .5em;';
+
+                            const up = document.createElement('button');
+                            up.innerHTML = '&#9650;';
+                            up.setAttribute('data-dojo-type', 
'dijit/form/Button');
+                            up.setAttribute('type', 'button');
+                            up.addEventListener('click', (el) => 
moveUp(el.target));
+                            up.style = 'width: 1.5em; height: 1.5em; 
margin-right: .2em; display: flex; '
+                                       + 'align-content: center; align-items: 
center; justify-content: center;';
+                            buttonContainer.appendChild(up);
+
+                            const down = document.createElement('button');
+                            down.innerHTML = '&#9660;';
+                            down.setAttribute('data-dojo-type', 
'dijit/form/Button');
+                            down.setAttribute('type', 'button');
+                            down.addEventListener('click',(el) => 
moveDown(el.target));
+                            down.style = 'width: 1.5em; height: 1.5em; 
margin-right: .2em; display: flex; '
+                                         + 'align-content: center; 
align-items: center; justify-content: center;';
+                            buttonContainer.appendChild(down);
+
+                            row.appendChild(checkboxContainer);
+                            row.appendChild(buttonContainer);
+
+                            authProvidersMultiSelect.appendChild(row);
+                        }
+
+                        if (that.data?.data?.delegates)
+                        {
+                            for (const delegate of that.data.data.delegates)
+                            {
+                                dom.byId(delegate + 
'-checkbox').setAttribute('checked', 'checked');
+                            }
+                        }
+
+                    }, util.xhrErrorHandler);
+
+                function moveUp(el) {
+                    const row = el.parentNode.parentNode;
+                    if (row.previousElementSibling)
+                    {
+                        row.parentNode.insertBefore(row, 
row.previousElementSibling);
+                    }
+                }
+
+                function moveDown(el) {
+                    const row = el.parentNode.parentNode;
+                    if (row.nextElementSibling)
+                    {
+                        row.parentNode.insertBefore(row.nextElementSibling, 
row);
+                    }
+                }
+            });
+        },
+        _preSubmit: function(formData)
+        {
+            let result = [];
+            const rows = 
document.getElementById('composite.delegates.container').children;
+            for (let i = 0; i < rows.length; i ++)
+            {
+                if (rows.item(i).firstChild?.firstChild?.checked)
+                {
+                    result.push(rows.item(i).firstChild?.innerText);
+                }
+            }
+            formData.delegates = result;
+        }
+    };
+});
+
diff --git 
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
new file mode 100644
index 0000000..0d98104
--- /dev/null
+++ 
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
@@ -0,0 +1,38 @@
+/*
+ *
+ * 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.
+ *
+ */
+define(["qpid/common/util", "dojo/domReady!"], function (util)
+{
+
+    function CompositeAuthenticationProvider(data)
+    {
+        util.buildUI(data.containerNode, data.parent, 
"authenticationprovider/composite/show.html", ["delegates"], this);
+    }
+
+    CompositeAuthenticationProvider.prototype.update = function (data)
+    {
+        if (this['delegates'])
+        {
+            this['delegates'].innerHTML = data?.delegates?.join(', ');
+        }
+    }
+
+    return CompositeAuthenticationProvider;
+});
diff --git 
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
new file mode 100644
index 0000000..ce09492
--- /dev/null
+++ 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!--
+
+ 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.
+
+-->
+
+<section xmlns="http://docbook.org/ns/docbook"; version="5.0" 
xml:id="Java-Broker-Security-Composite-Provider">
+    <title>Composite Provider</title>
+
+    <para>Composite Provider uses existing username / password authentication 
providers allowing to perform authentication
+        against them in order defined. It can contains following 
authentication providers:
+        <itemizedlist>
+            <listitem><para><emphasis><link 
linkend="Java-Broker-Security-MD5-Provider">MD5 
Provider</link></emphasis></para></listitem>
+            <listitem><para><emphasis><link 
linkend="Java-Broker-Security-Plain-Provider">Plain 
Provider</link></emphasis></para></listitem>
+            <listitem><para><emphasis><link 
linkend="Java-Broker-Security-ScramSha-Providers">SCRAM SHA 
Providers</link></emphasis></para></listitem>
+            <listitem><para><emphasis><link 
linkend="Java-Broker-Security-LDAP-Provider">Simple LDAP 
Providers</link></emphasis></para></listitem>
+        </itemizedlist>
+    </para>
+
+    <para>When performing authentication, composite provider checks presence 
of a user with a given username in the first
+    delegate provider and if found, performs authentication. It should be 
considered, that in case of name collision
+    (when delegate providers contains users with same username but different 
passwords), it's not guaranteed that
+    authentication will succeed even with the correct credentials. Therefore 
username collision should be avoided, i.e.
+    each delegate provider should contain users with unique usernames.</para>
+
+    <table pgwide="1">
+        <title>SASL Mechanisms</title>
+        <tgroup cols="2">
+            <colspec colnum="1" colname="authentication_provider" 
colwidth="1*"/>
+            <colspec colnum="2" colname="sasl_mechanisms" colwidth="1*"/>
+            <thead>
+                <row>
+                    <entry>Authentication provider</entry>
+                    <entry>SASL mechanisms</entry>
+                </row>
+            </thead>
+            <tbody>
+                <row 
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-MD5">
+                    <entry><link 
linkend="Java-Broker-Security-MD5-Provider">MD5 Provider</link></entry>
+                    <entry>PLAIN, CRAM-MD5-HASHED, CRAM-MD5-HEX</entry>
+                </row>
+                <row 
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-Plain">
+                    <entry><link 
linkend="Java-Broker-Security-Plain-Provider">Plain</link></entry>
+                    <entry>PLAIN, CRAM-MD5, SCRAM-SHA-1, SCRAM-SHA-256</entry>
+                </row>
+                <row 
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-ScramSha">
+                    <entry><link 
linkend="Java-Broker-Security-ScramSha-Providers">SCRAM SHA 
Providers</link></entry>
+                    <entry>PLAIN, SCRAM-SHA-1, SCRAM-SHA-256</entry>
+                </row>
+                <row 
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-Simple-LDAP">
+                    <entry><link 
linkend="Java-Broker-Security-LDAP-Provider">Simple LDAP 
Providers</link></entry>
+                    <entry>PLAIN</entry>
+                </row>
+            </tbody>
+        </tgroup>
+    </table>
+
+    <para>Composite provider exposes intersection of SASL mechanism provided 
by its delegates.</para>
+
+</section>
diff --git 
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
index fa733fb..3ca0e2c 100644
--- 
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
+++ 
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
@@ -63,4 +63,5 @@
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"; 
href="Java-Broker-Security-Authentication-Providers-PlainPasswordFile.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"; 
href="Java-Broker-Security-Authentication-Providers-MD5.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"; 
href="Java-Broker-Security-Authentication-Providers-Base64MD5PasswordFile.xml"/>
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"; 
href="Java-Broker-Security-Authentication-Providers-Composite.xml"/>
  </section>

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

Reply via email to