summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Godfrey <rgodfrey@apache.org>2014-03-21 23:08:42 +0000
committerRobert Godfrey <rgodfrey@apache.org>2014-03-21 23:08:42 +0000
commiteeba35b5cda54b962b7b1e10659418c12c3ba324 (patch)
treef69800a9b3b30e014c9413fda742c1ec16b5bb10
parent668b043aca23619552d860889e5c44b88bbe93ad (diff)
downloadqpid-python-eeba35b5cda54b962b7b1e10659418c12c3ba324.tar.gz
QPID-5639 : [Java Broker] Add SCRAM-SHA-1 SASL support
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk@1580082 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/configuration/startup/AuthenticationProviderRecoverer.java22
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/model/AuthenticationProvider.java2
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java8
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java696
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerFactory.java74
-rw-r--r--qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java273
-rw-r--r--qpid/java/broker-core/src/main/resources/META-INF/services/org.apache.qpid.server.plugin.AuthenticationManagerFactory2
-rw-r--r--qpid/java/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerTest.java203
-rw-r--r--qpid/java/broker-plugins/management-http/pom.xml6
-rw-r--r--qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/FileServlet.java4
-rw-r--r--qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/SaslServlet.java12
-rw-r--r--qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js221
-rw-r--r--qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js2
-rw-r--r--qpid/java/build.deps4
-rw-r--r--qpid/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties2
-rw-r--r--qpid/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties1
-rw-r--r--qpid/java/ivy.retrieve.xml1
17 files changed, 1489 insertions, 44 deletions
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/configuration/startup/AuthenticationProviderRecoverer.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/configuration/startup/AuthenticationProviderRecoverer.java
index 8eec88d556..46f3cd458b 100644
--- a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/configuration/startup/AuthenticationProviderRecoverer.java
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/configuration/startup/AuthenticationProviderRecoverer.java
@@ -32,6 +32,7 @@ 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.PreferencesProvider;
+import org.apache.qpid.server.model.User;
import org.apache.qpid.server.model.adapter.AuthenticationProviderFactory;
public class AuthenticationProviderRecoverer implements ConfiguredObjectRecoverer<AuthenticationProvider>
@@ -68,9 +69,24 @@ public class AuthenticationProviderRecoverer implements ConfiguredObjectRecovere
Map<String, Collection<ConfigurationEntry>> childEntries,
String type)
{
- ConfiguredObjectRecoverer<?> recoverer = recovererProvider.getRecoverer(type);
+ ConfiguredObjectRecoverer<?> recoverer = null;
+
+ if(authenticationProvider instanceof RecovererProvider)
+ {
+ recoverer = ((RecovererProvider)authenticationProvider).getRecoverer(type);
+ }
+
+ if(recoverer == null)
+ {
+ recoverer = recovererProvider.getRecoverer(type);
+ }
+
if (recoverer == null)
{
+ if(authenticationProvider instanceof RecovererProvider)
+ {
+ ((RecovererProvider)authenticationProvider).getRecoverer(type);
+ }
throw new IllegalConfigurationException("Cannot recover entry for the type '" + type + "' from broker");
}
Collection<ConfigurationEntry> entries = childEntries.get(type);
@@ -85,6 +101,10 @@ public class AuthenticationProviderRecoverer implements ConfiguredObjectRecovere
{
authenticationProvider.setPreferencesProvider((PreferencesProvider)object);
}
+ else if(object instanceof User)
+ {
+ authenticationProvider.recoverUser((User)object);
+ }
else
{
throw new IllegalConfigurationException("Cannot associate " + object + " with authentication provider " + authenticationProvider);
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/model/AuthenticationProvider.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/model/AuthenticationProvider.java
index 8e1ea39cec..fc0a8ab7e5 100644
--- a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/model/AuthenticationProvider.java
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/model/AuthenticationProvider.java
@@ -55,4 +55,6 @@ public interface AuthenticationProvider<X extends AuthenticationProvider<X>> ext
* @param preferencesProvider
*/
void setPreferencesProvider(PreferencesProvider preferencesProvider);
+
+ void recoverUser(User user);
}
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java
index 7c521c1f8a..f15195b812 100644
--- a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/AbstractAuthenticationManager.java
@@ -21,6 +21,7 @@
package org.apache.qpid.server.security.auth.manager;
import org.apache.log4j.Logger;
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.configuration.updater.TaskExecutor;
import org.apache.qpid.server.model.*;
import org.apache.qpid.server.model.adapter.AbstractConfiguredObject;
@@ -85,8 +86,11 @@ public abstract class AbstractAuthenticationManager<T extends AbstractAuthentica
_preferencesProvider = preferencesProvider;
}
-
-
+ @Override
+ public void recoverUser(final User user)
+ {
+ throw new IllegalConfigurationException("Cannot associate " + user + " with authentication provider " + this);
+ }
@Override
public String setName(final String currentName, final String desiredName)
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java
new file mode 100644
index 0000000000..097d0bfb9d
--- /dev/null
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManager.java
@@ -0,0 +1,696 @@
+/*
+ *
+ * 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 org.apache.qpid.server.configuration.ConfigurationEntry;
+import org.apache.qpid.server.configuration.ConfiguredObjectRecoverer;
+import org.apache.qpid.server.configuration.RecovererProvider;
+import org.apache.qpid.server.configuration.updater.ChangeAttributesTask;
+import org.apache.qpid.server.configuration.updater.TaskExecutor;
+import org.apache.qpid.server.model.*;
+import org.apache.qpid.server.model.adapter.AbstractConfiguredObject;
+import org.apache.qpid.server.security.SecurityManager;
+import org.apache.qpid.server.security.access.Operation;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.UsernamePrincipal;
+import org.apache.qpid.server.security.auth.sasl.scram.ScramSHA1SaslServer;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.login.AccountNotFoundException;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+import javax.xml.bind.DatatypeConverter;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.AccessControlException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ScramSHA1AuthenticationManager
+ extends AbstractAuthenticationManager<ScramSHA1AuthenticationManager>
+ implements PasswordCredentialManagingAuthenticationProvider<ScramSHA1AuthenticationManager>,
+ RecovererProvider
+{
+ public static final String SCRAM_USER_TYPE = "scram";
+ private static final Charset ASCII = Charset.forName("ASCII");
+ public static final String HMAC_SHA_1 = "HmacSHA1";
+ private final SecureRandom _random = new SecureRandom();
+ private int _iterationCount = 4096;
+ private Map<String, ScramAuthUser> _users = new ConcurrentHashMap<String, ScramAuthUser>();
+
+
+ protected ScramSHA1AuthenticationManager(final Broker broker,
+ final Map<String, Object> defaults,
+ final Map<String, Object> attributes,
+ final boolean recovering)
+ {
+ super(broker, defaults, attributes);
+ }
+
+ @Override
+ public void initialise()
+ {
+
+ }
+
+ @Override
+ public String getMechanisms()
+ {
+ return ScramSHA1SaslServer.MECHANISM;
+ }
+
+ @Override
+ public SaslServer createSaslServer(final String mechanism,
+ final String localFQDN,
+ final Principal externalPrincipal)
+ throws SaslException
+ {
+ return new ScramSHA1SaslServer(this);
+ }
+
+ @Override
+ public AuthenticationResult authenticate(final SaslServer server, final byte[] response)
+ {
+ try
+ {
+ // Process response from the client
+ byte[] challenge = server.evaluateResponse(response != null ? response : new byte[0]);
+
+ if (server.isComplete())
+ {
+ final String userId = server.getAuthorizationID();
+ return new AuthenticationResult(new UsernamePrincipal(userId));
+ }
+ else
+ {
+ return new AuthenticationResult(challenge, AuthenticationResult.AuthenticationStatus.CONTINUE);
+ }
+ }
+ catch (SaslException e)
+ {
+ return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
+ }
+ }
+
+ @Override
+ public AuthenticationResult authenticate(final String username, final String password)
+ {
+ ScramAuthUser user = getUser(username);
+ if(user != null)
+ {
+ final String[] usernamePassword = user.getPassword().split(",");
+ byte[] salt = DatatypeConverter.parseBase64Binary(usernamePassword[0]);
+ try
+ {
+ if(Arrays.equals(DatatypeConverter.parseBase64Binary(usernamePassword[1]),createSaltedPassword(salt, password)))
+ {
+ return new AuthenticationResult(new UsernamePrincipal(username));
+ }
+ }
+ catch (SaslException e)
+ {
+ return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR,e);
+ }
+
+ }
+
+ return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+
+
+ }
+
+ @Override
+ public void delete()
+ {
+
+ }
+
+ @Override
+ public void close()
+ {
+
+ }
+
+ public int getIterationCount()
+ {
+ return _iterationCount;
+ }
+
+ public byte[] getSalt(final String username)
+ {
+ ScramAuthUser user = getUser(username);
+
+ if(user == null)
+ {
+ // don't disclose that the user doesn't exist, just generate random data so the failure is indistinguishable
+ // from the "wrong password" case
+
+ byte[] salt = new byte[32];
+ _random.nextBytes(salt);
+ return salt;
+ }
+ else
+ {
+ return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[0]);
+ }
+ }
+
+ private static final byte[] INT_1 = new byte[]{0, 0, 0, 1};
+
+ public byte[] getSaltedPassword(final String username) throws SaslException
+ {
+ ScramAuthUser user = getUser(username);
+ if(user == null)
+ {
+ throw new SaslException("Authentication Failed");
+ }
+ else
+ {
+ return DatatypeConverter.parseBase64Binary(user.getPassword().split(",")[1]);
+ }
+ }
+
+ private ScramAuthUser getUser(final String username)
+ {
+ return _users.get(username);
+ }
+
+ private byte[] createSaltedPassword(byte[] salt, String password) throws SaslException
+ {
+ Mac mac = createSha1Hmac(password.getBytes(ASCII));
+
+ mac.update(salt);
+ mac.update(INT_1);
+ byte[] result = mac.doFinal();
+
+ byte[] previous = null;
+ for(int i = 1; i < getIterationCount(); 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 Mac createSha1Hmac(final byte[] keyBytes)
+ throws SaslException
+ {
+ try
+ {
+ SecretKeySpec key = new SecretKeySpec(keyBytes, HMAC_SHA_1);
+ Mac mac = Mac.getInstance(HMAC_SHA_1);
+ mac.init(key);
+ return mac;
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ catch (InvalidKeyException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean createUser(final String username, final String password, final Map<String, String> attributes)
+ {
+ if (getTaskExecutor().isTaskExecutorThread())
+ {
+ getSecurityManager().authoriseUserOperation(Operation.CREATE, username);
+ if(_users.containsKey(username))
+ {
+ throw new IllegalArgumentException("User '"+username+"' already exists");
+ }
+ try
+ {
+ Map<String,Object> userAttrs = new HashMap<String, Object>();
+ userAttrs.put(User.ID, UUID.randomUUID());
+ userAttrs.put(User.NAME, username);
+ userAttrs.put(User.PASSWORD, createStoredPassword(password));
+ userAttrs.put(User.TYPE, SCRAM_USER_TYPE);
+ ScramAuthUser user = new ScramAuthUser(userAttrs);
+ _users.put(username, user);
+
+ return true;
+ }
+ catch (SaslException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ else
+ {
+ return getTaskExecutor().submitAndWait(new TaskExecutor.Task<Boolean>()
+ {
+ @Override
+ public Boolean call()
+ {
+ return createUser(username, password, attributes);
+ }
+ });
+ }
+
+ }
+
+ private SecurityManager getSecurityManager()
+ {
+ return getBroker().getSecurityManager();
+ }
+
+ @Override
+ public void deleteUser(final String user) throws AccountNotFoundException
+ {
+ if (getTaskExecutor().isTaskExecutorThread())
+ {
+
+ final ScramAuthUser authUser = getUser(user);
+ if(authUser != null)
+ {
+ authUser.setState(State.ACTIVE, State.DELETED);
+ }
+ else
+ {
+ throw new AccountNotFoundException("No such user: '" + user + "'");
+ }
+ }
+ else
+ {
+ AccountNotFoundException e =
+ getTaskExecutor().submitAndWait(new TaskExecutor.Task<AccountNotFoundException>() {
+
+ @Override
+ public AccountNotFoundException call()
+ {
+ try
+ {
+ deleteUser(user);
+ return null;
+ }
+ catch (AccountNotFoundException e)
+ {
+ return e;
+ }
+
+ }
+ });
+
+ if(e != null)
+ {
+ throw e;
+ }
+ }
+ }
+
+ @Override
+ public void setPassword(final String username, final String password) throws AccountNotFoundException
+ {
+ if (getTaskExecutor().isTaskExecutorThread())
+ {
+ final ScramAuthUser authUser = getUser(username);
+ if(authUser != null)
+ {
+ authUser.setPassword(password);
+ }
+ else
+ {
+ throw new AccountNotFoundException("No such user: '" + username + "'");
+ }
+ }
+ else
+ {
+ AccountNotFoundException e =
+ getTaskExecutor().submitAndWait(new TaskExecutor.Task<AccountNotFoundException>()
+ {
+
+ @Override
+ public AccountNotFoundException call()
+ {
+ try
+ {
+ setPassword(username, password);
+ return null;
+ }
+ catch (AccountNotFoundException e)
+ {
+ return e;
+ }
+
+ }
+ });
+
+ if (e != null)
+ {
+ throw e;
+ }
+ }
+
+ }
+
+ @Override
+ public Map<String, Map<String, String>> getUsers()
+ {
+ if (getTaskExecutor().isTaskExecutorThread())
+ {
+ Map<String, Map<String,String>> users = new HashMap<String, Map<String, String>>();
+ for(String user : _users.keySet())
+ {
+ users.put(user, Collections.<String,String>emptyMap());
+ }
+ return users;
+ }
+ else
+ {
+ return getTaskExecutor().submitAndWait(new TaskExecutor.Task<Map<String, Map<String, String>>>()
+ {
+ @Override
+ public Map<String, Map<String, String>> call()
+ {
+ return getUsers();
+ }
+ });
+ }
+ }
+
+ @Override
+ public void reload() throws IOException
+ {
+
+ }
+
+ private static Map<Class<? extends ConfiguredObject>, ConfiguredObject<?>> parentsMap(final ScramSHA1AuthenticationManager scramSHA1AuthenticationManager)
+ {
+
+ final Map<Class<? extends ConfiguredObject>, ConfiguredObject<?>> map = new HashMap<Class<? extends ConfiguredObject>, ConfiguredObject<?>>();
+ map.put(AuthenticationProvider.class, scramSHA1AuthenticationManager);
+ return map;
+ }
+
+ @Override
+ public ConfiguredObjectRecoverer<? extends ConfiguredObject> getRecoverer(final String type)
+ {
+ if("User".equals(type))
+ {
+ return new UserRecoverer();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ private class ScramAuthUser extends AbstractConfiguredObject<ScramAuthUser> implements User<ScramAuthUser>
+ {
+
+
+ protected ScramAuthUser(final Map<String, Object> attributes)
+ {
+ super(parentsMap(ScramSHA1AuthenticationManager.this),
+ Collections.<String,Object>emptyMap(),
+ attributes, ScramSHA1AuthenticationManager.this.getTaskExecutor());
+
+ if(!ASCII.newEncoder().canEncode(getName()))
+ {
+ throw new IllegalArgumentException("Scram SHA1 user names are restricted to characters in the ASCII charset");
+ }
+ }
+
+ @Override
+ protected boolean setState(final State currentState, final State desiredState)
+ {
+ if(desiredState == State.DELETED)
+ {
+ getSecurityManager().authoriseUserOperation(Operation.DELETE, getName());
+ _users.remove(getName());
+ ScramSHA1AuthenticationManager.this.childRemoved(this);
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ @Override
+ public void setAttributes(final Map<String, Object> attributes)
+ throws IllegalStateException, AccessControlException, IllegalArgumentException
+ {
+ if (getTaskExecutor().isTaskExecutorThread())
+ {
+ Map<String,Object> modifiedAttributes = new HashMap<String, Object>(attributes);
+ final String newPassword = (String) attributes.get(User.PASSWORD);
+ if(attributes.containsKey(User.PASSWORD) && !newPassword.equals(getActualAttributes().get(User.PASSWORD)))
+ {
+ try
+ {
+ modifiedAttributes.put(User.PASSWORD, createStoredPassword(newPassword));
+ }
+ catch (SaslException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ super.setAttributes(modifiedAttributes);
+ }
+ else
+ {
+ getTaskExecutor().submitAndWait(new ChangeAttributesTask(this, attributes));
+ }
+
+ }
+
+ @Override
+ public Object getAttribute(final String name)
+ {
+ if(PASSWORD.equals(name))
+ {
+ return null; // for security reasons we don't expose the password
+ }
+ return super.getAttribute(name);
+ }
+
+ @Override
+ public String getPassword()
+ {
+ return (String) getActualAttributes().get(PASSWORD);
+ }
+
+ @Override
+ public void setPassword(final String password)
+ {
+ getSecurityManager().authoriseUserOperation(Operation.UPDATE, getName());
+
+ try
+ {
+ changeAttribute(User.PASSWORD, getAttribute(User.PASSWORD), createStoredPassword(password));
+ }
+ catch (SaslException e)
+ {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public String setName(final String currentName, final String desiredName)
+ throws IllegalStateException, AccessControlException
+ {
+ throw new IllegalStateException("Names cannot be updated");
+ }
+
+ @Override
+ public State getState()
+ {
+ return State.ACTIVE;
+ }
+
+ @Override
+ public boolean isDurable()
+ {
+ return true;
+ }
+
+ @Override
+ public void setDurable(final boolean durable)
+ throws IllegalStateException, AccessControlException, IllegalArgumentException
+ {
+
+ }
+
+ @Override
+ public LifetimePolicy getLifetimePolicy()
+ {
+ return LifetimePolicy.PERMANENT;
+ }
+
+ @Override
+ public LifetimePolicy setLifetimePolicy(final LifetimePolicy expected, final LifetimePolicy desired)
+ throws IllegalStateException, AccessControlException, IllegalArgumentException
+ {
+ if(expected == desired && expected == LifetimePolicy.PERMANENT)
+ {
+ return LifetimePolicy.PERMANENT;
+ }
+ throw new IllegalArgumentException("Cannot change lifetime policy of a user");
+
+ }
+
+ @Override
+ public <C extends ConfiguredObject> Collection<C> getChildren(final Class<C> clazz)
+ {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Map<String, Object> getPreferences()
+ {
+ PreferencesProvider preferencesProvider = getPreferencesProvider();
+ if (preferencesProvider == null)
+ {
+ return null;
+ }
+ return preferencesProvider.getPreferences(this.getName());
+ }
+
+ @Override
+ public Object getPreference(String name)
+ {
+ Map<String, Object> preferences = getPreferences();
+ if (preferences == null)
+ {
+ return null;
+ }
+ return preferences.get(name);
+ }
+
+ @Override
+ public Map<String, Object> setPreferences(Map<String, Object> preferences)
+ {
+ PreferencesProvider preferencesProvider = getPreferencesProvider();
+ if (preferencesProvider == null)
+ {
+ return null;
+ }
+ return preferencesProvider.setPreferences(this.getName(), preferences);
+ }
+
+ @Override
+ public boolean deletePreferences()
+ {
+ PreferencesProvider preferencesProvider = getPreferencesProvider();
+ if (preferencesProvider == null)
+ {
+ return false;
+ }
+ String[] deleted = preferencesProvider.deletePreferences(this.getName());
+ return deleted.length == 1;
+ }
+
+ @Override
+ public Collection<String> getAttributeNames()
+ {
+ return getAttributeNames(getClass());
+ }
+ }
+
+ @Override
+ public void recoverUser(final User user)
+ {
+ _users.put(user.getName(), (ScramAuthUser) user);
+ }
+
+ protected String createStoredPassword(final String password) throws SaslException
+ {
+ byte[] salt = new byte[32];
+ _random.nextBytes(salt);
+ byte[] passwordBytes = createSaltedPassword(salt, password);
+ return DatatypeConverter.printBase64Binary(salt) + "," + DatatypeConverter.printBase64Binary(passwordBytes);
+ }
+
+ @Override
+ public <C extends ConfiguredObject> C addChild(final Class<C> childClass,
+ final Map<String, Object> attributes,
+ final ConfiguredObject... otherParents)
+ {
+ if(childClass == User.class)
+ {
+ String username = (String) attributes.get("name");
+ String password = (String) attributes.get("password");
+ Principal p = new UsernamePrincipal(username);
+
+ if(createUser(username, password,null))
+ {
+ @SuppressWarnings("unchecked")
+ C principalAdapter = (C) _users.get(username);
+ return principalAdapter;
+ }
+ else
+ {
+ return null;
+
+ }
+ }
+ return super.addChild(childClass, attributes, otherParents);
+ }
+
+ public <C extends ConfiguredObject> Collection<C> getChildren(Class<C> clazz)
+ {
+ if(clazz == User.class)
+ {
+ return new ArrayList(_users.values());
+ }
+ else
+ {
+ return super.getChildren(clazz);
+ }
+ }
+
+ private class UserRecoverer implements ConfiguredObjectRecoverer<ScramAuthUser>
+ {
+ @Override
+ public ScramAuthUser create(final RecovererProvider recovererProvider,
+ final ConfigurationEntry entry,
+ final ConfiguredObject... parents)
+ {
+
+ Map<String,Object> attributes = new HashMap<String, Object>(entry.getAttributes());
+ attributes.put(User.ID,entry.getId());
+ return new ScramAuthUser(attributes);
+ }
+ }
+}
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerFactory.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerFactory.java
new file mode 100644
index 0000000000..dd6f77e474
--- /dev/null
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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 org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.plugin.AuthenticationManagerFactory;
+import org.apache.qpid.server.util.ResourceBundleLoader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public class ScramSHA1AuthenticationManagerFactory implements AuthenticationManagerFactory
+{
+
+ public static final String PROVIDER_TYPE = "SCRAM-SHA1";
+
+ public static final String ATTRIBUTE_NAME = "name";
+
+ public static final Collection<String> ATTRIBUTES = Collections.<String> unmodifiableList(Arrays.asList(
+ AuthenticationProvider.TYPE
+ ));
+
+ @Override
+ public ScramSHA1AuthenticationManager createInstance(Broker broker,
+ Map<String, Object> attributes,
+ final boolean recovering)
+ {
+ if (attributes == null || !PROVIDER_TYPE.equals(attributes.get(AuthenticationProvider.TYPE)))
+ {
+ return null;
+ }
+
+
+ return new ScramSHA1AuthenticationManager(broker, Collections.<String,Object>emptyMap(),attributes, false);
+ }
+
+ @Override
+ public Collection<String> getAttributeNames()
+ {
+ return ATTRIBUTES;
+ }
+
+ @Override
+ public String getType()
+ {
+ return PROVIDER_TYPE;
+ }
+
+ @Override
+ public Map<String, String> getAttributeDescriptions()
+ {
+ return Collections.emptyMap();
+ }
+}
diff --git a/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java
new file mode 100644
index 0000000000..71ef386e3e
--- /dev/null
+++ b/qpid/java/broker-core/src/main/java/org/apache/qpid/server/security/auth/sasl/scram/ScramSHA1SaslServer.java
@@ -0,0 +1,273 @@
+/*
+ *
+ * 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.sasl.scram;
+
+import org.apache.qpid.server.security.auth.manager.ScramSHA1AuthenticationManager;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.sasl.SaslException;
+import javax.security.sasl.SaslServer;
+import javax.xml.bind.DatatypeConverter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.UUID;
+
+public class ScramSHA1SaslServer implements SaslServer
+{
+ public static final String MECHANISM = "SCRAM-SHA-1";
+
+ private static final Charset ASCII = Charset.forName("ASCII");
+
+ private final ScramSHA1AuthenticationManager _authManager;
+ private State _state = State.INITIAL;
+ private String _nonce;
+ private String _username;
+ private byte[] _gs2Header;
+ private String _serverFirstMessage;
+ private String _clientFirstMessageBare;
+ private byte[] _serverSignature;
+
+ public ScramSHA1SaslServer(final ScramSHA1AuthenticationManager authenticationManager)
+ {
+ _authManager = authenticationManager;
+ }
+
+ enum State
+ {
+ INITIAL,
+ SERVER_FIRST_MESSAGE_SENT,
+ COMPLETE
+ }
+
+ @Override
+ public String getMechanismName()
+ {
+ return MECHANISM;
+ }
+
+ @Override
+ public byte[] evaluateResponse(final byte[] response) throws SaslException
+ {
+ byte[] challenge;
+ switch (_state)
+ {
+ case INITIAL:
+ challenge = generateServerFirstMessage(response);
+ _state = State.SERVER_FIRST_MESSAGE_SENT;
+ break;
+ case SERVER_FIRST_MESSAGE_SENT:
+ challenge = generateServerFinalMessage(response);
+ _state = State.COMPLETE;
+ break;
+ default:
+ throw new SaslException("No response expected in state " + _state);
+
+ }
+ return challenge;
+ }
+
+ private byte[] generateServerFirstMessage(final byte[] response) throws SaslException
+ {
+ String clientFirstMessage = new String(response, ASCII);
+ if(!clientFirstMessage.startsWith("n"))
+ {
+ throw new SaslException("Cannot parse gs2-header");
+ }
+ String[] parts = clientFirstMessage.split(",");
+ if(parts.length < 4)
+ {
+ throw new SaslException("Cannot parse client first message");
+ }
+ _gs2Header = ("n,"+parts[1]+",").getBytes(ASCII);
+ _clientFirstMessageBare = clientFirstMessage.substring(_gs2Header.length);
+ if(!parts[2].startsWith("n="))
+ {
+ throw new SaslException("Cannot parse client first message");
+ }
+ _username = decodeUsername(parts[2].substring(2));
+ if(!parts[3].startsWith("r="))
+ {
+ throw new SaslException("Cannot parse client first message");
+ }
+ _nonce = parts[3].substring(2) + UUID.randomUUID().toString();
+
+ int count = _authManager.getIterationCount();
+ byte[] saltBytes = _authManager.getSalt(_username);
+ _serverFirstMessage = "r="+_nonce+",s="+ DatatypeConverter.printBase64Binary(saltBytes)+",i=" + count;
+ return _serverFirstMessage.getBytes(ASCII);
+ }
+
+ private String decodeUsername(String username) throws SaslException
+ {
+ if(username.contains("="))
+ {
+ String check = username;
+ while (check.contains("="))
+ {
+ check = check.substring(check.indexOf('=') + 1);
+ if (!(check.startsWith("2C") || check.startsWith("3D")))
+ {
+ throw new SaslException("Invalid username");
+ }
+ }
+ username = username.replace("=2C", ",");
+ username = username.replace("=3D","=");
+ }
+ return username;
+ }
+
+
+ private byte[] generateServerFinalMessage(final byte[] response) throws SaslException
+ {
+ try
+ {
+ String clientFinalMessage = new String(response, ASCII);
+ String[] parts = clientFinalMessage.split(",");
+ if(!parts[0].startsWith("c="))
+ {
+ throw new SaslException("Cannot parse client final message");
+ }
+ if(!Arrays.equals(_gs2Header,DatatypeConverter.parseBase64Binary(parts[0].substring(2))))
+ {
+ throw new SaslException("Client final message channel bind data invalid");
+ }
+ if(!parts[1].startsWith("r="))
+ {
+ throw new SaslException("Cannot parse client final message");
+ }
+ if(!parts[1].substring(2).equals(_nonce))
+ {
+ throw new SaslException("Client final message has incorrect nonce value");
+ }
+ if(!parts[parts.length-1].startsWith("p="))
+ {
+ throw new SaslException("Client final message does not have proof");
+ }
+
+ String clientFinalMessageWithoutProof = clientFinalMessage.substring(0,clientFinalMessage.length()-(1+parts[parts.length-1].length()));
+ byte[] proofBytes = DatatypeConverter.parseBase64Binary(parts[parts.length-1].substring(2));
+
+ String authMessage = _clientFirstMessageBare + "," + _serverFirstMessage + "," + clientFinalMessageWithoutProof;
+
+ byte[] saltedPassword = _authManager.getSaltedPassword(_username);
+
+ byte[] clientKey = computeHmacSHA1(saltedPassword, "Client Key");
+
+ byte[] storedKey = MessageDigest.getInstance("SHA1").digest(clientKey);
+
+ byte[] clientSignature = computeHmacSHA1(storedKey, authMessage);
+
+ byte[] clientProof = clientKey.clone();
+ for(int i = 0 ; i < clientProof.length; i++)
+ {
+ clientProof[i] ^= clientSignature[i];
+ }
+
+ if(!Arrays.equals(clientProof, proofBytes))
+ {
+ throw new SaslException("Authentication failed");
+ }
+ byte[] serverKey = computeHmacSHA1(saltedPassword, "Server Key");
+ String finalResponse = "v=" + DatatypeConverter.printBase64Binary(computeHmacSHA1(serverKey, authMessage));
+
+ return finalResponse.getBytes(ASCII);
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ catch (UnsupportedEncodingException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isComplete()
+ {
+ return _state == State.COMPLETE;
+ }
+
+ @Override
+ public String getAuthorizationID()
+ {
+ return _username;
+ }
+
+ @Override
+ public byte[] unwrap(final byte[] incoming, final int offset, final int len) throws SaslException
+ {
+ throw new IllegalStateException("No security layer supported");
+ }
+
+ @Override
+ public byte[] wrap(final byte[] outgoing, final int offset, final int len) throws SaslException
+ {
+ throw new IllegalStateException("No security layer supported");
+ }
+
+ @Override
+ public Object getNegotiatedProperty(final String propName)
+ {
+ return null;
+ }
+
+ @Override
+ public void dispose() throws SaslException
+ {
+
+ }
+
+ private byte[] computeHmacSHA1(final byte[] key, final String string)
+ throws SaslException, UnsupportedEncodingException
+ {
+ Mac mac = createSha1Hmac(key);
+ mac.update(string.getBytes(ASCII));
+ return mac.doFinal();
+ }
+
+
+ private Mac createSha1Hmac(final byte[] keyBytes)
+ throws SaslException
+ {
+ try
+ {
+ SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1");
+ Mac mac = Mac.getInstance("HmacSHA1");
+ mac.init(key);
+ return mac;
+ }
+ catch (NoSuchAlgorithmException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ catch (InvalidKeyException e)
+ {
+ throw new SaslException(e.getMessage(), e);
+ }
+ }
+
+}
diff --git a/qpid/java/broker-core/src/main/resources/META-INF/services/org.apache.qpid.server.plugin.AuthenticationManagerFactory b/qpid/java/broker-core/src/main/resources/META-INF/services/org.apache.qpid.server.plugin.AuthenticationManagerFactory
index 8ff67030ef..a1139b386c 100644
--- a/qpid/java/broker-core/src/main/resources/META-INF/services/org.apache.qpid.server.plugin.AuthenticationManagerFactory
+++ b/qpid/java/broker-core/src/main/resources/META-INF/services/org.apache.qpid.server.plugin.AuthenticationManagerFactory
@@ -22,3 +22,5 @@ org.apache.qpid.server.security.auth.manager.ExternalAuthenticationManagerFactor
org.apache.qpid.server.security.auth.manager.KerberosAuthenticationManagerFactory
org.apache.qpid.server.security.auth.manager.PlainPasswordFileAuthenticationManagerFactory
org.apache.qpid.server.security.auth.manager.SimpleLDAPAuthenticationManagerFactory
+org.apache.qpid.server.security.auth.manager.ScramSHA1AuthenticationManagerFactory
+
diff --git a/qpid/java/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerTest.java b/qpid/java/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerTest.java
new file mode 100644
index 0000000000..78b4b22ff3
--- /dev/null
+++ b/qpid/java/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/ScramSHA1AuthenticationManagerTest.java
@@ -0,0 +1,203 @@
+/*
+ *
+ * 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 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.State;
+import org.apache.qpid.server.model.User;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.test.utils.QpidTestCase;
+ import org.apache.qpid.server.security.SecurityManager;
+
+import javax.security.auth.login.AccountNotFoundException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ScramSHA1AuthenticationManagerTest extends QpidTestCase
+{
+ private ScramSHA1AuthenticationManager _authManager;
+ private Broker _broker;
+ private SecurityManager _securityManager;
+ private TaskExecutor _executor;
+
+ @Override
+ public void setUp() throws Exception
+ {
+ super.setUp();
+ _executor = new TaskExecutor();
+ _executor.start();
+ _broker = mock(Broker.class);
+ _securityManager = mock(SecurityManager.class);
+ when(_broker.getTaskExecutor()).thenReturn(_executor);
+ when(_broker.getSecurityManager()).thenReturn(_securityManager);
+ final Map<String, Object> attributesMap = new HashMap<String, Object>();
+ attributesMap.put(AuthenticationProvider.NAME, getTestName());
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ _authManager = new ScramSHA1AuthenticationManager(_broker, Collections.<String,Object>emptyMap(),attributesMap,false);
+ }
+
+ @Override
+ public void tearDown() throws Exception
+ {
+ _executor.stop();
+ super.tearDown();
+ }
+
+ public void testAddChildAndThenDelete()
+ {
+ // No children should be present before the test starts
+ assertEquals("No users should be present before the test starts", 0, _authManager.getChildren(User.class).size());
+ assertEquals("No users should be present before the test starts", 0, _authManager.getUsers().size());
+
+ final Map<String, Object> childAttrs = new HashMap<String, Object>();
+
+ childAttrs.put(User.NAME, getTestName());
+ childAttrs.put(User.PASSWORD, "password");
+ User user = _authManager.addChild(User.class, childAttrs);
+ assertNotNull("User should be created but addChild returned null", user);
+ assertEquals(getTestName(), user.getName());
+ // password shouldn't actually be the given string, but instead salt and the hashed value
+ assertFalse("Password shouldn't actually be the given string, but instead salt and the hashed value", "password".equals(user.getPassword()));
+
+ AuthenticationResult authResult =
+ _authManager.authenticate(getTestName(), "password");
+
+ assertEquals("User should authenticate with given password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+
+ assertEquals("Manager should have exactly one user child",1, _authManager.getChildren(User.class).size());
+ assertEquals("Manager should have exactly one user child",1, _authManager.getUsers().size());
+
+
+ user.setDesiredState(State.ACTIVE, State.DELETED);
+
+ assertEquals("No users should be present after child deletion", 0, _authManager.getChildren(User.class).size());
+
+
+ authResult = _authManager.authenticate(getTestName(), "password");
+ assertEquals("User should no longer authenticate with given password", AuthenticationResult.AuthenticationStatus.ERROR, authResult.getStatus());
+
+ }
+
+ public void testCreateUser()
+ {
+ assertEquals("No users should be present before the test starts", 0, _authManager.getChildren(User.class).size());
+ assertTrue(_authManager.createUser(getTestName(), "password", Collections.<String, String>emptyMap()));
+ assertEquals("Manager should have exactly one user child",1, _authManager.getChildren(User.class).size());
+ User user = _authManager.getChildren(User.class).iterator().next();
+ assertEquals(getTestName(), user.getName());
+ // password shouldn't actually be the given string, but instead salt and the hashed value
+ assertFalse("Password shouldn't actually be the given string, but instead salt and the hashed value", "password".equals(user.getPassword()));
+ final Map<String, Object> childAttrs = new HashMap<String, Object>();
+
+ childAttrs.put(User.NAME, getTestName());
+ childAttrs.put(User.PASSWORD, "password");
+ try
+ {
+ user = _authManager.addChild(User.class, childAttrs);
+ fail("Should not be able to create a second user with the same name");
+ }
+ catch(IllegalArgumentException e)
+ {
+ // pass
+ }
+ try
+ {
+ _authManager.deleteUser(getTestName());
+ }
+ catch (AccountNotFoundException e)
+ {
+ fail("AccountNotFoundException thrown when none was expected: " + e.getMessage());
+ }
+ try
+ {
+ _authManager.deleteUser(getTestName());
+ fail("AccountNotFoundException not thrown when was expected");
+ }
+ catch (AccountNotFoundException e)
+ {
+ // pass
+ }
+ }
+
+ public void testUpdateUser()
+ {
+ assertTrue(_authManager.createUser(getTestName(), "password", Collections.<String, String>emptyMap()));
+ assertTrue(_authManager.createUser(getTestName()+"_2", "password", Collections.<String, String>emptyMap()));
+ assertEquals("Manager should have exactly two user children",2, _authManager.getChildren(User.class).size());
+
+ AuthenticationResult authResult = _authManager.authenticate(getTestName(), "password");
+
+ assertEquals("User should authenticate with given password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+ authResult = _authManager.authenticate(getTestName()+"_2", "password");
+ assertEquals("User should authenticate with given password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+
+ for(User user : _authManager.getChildren(User.class))
+ {
+ if(user.getName().equals(getTestName()))
+ {
+ user.setAttributes(Collections.singletonMap(User.PASSWORD, "newpassword"));
+ }
+ }
+
+ authResult = _authManager.authenticate(getTestName(), "newpassword");
+ assertEquals("User should authenticate with updated password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+ authResult = _authManager.authenticate(getTestName()+"_2", "password");
+ assertEquals("User should authenticate with original password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+
+ authResult = _authManager.authenticate(getTestName(), "password");
+ assertEquals("User not authenticate with original password", AuthenticationResult.AuthenticationStatus.ERROR, authResult.getStatus());
+
+ for(User user : _authManager.getChildren(User.class))
+ {
+ if(user.getName().equals(getTestName()))
+ {
+ user.setPassword("newerpassword");
+ }
+ }
+
+ authResult = _authManager.authenticate(getTestName(), "newerpassword");
+ assertEquals("User should authenticate with updated password", AuthenticationResult.AuthenticationStatus.SUCCESS, authResult.getStatus());
+
+
+
+ }
+
+ public void testNonASCIIUser()
+ {
+ try
+ {
+ _authManager.createUser(getTestName()+"£", "password", Collections.<String, String>emptyMap());
+ fail("Expected exception when attempting to create a user with a non ascii name");
+ }
+ catch(IllegalArgumentException e)
+ {
+ // pass
+ }
+ }
+
+}
diff --git a/qpid/java/broker-plugins/management-http/pom.xml b/qpid/java/broker-plugins/management-http/pom.xml
index 4bfaf5e5d2..afb295f7cd 100644
--- a/qpid/java/broker-plugins/management-http/pom.xml
+++ b/qpid/java/broker-plugins/management-http/pom.xml
@@ -69,6 +69,12 @@
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.webjars</groupId>
+ <artifactId>cryptojs</artifactId>
+ <version>3.1.2</version>
+ </dependency>
+
<!-- test dependencies -->
<dependency>
<groupId>org.apache.qpid</groupId>
diff --git a/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/FileServlet.java b/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/FileServlet.java
index 618aaed319..72e9bac29b 100644
--- a/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/FileServlet.java
+++ b/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/FileServlet.java
@@ -89,6 +89,10 @@ public class FileServlet extends HttpServlet
}
URL resourceURL = getClass().getResource(_resourcePathPrefix + filename);
+ if(resourceURL == null)
+ {
+ resourceURL = getClass().getResource("/META-INF" + _resourcePathPrefix + filename);
+ }
if(resourceURL != null)
{
response.setStatus(HttpServletResponse.SC_OK);
diff --git a/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/SaslServlet.java b/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/SaslServlet.java
index 2ca67fadc9..af3973c7b3 100644
--- a/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/SaslServlet.java
+++ b/qpid/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/SaslServlet.java
@@ -59,7 +59,7 @@ public class SaslServlet extends AbstractServlet
private static final String ATTR_ID = "SaslServlet.ID";
private static final String ATTR_SASL_SERVER = "SaslServlet.SaslServer";
private static final String ATTR_EXPIRY = "SaslServlet.Expiry";
- private static final long SASL_EXCHANGE_EXPIRY = 1000L;
+ private static final long SASL_EXCHANGE_EXPIRY = 3000L;
public SaslServlet()
{
@@ -260,7 +260,17 @@ public class SaslServlet extends AbstractServlet
session.removeAttribute(ATTR_ID);
session.removeAttribute(ATTR_SASL_SERVER);
session.removeAttribute(ATTR_EXPIRY);
+ if(challenge != null && challenge.length != 0)
+ {
+ Map<String, Object> outputObject = new LinkedHashMap<String, Object>();
+ outputObject.put("challenge", new String(Base64.encodeBase64(challenge)));
+
+ final PrintWriter writer = response.getWriter();
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(SerializationConfig.Feature.INDENT_OUTPUT, true);
+ mapper.writeValue(writer, outputObject);
+ }
response.setStatus(HttpServletResponse.SC_OK);
}
else
diff --git a/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js b/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js
index 2d99f886ed..a25375a669 100644
--- a/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js
+++ b/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/authorization/sasl.js
@@ -18,8 +18,8 @@
* under the License.
*
*/
-define(["dojo/_base/xhr", "dojox/encoding/base64", "dojox/encoding/digests/_base", "dojox/encoding/digests/MD5"],
- function (xhr, base64, digestsBase, MD5) {
+define(["dojo/_base/xhr", "dojox/encoding/base64", "dojox/encoding/digests/_base", "dojox/encoding/digests/MD5", "dojox/uuid/generateRandomUuid", "dojo/request/script"],
+ function (xhr, base64, digestsBase, MD5, uuid, script) {
var encodeUTF8 = function encodeUTF8(str) {
var byteArray = [];
@@ -83,34 +83,32 @@ var saslPlain = function saslPlain(user, password, callbackFunction)
var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFunction)
{
-
- // Using dojo.xhrGet, as very little information is being sent
- dojo.xhrPost({
- // The URL of the request
- url: "rest/sasl",
- content: {
- mechanism: saslMechanism
- },
- handleAs: "json",
- failOk: true
- }).then(function(data)
- {
-
- var challengeBytes = base64.decode(data.challenge);
- var wa=[];
- var bitLength = challengeBytes.length*8;
- for(var i=0; i<bitLength; i+=8)
+ dojo.xhrPost({
+ // The URL of the request
+ url: "rest/sasl",
+ content: {
+ mechanism: saslMechanism
+ },
+ handleAs: "json",
+ failOk: true
+ }).then(function(data)
{
- wa[i>>5] |= (challengeBytes[i/8] & 0xFF)<<(i%32);
- }
- var challengeStr = digestsBase.wordToString(wa).substring(0,challengeBytes.length);
- var digest = user + " " + MD5._hmac(challengeStr, password, digestsBase.outputTypes.Hex);
- var id = data.id;
+ var challengeBytes = base64.decode(data.challenge);
+ var wa=[];
+ var bitLength = challengeBytes.length*8;
+ for(var i=0; i<bitLength; i+=8)
+ {
+ wa[i>>5] |= (challengeBytes[i/8] & 0xFF)<<(i%32);
+ }
+ var challengeStr = digestsBase.wordToString(wa).substring(0,challengeBytes.length);
+
+ var digest = user + " " + MD5._hmac(challengeStr, password, digestsBase.outputTypes.Hex);
+ var id = data.id;
- var response = base64.encode(encodeUTF8( digest ));
+ var response = base64.encode(encodeUTF8( digest ));
- dojo.xhrPost({
+ dojo.xhrPost({
// The URL of the request
url: "rest/sasl",
content: {
@@ -121,20 +119,163 @@ var saslCramMD5 = function saslCramMD5(user, password, saslMechanism, callbackFu
failOk: true
}).then(callbackFunction, errorHandler);
- },
- function(error)
- {
- if(error.status == 403)
+ },
+ function(error)
{
- alert("Authentication Failed");
- }
- else
- {
- alert(error);
- }
- });
+ if(error.status == 403)
+ {
+ alert("Authentication Failed");
+ }
+ else
+ {
+ alert(error);
+ }
+ });
+
+
+
};
+ var saslScramSha1 = function saslScramSha1(user, password, saslMechanism, callbackFunction)
+ {
+
+ script.get("webjars/cryptojs/3.1.2/rollups/hmac-sha1.js").then( function()
+ {
+ script.get("webjars/cryptojs/3.1.2/components/enc-base64-min.js").then ( function()
+ {
+
+ var toBase64 = function toBase64( input )
+ {
+ var result = [];
+ for(var i = 0; i < input.length; i++)
+ {
+ result[i] = input.charCodeAt(i);
+ }
+ return base64.encode( result )
+ };
+
+ var fromBase64 = function fromBase64( input )
+ {
+ var decoded = base64.decode( input );
+ var result = "";
+ for(var i = 0; i < decoded.length; i++)
+ {
+ result+= String.fromCharCode(decoded[i]);
+ }
+ return result;
+ };
+
+ var xor = function xor(lhs, rhs) {
+ var words = [];
+ for(var i = 0; i < lhs.words.length; i++)
+ {
+ words.push(lhs.words[i]^rhs.words[i]);
+ }
+ return CryptoJS.lib.WordArray.create(words);
+ };
+
+ var hasNonAscii = function hasNonAscii(name) {
+ for(var i = 0; i < name.length; i++) {
+ if(name.charCodeAt(i) > 127) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ var generateSaltedPassword = function generateSaltedPassword(salt, password, iterationCount)
+ {
+ var hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, password);
+
+ hmac.update(salt);
+ hmac.update(CryptoJS.enc.Hex.parse("00000001"));
+
+ var result = hmac.finalize();
+ var previous = null;
+ for(var i = 1 ;i < iterationCount; i++)
+ {
+ hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA1, password);
+ hmac.update( previous != null ? previous : result );
+ previous = hmac.finalize();
+ result = xor(result, previous);
+ }
+ return result;
+
+ };
+
+ GS2_HEADER = "n,,";
+
+ if(!hasNonAscii(user)) {
+
+ user = user.replace(/=/g, "=3D");
+ user = user.replace(/,/g, "=2C");
+
+ clientNonce = uuid();
+ clientFirstMessageBare = "n=" + user + ",r=" + clientNonce;
+ dojo.xhrPost({
+ // The URL of the request
+ url: "rest/sasl",
+ content: {
+ mechanism: saslMechanism,
+ response: toBase64(GS2_HEADER + clientFirstMessageBare)
+ },
+ handleAs: "json",
+ failOk: true
+ }).then(function (data) {
+ var serverFirstMessage = fromBase64(data.challenge);
+ var id = data.id;
+
+ var parts = serverFirstMessage.split(",");
+ nonce = parts[0].substring(2);
+ if (!nonce.substr(0, clientNonce.length) == clientNonce) {
+ alert("Authentication error - server nonce does not start with client nonce")
+ }
+ else {
+ var salt = CryptoJS.enc.Base64.parse(parts[1].substring(2));
+ var iterationCount = parts[2].substring(2);
+ var saltedPassword = generateSaltedPassword(salt, password, iterationCount)
+ var clientFinalMessageWithoutProof = "c=" + toBase64(GS2_HEADER) + ",r=" + nonce;
+ var authMessage = clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
+ var clientKey = CryptoJS.HmacSHA1("Client Key", saltedPassword);
+ var storedKey = CryptoJS.SHA1(clientKey);
+ var clientSignature = CryptoJS.HmacSHA1(authMessage, storedKey);
+ var clientProof = xor(clientKey, clientSignature);
+ var serverKey = CryptoJS.HmacSHA1("Server Key", saltedPassword);
+ serverSignature = CryptoJS.HmacSHA1(authMessage, serverKey);
+ dojo.xhrPost({
+ // The URL of the request
+ url: "rest/sasl",
+ content: {
+ id: id,
+ response: toBase64(clientFinalMessageWithoutProof
+ + ",p=" + clientProof.toString(CryptoJS.enc.Base64))
+ },
+ handleAs: "json",
+ failOk: true
+ }).then(function (data) {
+ var serverFinalMessage = fromBase64(data.challenge);
+ if (serverSignature.toString(CryptoJS.enc.Base64) == serverFinalMessage.substring(2)) {
+ callbackFunction();
+ }
+ else {
+ errorHandler("Server signature did not match");
+ }
+
+
+ }, errorHandler);
+ }
+
+ }, errorHandler);
+ }
+ else
+ {
+ alert("Username '"+name+"' is invalid");
+ }
+
+ }, errorHandler);
+ }, errorHandler);
+ };
+
var containsMechanism = function containsMechanism(mechanisms, mech)
{
for (var i = 0; i < mechanisms.length; i++) {
@@ -157,7 +298,11 @@ SaslClient.authenticate = function(username, password, callbackFunction)
}).then(function(data)
{
var mechMap = data.mechanisms;
- if (containsMechanism(mechMap, "CRAM-MD5"))
+ if(containsMechanism(mechMap, "SCRAM-SHA-1"))
+ {
+ saslScramSha1(username, password, "SCRAM-SHA-1", callbackFunction)
+ }
+ else if (containsMechanism(mechMap, "CRAM-MD5"))
{
saslCramMD5(username, password, "CRAM-MD5", callbackFunction);
}
diff --git a/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js b/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js
index 3d349830ac..6d83a68fb4 100644
--- a/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js
+++ b/qpid/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js
@@ -140,7 +140,7 @@ define(["dojo/_base/xhr",
util.isProviderManagingUsers = function(type)
{
- return (type === "PlainPasswordFile" || type === "Base64MD5PasswordFile");
+ return (type === "PlainPasswordFile" || type === "Base64MD5PasswordFile" || type === "SCRAM-SHA1");
};
util.showSetAttributesDialog = function(attributeWidgetFactories, data, putURL, dialogTitle, appendNameToUrl)
diff --git a/qpid/java/build.deps b/qpid/java/build.deps
index 0aa35954bf..d799899171 100644
--- a/qpid/java/build.deps
+++ b/qpid/java/build.deps
@@ -59,6 +59,8 @@ jetty-servlet=lib/required/jetty-servlet-8.1.14.v20131031.jar
jetty-websocket=lib/required/jetty-websocket-8.1.14.v20131031.jar
servlet-api=${geronimo-servlet}
+cryptojs=lib/required/cryptojs-3.1.2.jar
+
dojo-version=1.9.1
dojo=lib/required/dojo-${dojo-version}.zip
@@ -83,7 +85,7 @@ broker-core.libs=${commons-cli} ${commons-logging} ${log4j} ${slf4j-log4j} \
#Borrow the broker-core libs, hack for release binary generation
broker.libs=${broker-core.libs}
-broker-plugins-management-http.libs=${dojo}
+broker-plugins-management-http.libs=${dojo} ${cryptojs}
broker-plugins.libs=${log4j} ${commons-configuration.libs}
test.libs=${slf4j-log4j} ${log4j} ${junit} ${slf4j-api} ${mockito-all}
diff --git a/qpid/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties b/qpid/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties
index 8855a040ea..109ff9942a 100644
--- a/qpid/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties
+++ b/qpid/java/client/src/main/java/org/apache/qpid/client/security/CallbackHandlerRegistry.properties
@@ -31,3 +31,5 @@ CRAM-MD5.4=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
AMQPLAIN.5=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
PLAIN.6=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
ANONYMOUS.7=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
+SCRAM-SHA1.8=org.apache.qpid.client.security.UsernamePasswordCallbackHandler
+
diff --git a/qpid/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties b/qpid/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties
index b903208927..c2cd671bdf 100644
--- a/qpid/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties
+++ b/qpid/java/client/src/main/java/org/apache/qpid/client/security/DynamicSaslRegistrar.properties
@@ -19,3 +19,4 @@
AMQPLAIN=org.apache.qpid.client.security.amqplain.AmqPlainSaslClientFactory
CRAM-MD5-HASHED=org.apache.qpid.client.security.crammd5hashed.CRAMMD5HashedSaslClientFactory
ANONYMOUS=org.apache.qpid.client.security.anonymous.AnonymousSaslClientFactory
+SCRAM-SHA1=org.apache.qpid.client.security.scram.ScramSHA1SaslClientFactory
diff --git a/qpid/java/ivy.retrieve.xml b/qpid/java/ivy.retrieve.xml
index 59b3fa70af..d4020b8534 100644
--- a/qpid/java/ivy.retrieve.xml
+++ b/qpid/java/ivy.retrieve.xml
@@ -72,6 +72,7 @@
<dependency org="xalan" name="xalan" rev="2.7.0" transitive="false"/>
<dependency org="velocity" name="velocity" rev="1.4" transitive="false"/>
<dependency org="velocity" name="velocity-dep" rev="1.4" transitive="false"/>
+ <dependency org="org.webjars" name="cryptojs" rev="3.1.2" transitive="false"/>
<dependency org="org.dojotoolkit" name="dojo" rev="1.9.1" transitive="false">
<artifact name="dojo" type="zip"/>
</dependency>