[ 
https://issues.apache.org/jira/browse/ARTEMIS-5316?focusedWorklogId=973896&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-973896
 ]

ASF GitHub Bot logged work on ARTEMIS-5316:
-------------------------------------------

                Author: ASF GitHub Bot
            Created on: 08/Jul/25 22:54
            Start Date: 08/Jul/25 22:54
    Worklog Time Spent: 10m 
      Work Description: tabish121 commented on code in PR #5822:
URL: https://github.com/apache/activemq-artemis/pull/5822#discussion_r2193561268


##########
artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java:
##########
@@ -1204,42 +1208,95 @@ public static boolean isApplicable(final 
NettyConnection connection) {
       }
    }
 
+   private static class XOAuth2SASLMechanism implements ClientSASL {
+
+      private final String userName;
+      private final String token;
+
+      public XOAuth2SASLMechanism(String userName, String token) {
+         this.userName = userName;
+         this.token = token;
+      }
+
+      @Override
+      public String getName() {
+         return XOAUTH2;
+      }
+
+      @Override
+      public byte[] getInitialResponse() {
+         String response = String.format("user=%s\u0001auth=Bearer 
%s\u0001\u0001", userName, token);
+         return response.getBytes(StandardCharsets.UTF_8);
+      }
+
+      @Override
+      public byte[] getResponse(byte[] challenge) {
+         return EMPTY;
+      }
+
+      public static boolean isApplicable(final String username, final String 
password) {
+         return StringUtils.isNoneEmpty(username, password);
+      }
+   }
+
    private static final class SaslFactory implements ClientSASLFactory {
 
       private final NettyConnection connection;
       private final AMQPBrokerConnectConfiguration brokerConnectConfiguration;
+      private final String[] availableSaslMechanisms;
 
-      SaslFactory(NettyConnection connection, AMQPBrokerConnectConfiguration 
brokerConnectConfiguration) {
+      SaslFactory(NettyConnection connection, AMQPBrokerConnectConfiguration 
brokerConnectConfiguration, String[] availableSaslMechanisms) {
          this.connection = connection;
          this.brokerConnectConfiguration = brokerConnectConfiguration;
+         this.availableSaslMechanisms = availableSaslMechanisms;
       }
 
       @Override
-      public ClientSASL chooseMechanism(String[] offeredMechanims) {
-         List<String> availableMechanisms = offeredMechanims == null ? 
Collections.emptyList() : Arrays.asList(offeredMechanims);
+      public ClientSASL chooseMechanism(String[] offeredMechanisms) {
+         Set<String> acceptedMechanisms = 
getAcceptedMechanisms(offeredMechanisms, availableSaslMechanisms);
 
-         if (availableMechanisms.contains(EXTERNAL) && 
ExternalSASLMechanism.isApplicable(connection)) {
+         if (acceptedMechanisms.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())) {
+               if (acceptedMechanisms.contains(scram.getName())) {
                   return new SCRAMClientSASL(scram, 
brokerConnectConfiguration.getUser(),
                                              
brokerConnectConfiguration.getPassword());
                }
             }
          }
-         if (availableMechanisms.contains(PLAIN) && 
PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
+
+         if (acceptedMechanisms.contains(PLAIN) && 
PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
             return new 
PlainSASLMechanism(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword());
          }
 
-         if (availableMechanisms.contains(ANONYMOUS)) {
+         if (acceptedMechanisms.contains(XOAUTH2) && 
XOAuth2SASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
+            return new 
XOAuth2SASLMechanism(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword());
+         }
+
+         if (acceptedMechanisms.contains(ANONYMOUS)) {
             return new AnonymousSASLMechanism();
          }
 
          return null;
       }
+
+      private Set<String> getAcceptedMechanisms(String[] 
offeredSaslMechanisms, String[] availableSaslMechanisms) {
+         Set<String> offeredSaslMechanismsSet = offeredSaslMechanisms == null 
? Set.of() : new HashSet<>(Arrays.asList(offeredSaslMechanisms));
+         Set<String> availableSaslMechanismsSet = availableSaslMechanisms == 
null ? Set.of() : new HashSet<>(Arrays.asList(availableSaslMechanisms));

Review Comment:
   This could be refactored to not allocate the enabled set of mechanisms 
unless the array is non-null and not empty as it would be most of the time.  It 
also wouldn't be necessary to create yet another Set for the result if instead 
of `Set.of()` for the offered set you used Collections.emptySet() which will 
just no-op on retainAll as it is treated as empty.



##########
artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java:
##########
@@ -1204,42 +1208,95 @@ public static boolean isApplicable(final 
NettyConnection connection) {
       }
    }
 
+   private static class XOAuth2SASLMechanism implements ClientSASL {
+
+      private final String userName;
+      private final String token;
+
+      public XOAuth2SASLMechanism(String userName, String token) {
+         this.userName = userName;
+         this.token = token;
+      }
+
+      @Override
+      public String getName() {
+         return XOAUTH2;
+      }
+
+      @Override
+      public byte[] getInitialResponse() {
+         String response = String.format("user=%s\u0001auth=Bearer 
%s\u0001\u0001", userName, token);
+         return response.getBytes(StandardCharsets.UTF_8);
+      }
+
+      @Override
+      public byte[] getResponse(byte[] challenge) {
+         return EMPTY;
+      }
+
+      public static boolean isApplicable(final String username, final String 
password) {
+         return StringUtils.isNoneEmpty(username, password);
+      }
+   }
+
    private static final class SaslFactory implements ClientSASLFactory {
 
       private final NettyConnection connection;
       private final AMQPBrokerConnectConfiguration brokerConnectConfiguration;
+      private final String[] availableSaslMechanisms;
 
-      SaslFactory(NettyConnection connection, AMQPBrokerConnectConfiguration 
brokerConnectConfiguration) {
+      SaslFactory(NettyConnection connection, AMQPBrokerConnectConfiguration 
brokerConnectConfiguration, String[] availableSaslMechanisms) {
          this.connection = connection;
          this.brokerConnectConfiguration = brokerConnectConfiguration;
+         this.availableSaslMechanisms = availableSaslMechanisms;
       }
 
       @Override
-      public ClientSASL chooseMechanism(String[] offeredMechanims) {
-         List<String> availableMechanisms = offeredMechanims == null ? 
Collections.emptyList() : Arrays.asList(offeredMechanims);
+      public ClientSASL chooseMechanism(String[] offeredMechanisms) {
+         Set<String> acceptedMechanisms = 
getAcceptedMechanisms(offeredMechanisms, availableSaslMechanisms);
 
-         if (availableMechanisms.contains(EXTERNAL) && 
ExternalSASLMechanism.isApplicable(connection)) {
+         if (acceptedMechanisms.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())) {
+               if (acceptedMechanisms.contains(scram.getName())) {
                   return new SCRAMClientSASL(scram, 
brokerConnectConfiguration.getUser(),
                                              
brokerConnectConfiguration.getPassword());
                }
             }
          }
-         if (availableMechanisms.contains(PLAIN) && 
PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
+
+         if (acceptedMechanisms.contains(PLAIN) && 
PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
             return new 
PlainSASLMechanism(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword());
          }
 
-         if (availableMechanisms.contains(ANONYMOUS)) {
+         if (acceptedMechanisms.contains(XOAUTH2) && 
XOAuth2SASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword())) {
+            return new 
XOAuth2SASLMechanism(brokerConnectConfiguration.getUser(), 
brokerConnectConfiguration.getPassword());
+         }
+
+         if (acceptedMechanisms.contains(ANONYMOUS)) {
             return new AnonymousSASLMechanism();
          }
 
          return null;
       }
+
+      private Set<String> getAcceptedMechanisms(String[] 
offeredSaslMechanisms, String[] availableSaslMechanisms) {

Review Comment:
   As Dom pointed out above using enabledMechanisms probably makes it more 
clear what's being filtered for in the offered set.



##########
tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPConnectSaslTest.java:
##########
@@ -115,6 +117,81 @@ public void testConnectsWithPlain() throws Exception {
       }
    }
 
+   @Test
+   @Timeout(20)
+   public void testConnectsWithXOauth2() throws Exception {
+      try (ProtonTestServer peer = new ProtonTestServer()) {
+         peer.expectSaslXOauth2Connect(USER, PASSWD);
+         peer.expectOpen().respond();
+         peer.expectBegin().respond();
+         peer.start();
+
+         final URI remoteURI = peer.getServerURI();
+         logger.debug("Connect test started, peer listening on: {}", 
remoteURI);
+
+         AMQPBrokerConnectConfiguration amqpConnection =
+             new AMQPBrokerConnectConfiguration(getTestName(), 
"tcp://localhost:" + remoteURI.getPort() + "?saslMechanisms=" + XOAUTH2);
+         amqpConnection.setReconnectAttempts(0);// No reconnects
+         amqpConnection.setUser(USER);
+         amqpConnection.setPassword(PASSWD);
+
+         server.getConfiguration().addAMQPConnection(amqpConnection);
+         server.start();
+
+         peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+      }
+   }
+
+   @Test
+   @Timeout(20)
+   public void testConnectsWithXOauth2WithoutUserAndPassword() throws 
Exception {

Review Comment:
   suggest name change to 
`testXOAuth2NotSelectedWhenUserAndPasswordNotProvided` to better describe the 
test





Issue Time Tracking
-------------------

    Worklog Id:     (was: 973896)
    Time Spent: 3h 20m  (was: 3h 10m)

> Support for SASL XOAUTH2 Mechanism in Broker Connection
> -------------------------------------------------------
>
>                 Key: ARTEMIS-5316
>                 URL: https://issues.apache.org/jira/browse/ARTEMIS-5316
>             Project: ActiveMQ Artemis
>          Issue Type: New Feature
>            Reporter: Tomasz Ɓukasiewicz
>            Assignee: Timothy A. Bish
>            Priority: Major
>              Labels: pull-request-available
>          Time Spent: 3h 20m
>  Remaining Estimate: 0h
>
> There is a need to support XOAUTH2 authentication between two AMQP brokers, 
> as the existing mechanisms are not sufficiently secure for certain use cases. 
> Currently, Artemis does not support this authentication method on the client 
> side, and the SaslFactory implementation is both private and final, making it 
> impossible to extend.
> To address this, an XOAuth2SASLMechanism should be implemented within the 
> AMQPBrokerConnection class and integrated into the SaslFactory. The new SASL 
> mechanism should return its name as "XOAUTH2" and include the appropriate 
> authentication headers.
> A working example of this approach has been successfully tested with the 
> Solace broker:
> {code:java}
> @Override
> public byte[] getInitialResponse() {
>    String response = String.format("user=%s\u0001auth=Bearer %s\u0001\u0001", 
> userName, token);
>    return response.getBytes(StandardCharsets.UTF_8);
> }
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact


Reply via email to