This is an automated email from the ASF dual-hosted git repository. gtully pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/artemis.git
commit 6795672b8f5df7284ccd6cc7ad2d257fb8ef340c Author: Grzegorz Grzybek <[email protected]> AuthorDate: Tue Mar 24 14:05:01 2026 +0100 ARTEMIS-5200 Implement server SASL for XOAUTH2 and OAUTHBEARER --- .../amqp/proton/AMQPConnectionContext.java | 5 + .../protocol/amqp/sasl/OAuthBearerSASL.java | 95 +++++++++++++++++++ .../amqp/sasl/OAuthBearerServerSASLFactory.java | 48 ++++++++++ .../protocol/amqp/sasl/ServerSASLToken.java | 57 ++++++++++++ .../protocol/amqp/sasl/TokenSASLResult.java | 56 +++++++++++ .../artemis/protocol/amqp/sasl/XOAuth2SASL.java | 89 ++++++++++++++++++ .../amqp/sasl/XOAuth2ServerSASLFactory.java | 48 ++++++++++ ...mq.artemis.protocol.amqp.sasl.ServerSASLFactory | 2 + .../protocol/amqp/sasl/OAuthBearerSASLTest.java | 103 +++++++++++++++++++++ .../protocol/amqp/sasl/XOAuth2SASLTest.java | 103 +++++++++++++++++++++ 10 files changed, 606 insertions(+) diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java index 82c5132ad7..7dce000344 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java @@ -59,6 +59,7 @@ import org.apache.activemq.artemis.protocol.amqp.sasl.AnonymousServerSASL; import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASLFactory; import org.apache.activemq.artemis.protocol.amqp.sasl.PlainSASLResult; import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult; +import org.apache.activemq.artemis.protocol.amqp.sasl.TokenSASLResult; import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; import org.apache.activemq.artemis.spi.core.remoting.ReadyListener; import org.apache.activemq.artemis.utils.ByteUtil; @@ -728,6 +729,8 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH user = saslResult.getUser(); if (saslResult instanceof PlainSASLResult plainSASLResult) { password = plainSASLResult.getPassword(); + } else if (saslResult instanceof TokenSASLResult tokenSASLResult) { + password = tokenSASLResult.getToken(); } } @@ -987,6 +990,8 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH if (saslResult != null) { if (saslResult instanceof PlainSASLResult plainSASLResult) { password = plainSASLResult.getPassword(); + } else if (saslResult instanceof TokenSASLResult tokenSASLResult) { + password = tokenSASLResult.getToken(); } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASL.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASL.java new file mode 100644 index 0000000000..f13c809746 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASL.java @@ -0,0 +1,95 @@ +/* + * 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 org.apache.activemq.artemis.core.security.SecurityStore; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class OAuthBearerSASL extends ServerSASLToken { + + // https://datatracker.ietf.org/doc/html/rfc7628#section-3 + static final String MECHANISM_NAME = "OAUTHBEARER"; + + public OAuthBearerSASL(SecurityStore securityStore, String securityDomain, RemotingConnection remotingConnection) { + super(securityStore, securityDomain, remotingConnection); + } + + @Override + public String getName() { + return MECHANISM_NAME; + } + + @Override + public byte[] processSASL(byte[] bytes) { + // expecting `n,a=<user>,\x01host=<host>\x01port=<port>\x01auth=Bearer <token>\x01\x01` + // according to https://datatracker.ietf.org/doc/html/rfc7628#section-3.1 + // `n,a=<user>,` is from https://datatracker.ietf.org/doc/html/rfc5801 + // - gs2-cb-flag: "n" = client does not support channel binding, + // "p" = client supports and used channel binding + // "y" = client supports CB, thinks the server does not + // - gs2-authzid: "a=<saslname>" + if (bytes == null || bytes.length == 0) { + result = new TokenSASLResult(false, null, null); + } else { + if (bytes.length < 2 || !(bytes[bytes.length - 2] == '\001' && bytes[bytes.length - 1] == '\001')) { + result = new TokenSASLResult(false, null, null); + } else { + String data = new String(bytes, StandardCharsets.UTF_8); + String[] segments = data.split("\001"); + if (segments.length < 2) { + result = new TokenSASLResult(false, null, null); + } else { + String gs2Header = segments[0]; + String[] headersSegments = gs2Header.split(","); + String user = null; + for (String s : headersSegments) { + if (s.startsWith("a=")) { + user = s.substring(2); + } + } + + String token = null; + for (int i = 1; i < segments.length; i++) { + if (segments[i].startsWith("auth=Bearer ")) { + token = segments[i].substring(12); + } + } + + if (token == null) { + result = new TokenSASLResult(false, null, null); + } else { + boolean success = authenticate(user, token); + result = new TokenSASLResult(success, user, token); + } + } + } + } + + // TODO: validate host and port from the SASL OAUTHBEARER initial response? + + if (result.isSuccess()) { + return null; + } + + // see https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2 + return Base64.getEncoder().encode("{\"status\":\"invalid_token\"}".getBytes()); + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerServerSASLFactory.java new file mode 100644 index 0000000000..b3c996e66e --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerServerSASLFactory.java @@ -0,0 +1,48 @@ +/* + * 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 org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRoutingHandler; +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; + +public class OAuthBearerServerSASLFactory implements ServerSASLFactory { + + @Override + public String getMechanism() { + return OAuthBearerSASL.MECHANISM_NAME; + } + + @Override + public ServerSASL create(ActiveMQServer server, ProtocolManager<AmqpInterceptor, AMQPRoutingHandler> manager, Connection connection, RemotingConnection remotingConnection) { + return new OAuthBearerSASL(server.getSecurityStore(), manager.getSecurityDomain(), connection.getProtocolConnection()); + } + + @Override + public int getPrecedence() { + return 32; + } + + @Override + public boolean isDefaultPermitted() { + return false; + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLToken.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLToken.java new file mode 100644 index 0000000000..62d1143255 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/ServerSASLToken.java @@ -0,0 +1,57 @@ +/* + * 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 org.apache.activemq.artemis.core.security.SecurityStore; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; + +public abstract class ServerSASLToken implements ServerSASL { + + protected final SecurityStore securityStore; + protected final String securityDomain; + protected RemotingConnection remotingConnection; + + protected SASLResult result = null; + + public ServerSASLToken(SecurityStore securityStore, String securityDomain, RemotingConnection remotingConnection) { + this.securityStore = securityStore; + this.securityDomain = securityDomain; + this.remotingConnection = remotingConnection; + } + + @Override + public SASLResult result() { + return result; + } + + @Override + public void done() { + } + + protected boolean authenticate(String user, String token) { + if (securityStore != null && securityStore.isSecurityEnabled()) { + try { + securityStore.authenticate(user, token, remotingConnection, securityDomain); + return true; + } catch (Exception e) { + return false; + } + } + return true; + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/TokenSASLResult.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/TokenSASLResult.java new file mode 100644 index 0000000000..7ef55ff81c --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/TokenSASLResult.java @@ -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.protocol.amqp.sasl; + +import javax.security.auth.Subject; + +/** + * A {@link SASLResult} containing a String representation of a token (like JWT) and optional username (usually + * not relevant). + */ +public class TokenSASLResult implements SASLResult { + + private final boolean success; + private final String user; + private final String token; + + public TokenSASLResult(boolean success, String user, String token) { + this.success = success; + this.user = user; + this.token = token; + } + + @Override + public boolean isSuccess() { + return success; + } + + @Override + public String getUser() { + return user; + } + + public String getToken() { + return token; + } + + @Override + public Subject getSubject() { + return null; + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASL.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASL.java new file mode 100644 index 0000000000..d49cf415a7 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASL.java @@ -0,0 +1,89 @@ +/* + * 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 org.apache.activemq.artemis.core.security.SecurityStore; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; + +import java.util.Base64; + +public class XOAuth2SASL extends ServerSASLToken { + + // https://developers.google.com/workspace/gmail/imap/xoauth2-protocol + // marked OBSOLETE at https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml + // Wikipedia mentions XOAUTH2 only here: https://en.wikipedia.org/wiki/SMTP_Authentication + static final String MECHANISM_NAME = "XOAUTH2"; + + public XOAuth2SASL(SecurityStore securityStore, String securityDomain, RemotingConnection remotingConnection) { + super(securityStore, securityDomain, remotingConnection); + } + + @Override + public String getName() { + return MECHANISM_NAME; + } + + @Override + public byte[] processSASL(byte[] bytes) { + // expecting `user=<username>\x01auth=Bearer <token>\x01\x01` + // according to https://developers.google.com/workspace/gmail/imap/xoauth2-protocol + if (bytes == null || bytes.length == 0) { + result = new TokenSASLResult(false, null, null); + } else { + String userPart = null; + String tokenPart = null; + int from = 0, to = 0; + while (to < bytes.length && bytes[to] != '\001') { + to++; + } + if (to - from >= 5) { + userPart = new String(bytes, from, to - from); + ++to; + from = to; + } + while (to < bytes.length && bytes[to] != '\001') { + to++; + } + if (to - from >= 12) { + tokenPart = new String(bytes, from, to - from); + } + boolean valid = userPart != null && tokenPart != null; + if (valid) { + valid = userPart.startsWith("user="); + valid &= tokenPart.startsWith("auth=Bearer "); + valid &= to == bytes.length - 2; // trailing { 0x01, 0x01 } + } + if (valid) { + String user = userPart.substring(5); + String token = tokenPart.substring(12); + boolean success = authenticate(user, token); + result = new TokenSASLResult(success, user, token); + } else { + result = new TokenSASLResult(false, null, null); + } + } + + if (result.isSuccess()) { + return null; + } + + // see https://developers.google.com/workspace/gmail/imap/xoauth2-protocol#error_response + String address = remotingConnection.getTransportLocalAddress(); + return Base64.getEncoder().encode(String.format("{\"status\":\"401\",\"schemes\":\"bearer\",\"scope\":\"%s\"}", address).getBytes()); + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2ServerSASLFactory.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2ServerSASLFactory.java new file mode 100644 index 0000000000..00dd931942 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2ServerSASLFactory.java @@ -0,0 +1,48 @@ +/* + * 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 org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPRoutingHandler; +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; + +public class XOAuth2ServerSASLFactory implements ServerSASLFactory { + + @Override + public String getMechanism() { + return XOAuth2SASL.MECHANISM_NAME; + } + + @Override + public ServerSASL create(ActiveMQServer server, ProtocolManager<AmqpInterceptor, AMQPRoutingHandler> manager, Connection connection, RemotingConnection remotingConnection) { + return new XOAuth2SASL(server.getSecurityStore(), manager.getSecurityDomain(), connection.getProtocolConnection()); + } + + @Override + public int getPrecedence() { + return 16; + } + + @Override + public boolean isDefaultPermitted() { + return false; + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory b/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory index c8f54d2636..6587ae6ef4 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory +++ b/artemis-protocols/artemis-amqp-protocol/src/main/resources/META-INF/services/org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory @@ -2,5 +2,7 @@ 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.OAuthBearerServerSASLFactory +org.apache.activemq.artemis.protocol.amqp.sasl.XOAuth2ServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512SCRAMServerSASLFactory \ No newline at end of file diff --git a/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASLTest.java b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASLTest.java new file mode 100644 index 0000000000..5299a74d7f --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/OAuthBearerSASLTest.java @@ -0,0 +1,103 @@ +/* + * 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 org.apache.activemq.artemis.api.core.JsonUtil; +import org.apache.activemq.artemis.core.security.SecurityStore; +import org.apache.activemq.artemis.json.JsonObject; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OAuthBearerSASLTest { + + @Test + public void properOAUTHBEARERSASL() throws Exception { + SecurityStore securityStore = mock(SecurityStore.class); + when(securityStore.isSecurityEnabled()).thenReturn(true); + when(securityStore.authenticate("me", "JWT-token", null, null)).thenReturn("OK"); + OAuthBearerSASL sasl = new OAuthBearerSASL(securityStore, null, null); + assertNull(sasl.processSASL(String.format("n,a=%s,\001host=localhost\001port=1234\001auth=Bearer %s\001\001", "me", "JWT-token").getBytes())); + + SASLResult result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + TokenSASLResult tokenResult = (TokenSASLResult) result; + assertTrue(tokenResult.isSuccess()); + assertEquals("me", tokenResult.getUser()); + assertEquals("JWT-token", tokenResult.getToken()); + } + + @Test + public void noInitialResponse() { + SecurityStore securityStore = mock(SecurityStore.class); + RemotingConnection remotingConnection = mock(RemotingConnection.class); + when(remotingConnection.getTransportLocalAddress()).thenReturn("tcp://localhost:5672"); + + OAuthBearerSASL sasl = new OAuthBearerSASL(securityStore, null, remotingConnection); + + byte[] challenge = sasl.processSASL(null); + assertNotNull(challenge); + SASLResult result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + TokenSASLResult tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(new byte[0]); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(String.format("user=%s\001auth=Bearer %s\001", "me", "JWT-token").getBytes()); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(String.format("user=%s\002auth=Bearer %s\001\001", "me", "JWT-token").getBytes()); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + String json = new String(Base64.getDecoder().decode(challenge)); + JsonObject jsonObject = JsonUtil.readJsonObject(json); + assertEquals("invalid_token", jsonObject.getString("status")); + } + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASLTest.java b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASLTest.java new file mode 100644 index 0000000000..e2be7acca0 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/sasl/XOAuth2SASLTest.java @@ -0,0 +1,103 @@ +/* + * 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 org.apache.activemq.artemis.api.core.JsonUtil; +import org.apache.activemq.artemis.core.security.SecurityStore; +import org.apache.activemq.artemis.json.JsonObject; +import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection; +import org.junit.jupiter.api.Test; + +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class XOAuth2SASLTest { + + @Test + public void properXOAUTH2SASL() throws Exception { + SecurityStore securityStore = mock(SecurityStore.class); + when(securityStore.isSecurityEnabled()).thenReturn(true); + when(securityStore.authenticate("me", "JWT-token", null, null)).thenReturn("OK"); + XOAuth2SASL sasl = new XOAuth2SASL(securityStore, null, null); + assertNull(sasl.processSASL(String.format("user=%s\001auth=Bearer %s\001\001", "me", "JWT-token").getBytes())); + + SASLResult result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + TokenSASLResult tokenResult = (TokenSASLResult) result; + assertTrue(tokenResult.isSuccess()); + assertEquals("me", tokenResult.getUser()); + assertEquals("JWT-token", tokenResult.getToken()); + } + + @Test + public void noInitialResponse() { + SecurityStore securityStore = mock(SecurityStore.class); + RemotingConnection remotingConnection = mock(RemotingConnection.class); + when(remotingConnection.getTransportLocalAddress()).thenReturn("tcp://localhost:5672"); + + XOAuth2SASL sasl = new XOAuth2SASL(securityStore, null, remotingConnection); + + byte[] challenge = sasl.processSASL(null); + assertNotNull(challenge); + SASLResult result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + TokenSASLResult tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(new byte[0]); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(String.format("user=%s\001auth=Bearer %s\001", "me", "JWT-token").getBytes()); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + challenge = sasl.processSASL(String.format("user=%s\002auth=Bearer %s\001\001", "me", "JWT-token").getBytes()); + assertNotNull(challenge); + result = sasl.result(); + assertInstanceOf(TokenSASLResult.class, result); + tokenResult = (TokenSASLResult) result; + assertFalse(tokenResult.isSuccess()); + assertNull(tokenResult.getUser()); + assertNull(tokenResult.getToken()); + + String json = new String(Base64.getDecoder().decode(challenge)); + JsonObject jsonObject = JsonUtil.readJsonObject(json); + assertEquals("401", jsonObject.getString("status")); + } + +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
