ARTEMIS-3106 Support for SASL-SCRAM
adds the implementation necessary to perform SASL-SCRAM authentication with ActiveMQ Artemis
This commit is contained in:
parent
0edbf890a2
commit
5313a800a3
|
@ -37,6 +37,11 @@
|
|||
<artifactId>artemis-selector</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.activemq</groupId>
|
||||
<artifactId>artemis-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.activemq</groupId>
|
||||
<artifactId>artemis-core-client</artifactId>
|
||||
|
|
|
@ -63,10 +63,12 @@ import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContex
|
|||
import org.apache.activemq.artemis.protocol.amqp.proton.SenderController;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASLFactory;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
|
||||
import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry;
|
||||
import org.apache.activemq.artemis.spi.core.remoting.ClientConnectionLifeCycleListener;
|
||||
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager;
|
||||
import org.apache.activemq.artemis.spi.core.remoting.Connection;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.utils.ConfigurationHelper;
|
||||
import org.apache.activemq.artemis.utils.UUIDGenerator;
|
||||
import org.apache.qpid.proton.amqp.Symbol;
|
||||
|
@ -96,8 +98,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
|||
private int retryCounter = 0;
|
||||
private boolean connecting = false;
|
||||
private volatile ScheduledFuture reconnectFuture;
|
||||
private Set<Queue> senders = new HashSet<>();
|
||||
private Set<Queue> receivers = new HashSet<>();
|
||||
private final Set<Queue> senders = new HashSet<>();
|
||||
private final Set<Queue> receivers = new HashSet<>();
|
||||
|
||||
final Executor connectExecutor;
|
||||
final ScheduledExecutorService scheduledExecutorService;
|
||||
|
@ -676,7 +678,15 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
|||
if (availableMechanisms.contains(EXTERNAL) && ExternalSASLMechanism.isApplicable(connection)) {
|
||||
return new ExternalSASLMechanism();
|
||||
}
|
||||
|
||||
if (SCRAMClientSASL.isApplicable(brokerConnectConfiguration.getUser(),
|
||||
brokerConnectConfiguration.getPassword())) {
|
||||
for (SCRAM scram : SCRAM.values()) {
|
||||
if (availableMechanisms.contains(scram.getName())) {
|
||||
return new SCRAMClientSASL(scram, brokerConnectConfiguration.getUser(),
|
||||
brokerConnectConfiguration.getPassword());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (availableMechanisms.contains(PLAIN) && PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword())) {
|
||||
return new PlainSASLMechanism(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramClientFunctionality.State;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.qpid.proton.codec.DecodeException;
|
||||
|
||||
/**
|
||||
* implements the client part of SASL-SCRAM for broker interconnect
|
||||
*/
|
||||
public class SCRAMClientSASL implements ClientSASL {
|
||||
private final SCRAM scramType;
|
||||
private final ScramClientFunctionalityImpl client;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
* @param scram the SCRAM mechanism to use
|
||||
* @param username the username for authentication
|
||||
* @param password the password for authentication
|
||||
*/
|
||||
|
||||
public SCRAMClientSASL(SCRAM scram, String username, String password) {
|
||||
this(scram, username, password, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
protected SCRAMClientSASL(SCRAM scram, String username, String password, String nonce) {
|
||||
Objects.requireNonNull(scram);
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(password);
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.scramType = scram;
|
||||
client = new ScramClientFunctionalityImpl(scram.getDigest(), scram.getHmac(), nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return scramType.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getInitialResponse() {
|
||||
try {
|
||||
String firstMessage = client.prepareFirstMessage(username);
|
||||
return firstMessage.getBytes(StandardCharsets.US_ASCII);
|
||||
} catch (ScramException e) {
|
||||
throw new DecodeException("prepareFirstMessage failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getResponse(byte[] challenge) {
|
||||
String msg = new String(challenge, StandardCharsets.US_ASCII);
|
||||
if (client.getState() == State.FIRST_PREPARED) {
|
||||
try {
|
||||
String finalMessage = client.prepareFinalMessage(password, msg);
|
||||
return finalMessage.getBytes(StandardCharsets.US_ASCII);
|
||||
} catch (ScramException e) {
|
||||
throw new DecodeException("prepareFinalMessage failed", e);
|
||||
}
|
||||
} else if (client.getState() == State.FINAL_PREPARED) {
|
||||
try {
|
||||
client.checkServerFinalMessage(msg);
|
||||
} catch (ScramException e) {
|
||||
throw new DecodeException("checkServerFinalMessage failed", e);
|
||||
}
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public static boolean isApplicable(String username, String password) {
|
||||
return username != null && username.length() > 0 && password != null && password.length() > 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
|
||||
public abstract class SCRAMServerSASL implements ServerSASL {
|
||||
|
||||
protected final ScramServerFunctionality scram;
|
||||
protected final SCRAM mechanism;
|
||||
private SASLResult result;
|
||||
|
||||
public SCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
|
||||
this(mechanism, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
protected SCRAMServerSASL(SCRAM mechanism, String nonce) throws NoSuchAlgorithmException {
|
||||
this.mechanism = mechanism;
|
||||
this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), nonce);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return mechanism.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] processSASL(byte[] bytes) {
|
||||
String message = new String(bytes, StandardCharsets.US_ASCII);
|
||||
try {
|
||||
switch (scram.getState()) {
|
||||
case INITIAL: {
|
||||
String userName = scram.handleClientFirstMessage(message);
|
||||
UserData userData = aquireUserData(userName);
|
||||
result = new SCRAMSASLResult(userName, scram, createSaslSubject(userName, userData));
|
||||
String challenge = scram.prepareFirstMessage(userData);
|
||||
return challenge.getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
case PREPARED_FIRST: {
|
||||
String finalMessage = scram.prepareFinalMessage(message);
|
||||
return finalMessage.getBytes(StandardCharsets.US_ASCII);
|
||||
}
|
||||
default:
|
||||
result = new SCRAMFailedSASLResult();
|
||||
break;
|
||||
}
|
||||
} catch (GeneralSecurityException | ScramException | RuntimeException e) {
|
||||
result = new SCRAMFailedSASLResult();
|
||||
failed(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract UserData aquireUserData(String userName) throws LoginException;
|
||||
|
||||
protected abstract void failed(Exception e);
|
||||
|
||||
protected Subject createSaslSubject(String userName, UserData userData) {
|
||||
UserPrincipal userPrincipal = new UserPrincipal(userName);
|
||||
Subject saslSubject = new Subject(true, Collections.singleton(userPrincipal), Collections.singleton(userData),
|
||||
Collections.emptySet());
|
||||
return saslSubject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SASLResult result() {
|
||||
if (result instanceof SCRAMSASLResult) {
|
||||
return scram.isEnded() ? result : null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isEnded() {
|
||||
return scram.isEnded();
|
||||
}
|
||||
|
||||
private static final class SCRAMSASLResult implements SASLResult {
|
||||
|
||||
private final String userName;
|
||||
private final ScramServerFunctionality scram;
|
||||
private final Subject subject;
|
||||
|
||||
SCRAMSASLResult(String userName, ScramServerFunctionality scram, Subject subject) {
|
||||
this.userName = userName;
|
||||
this.scram = scram;
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUser() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subject getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return userName != null && scram.isEnded() && scram.isSuccessful();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SCRAMSASLResult: userName = " + userName + ", state = " + scram.getState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class SCRAMFailedSASLResult implements SASLResult {
|
||||
|
||||
@Override
|
||||
public String getUser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subject getSubject() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SCRAMFailedSASLResult";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.NameCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.LoginContext;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer;
|
||||
import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor;
|
||||
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory;
|
||||
import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager;
|
||||
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
|
||||
import org.apache.activemq.artemis.spi.core.remoting.Connection;
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.DigestCallback;
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.HmacCallback;
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.SCRAMMechanismCallback;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* abstract class that implements the SASL-SCRAM authentication scheme, concrete implementations
|
||||
* must supply the {@link SCRAM} type to use and be register via SPI
|
||||
*/
|
||||
public abstract class SCRAMServerSASLFactory implements ServerSASLFactory {
|
||||
|
||||
private final Logger logger = Logger.getLogger(getClass());
|
||||
private final SCRAM scramType;
|
||||
|
||||
public SCRAMServerSASLFactory(SCRAM scram) {
|
||||
this.scramType = scram;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return scramType.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDefaultPermitted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerSASL create(ActiveMQServer server, ProtocolManager<AmqpInterceptor> manager, Connection connection,
|
||||
RemotingConnection remotingConnection) {
|
||||
try {
|
||||
if (manager instanceof ProtonProtocolManager) {
|
||||
String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope();
|
||||
return new JAASSCRAMServerSASL(scramType, loginConfigScope, logger);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// can't be used then...
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class JAASSCRAMServerSASL extends SCRAMServerSASL {
|
||||
|
||||
private final String loginConfigScope;
|
||||
private LoginContext loginContext = null;
|
||||
private Subject loginSubject;
|
||||
private final Logger logger;
|
||||
|
||||
JAASSCRAMServerSASL(SCRAM scram, String loginConfigScope, Logger logger) throws NoSuchAlgorithmException {
|
||||
super(scram);
|
||||
this.loginConfigScope = loginConfigScope;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserData aquireUserData(String userName) throws LoginException {
|
||||
loginContext = new LoginContext(loginConfigScope, new CallbackHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
for (Callback callback : callbacks) {
|
||||
if (callback instanceof NameCallback) {
|
||||
((NameCallback) callback).setName(userName);
|
||||
} else if (callback instanceof SCRAMMechanismCallback) {
|
||||
((SCRAMMechanismCallback) callback).setMechanism(mechanism.getName());
|
||||
} else if (callback instanceof DigestCallback) {
|
||||
((DigestCallback) callback).setDigest(scram.getDigest());
|
||||
} else if (callback instanceof HmacCallback) {
|
||||
((HmacCallback) callback).setHmac(scram.getHmac());
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callback, "Unrecognized Callback " +
|
||||
callback.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
loginContext.login();
|
||||
loginSubject = loginContext.getSubject();
|
||||
Iterator<UserData> credentials = loginSubject.getPublicCredentials(UserData.class).iterator();
|
||||
if (credentials.hasNext()) {
|
||||
return credentials.next();
|
||||
}
|
||||
throw new LoginException("can't aquire user data through configured login config scope (" + loginConfigScope +
|
||||
")");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Subject createSaslSubject(String userName, UserData userData) {
|
||||
if (loginSubject != null) {
|
||||
return new Subject(true, loginSubject.getPrincipals(), loginSubject.getPublicCredentials(),
|
||||
loginSubject.getPrivateCredentials());
|
||||
}
|
||||
return super.createSaslSubject(userName, userData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
if (loginContext != null) {
|
||||
try {
|
||||
loginContext.logout();
|
||||
} catch (LoginException e1) {
|
||||
// we can't do anything useful then...
|
||||
}
|
||||
}
|
||||
loginContext = null;
|
||||
loginSubject = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed(Exception e) {
|
||||
logger.warn("SASL-SCRAM Authentication failed", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
|
||||
/**
|
||||
* provides SASL SRAM-SHA256
|
||||
*/
|
||||
public class SHA256SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
|
||||
|
||||
public SHA256SCRAMServerSASLFactory() {
|
||||
super(SCRAM.SHA256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrecedence() {
|
||||
return 256;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
|
||||
/**
|
||||
* provides SASL SRAM-SHA512
|
||||
*/
|
||||
public class SHA512SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
|
||||
|
||||
public SHA512SCRAMServerSASLFactory() {
|
||||
super(SCRAM.SHA512);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPrecedence() {
|
||||
return 512;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
|
||||
/**
|
||||
* Provides building blocks for creating SCRAM authentication client
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface ScramClientFunctionality {
|
||||
/**
|
||||
* Prepares the first client message
|
||||
* @param username Username of the user
|
||||
* @return First client message
|
||||
* @throws ScramException if username contains prohibited characters
|
||||
*/
|
||||
String prepareFirstMessage(String username) throws ScramException;
|
||||
|
||||
/**
|
||||
* Prepares client's final message
|
||||
* @param password User password
|
||||
* @param serverFirstMessage Server's first message
|
||||
* @return Client's final message
|
||||
* @throws ScramException if there is an error processing server's message, i.e. it violates the
|
||||
* protocol
|
||||
*/
|
||||
String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException;
|
||||
|
||||
/**
|
||||
* Checks if the server's final message is valid
|
||||
* @param serverFinalMessage Server's final message
|
||||
* @throws ScramException if there is an error processing server's message, i.e. it violates the
|
||||
* protocol
|
||||
*/
|
||||
void checkServerFinalMessage(String serverFinalMessage) throws ScramException;
|
||||
|
||||
/**
|
||||
* Checks if authentication is successful. You can call this method only if authentication is
|
||||
* completed. Ensure that using {@link #isEnded()}
|
||||
* @return true if successful, false otherwise
|
||||
*/
|
||||
boolean isSuccessful();
|
||||
|
||||
/**
|
||||
* Checks if authentication is completed, either successfully or not. Authentication is completed
|
||||
* if {@link #getState()} returns ENDED.
|
||||
* @return true if authentication has ended
|
||||
*/
|
||||
boolean isEnded();
|
||||
|
||||
/**
|
||||
* Gets the state of the authentication procedure
|
||||
* @return Current state
|
||||
*/
|
||||
State getState();
|
||||
|
||||
/**
|
||||
* State of the authentication procedure
|
||||
*/
|
||||
enum State {
|
||||
/**
|
||||
* Initial state
|
||||
*/
|
||||
INITIAL,
|
||||
/**
|
||||
* State after first message is prepared
|
||||
*/
|
||||
FIRST_PREPARED,
|
||||
/**
|
||||
* State after final message is prepared
|
||||
*/
|
||||
FINAL_PREPARED,
|
||||
/**
|
||||
* Authentication is completes, either successfully or not
|
||||
*/
|
||||
ENDED
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
|
||||
|
||||
/**
|
||||
* Provides building blocks for creating SCRAM authentication client
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ScramClientFunctionalityImpl implements ScramClientFunctionality {
|
||||
private static final Pattern SERVER_FIRST_MESSAGE = Pattern.compile("r=([^,]*),s=([^,]*),i=(.*)$");
|
||||
private static final Pattern SERVER_FINAL_MESSAGE = Pattern.compile("v=([^,]*)$");
|
||||
|
||||
private static final String GS2_HEADER = "n,,";
|
||||
private static final Charset ASCII = Charset.forName("ASCII");
|
||||
|
||||
private final String mDigestName;
|
||||
private final String mHmacName;
|
||||
private final String mClientNonce;
|
||||
private String mClientFirstMessageBare;
|
||||
|
||||
private final boolean mIsSuccessful = false;
|
||||
private byte[] mSaltedPassword;
|
||||
private String mAuthMessage;
|
||||
|
||||
private State mState = State.INITIAL;
|
||||
|
||||
/**
|
||||
* Create new ScramClientFunctionalityImpl
|
||||
* @param digestName Digest to be used
|
||||
* @param hmacName HMAC to be used
|
||||
*/
|
||||
public ScramClientFunctionalityImpl(String digestName, String hmacName) {
|
||||
this(digestName, hmacName, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new ScramClientFunctionalityImpl
|
||||
* @param digestName Digest to be used
|
||||
* @param hmacName HMAC to be used
|
||||
* @param clientNonce Client nonce to be used
|
||||
*/
|
||||
public ScramClientFunctionalityImpl(String digestName, String hmacName, String clientNonce) {
|
||||
if (ScramUtils.isNullOrEmpty(digestName)) {
|
||||
throw new NullPointerException("digestName cannot be null or empty");
|
||||
}
|
||||
if (ScramUtils.isNullOrEmpty(hmacName)) {
|
||||
throw new NullPointerException("hmacName cannot be null or empty");
|
||||
}
|
||||
if (ScramUtils.isNullOrEmpty(clientNonce)) {
|
||||
throw new NullPointerException("clientNonce cannot be null or empty");
|
||||
}
|
||||
|
||||
mDigestName = digestName;
|
||||
mHmacName = hmacName;
|
||||
mClientNonce = clientNonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares first client message You may want to use
|
||||
* {@link StringPrep#isContainingProhibitedCharacters(String)} in order to check if the username
|
||||
* contains only valid characters
|
||||
* @param username Username
|
||||
* @return prepared first message
|
||||
* @throws ScramException if <code>username</code> contains prohibited characters
|
||||
*/
|
||||
@Override
|
||||
public String prepareFirstMessage(String username) throws ScramException {
|
||||
if (mState != State.INITIAL) {
|
||||
throw new IllegalStateException("You can call this method only once");
|
||||
}
|
||||
|
||||
try {
|
||||
mClientFirstMessageBare = "n=" + StringPrep.prepAsQueryString(username) + ",r=" + mClientNonce;
|
||||
mState = State.FIRST_PREPARED;
|
||||
return GS2_HEADER + mClientFirstMessageBare;
|
||||
} catch (StringPrep.StringPrepError e) {
|
||||
mState = State.ENDED;
|
||||
throw new ScramException("Username contains prohibited character");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException {
|
||||
if (mState != State.FIRST_PREPARED) {
|
||||
throw new IllegalStateException("You can call this method once only after " + "calling prepareFirstMessage()");
|
||||
}
|
||||
|
||||
Matcher m = SERVER_FIRST_MESSAGE.matcher(serverFirstMessage);
|
||||
if (!m.matches()) {
|
||||
mState = State.ENDED;
|
||||
return null;
|
||||
}
|
||||
|
||||
String nonce = m.group(1);
|
||||
|
||||
if (!nonce.startsWith(mClientNonce)) {
|
||||
mState = State.ENDED;
|
||||
return null;
|
||||
}
|
||||
|
||||
String salt = m.group(2);
|
||||
String iterationCountString = m.group(3);
|
||||
int iterations = Integer.parseInt(iterationCountString);
|
||||
if (iterations <= 0) {
|
||||
mState = State.ENDED;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
mSaltedPassword = ScramUtils.generateSaltedPassword(password, Base64.getDecoder().decode(salt), iterations,
|
||||
Mac.getInstance(mHmacName));
|
||||
|
||||
String clientFinalMessageWithoutProof =
|
||||
"c=" + Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII)) + ",r=" + nonce;
|
||||
|
||||
mAuthMessage = mClientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
|
||||
|
||||
byte[] clientKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Client Key");
|
||||
byte[] storedKey = MessageDigest.getInstance(mDigestName).digest(clientKey);
|
||||
|
||||
byte[] clientSignature = ScramUtils.computeHmac(storedKey, mHmacName, mAuthMessage);
|
||||
|
||||
byte[] clientProof = clientKey.clone();
|
||||
for (int i = 0; i < clientProof.length; i++) {
|
||||
clientProof[i] ^= clientSignature[i];
|
||||
}
|
||||
|
||||
mState = State.FINAL_PREPARED;
|
||||
return clientFinalMessageWithoutProof + ",p=" + Base64.getEncoder().encodeToString(clientProof);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
mState = State.ENDED;
|
||||
throw new ScramException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerFinalMessage(String serverFinalMessage) throws ScramException {
|
||||
if (mState != State.FINAL_PREPARED) {
|
||||
throw new IllegalStateException("You can call this method only once after " + "calling prepareFinalMessage()");
|
||||
}
|
||||
|
||||
Matcher m = SERVER_FINAL_MESSAGE.matcher(serverFinalMessage);
|
||||
if (!m.matches()) {
|
||||
mState = State.ENDED;
|
||||
throw new ScramException("invalid message format");
|
||||
}
|
||||
|
||||
byte[] serverSignature = Base64.getDecoder().decode(m.group(1));
|
||||
|
||||
mState = State.ENDED;
|
||||
if (!Arrays.equals(serverSignature, getExpectedServerSignature())) {
|
||||
throw new ScramException("Server signature missmatch");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccessful() {
|
||||
if (mState == State.ENDED) {
|
||||
return mIsSuccessful;
|
||||
} else {
|
||||
throw new IllegalStateException("You cannot call this method before authentication is ended. " +
|
||||
"Use isEnded() to check that");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return mState == State.ENDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
private byte[] getExpectedServerSignature() throws ScramException {
|
||||
try {
|
||||
byte[] serverKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Server Key");
|
||||
return ScramUtils.computeHmac(serverKey, mHmacName, mAuthMessage);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
|
||||
mState = State.ENDED;
|
||||
throw new ScramException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
|
||||
/**
|
||||
* Provides building blocks for creating SCRAM authentication server
|
||||
*/
|
||||
public interface ScramServerFunctionality {
|
||||
/**
|
||||
* Handles client's first message
|
||||
* @param message Client's first message
|
||||
* @return username extracted from the client message
|
||||
* @throws ScramException
|
||||
*/
|
||||
String handleClientFirstMessage(String message) throws ScramException;
|
||||
|
||||
/**
|
||||
* Prepares server's first message
|
||||
* @param userData user data needed to prepare the message
|
||||
* @return Server's first message
|
||||
*/
|
||||
String prepareFirstMessage(UserData userData);
|
||||
|
||||
/**
|
||||
* Prepares server's final message
|
||||
* @param clientFinalMessage Client's final message
|
||||
* @return Server's final message
|
||||
* @throws ScramException
|
||||
*/
|
||||
String prepareFinalMessage(String clientFinalMessage) throws ScramException;
|
||||
|
||||
/**
|
||||
* Checks if authentication is completed, either successfully or not. Authentication is completed
|
||||
* if {@link #getState()} returns ENDED.
|
||||
* @return true if authentication has ended
|
||||
*/
|
||||
boolean isSuccessful();
|
||||
|
||||
/**
|
||||
* Checks if authentication is completed, either successfully or not. Authentication is completed
|
||||
* if {@link #getState()} returns ENDED.
|
||||
* @return true if authentication has ended
|
||||
*/
|
||||
boolean isEnded();
|
||||
|
||||
/**
|
||||
* Gets the state of the authentication procedure
|
||||
* @return Current state
|
||||
*/
|
||||
State getState();
|
||||
|
||||
/**
|
||||
* State of the authentication procedure
|
||||
*/
|
||||
enum State {
|
||||
/**
|
||||
* Initial state
|
||||
*/
|
||||
INITIAL,
|
||||
/**
|
||||
* First client message is handled (username is extracted)
|
||||
*/
|
||||
FIRST_CLIENT_MESSAGE_HANDLED,
|
||||
/**
|
||||
* First server message is prepared
|
||||
*/
|
||||
PREPARED_FIRST,
|
||||
/**
|
||||
* Authentication is completes, either successfully or not
|
||||
*/
|
||||
ENDED
|
||||
}
|
||||
|
||||
MessageDigest getDigest();
|
||||
|
||||
Mac getHmac();
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.protocol.amqp.sasl.scram;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
|
||||
/**
|
||||
* Provides building blocks for creating SCRAM authentication server
|
||||
*/
|
||||
public class ScramServerFunctionalityImpl implements ScramServerFunctionality {
|
||||
private static final Pattern CLIENT_FIRST_MESSAGE =
|
||||
Pattern.compile("^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$");
|
||||
private static final Pattern CLIENT_FINAL_MESSAGE = Pattern.compile("(c=([^,]*),r=([^,]*)),p=(.*)$");
|
||||
|
||||
private final String mServerPartNonce;
|
||||
|
||||
private boolean mIsSuccessful = false;
|
||||
private State mState = State.INITIAL;
|
||||
private String mClientFirstMessageBare;
|
||||
private String mNonce;
|
||||
private String mServerFirstMessage;
|
||||
private UserData mUserData;
|
||||
private final MessageDigest digest;
|
||||
private final Mac hmac;
|
||||
|
||||
/**
|
||||
* Creates new ScramServerFunctionalityImpl
|
||||
* @param digestName Digest to be used
|
||||
* @param hmacName HMAC to be used
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
public ScramServerFunctionalityImpl(String digestName, String hmacName) throws NoSuchAlgorithmException {
|
||||
this(digestName, hmacName, UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* /** Creates new ScramServerFunctionalityImpl
|
||||
* @param digestName Digest to be used
|
||||
* @param hmacName HMAC to be used
|
||||
* @param serverPartNonce Server's part of the nonce
|
||||
* @throws NoSuchAlgorithmException
|
||||
*/
|
||||
public ScramServerFunctionalityImpl(String digestName, String hmacName,
|
||||
String serverPartNonce) throws NoSuchAlgorithmException {
|
||||
if (ScramUtils.isNullOrEmpty(digestName)) {
|
||||
throw new NullPointerException("digestName cannot be null or empty");
|
||||
}
|
||||
if (ScramUtils.isNullOrEmpty(hmacName)) {
|
||||
throw new NullPointerException("hmacName cannot be null or empty");
|
||||
}
|
||||
if (ScramUtils.isNullOrEmpty(serverPartNonce)) {
|
||||
throw new NullPointerException("serverPartNonce cannot be null or empty");
|
||||
}
|
||||
digest = MessageDigest.getInstance(digestName);
|
||||
hmac = Mac.getInstance(hmacName);
|
||||
mServerPartNonce = serverPartNonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles client's first message
|
||||
* @param message Client's first message
|
||||
* @return username extracted from the client message
|
||||
* @throws ScramException
|
||||
*/
|
||||
@Override
|
||||
public String handleClientFirstMessage(String message) throws ScramException {
|
||||
Matcher m = CLIENT_FIRST_MESSAGE.matcher(message);
|
||||
if (!m.matches()) {
|
||||
mState = State.ENDED;
|
||||
throw new ScramException("Invalid message received");
|
||||
}
|
||||
|
||||
mClientFirstMessageBare = m.group(5);
|
||||
String username = m.group(6);
|
||||
String clientNonce = m.group(7);
|
||||
mNonce = clientNonce + mServerPartNonce;
|
||||
|
||||
mState = State.FIRST_CLIENT_MESSAGE_HANDLED;
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prepareFirstMessage(UserData userData) {
|
||||
mUserData = userData;
|
||||
mState = State.PREPARED_FIRST;
|
||||
mServerFirstMessage = String.format("r=%s,s=%s,i=%d", mNonce, userData.salt, userData.iterations);
|
||||
|
||||
return mServerFirstMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String prepareFinalMessage(String clientFinalMessage) throws ScramException {
|
||||
String finalMessage = prepareFinalMessageUnchecked(clientFinalMessage);
|
||||
if (!mIsSuccessful) {
|
||||
throw new ScramException("client credentials missmatch");
|
||||
}
|
||||
return finalMessage;
|
||||
}
|
||||
|
||||
public String prepareFinalMessageUnchecked(String clientFinalMessage) throws ScramException {
|
||||
mState = State.ENDED;
|
||||
Matcher m = CLIENT_FINAL_MESSAGE.matcher(clientFinalMessage);
|
||||
if (!m.matches()) {
|
||||
throw new ScramException("Invalid message received");
|
||||
}
|
||||
|
||||
String clientFinalMessageWithoutProof = m.group(1);
|
||||
String clientNonce = m.group(3);
|
||||
String proof = m.group(4);
|
||||
|
||||
if (!mNonce.equals(clientNonce)) {
|
||||
throw new ScramException("Nonce mismatch");
|
||||
}
|
||||
|
||||
String authMessage = mClientFirstMessageBare + "," + mServerFirstMessage + "," + clientFinalMessageWithoutProof;
|
||||
|
||||
byte[] storedKeyArr = Base64.getDecoder().decode(mUserData.storedKey);
|
||||
byte[] clientSignature = ScramUtils.computeHmac(storedKeyArr, hmac, authMessage);
|
||||
byte[] serverSignature =
|
||||
ScramUtils.computeHmac(Base64.getDecoder().decode(mUserData.serverKey), hmac, authMessage);
|
||||
byte[] clientKey = clientSignature.clone();
|
||||
byte[] decodedProof = Base64.getDecoder().decode(proof);
|
||||
for (int i = 0; i < clientKey.length; i++) {
|
||||
clientKey[i] ^= decodedProof[i];
|
||||
}
|
||||
|
||||
byte[] resultKey = digest.digest(clientKey);
|
||||
mIsSuccessful = Arrays.equals(storedKeyArr, resultKey);
|
||||
return "v=" + Base64.getEncoder().encodeToString(serverSignature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccessful() {
|
||||
if (mState == State.ENDED) {
|
||||
return mIsSuccessful;
|
||||
} else {
|
||||
throw new IllegalStateException("You cannot call this method before authentication is ended. " +
|
||||
"Use isEnded() to check that");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return mState == State.ENDED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageDigest getDigest() {
|
||||
try {
|
||||
return (MessageDigest) digest.clone();
|
||||
} catch (CloneNotSupportedException cns) {
|
||||
try {
|
||||
return MessageDigest.getInstance(digest.getAlgorithm());
|
||||
} catch (NoSuchAlgorithmException nsa) {
|
||||
throw new AssertionError(nsa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mac getHmac() {
|
||||
try {
|
||||
return (Mac) hmac.clone();
|
||||
} catch (CloneNotSupportedException cns) {
|
||||
try {
|
||||
return Mac.getInstance(hmac.getAlgorithm());
|
||||
} catch (NoSuchAlgorithmException nsa) {
|
||||
throw new AssertionError(nsa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
org.apache.activemq.artemis.protocol.amqp.sasl.AnonymousServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.PlainServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory
|
||||
org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512SCRAMServerSASLFactory
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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.activemq.artemis.protocol.amqp.sasl;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramServerFunctionalityImpl;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
import org.apache.qpid.proton.codec.DecodeException;
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* test cases for the SASL-SCRAM
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class SCRAMTest {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private final SCRAM mechanism;
|
||||
private static final byte[] SALT = new byte[32];
|
||||
private static final String SNONCE = "server";
|
||||
private static final String CNONCE = "client";
|
||||
private static final String USERNAME = "test";
|
||||
private static final String PASSWORD = "123";
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static List<Object[]> data() {
|
||||
List<Object[]> list = new ArrayList<>();
|
||||
for (SCRAM scram : SCRAM.values()) {
|
||||
list.add(new Object[] {scram});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public SCRAMTest(SCRAM mechanism) {
|
||||
this.mechanism = mechanism;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() throws NoSuchAlgorithmException {
|
||||
TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
|
||||
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
|
||||
byte[] clientFirst = clientSASL.getInitialResponse();
|
||||
assertNotNull(clientFirst);
|
||||
byte[] serverFirst = serverSASL.processSASL(clientFirst);
|
||||
assertNotNull(serverFirst);
|
||||
assertNull(serverSASL.result());
|
||||
byte[] clientFinal = clientSASL.getResponse(serverFirst);
|
||||
assertNotNull(clientFinal);
|
||||
assertFalse(clientFinal.length == 0);
|
||||
byte[] serverFinal = serverSASL.processSASL(clientFinal);
|
||||
assertNotNull(serverFinal);
|
||||
assertNotNull(serverSASL.result());
|
||||
assertNotNull(serverSASL.result().getSubject());
|
||||
assertEquals(USERNAME, serverSASL.result().getUser());
|
||||
assertNull(serverSASL.exception);
|
||||
assertTrue(serverSASL.result().isSuccess());
|
||||
byte[] clientCheck = clientSASL.getResponse(serverFinal);
|
||||
assertNotNull(clientCheck);
|
||||
assertTrue(clientCheck.length == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongClientPassword() throws NoSuchAlgorithmException {
|
||||
TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
|
||||
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, "xyz");
|
||||
byte[] clientFirst = clientSASL.getInitialResponse();
|
||||
assertNotNull(clientFirst);
|
||||
byte[] serverFirst = serverSASL.processSASL(clientFirst);
|
||||
assertNotNull(serverFirst);
|
||||
assertNull(serverSASL.result());
|
||||
byte[] clientFinal = clientSASL.getResponse(serverFirst);
|
||||
assertNotNull(clientFinal);
|
||||
assertFalse(clientFinal.length == 0);
|
||||
byte[] serverFinal = serverSASL.processSASL(clientFinal);
|
||||
assertNull(serverFinal);
|
||||
assertNotNull(serverSASL.result());
|
||||
assertFalse(serverSASL.result().isSuccess());
|
||||
assertThat(serverSASL.exception, IsInstanceOf.instanceOf(ScramException.class));
|
||||
}
|
||||
|
||||
@Test(expected = DecodeException.class)
|
||||
public void testServerTryTrickClient() throws NoSuchAlgorithmException, ScramException {
|
||||
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
|
||||
ScramServerFunctionalityImpl bad =
|
||||
new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), SNONCE);
|
||||
byte[] clientFirst = clientSASL.getInitialResponse();
|
||||
assertNotNull(clientFirst);
|
||||
bad.handleClientFirstMessage(new String(clientFirst, StandardCharsets.US_ASCII));
|
||||
byte[] serverFirst =
|
||||
bad.prepareFirstMessage(generateUserData(mechanism, "bad")).getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] clientFinal = clientSASL.getResponse(serverFirst);
|
||||
assertNotNull(clientFinal);
|
||||
assertFalse(clientFinal.length == 0);
|
||||
byte[] serverFinal = bad.prepareFinalMessageUnchecked(new String(clientFinal, StandardCharsets.US_ASCII))
|
||||
.getBytes(StandardCharsets.US_ASCII);
|
||||
clientSASL.getResponse(serverFinal);
|
||||
}
|
||||
|
||||
private static UserData generateUserData(SCRAM mechanism, String password) throws NoSuchAlgorithmException,
|
||||
ScramException {
|
||||
MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
|
||||
Mac hmac = Mac.getInstance(mechanism.getHmac());
|
||||
ScramUtils.NewPasswordStringData data =
|
||||
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, SALT, 4096, digest, hmac));
|
||||
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
|
||||
}
|
||||
|
||||
|
||||
private static final class TestSCRAMClientSASL extends SCRAMClientSASL {
|
||||
|
||||
TestSCRAMClientSASL(SCRAM scram, String username, String password) {
|
||||
super(scram, username, password, CNONCE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
|
||||
|
||||
private Exception exception;
|
||||
private final String username;
|
||||
private final String password;
|
||||
|
||||
TestSCRAMServerSASL(SCRAM mechanism, String username, String password) throws NoSuchAlgorithmException {
|
||||
super(mechanism, SNONCE);
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserData aquireUserData(String userName) throws LoginException {
|
||||
if (!this.username.equals(userName)) {
|
||||
throw new LoginException("invalid username");
|
||||
}
|
||||
try {
|
||||
return generateUserData(mechanism, password);
|
||||
} catch (Exception e) {
|
||||
throw new LoginException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed(Exception e) {
|
||||
this.exception = e;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Abstract login module that uses an external authenticated principal
|
||||
*/
|
||||
public abstract class AbstractPrincipalLoginModule implements AuditLoginModule {
|
||||
private final Logger logger = Logger.getLogger(getClass());
|
||||
|
||||
private Subject subject;
|
||||
private final List<Principal> authenticatedPrincipals = new LinkedList<>();
|
||||
private CallbackHandler callbackHandler;
|
||||
private boolean loginSucceeded;
|
||||
private Principal[] principals;
|
||||
|
||||
@Override
|
||||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
|
||||
Map<String, ?> options) {
|
||||
this.subject = subject;
|
||||
this.callbackHandler = callbackHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean login() throws LoginException {
|
||||
Callback[] callbacks = new Callback[1];
|
||||
|
||||
callbacks[0] = new PrincipalsCallback();
|
||||
try {
|
||||
callbackHandler.handle(callbacks);
|
||||
principals = ((PrincipalsCallback) callbacks[0]).getPeerPrincipals();
|
||||
if (principals != null) {
|
||||
authenticatedPrincipals.addAll(Arrays.asList(principals));
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new LoginException(ioe.getMessage());
|
||||
} catch (UnsupportedCallbackException uce) {
|
||||
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
|
||||
}
|
||||
if (!authenticatedPrincipals.isEmpty()) {
|
||||
loginSucceeded = true;
|
||||
}
|
||||
logger.debug("login " + authenticatedPrincipals);
|
||||
return loginSucceeded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() throws LoginException {
|
||||
boolean result = loginSucceeded;
|
||||
if (result) {
|
||||
authenticatedPrincipals.add(new UserPrincipal(authenticatedPrincipals.get(0).getName()));
|
||||
subject.getPrincipals().addAll(authenticatedPrincipals);
|
||||
}
|
||||
|
||||
clear();
|
||||
|
||||
logger.debug("commit, result: " + result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean abort() throws LoginException {
|
||||
if (principals != null) {
|
||||
for (Principal principal : authenticatedPrincipals) {
|
||||
registerFailureForAudit(principal.getName());
|
||||
}
|
||||
}
|
||||
clear();
|
||||
|
||||
logger.debug("abort");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
principals = null;
|
||||
loginSucceeded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout() throws LoginException {
|
||||
subject.getPrincipals().removeAll(authenticatedPrincipals);
|
||||
authenticatedPrincipals.clear();
|
||||
clear();
|
||||
|
||||
logger.debug("logout");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
|
||||
/**
|
||||
* Callback to obtain a {@link MessageDigest} for login purpose
|
||||
*/
|
||||
public class DigestCallback implements Callback {
|
||||
|
||||
private MessageDigest digest;
|
||||
|
||||
/**
|
||||
* set the digest to use
|
||||
* @param digest the digest
|
||||
*/
|
||||
public void setDigest(MessageDigest digest) {
|
||||
this.digest = digest;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the digest or <code>null</code> if not known
|
||||
*/
|
||||
public MessageDigest getDigest() {
|
||||
return digest;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.security.auth.callback.Callback;
|
||||
|
||||
/**
|
||||
* Callback for obtaining information about a used H{@link Mac}
|
||||
*/
|
||||
public class HmacCallback implements Callback {
|
||||
|
||||
private Mac hmac;
|
||||
|
||||
/**
|
||||
* set the Hmac to use
|
||||
* @param hmac
|
||||
*/
|
||||
public void setHmac(Mac hmac) {
|
||||
this.hmac = hmac;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Hmac or <code>null</code> if non could be obtained
|
||||
*/
|
||||
public Mac getHmac() {
|
||||
return hmac;
|
||||
}
|
||||
|
||||
}
|
|
@ -16,7 +16,12 @@
|
|||
*/
|
||||
package org.apache.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
|
||||
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
|
||||
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
|
@ -25,11 +30,8 @@ import javax.security.auth.callback.NameCallback;
|
|||
import javax.security.auth.callback.PasswordCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.kerberos.KerberosPrincipal;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
|
||||
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
|
||||
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
|
||||
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
|
||||
|
||||
/**
|
||||
* A JAAS username password CallbackHandler.
|
||||
|
@ -67,18 +69,26 @@ public class JaasCallbackHandler implements CallbackHandler {
|
|||
CertificateCallback certCallback = (CertificateCallback) callback;
|
||||
|
||||
certCallback.setCertificates(getCertsFromConnection(remotingConnection));
|
||||
} else if (callback instanceof Krb5Callback) {
|
||||
Krb5Callback krb5Callback = (Krb5Callback) callback;
|
||||
} else if (callback instanceof PrincipalsCallback) {
|
||||
PrincipalsCallback principalsCallback = (PrincipalsCallback) callback;
|
||||
|
||||
Subject peerSubject = remotingConnection.getSubject();
|
||||
if (peerSubject != null) {
|
||||
for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
|
||||
krb5Callback.setPeerPrincipal(principal);
|
||||
for (KerberosPrincipal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
|
||||
principalsCallback.setPeerPrincipals(new Principal[] {principal});
|
||||
return;
|
||||
}
|
||||
Set<Principal> principals = peerSubject.getPrincipals();
|
||||
if (principals.size() > 0) {
|
||||
principalsCallback.setPeerPrincipals(principals.toArray(new Principal[0]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
krb5Callback.setPeerPrincipal(getPeerPrincipalFromConnection(remotingConnection));
|
||||
Principal peerPrincipalFromConnection = getPeerPrincipalFromConnection(remotingConnection);
|
||||
if (peerPrincipalFromConnection != null) {
|
||||
principalsCallback.setPeerPrincipals(new Principal[] {peerPrincipalFromConnection});
|
||||
}
|
||||
} else {
|
||||
throw new UnsupportedCallbackException(callback);
|
||||
}
|
||||
|
|
|
@ -16,102 +16,10 @@
|
|||
*/
|
||||
package org.apache.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.LoginException;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* populate a subject with kerberos credential from the handler
|
||||
*/
|
||||
public class Krb5LoginModule implements AuditLoginModule {
|
||||
public class Krb5LoginModule extends AbstractPrincipalLoginModule {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Krb5LoginModule.class);
|
||||
|
||||
private Subject subject;
|
||||
private final List<Principal> principals = new LinkedList<>();
|
||||
private CallbackHandler callbackHandler;
|
||||
private boolean loginSucceeded;
|
||||
private Principal principal;
|
||||
|
||||
@Override
|
||||
public void initialize(Subject subject,
|
||||
CallbackHandler callbackHandler,
|
||||
Map<String, ?> sharedState,
|
||||
Map<String, ?> options) {
|
||||
this.subject = subject;
|
||||
this.callbackHandler = callbackHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean login() throws LoginException {
|
||||
Callback[] callbacks = new Callback[1];
|
||||
|
||||
callbacks[0] = new Krb5Callback();
|
||||
try {
|
||||
callbackHandler.handle(callbacks);
|
||||
principal = ((Krb5Callback)callbacks[0]).getPeerPrincipal();
|
||||
if (principal != null) {
|
||||
principals.add(principal);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
throw new LoginException(ioe.getMessage());
|
||||
} catch (UnsupportedCallbackException uce) {
|
||||
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
|
||||
}
|
||||
if (!principals.isEmpty()) {
|
||||
loginSucceeded = true;
|
||||
}
|
||||
logger.debug("login " + principals);
|
||||
return loginSucceeded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() throws LoginException {
|
||||
boolean result = loginSucceeded;
|
||||
if (result) {
|
||||
principals.add(new UserPrincipal(principals.get(0).getName()));
|
||||
subject.getPrincipals().addAll(principals);
|
||||
}
|
||||
|
||||
clear();
|
||||
|
||||
logger.debug("commit, result: " + result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean abort() throws LoginException {
|
||||
registerFailureForAudit(principal != null ? principal.getName() : null);
|
||||
clear();
|
||||
|
||||
logger.debug("abort");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
principal = null;
|
||||
loginSucceeded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout() throws LoginException {
|
||||
subject.getPrincipals().removeAll(principals);
|
||||
principals.clear();
|
||||
clear();
|
||||
|
||||
logger.debug("logout");
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,31 +16,30 @@
|
|||
*/
|
||||
package org.apache.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* A Callback for kerberos peer principal.
|
||||
*/
|
||||
public class Krb5Callback implements Callback {
|
||||
import javax.security.auth.callback.Callback;
|
||||
|
||||
Principal peerPrincipal;
|
||||
/**
|
||||
* A Callback for getting the peer principals.
|
||||
*/
|
||||
public class PrincipalsCallback implements Callback {
|
||||
|
||||
Principal[] peerPrincipals;
|
||||
|
||||
/**
|
||||
* Setter for peer Principal.
|
||||
*
|
||||
* Setter for peer Principals.
|
||||
* @param principal The certificates to be returned.
|
||||
*/
|
||||
public void setPeerPrincipal(Principal principal) {
|
||||
peerPrincipal = principal;
|
||||
public void setPeerPrincipals(Principal[] principal) {
|
||||
peerPrincipals = principal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for peer Principal.
|
||||
*
|
||||
* Getter for peer Principals.
|
||||
* @return The principal being carried.
|
||||
*/
|
||||
public Principal getPeerPrincipal() {
|
||||
return peerPrincipal;
|
||||
public Principal[] getPeerPrincipals() {
|
||||
return peerPrincipals;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
/**
|
||||
* Handles the actual login after channel authentication has succeed
|
||||
*/
|
||||
public class SCRAMLoginModule extends AbstractPrincipalLoginModule {
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import javax.security.auth.callback.Callback;
|
||||
|
||||
/**
|
||||
* callback to obtain the a mechanism used in a SASL-SCRAM authentication
|
||||
*/
|
||||
public class SCRAMMechanismCallback implements Callback {
|
||||
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* sets the name of the mechanism
|
||||
* @param name the name of the mechanism
|
||||
*/
|
||||
public void setMechanism(String name) {
|
||||
this.name = name;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the name of the mechanism
|
||||
*/
|
||||
public String getMechanism() {
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.Principal;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.NameCallback;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep.StringPrepError;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
|
||||
|
||||
/**
|
||||
* Login modules that uses properties files similar to the {@link PropertiesLoginModule}. It can
|
||||
* either store the username-password in plain text or in an encrypted/hashed form. the
|
||||
* {@link #main(String[])} method provides a way to prepare unencrypted data to be encrypted/hashed.
|
||||
*/
|
||||
public class SCRAMPropertiesLoginModule extends PropertiesLoader implements AuditLoginModule {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String SEPARATOR_MECHANISM = "|";
|
||||
private static final String SEPARATOR_PARAMETER = ":";
|
||||
private static final int MIN_ITERATIONS = 4096;
|
||||
private static final SecureRandom RANDOM_GENERATOR = new SecureRandom();
|
||||
private Subject subject;
|
||||
private CallbackHandler callbackHandler;
|
||||
private Properties users;
|
||||
private Map<String, Set<String>> roles;
|
||||
private UserData userData;
|
||||
private String user;
|
||||
private final Set<Principal> principals = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
|
||||
Map<String, ?> options) {
|
||||
this.subject = subject;
|
||||
this.callbackHandler = callbackHandler;
|
||||
|
||||
init(options);
|
||||
users = load(PropertiesLoginModule.USER_FILE_PROP_NAME, "user", options).getProps();
|
||||
roles = load(PropertiesLoginModule.ROLE_FILE_PROP_NAME, "role", options).invertedPropertiesValuesMap();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean login() throws LoginException {
|
||||
NameCallback nameCallback = new NameCallback("Username: ");
|
||||
executeCallbacks(nameCallback);
|
||||
user = nameCallback.getName();
|
||||
SCRAMMechanismCallback mechanismCallback = new SCRAMMechanismCallback();
|
||||
executeCallbacks(mechanismCallback);
|
||||
SCRAM scram = getTypeByString(mechanismCallback.getMechanism());
|
||||
if (user == null) {
|
||||
userData = generateUserData(null); // generate random user data
|
||||
} else {
|
||||
String password = users.getProperty(user + SEPARATOR_MECHANISM + scram.name());
|
||||
if (password == null) {
|
||||
// fallback for probably unencoded user/password or a single encoded entry
|
||||
password = users.getProperty(user);
|
||||
}
|
||||
if (PasswordMaskingUtil.isEncMasked(password)) {
|
||||
String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR_PARAMETER);
|
||||
userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]);
|
||||
} else {
|
||||
userData = generateUserData(password);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private UserData generateUserData(String plainTextPassword) throws LoginException {
|
||||
if (plainTextPassword == null) {
|
||||
// if the user is not available (or the password) generate a random password here so an
|
||||
// attacker can't
|
||||
// distinguish between a missing username and a wrong password
|
||||
byte[] randomPassword = new byte[256];
|
||||
RANDOM_GENERATOR.nextBytes(randomPassword);
|
||||
plainTextPassword = new String(randomPassword);
|
||||
}
|
||||
DigestCallback digestCallback = new DigestCallback();
|
||||
HmacCallback hmacCallback = new HmacCallback();
|
||||
executeCallbacks(digestCallback, hmacCallback);
|
||||
byte[] salt = generateSalt();
|
||||
try {
|
||||
ScramUtils.NewPasswordStringData data =
|
||||
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(plainTextPassword, salt, 4096,
|
||||
digestCallback.getDigest(),
|
||||
hmacCallback.getHmac()));
|
||||
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
|
||||
} catch (ScramException e) {
|
||||
throw new LoginException();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateSalt() {
|
||||
byte[] salt = new byte[32];
|
||||
RANDOM_GENERATOR.nextBytes(salt);
|
||||
return salt;
|
||||
}
|
||||
|
||||
private void executeCallbacks(Callback... callbacks) throws LoginException {
|
||||
try {
|
||||
callbackHandler.handle(callbacks);
|
||||
} catch (UnsupportedCallbackException | IOException e) {
|
||||
throw new LoginException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() throws LoginException {
|
||||
if (userData == null) {
|
||||
throw new LoginException();
|
||||
}
|
||||
subject.getPublicCredentials().add(userData);
|
||||
Set<UserPrincipal> authenticatedUsers = subject.getPrincipals(UserPrincipal.class);
|
||||
UserPrincipal principal = new UserPrincipal(user);
|
||||
principals.add(principal);
|
||||
authenticatedUsers.add(principal);
|
||||
for (UserPrincipal userPrincipal : authenticatedUsers) {
|
||||
Set<String> matchedRoles = roles.get(userPrincipal.getName());
|
||||
if (matchedRoles != null) {
|
||||
for (String entry : matchedRoles) {
|
||||
principals.add(new RolePrincipal(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
subject.getPrincipals().addAll(principals);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean abort() throws LoginException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean logout() throws LoginException {
|
||||
subject.getPrincipals().removeAll(principals);
|
||||
principals.clear();
|
||||
subject.getPublicCredentials().remove(userData);
|
||||
userData = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main method that could be used to encrypt given credentials for use in properties files
|
||||
* @param args username password type [iterations]
|
||||
* @throws GeneralSecurityException if any security mechanism is not available on this JVM
|
||||
* @throws ScramException if invalid data is supplied
|
||||
* @throws StringPrepError if username can't be encoded according to SASL StringPrep
|
||||
* @throws IOException if writing as properties failed
|
||||
*/
|
||||
public static void main(String[] args) throws GeneralSecurityException, ScramException, StringPrepError,
|
||||
IOException {
|
||||
if (args.length < 2) {
|
||||
System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() +
|
||||
" <username> <password> [<iterations>]");
|
||||
System.out.println("\ttype: " + getSupportedTypes());
|
||||
System.out.println("\titerations desired number of iteration (min value: " + MIN_ITERATIONS + ")");
|
||||
return;
|
||||
}
|
||||
String username = args[0];
|
||||
String password = args[1];
|
||||
Properties properties = new Properties();
|
||||
String encodedUser = StringPrep.prepAsQueryString(username);
|
||||
for (SCRAM scram : SCRAM.values()) {
|
||||
MessageDigest digest = MessageDigest.getInstance(scram.getDigest());
|
||||
Mac hmac = Mac.getInstance(scram.getHmac());
|
||||
byte[] salt = generateSalt();
|
||||
int iterations;
|
||||
if (args.length > 2) {
|
||||
iterations = Integer.parseInt(args[2]);
|
||||
if (iterations < MIN_ITERATIONS) {
|
||||
throw new IllegalArgumentException("minimum of " + MIN_ITERATIONS + " required!");
|
||||
}
|
||||
} else {
|
||||
iterations = MIN_ITERATIONS;
|
||||
}
|
||||
ScramUtils.NewPasswordStringData data =
|
||||
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, iterations, digest, hmac));
|
||||
String encodedPassword = PasswordMaskingUtil.wrap(data.salt + SEPARATOR_PARAMETER + data.iterations +
|
||||
SEPARATOR_PARAMETER + data.serverKey + SEPARATOR_PARAMETER + data.storedKey);
|
||||
properties.setProperty(encodedUser + SEPARATOR_MECHANISM + scram.name(), encodedPassword);
|
||||
}
|
||||
properties.store(System.out,
|
||||
"Insert the lines stating with '" + encodedUser + "' into the desired user properties file");
|
||||
}
|
||||
|
||||
private static SCRAM getTypeByString(String type) {
|
||||
SCRAM scram = Arrays.stream(SCRAM.values())
|
||||
.filter(v -> v.getName().equals(type))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("unkown type " + type +
|
||||
", supported ones are " + getSupportedTypes()));
|
||||
return scram;
|
||||
}
|
||||
|
||||
private static String getSupportedTypes() {
|
||||
return String.join(", ", Arrays.stream(SCRAM.values()).map(SCRAM::getName).toArray(String[]::new));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.activemq.artemis.spi.core.security.scram;
|
||||
|
||||
/**
|
||||
* Defines sets of known SCRAM types with methods to fetch matching digest and hmac names
|
||||
*/
|
||||
public enum SCRAM {
|
||||
// ordered by precedence
|
||||
SHA512,
|
||||
SHA256;
|
||||
|
||||
public String getName() {
|
||||
switch (this) {
|
||||
case SHA256:
|
||||
return "SCRAM-SHA-256";
|
||||
case SHA512:
|
||||
return "SCRAM-SHA-512";
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public String getDigest() {
|
||||
switch (this) {
|
||||
case SHA256:
|
||||
return "SHA-256";
|
||||
case SHA512:
|
||||
return "SHA-512";
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public String getHmac() {
|
||||
switch (this) {
|
||||
case SHA256:
|
||||
return "HmacSHA256";
|
||||
case SHA512:
|
||||
return "HmacSHA512";
|
||||
}
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.spi.core.security.scram;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
/**
|
||||
* Indicates error while processing SCRAM sequence
|
||||
*/
|
||||
public class ScramException extends Exception {
|
||||
/**
|
||||
* Creates new ScramException
|
||||
* @param message Exception message
|
||||
*/
|
||||
public ScramException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ScramException(String message, GeneralSecurityException e) {
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new ScramException
|
||||
* @param cause Throwable
|
||||
*/
|
||||
public ScramException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.spi.core.security.scram;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Provides static methods for working with SCRAM/SASL
|
||||
*/
|
||||
public class ScramUtils {
|
||||
private static final byte[] INT_1 = new byte[] {0, 0, 0, 1};
|
||||
|
||||
private ScramUtils() {
|
||||
throw new AssertionError("non-instantiable utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates salted password.
|
||||
* @param password Clear form password, i.e. what user typed
|
||||
* @param salt Salt to be used
|
||||
* @param iterationsCount Iterations for 'salting'
|
||||
* @param mac HMAC to be used
|
||||
* @return salted password
|
||||
* @throws ScramException
|
||||
*/
|
||||
public static byte[] generateSaltedPassword(final String password, byte[] salt, int iterationsCount,
|
||||
Mac mac) throws ScramException {
|
||||
SecretKeySpec key = new SecretKeySpec(password.getBytes(StandardCharsets.US_ASCII), mac.getAlgorithm());
|
||||
try {
|
||||
mac.init(key);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new ScramException("Incompatible key", e);
|
||||
}
|
||||
mac.update(salt);
|
||||
mac.update(INT_1);
|
||||
byte[] result = mac.doFinal();
|
||||
|
||||
byte[] previous = null;
|
||||
for (int i = 1; i < iterationsCount; i++) {
|
||||
mac.update(previous != null ? previous : result);
|
||||
previous = mac.doFinal();
|
||||
for (int x = 0; x < result.length; x++) {
|
||||
result[x] ^= previous[x];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates HMAC
|
||||
* @param keyBytes key
|
||||
* @param hmacName HMAC name
|
||||
* @return Mac
|
||||
* @throws InvalidKeyException if internal error occur while working with SecretKeySpec
|
||||
* @throws NoSuchAlgorithmException if hmacName is not supported by the java
|
||||
*/
|
||||
public static Mac createHmac(final byte[] keyBytes, String hmacName) throws NoSuchAlgorithmException,
|
||||
InvalidKeyException {
|
||||
|
||||
Mac mac = Mac.getInstance(hmacName);
|
||||
SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
|
||||
mac.init(key);
|
||||
return mac;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes HMAC byte array for given string
|
||||
* @param key key
|
||||
* @param hmacName HMAC name
|
||||
* @param string string for which HMAC will be computed
|
||||
* @return computed HMAC
|
||||
* @throws InvalidKeyException if internal error occur while working with SecretKeySpec
|
||||
* @throws NoSuchAlgorithmException if hmacName is not supported by the java
|
||||
*/
|
||||
public static byte[] computeHmac(final byte[] key, String hmacName, final String string) throws InvalidKeyException,
|
||||
NoSuchAlgorithmException {
|
||||
|
||||
Mac mac = createHmac(key, hmacName);
|
||||
mac.update(string.getBytes(StandardCharsets.US_ASCII));
|
||||
return mac.doFinal();
|
||||
}
|
||||
|
||||
public static byte[] computeHmac(final byte[] key, Mac hmac, final String string) throws ScramException {
|
||||
|
||||
try {
|
||||
hmac.init(new SecretKeySpec(key, hmac.getAlgorithm()));
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new ScramException("invalid key", e);
|
||||
}
|
||||
hmac.update(string.getBytes(StandardCharsets.US_ASCII));
|
||||
return hmac.doFinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is null or empty
|
||||
* @param string String to be tested
|
||||
* @return true if the string is null or empty, false otherwise
|
||||
*/
|
||||
public static boolean isNullOrEmpty(String string) {
|
||||
return string == null || string.length() == 0; // string.isEmpty() in Java 6
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the data associated with new password like salted password, keys, etc
|
||||
* <p>
|
||||
* This method is supposed to be used by a server when user provides new clear form password. We
|
||||
* don't want to save it that way so we generate salted password and store it along with other
|
||||
* data required by the SCRAM mechanism
|
||||
* @param passwordClearText Clear form password, i.e. as provided by the user
|
||||
* @param salt Salt to be used
|
||||
* @param iterations Iterations for 'salting'
|
||||
* @param mac HMAC name to be used
|
||||
* @param messageDigest Digest name to be used
|
||||
* @return new password data while working with SecretKeySpec
|
||||
* @throws ScramException
|
||||
*/
|
||||
public static NewPasswordByteArrayData newPassword(String passwordClearText, byte[] salt, int iterations,
|
||||
MessageDigest messageDigest, Mac mac) throws ScramException {
|
||||
byte[] saltedPassword = ScramUtils.generateSaltedPassword(passwordClearText, salt, iterations, mac);
|
||||
|
||||
byte[] clientKey = ScramUtils.computeHmac(saltedPassword, mac, "Client Key");
|
||||
byte[] storedKey = messageDigest.digest(clientKey);
|
||||
byte[] serverKey = ScramUtils.computeHmac(saltedPassword, mac, "Server Key");
|
||||
|
||||
return new NewPasswordByteArrayData(saltedPassword, salt, clientKey, storedKey, serverKey, iterations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms NewPasswordByteArrayData into NewPasswordStringData into database friendly (string)
|
||||
* representation Uses Base64 to encode the byte arrays into strings
|
||||
* @param ba Byte array data
|
||||
* @return String data
|
||||
*/
|
||||
public static NewPasswordStringData byteArrayToStringData(NewPasswordByteArrayData ba) {
|
||||
return new NewPasswordStringData(Base64.getEncoder().encodeToString(ba.saltedPassword),
|
||||
Base64.getEncoder().encodeToString(ba.salt),
|
||||
Base64.getEncoder().encodeToString(ba.clientKey),
|
||||
Base64.getEncoder().encodeToString(ba.storedKey),
|
||||
Base64.getEncoder().encodeToString(ba.serverKey), ba.iterations);
|
||||
}
|
||||
|
||||
/**
|
||||
* New password data in database friendly format, i.e. Base64 encoded strings
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static class NewPasswordStringData {
|
||||
/**
|
||||
* Salted password
|
||||
*/
|
||||
public final String saltedPassword;
|
||||
/**
|
||||
* Used salt
|
||||
*/
|
||||
public final String salt;
|
||||
/**
|
||||
* Client key
|
||||
*/
|
||||
public final String clientKey;
|
||||
/**
|
||||
* Stored key
|
||||
*/
|
||||
public final String storedKey;
|
||||
/**
|
||||
* Server key
|
||||
*/
|
||||
public final String serverKey;
|
||||
/**
|
||||
* Iterations for slating
|
||||
*/
|
||||
public final int iterations;
|
||||
|
||||
/**
|
||||
* Creates new NewPasswordStringData
|
||||
* @param saltedPassword Salted password
|
||||
* @param salt Used salt
|
||||
* @param clientKey Client key
|
||||
* @param storedKey Stored key
|
||||
* @param serverKey Server key
|
||||
* @param iterations Iterations for slating
|
||||
*/
|
||||
public NewPasswordStringData(String saltedPassword, String salt, String clientKey, String storedKey,
|
||||
String serverKey, int iterations) {
|
||||
this.saltedPassword = saltedPassword;
|
||||
this.salt = salt;
|
||||
this.clientKey = clientKey;
|
||||
this.storedKey = storedKey;
|
||||
this.serverKey = serverKey;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* New password data in byte array format
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static class NewPasswordByteArrayData {
|
||||
/**
|
||||
* Salted password
|
||||
*/
|
||||
public final byte[] saltedPassword;
|
||||
/**
|
||||
* Used salt
|
||||
*/
|
||||
public final byte[] salt;
|
||||
/**
|
||||
* Client key
|
||||
*/
|
||||
public final byte[] clientKey;
|
||||
/**
|
||||
* Stored key
|
||||
*/
|
||||
public final byte[] storedKey;
|
||||
/**
|
||||
* Server key
|
||||
*/
|
||||
public final byte[] serverKey;
|
||||
/**
|
||||
* Iterations for slating
|
||||
*/
|
||||
public final int iterations;
|
||||
|
||||
/**
|
||||
* Creates new NewPasswordByteArrayData
|
||||
* @param saltedPassword Salted password
|
||||
* @param salt Used salt
|
||||
* @param clientKey Client key
|
||||
* @param storedKey Stored key
|
||||
* @param serverKey Server key
|
||||
* @param iterations Iterations for slating
|
||||
*/
|
||||
public NewPasswordByteArrayData(byte[] saltedPassword, byte[] salt, byte[] clientKey, byte[] storedKey,
|
||||
byte[] serverKey, int iterations) {
|
||||
|
||||
this.saltedPassword = saltedPassword;
|
||||
this.salt = salt;
|
||||
this.clientKey = clientKey;
|
||||
this.storedKey = storedKey;
|
||||
this.serverKey = serverKey;
|
||||
this.iterations = iterations;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2016 Ognyan Bankov
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.spi.core.security.scram;
|
||||
|
||||
/**
|
||||
* Wrapper for user data needed for the SCRAM authentication
|
||||
*/
|
||||
public class UserData {
|
||||
/**
|
||||
* Salt
|
||||
*/
|
||||
public final String salt;
|
||||
/**
|
||||
* Iterations used to salt the password
|
||||
*/
|
||||
public final int iterations;
|
||||
/**
|
||||
* Server key
|
||||
*/
|
||||
public final String serverKey;
|
||||
/**
|
||||
* Stored key
|
||||
*/
|
||||
public final String storedKey;
|
||||
|
||||
/**
|
||||
* Creates new UserData
|
||||
* @param salt Salt
|
||||
* @param iterations Iterations for salting
|
||||
* @param serverKey Server key
|
||||
* @param storedKey Stored key
|
||||
*/
|
||||
public UserData(String salt, int iterations, String serverKey, String storedKey) {
|
||||
this.salt = salt;
|
||||
this.iterations = iterations;
|
||||
this.serverKey = serverKey;
|
||||
this.storedKey = storedKey;
|
||||
}
|
||||
}
|
|
@ -16,17 +16,18 @@
|
|||
*/
|
||||
package org.apache.activemq.artemis.spi.core.security.jaas;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
|
||||
import javax.security.auth.Subject;
|
||||
import javax.security.auth.callback.Callback;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
import javax.security.auth.callback.UnsupportedCallbackException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Test;
|
||||
|
||||
public class Krb5LoginModuleTest {
|
||||
|
||||
|
@ -52,7 +53,7 @@ public class Krb5LoginModuleTest {
|
|||
underTest.initialize(subject, new CallbackHandler() {
|
||||
@Override
|
||||
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
|
||||
((Krb5Callback) callbacks[0]).setPeerPrincipal(new UserPrincipal("A"));
|
||||
((PrincipalsCallback) callbacks[0]).setPeerPrincipals(new Principal[] {new UserPrincipal("A")});
|
||||
}
|
||||
}, null, null);
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ under the License.
|
|||
<module>proton-clustered-cpp</module>
|
||||
<module>queue</module>
|
||||
<module>proton-ruby</module>
|
||||
<module>sasl-scram</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version='1.0'?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.activemq.examples.amqp</groupId>
|
||||
<artifactId>amqp</artifactId>
|
||||
<version>2.18.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<properties>
|
||||
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
|
||||
</properties>
|
||||
|
||||
<artifactId>sasl-scram</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>ActiveMQ Artemis SASL-SCRAM Example</name>
|
||||
|
||||
<modules>
|
||||
<module>sasl-client</module>
|
||||
<module>sasl-server</module>
|
||||
</modules>
|
||||
</project>
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version='1.0'?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.activemq.examples.amqp</groupId>
|
||||
<artifactId>sasl-scram</artifactId>
|
||||
<version>2.18.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>sasl-scram-client</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>ActiveMQ Artemis SASL-SCRAM-Client Example</name>
|
||||
|
||||
<properties>
|
||||
<activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
|
||||
<artemis-version>${project.version}</artemis-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.qpid</groupId>
|
||||
<artifactId>qpid-jms-client</artifactId>
|
||||
<version>${qpid.jms.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,3 @@
|
|||
# Artemis SASL-SCRAM Server and Client Example
|
||||
|
||||
demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.jms.example;
|
||||
|
||||
import javax.jms.Connection;
|
||||
import javax.jms.ConnectionFactory;
|
||||
import javax.jms.JMSException;
|
||||
import javax.jms.MessageConsumer;
|
||||
import javax.jms.MessageProducer;
|
||||
import javax.jms.Queue;
|
||||
import javax.jms.Session;
|
||||
import javax.jms.TextMessage;
|
||||
|
||||
import org.apache.qpid.jms.JmsConnectionFactory;
|
||||
|
||||
public class QPIDClient {
|
||||
public static void main(String[] args) throws JMSException {
|
||||
sendReceive("SCRAM-SHA-1", "hello", "ogre1234");
|
||||
sendReceive("SCRAM-SHA-256", "test", "test");
|
||||
}
|
||||
|
||||
private static void sendReceive(String method, String username, String password) throws JMSException {
|
||||
ConnectionFactory connectionFactory =
|
||||
new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
|
||||
try (Connection connection = connectionFactory.createConnection(username, password)) {
|
||||
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
Queue queue = session.createQueue("exampleQueue");
|
||||
MessageProducer sender = session.createProducer(queue);
|
||||
sender.send(session.createTextMessage("Hello " + method));
|
||||
connection.start();
|
||||
MessageConsumer consumer = session.createConsumer(queue);
|
||||
TextMessage m = (TextMessage) consumer.receive(5000);
|
||||
System.out.println("message = " + m.getText());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version='1.0'?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.activemq.examples.amqp</groupId>
|
||||
<artifactId>sasl-scram</artifactId>
|
||||
<version>2.18.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>sasl-scram-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>ActiveMQ Artemis SASL-SCRAM-Server Example</name>
|
||||
|
||||
<properties>
|
||||
<activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
|
||||
<artemis-version>${project.version}</artemis-version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.activemq</groupId>
|
||||
<artifactId>artemis-server</artifactId>
|
||||
<version>${artemis-version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.activemq</groupId>
|
||||
<artifactId>artemis-amqp-protocol</artifactId>
|
||||
<version>${artemis-version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,3 @@
|
|||
# Artemis SASL-SCRAM Server and Client Example
|
||||
|
||||
demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* <p>
|
||||
* All rights reserved. Licensed 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.jms.example;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
|
||||
|
||||
public class TestServer {
|
||||
public static void main(String[] args) throws Exception {
|
||||
File configFolder = new File(args.length > 0 ? args[0] : "src/main/resources/").getAbsoluteFile();
|
||||
System.setProperty("java.security.auth.login.config", new File(configFolder, "login.conf").getAbsolutePath());
|
||||
EmbeddedActiveMQ embedded = new EmbeddedActiveMQ();
|
||||
embedded.setSecurityManager(new ActiveMQJAASSecurityManager("artemis"));
|
||||
embedded.setConfigResourcePath(new File(configFolder, "broker.xml").getAbsoluteFile().toURI().toASCIIString());
|
||||
embedded.start();
|
||||
while (true) {
|
||||
// intentional empty
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
## ---------------------------------------------------------------------------
|
||||
## 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.
|
||||
## ---------------------------------------------------------------------------
|
||||
user=hello,test
|
||||
admin=test
|
|
@ -0,0 +1,24 @@
|
|||
## ---------------------------------------------------------------------------
|
||||
## 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.
|
||||
## ---------------------------------------------------------------------------
|
||||
##
|
||||
|
||||
# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
|
||||
test|SHA512 = ENC(7TilOEFipzE4KNkDUTlfnuMkYE1yveyXmK6iBx8/fnE=:4096:yPl/n8eZQEyVmkhuYvrgZCchEpO+a9QiGLXwJfqBWOIfTxMX5TkoHp5eYGABc68cUvoynqCnoqRLDPac+H1urg==:eX5X39hbChbXz00TCkMpmsHqsJTiMGCwamty6yjUS0M+HoE/SLtd2MYY1Shyn+5mu30qFsbXz0WlRA+dZ3Lv3A==)
|
||||
test|SHA256 = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)
|
||||
test|SHA1 = ENC(ehArM+Qzko2eua0hMq0o+NQ9BaTTf4q8xY0tzfy2Zvw=:4096:LvpLr4ezL4ICxeiXAkXEVH9EhO0=:gLELi8NpLVorxXbPIIbVZF/oqh8=)
|
||||
# Example for a plain username/password, don't use this on public servers!
|
||||
hello = ogre1234
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version='1.0'?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<configuration
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="urn:activemq"
|
||||
xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
|
||||
<core xmlns="urn:activemq:core">
|
||||
|
||||
<persistence-enabled>false</persistence-enabled>
|
||||
|
||||
<security-enabled>true</security-enabled>
|
||||
|
||||
<acceptors>
|
||||
<acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
|
||||
</acceptor>
|
||||
</acceptors>
|
||||
<security-settings>
|
||||
<security-setting match="#">
|
||||
<permission type="createAddress" roles="user" />
|
||||
<permission type="createDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="deleteDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="createNonDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="deleteNonDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="consume" roles="user" />
|
||||
<permission type="send" roles="user" />
|
||||
</security-setting>
|
||||
</security-settings>
|
||||
</core>
|
||||
</configuration>
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
amqp-sasl-scram {
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
|
||||
debug=false
|
||||
org.apache.activemq.jaas.properties.user="artemis-users.properties"
|
||||
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
|
||||
};
|
||||
|
||||
|
||||
artemis {
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
|
||||
;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
/.apt_generated/
|
||||
/.apt_generated_tests/
|
|
@ -17,13 +17,24 @@
|
|||
package org.apache.activemq.artemis.tests.integration.amqp.connect;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration;
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
|
||||
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
|
||||
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
|
||||
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
|
||||
import org.apache.qpid.proton.engine.Sasl;
|
||||
import org.apache.qpid.proton.engine.Sasl.SaslOutcome;
|
||||
|
@ -107,7 +118,7 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
|
||||
// No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered
|
||||
AMQPBrokerConnectConfiguration amqpConnection =
|
||||
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
|
||||
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
|
||||
amqpConnection.setReconnectAttempts(0);// No reconnects
|
||||
|
||||
server.getConfiguration().addAMQPConnection(amqpConnection);
|
||||
|
@ -135,7 +146,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
});
|
||||
|
||||
// User and pass are given, it will select PLAIN
|
||||
AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
|
||||
AMQPBrokerConnectConfiguration amqpConnection =
|
||||
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
|
||||
amqpConnection.setReconnectAttempts(0);// No reconnects
|
||||
amqpConnection.setUser(USER);
|
||||
amqpConnection.setPassword(PASSWD);
|
||||
|
@ -151,6 +163,36 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
assertArrayEquals(expectedPlainInitialResponse(USER, PASSWD), authenticator.getInitialResponse());
|
||||
}
|
||||
|
||||
@Test(timeout = 200000)
|
||||
public void testConnectsWithSCRAM() throws Exception {
|
||||
CountDownLatch serverConnectionOpen = new CountDownLatch(1);
|
||||
SCRAMTestAuthenticator authenticator = new SCRAMTestAuthenticator(SCRAM.SHA512);
|
||||
|
||||
mockServer = new MockServer(vertx, () -> authenticator, serverConnection -> {
|
||||
serverConnection.openHandler(serverSender -> {
|
||||
serverConnectionOpen.countDown();
|
||||
serverConnection.closeHandler(x -> serverConnection.close());
|
||||
serverConnection.open();
|
||||
});
|
||||
});
|
||||
|
||||
AMQPBrokerConnectConfiguration amqpConnection =
|
||||
new AMQPBrokerConnectConfiguration("testSScramConnect", "tcp://localhost:" + mockServer.actualPort());
|
||||
amqpConnection.setReconnectAttempts(0);// No reconnects
|
||||
amqpConnection.setUser(USER);
|
||||
amqpConnection.setPassword(PASSWD);
|
||||
|
||||
server.getConfiguration().addAMQPConnection(amqpConnection);
|
||||
|
||||
server.start();
|
||||
|
||||
boolean awaitConnectionOpen = serverConnectionOpen.await(10, TimeUnit.SECONDS);
|
||||
assertTrue("Broker did not open connection in alotted time", awaitConnectionOpen);
|
||||
assertEquals(SCRAM.SHA512.getName(), authenticator.chosenMech);
|
||||
assertTrue(authenticator.succeeded());
|
||||
|
||||
}
|
||||
|
||||
@Test(timeout = 20000)
|
||||
public void testConnectsWithExternal() throws Exception {
|
||||
doConnectWithExternalTestImpl(true);
|
||||
|
@ -161,10 +203,13 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
doConnectWithExternalTestImpl(false);
|
||||
}
|
||||
|
||||
private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException, InterruptedException, Exception {
|
||||
private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException,
|
||||
InterruptedException, Exception {
|
||||
CountDownLatch serverConnectionOpen = new CountDownLatch(1);
|
||||
// The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker only selects it when it actually
|
||||
// has a client-cert. Real servers shouldnt actually offer the mechanism to a client that didnt have to provide a cert.
|
||||
// The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker
|
||||
// only selects it when it actually
|
||||
// has a client-cert. Real servers shouldnt actually offer the mechanism to a client that
|
||||
// didnt have to provide a cert.
|
||||
TestAuthenticator authenticator = new TestAuthenticator(true, EXTERNAL, PLAIN);
|
||||
|
||||
final String keyStorePath = this.getClass().getClassLoader().getResource(SERVER_KEYSTORE_NAME).getFile();
|
||||
|
@ -191,14 +236,17 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
});
|
||||
|
||||
String amqpServerConnectionURI = "tcp://localhost:" + mockServer.actualPort() +
|
||||
"?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
|
||||
"?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
|
||||
if (requireClientCert) {
|
||||
amqpServerConnectionURI += ";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
|
||||
amqpServerConnectionURI +=
|
||||
";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
|
||||
}
|
||||
|
||||
AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
|
||||
AMQPBrokerConnectConfiguration amqpConnection =
|
||||
new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
|
||||
amqpConnection.setReconnectAttempts(0);// No reconnects
|
||||
amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate is provided, but will otherwise.
|
||||
amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate
|
||||
// is provided, but will otherwise.
|
||||
amqpConnection.setPassword(PASSWD);
|
||||
|
||||
server.getConfiguration().addAMQPConnection(amqpConnection);
|
||||
|
@ -236,8 +284,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
|
||||
private static final class TestAuthenticator implements ProtonSaslAuthenticator {
|
||||
private Sasl sasl;
|
||||
private boolean succeed;
|
||||
private String[] offeredMechs;
|
||||
private final boolean succeed;
|
||||
private final String[] offeredMechs;
|
||||
String chosenMech = null;
|
||||
byte[] initialResponse = null;
|
||||
boolean done = false;
|
||||
|
@ -268,7 +316,6 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
|
||||
initialResponse = new byte[sasl.pending()];
|
||||
sasl.recv(initialResponse, 0, initialResponse.length);
|
||||
|
||||
if (succeed) {
|
||||
sasl.done(SaslOutcome.PN_SASL_OK);
|
||||
} else {
|
||||
|
@ -296,4 +343,103 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class SCRAMTestAuthenticator implements ProtonSaslAuthenticator {
|
||||
|
||||
private final SCRAM mech;
|
||||
private Sasl sasl;
|
||||
private TestSCRAMServerSASL serverSASL;
|
||||
private String chosenMech;
|
||||
|
||||
SCRAMTestAuthenticator(SCRAM mech) {
|
||||
this.mech = mech;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(NetSocket socket, ProtonConnection protonConnection, Transport transport) {
|
||||
this.sasl = transport.sasl();
|
||||
sasl.server();
|
||||
sasl.allowSkip(false);
|
||||
sasl.setMechanisms(mech.getName(), PLAIN, ANONYMOUS);
|
||||
try {
|
||||
serverSASL = new TestSCRAMServerSASL(mech);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Handler<Boolean> completionHandler) {
|
||||
String[] remoteMechanisms = sasl.getRemoteMechanisms();
|
||||
int pending = sasl.pending();
|
||||
if (remoteMechanisms.length == 0 || pending == 0) {
|
||||
completionHandler.handle(false);
|
||||
return;
|
||||
}
|
||||
chosenMech = remoteMechanisms[0];
|
||||
byte[] msg = new byte[pending];
|
||||
sasl.recv(msg, 0, msg.length);
|
||||
byte[] result = serverSASL.processSASL(msg);
|
||||
if (result != null) {
|
||||
sasl.send(result, 0, result.length);
|
||||
}
|
||||
boolean ended = serverSASL.isEnded();
|
||||
if (ended) {
|
||||
if (succeeded()) {
|
||||
sasl.done(SaslOutcome.PN_SASL_OK);
|
||||
} else {
|
||||
sasl.done(SaslOutcome.PN_SASL_AUTH);
|
||||
}
|
||||
completionHandler.handle(true);
|
||||
} else {
|
||||
completionHandler.handle(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean succeeded() {
|
||||
SASLResult result = serverSASL.result();
|
||||
return result != null && result.isSuccess() && serverSASL.e == null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
|
||||
|
||||
private Exception e;
|
||||
|
||||
TestSCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
|
||||
super(mechanism);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void done() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserData aquireUserData(String userName) throws LoginException {
|
||||
if (!USER.equals(userName)) {
|
||||
throw new LoginException("invalid username");
|
||||
}
|
||||
byte[] salt = new byte[32];
|
||||
new SecureRandom().nextBytes(salt);
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
|
||||
Mac hmac = Mac.getInstance(mechanism.getHmac());
|
||||
ScramUtils.NewPasswordStringData data =
|
||||
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(PASSWD, salt, 4096, digest, hmac));
|
||||
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
|
||||
} catch (Exception e) {
|
||||
throw new LoginException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed(Exception e) {
|
||||
this.e = e;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* 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.activemq.artemis.tests.integration.amqp.sasl;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.jms.Connection;
|
||||
import javax.jms.ConnectionFactory;
|
||||
import javax.jms.JMSException;
|
||||
import javax.jms.MessageConsumer;
|
||||
import javax.jms.MessageProducer;
|
||||
import javax.jms.Queue;
|
||||
import javax.jms.Session;
|
||||
import javax.jms.TextMessage;
|
||||
|
||||
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
|
||||
import org.apache.qpid.jms.JmsConnectionFactory;
|
||||
import org.apache.qpid.jms.exceptions.JMSSecuritySaslException;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* This test SASL-SCRAM Support
|
||||
*/
|
||||
public class SaslScramTest {
|
||||
|
||||
private static EmbeddedActiveMQ BROKER;
|
||||
|
||||
@BeforeClass
|
||||
public static void startBroker() throws Exception {
|
||||
String loginConfPath = new File(SaslScramTest.class.getResource("/login.config").toURI()).getAbsolutePath();
|
||||
System.out.println(loginConfPath);
|
||||
System.setProperty("java.security.auth.login.config", loginConfPath);
|
||||
BROKER = new EmbeddedActiveMQ();
|
||||
BROKER.setConfigResourcePath(SaslScramTest.class.getResource("/broker-saslscram.xml").toExternalForm());
|
||||
BROKER.setSecurityManager(new ActiveMQJAASSecurityManager("artemis-sasl-scram"));
|
||||
BROKER.start();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void shutdownBroker() throws Exception {
|
||||
BROKER.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user with plain text password can login using all mechanisms
|
||||
* @throws JMSException should not happen
|
||||
*/
|
||||
@Test
|
||||
public void testUnencryptedWorksWithAllMechanism() throws JMSException {
|
||||
sendRcv("SCRAM-SHA-1", "hello", "ogre1234");
|
||||
sendRcv("SCRAM-SHA-256", "hello", "ogre1234");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a user that has encrypted passwords for all mechanism can login with any of them
|
||||
* @throws JMSException should not happen
|
||||
*/
|
||||
@Test
|
||||
public void testEncryptedWorksWithAllMechanism() throws JMSException {
|
||||
sendRcv("SCRAM-SHA-1", "multi", "worksforall");
|
||||
sendRcv("SCRAM-SHA-256", "multi", "worksforall");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a user that is only stored with one explicit mechanism can't use another mechanism
|
||||
* @throws JMSException is expected
|
||||
*/
|
||||
@Test(expected = JMSSecuritySaslException.class)
|
||||
public void testEncryptedWorksOnlyWithMechanism() throws JMSException {
|
||||
sendRcv("SCRAM-SHA-1", "test", "test");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a user that is only stored with one explicit mechanism can login with this
|
||||
* mechanism
|
||||
* @throws JMSException should not happen
|
||||
*/
|
||||
@Test
|
||||
public void testEncryptedWorksWithMechanism() throws JMSException {
|
||||
sendRcv("SCRAM-SHA-256", "test", "test");
|
||||
}
|
||||
|
||||
private void sendRcv(String method, String username, String password) throws JMSException {
|
||||
ConnectionFactory connectionFactory =
|
||||
new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
|
||||
try (Connection connection = connectionFactory.createConnection(username, password)) {
|
||||
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
Queue queue = session.createQueue("exampleQueue");
|
||||
MessageProducer sender = session.createProducer(queue);
|
||||
String text = "Hello " + method;
|
||||
sender.send(session.createTextMessage(text));
|
||||
connection.start();
|
||||
MessageConsumer consumer = session.createConsumer(queue);
|
||||
TextMessage m = (TextMessage) consumer.receive(5000);
|
||||
assertEquals(text, m.getText());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
## ---------------------------------------------------------------------------
|
||||
## 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.
|
||||
## ---------------------------------------------------------------------------
|
||||
user=hello,test,multi
|
||||
admin=multi
|
|
@ -0,0 +1,26 @@
|
|||
## ---------------------------------------------------------------------------
|
||||
## Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
## contributor license agreements. See the NOTICE file distributed with
|
||||
## this work for additional information regarding copyright ownership.
|
||||
## The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
## (the "License"); you may not use this file except in compliance with
|
||||
## the License. You may obtain a copy of the License at
|
||||
##
|
||||
## http://www.apache.org/licenses/LICENSE-2.0
|
||||
##
|
||||
## Unless required by applicable law or agreed to in writing, software
|
||||
## distributed under the License is distributed on an "AS IS" BASIS,
|
||||
## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
## See the License for the specific language governing permissions and
|
||||
## limitations under the License.
|
||||
## ---------------------------------------------------------------------------
|
||||
|
||||
# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
|
||||
multi|SHA256 = ENC(o3ljCITL4Cw6pu+fxvz4k68F8jQuZAITcRNpy2THufw=:4096:Niuc0/lWg/YQztHqJCJ5SodyxbWPtGj6zp/HHPqSDBY=:O1YL/w08fvuTvqctbHrr4TxpzKso+NCdqt4Amqp7r0k=)
|
||||
multi|SHA1 = ENC(cJyfpU4wgmoSz1GVc39+CooXY2jIv2ILe7486+l9vbg=:4096:sMCix9TeOvmo2eb4xbCjQt4navs=:fSKdLYAgdx36RMjjMSn1dZ7IpY8=)
|
||||
|
||||
# Example for a plain username/password, don't use this on public servers!
|
||||
hello = ogre1234
|
||||
|
||||
# just for unit-test purpose!
|
||||
test = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version='1.0'?>
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<configuration
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="urn:activemq"
|
||||
xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
|
||||
<core xmlns="urn:activemq:core">
|
||||
|
||||
<persistence-enabled>false</persistence-enabled>
|
||||
|
||||
<security-enabled>true</security-enabled>
|
||||
|
||||
<acceptors>
|
||||
<acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
|
||||
</acceptor>
|
||||
</acceptors>
|
||||
<security-settings>
|
||||
<security-setting match="#">
|
||||
<permission type="createAddress" roles="user" />
|
||||
<permission type="createDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="deleteDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="createNonDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="deleteNonDurableQueue"
|
||||
roles="user" />
|
||||
<permission type="consume" roles="user" />
|
||||
<permission type="send" roles="user" />
|
||||
</security-setting>
|
||||
</security-settings>
|
||||
</core>
|
||||
</configuration>
|
|
@ -319,3 +319,15 @@ amqp-jms-client {
|
|||
com.sun.security.auth.module.Krb5LoginModule required
|
||||
useKeyTab=true;
|
||||
};
|
||||
|
||||
amqp-sasl-scram {
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
|
||||
debug=false
|
||||
org.apache.activemq.jaas.properties.user="artemis-scram-users.properties"
|
||||
org.apache.activemq.jaas.properties.role="artemis-scram-roles.properties";
|
||||
};
|
||||
|
||||
artemis-sasl-scram {
|
||||
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
|
||||
;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue