summaryrefslogtreecommitdiff
path: root/qpid/java/broker-core/src
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 /qpid/java/broker-core/src
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
Diffstat (limited to 'qpid/java/broker-core/src')
-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
8 files changed, 1277 insertions, 3 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
+ }
+ }
+
+}