/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.apache.qpid.jms.integration; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.net.URLDecoder; import java.net.URLEncoder; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.JMSException; import javax.jms.JMSSecurityException; import javax.net.ssl.SSLContext; import org.apache.qpid.jms.JmsConnectionFactory; import org.apache.qpid.jms.test.QpidJmsTestCase; import org.apache.qpid.jms.test.testpeer.TestAmqpPeer; import org.apache.qpid.jms.transports.TransportSslOptions; import org.apache.qpid.jms.transports.TransportSupport; import org.apache.qpid.proton.amqp.Symbol; import org.junit.Test; public class SaslIntegrationTest extends QpidJmsTestCase { private static final Symbol ANONYMOUS = Symbol.valueOf("ANONYMOUS"); private static final Symbol PLAIN = Symbol.valueOf("PLAIN"); private static final Symbol CRAM_MD5 = Symbol.valueOf("CRAM-MD5"); private static final Symbol SCRAM_SHA_1 = Symbol.valueOf("SCRAM-SHA-1"); private static final Symbol SCRAM_SHA_256 = Symbol.valueOf("SCRAM-SHA-256"); private static final Symbol EXTERNAL = Symbol.valueOf("EXTERNAL"); private static final String BROKER_JKS_KEYSTORE = "src/test/resources/broker-jks.keystore"; private static final String BROKER_JKS_TRUSTSTORE = "src/test/resources/broker-jks.truststore"; private static final String CLIENT_JKS_KEYSTORE = "src/test/resources/client-jks.keystore"; private static final String CLIENT_JKS_TRUSTSTORE = "src/test/resources/client-jks.truststore"; private static final String PASSWORD = "password"; @Test(timeout = 20000) public void testSaslExternalConnection() throws Exception { TransportSslOptions sslOptions = new TransportSslOptions(); sslOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE); sslOptions.setKeyStorePassword(PASSWORD); sslOptions.setVerifyHost(false); sslOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE); sslOptions.setTrustStorePassword(PASSWORD); String connOptions = "?transport.trustStoreLocation=" + CLIENT_JKS_TRUSTSTORE + "&" + "transport.trustStorePassword=" + PASSWORD + "&" + "transport.keyStoreLocation=" + CLIENT_JKS_KEYSTORE + "&" + "transport.keyStorePassword=" + PASSWORD; SSLContext context = TransportSupport.createSslContext(sslOptions); try (TestAmqpPeer testPeer = new TestAmqpPeer(context, true);) { // Expect an EXTERNAL connection testPeer.expectSaslExternal(); testPeer.expectOpen(); // Each connection creates a session for managing temporary destinations etc testPeer.expectBegin(); ConnectionFactory factory = new JmsConnectionFactory("amqps://localhost:" + testPeer.getServerPort() + connOptions); Connection connection = factory.createConnection(); // Set a clientID to provoke the actual AMQP connection process to occur. connection.setClientID("clientName"); testPeer.waitForAllHandlersToComplete(1000); assertNull(testPeer.getThrowable()); testPeer.expectClose(); connection.close(); } } @Test(timeout = 20000) public void testSaslPlainConnection() throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { // Expect a PLAIN connection String user = "user"; String pass = "qwerty123456"; testPeer.expectSaslPlain(user, pass); testPeer.expectOpen(); // Each connection creates a session for managing temporary destinations etc testPeer.expectBegin(); ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort()); Connection connection = factory.createConnection(user, pass); // Set a clientID to provoke the actual AMQP connection process to occur. connection.setClientID("clientName"); testPeer.waitForAllHandlersToComplete(1000); assertNull(testPeer.getThrowable()); testPeer.expectClose(); connection.close(); } } @Test(timeout = 20000) public void testSaslPlainConnectionWithURIEncodedCredentials() throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { // Expect a PLAIN connection with decoded password from URL encoded value. String user = "user"; String pass = " CN24tCa+Hn/av"; // If double decoded this value results in " CN24tCa Hn/av" as the decoded plus // becomes a valid encoding for a space character and would be removed. String encodedPass = "+CN24tCa%2BHn%2Fav"; String urlEncodedPassword = URLEncoder.encode(pass, "UTF-8"); String urlDecodedPassword = URLDecoder.decode(pass, "UTF-8"); // Inadvertent double decoding of the password should result in a different value // which would fail this test. assertEquals(encodedPass, urlEncodedPassword); assertFalse(urlEncodedPassword.equals(urlDecodedPassword)); assertFalse(pass.equals(urlDecodedPassword)); testPeer.expectSaslPlain(user, pass); testPeer.expectOpen(); // Each connection creates a session for managing temporary destinations etc testPeer.expectBegin(); ConnectionFactory factory = new JmsConnectionFactory( "amqp://localhost:" + testPeer.getServerPort() + "?jms.username=" + user + "&jms.password=" + encodedPass); Connection connection = factory.createConnection(); // Set a clientID to provoke the actual AMQP connection process to occur. connection.setClientID("clientName"); testPeer.waitForAllHandlersToComplete(1000); assertNull(testPeer.getThrowable()); testPeer.expectClose(); connection.close(); } } @Test(timeout = 20000) public void testSaslAnonymousConnection() throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { // Expect an ANOYMOUS connection testPeer.expectSaslAnonymous(); testPeer.expectOpen(); // Each connection creates a session for managing temporary destinations etc testPeer.expectBegin(); ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort()); Connection connection = factory.createConnection(); // Set a clientID to provoke the actual AMQP connection process to occur. connection.setClientID("clientName"); testPeer.waitForAllHandlersToComplete(1000); assertNull(testPeer.getThrowable()); testPeer.expectClose(); connection.close(); } } /** * Add a small delay after the SASL process fails, test peer will throw if * any unexpected frames arrive, such as erroneous open+close. * * @throws Exception if an error occurs during the test. */ @Test(timeout = 20000) public void testWaitForUnexpectedFramesAfterSaslFailure() throws Exception { doMechanismSelectedTestImpl(null, null, ANONYMOUS, new Symbol[] {ANONYMOUS}, true); } @Test(timeout = 20000) public void testAnonymousSelectedWhenNoCredentialsWereSupplied() throws Exception { doMechanismSelectedTestImpl(null, null, ANONYMOUS, new Symbol[] {CRAM_MD5, PLAIN, ANONYMOUS}, false); } @Test(timeout = 20000) public void testAnonymousSelectedWhenNoPasswordWasSupplied() throws Exception { doMechanismSelectedTestImpl("username", null, ANONYMOUS, new Symbol[] {CRAM_MD5, PLAIN, ANONYMOUS}, false); } @Test(timeout = 20000) public void testCramMd5SelectedWhenCredentialsPresent() throws Exception { doMechanismSelectedTestImpl("username", "password", CRAM_MD5, new Symbol[] {CRAM_MD5, PLAIN, ANONYMOUS}, false); } @Test(timeout = 20000) public void testScramSha1SelectedWhenCredentialsPresent() throws Exception { doMechanismSelectedTestImpl("username", "password", SCRAM_SHA_1, new Symbol[] {SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}, false); } @Test(timeout = 20000) public void testScramSha256SelectedWhenCredentialsPresent() throws Exception { doMechanismSelectedTestImpl("username", "password", SCRAM_SHA_256, new Symbol[] {SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}, false); } private void doMechanismSelectedTestImpl(String username, String password, Symbol clientSelectedMech, Symbol[] serverMechs, boolean wait) throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { testPeer.expectFailingSaslAuthentication(serverMechs, clientSelectedMech); ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + "?jms.clientID=myclientid"); try { factory.createConnection(username, password); fail("Excepted exception to be thrown"); }catch (JMSSecurityException jmsse) { // Expected, we deliberately failed the SASL process, // we only wanted to verify the correct mechanism // was selected, other tests verify the remainder. } if (wait) { Thread.sleep(200); } testPeer.waitForAllHandlersToComplete(1000); } } @Test(timeout = 20000) public void testExternalSelectedWhenLocalPrincipalPresent() throws Exception { doMechanismSelectedExternalTestImpl(true, EXTERNAL, new Symbol[] {EXTERNAL, SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}); } @Test(timeout = 20000) public void testExternalNotSelectedWhenLocalPrincipalMissing() throws Exception { doMechanismSelectedExternalTestImpl(false, ANONYMOUS, new Symbol[] {EXTERNAL, SCRAM_SHA_256, SCRAM_SHA_1, CRAM_MD5, PLAIN, ANONYMOUS}); } private void doMechanismSelectedExternalTestImpl(boolean requireClientCert, Symbol clientSelectedMech, Symbol[] serverMechs) throws Exception { TransportSslOptions sslOptions = new TransportSslOptions(); sslOptions.setKeyStoreLocation(BROKER_JKS_KEYSTORE); sslOptions.setKeyStorePassword(PASSWORD); sslOptions.setVerifyHost(false); if (requireClientCert) { sslOptions.setTrustStoreLocation(BROKER_JKS_TRUSTSTORE); sslOptions.setTrustStorePassword(PASSWORD); } SSLContext context = TransportSupport.createSslContext(sslOptions); try (TestAmqpPeer testPeer = new TestAmqpPeer(context, requireClientCert);) { String connOptions = "?transport.trustStoreLocation=" + CLIENT_JKS_TRUSTSTORE + "&" + "transport.trustStorePassword=" + PASSWORD + "&" + "jms.clientID=myclientid"; if (requireClientCert) { connOptions += "&transport.keyStoreLocation=" + CLIENT_JKS_KEYSTORE + "&" + "transport.keyStorePassword=" + PASSWORD; } testPeer.expectFailingSaslAuthentication(serverMechs, clientSelectedMech); JmsConnectionFactory factory = new JmsConnectionFactory("amqps://localhost:" + testPeer.getServerPort() + connOptions); try { factory.createConnection(); fail("Expected exception to be thrown"); } catch (JMSException jmse) { // Expected } testPeer.waitForAllHandlersToComplete(1000); } } @Test(timeout = 20000) public void testSaslLayerDisabledConnection() throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { // Expect a connection with no SASL layer. testPeer.expectSaslLayerDisabledConnect(null); // Each connection creates a session for managing temporary destinations etc testPeer.expectBegin(); ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + "?amqp.saslLayer=false"); Connection connection = factory.createConnection(); // Set a clientID to provoke the actual AMQP connection process to occur. connection.setClientID("clientName"); testPeer.waitForAllHandlersToComplete(1000); assertNull(testPeer.getThrowable()); testPeer.expectClose(); connection.close(); } } @Test(timeout = 20000) public void testRestrictSaslMechanismsWithSingleMech() throws Exception { // Check PLAIN gets picked when we don't specify a restriction doMechanismSelectionRestrictedTestImpl("username", "password", PLAIN, new Symbol[] { PLAIN, ANONYMOUS}, null); // Check ANONYMOUS gets picked when we do specify a restriction doMechanismSelectionRestrictedTestImpl("username", "password", ANONYMOUS, new Symbol[] { PLAIN, ANONYMOUS}, "ANONYMOUS"); } @Test(timeout = 20000) public void testRestrictSaslMechanismsWithMultipleMechs() throws Exception { // Check CRAM-MD5 gets picked when we dont specify a restriction doMechanismSelectionRestrictedTestImpl("username", "password", CRAM_MD5, new Symbol[] {CRAM_MD5, PLAIN, ANONYMOUS}, null); // Check PLAIN gets picked when we specify a restriction with multiple mechs doMechanismSelectionRestrictedTestImpl("username", "password", PLAIN, new Symbol[] { CRAM_MD5, PLAIN, ANONYMOUS}, "PLAIN,ANONYMOUS"); } @Test(timeout = 20000) public void testRestrictSaslMechanismsWithMultipleMechsNoPassword() throws Exception { // Check ANONYMOUS gets picked when we specify a restriction with multiple mechs but don't give a password doMechanismSelectionRestrictedTestImpl("username", null, ANONYMOUS, new Symbol[] { CRAM_MD5, PLAIN, ANONYMOUS}, "PLAIN,ANONYMOUS"); } private void doMechanismSelectionRestrictedTestImpl(String username, String password, Symbol clientSelectedMech, Symbol[] serverMechs, String mechanismsOptionValue) throws Exception { try (TestAmqpPeer testPeer = new TestAmqpPeer();) { testPeer.expectFailingSaslAuthentication(serverMechs, clientSelectedMech); String uriOptions = "?jms.clientID=myclientid"; if(mechanismsOptionValue != null) { uriOptions += "&amqp.saslMechanisms=" + mechanismsOptionValue; } ConnectionFactory factory = new JmsConnectionFactory("amqp://localhost:" + testPeer.getServerPort() + uriOptions); try { factory.createConnection(username, password); fail("Excepted exception to be thrown"); }catch (JMSSecurityException jmsse) { // Expected, we deliberately failed the SASL process, // we only wanted to verify the correct mechanism // was selected, other tests verify the remainder. } testPeer.waitForAllHandlersToComplete(1000); } } }