This is an automated email from the ASF dual-hosted git repository.

tabish pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-protonj2.git


The following commit(s) were added to refs/heads/main by this push:
     new 12b0f9b1 PROTON-2927 Add SASL mechanism support for OAUTHBEARER
12b0f9b1 is described below

commit 12b0f9b13b4ac60ccdd6965bd21a2a0daaf53aed
Author: Timothy Bish <[email protected]>
AuthorDate: Mon Mar 30 18:18:14 2026 -0400

    PROTON-2927 Add SASL mechanism support for OAUTHBEARER
    
    Add a SASL mechanism for OAUTHBEARER SASL exchanges to the client and the
    test peer.
---
 .../protonj2/client/impl/ClientConnection.java     |   5 +
 .../protonj2/client/impl/SaslConnectionTest.java   | 112 ++++++++++++++++
 .../qpid/protonj2/test/driver/ScriptWriter.java    | 148 +++++++++++++++++++++
 .../protonj2/test/driver/ProtonTestServerTest.java |  56 ++++++++
 .../engine/sasl/client/OauthBearerMechanism.java   | 122 +++++++++++++++++
 .../sasl/client/SaslCredentialsProvider.java       |   8 ++
 .../engine/sasl/client/SaslMechanisms.java         |  12 ++
 .../engine/impl/sasl/ProtonSaslClientTest.java     |   5 +
 .../engine/sasl/client/MechanismTestBase.java      |  33 ++++-
 .../sasl/client/OauthBearerMechanismTest.java      | 107 +++++++++++++++
 10 files changed, 603 insertions(+), 5 deletions(-)

diff --git 
a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
 
b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
index f12df017..d1520be5 100644
--- 
a/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
+++ 
b/protonj2-client/src/main/java/org/apache/qpid/protonj2/client/impl/ClientConnection.java
@@ -870,6 +870,11 @@ public final class ClientConnection implements Connection {
                 public Principal localPrincipal() {
                     return transport.getLocalPrincipal();
                 }
+
+                @Override
+                public boolean isSecure() {
+                    return transport.isSecure();
+                }
             }));
         }
 
diff --git 
a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
 
b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
index 73af0a05..bec4fedb 100644
--- 
a/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
+++ 
b/protonj2-client/src/test/java/org/apache/qpid/protonj2/client/impl/SaslConnectionTest.java
@@ -198,6 +198,118 @@ public class SaslConnectionTest extends 
ImperativeClientTestCase {
         }
     }
 
+    @Test
+    public void testSaslOauthBearerConnection() throws Exception {
+        final String username = "user";
+        final String password = "eyB1c2VyPSJ1c2VyIiB9";
+
+        final ProtonTestServerOptions serverOptions = 
serverOptions().setSecure(true)
+                                                                     
.setKeyStoreLocation(BROKER_JKS_KEYSTORE)
+                                                                     
.setKeyStorePassword(PASSWORD)
+                                                                     
.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSaslOauthBearerConnect(username, password, "localhost");
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+            clientOptions.sslOptions()
+                         .sslEnabled(true)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), 
remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslOauthBearerConnectionWithVirtualHost() throws 
Exception {
+        final String virtualHost = "example.com";
+        final String username = "user";
+        final String password = "eyB1c2VyPSJ1c2VyIiB9";
+
+        final ProtonTestServerOptions serverOptions = 
serverOptions().setSecure(true)
+                                                                     
.setKeyStoreLocation(BROKER_JKS_KEYSTORE)
+                                                                     
.setKeyStorePassword(PASSWORD)
+                                                                     
.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSaslOauthBearerConnect(username, password, virtualHost);
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            ConnectionOptions clientOptions = connectionOptions();
+            clientOptions.user(username);
+            clientOptions.password(password);
+            clientOptions.virtualHost(virtualHost);
+            clientOptions.sslOptions()
+                         .sslEnabled(true)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), 
remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    public void testSaslOauthBearerConnectionWithVirtualHostForcedOmit() 
throws Exception {
+        final String username = "user";
+        final String password = "eyB1c2VyPSJ1c2VyIiB9";
+
+        final ProtonTestServerOptions serverOptions = 
serverOptions().setSecure(true)
+                                                                     
.setKeyStoreLocation(BROKER_JKS_KEYSTORE)
+                                                                     
.setKeyStorePassword(PASSWORD)
+                                                                     
.setVerifyHost(false);
+
+        try (ProtonTestServer peer = new ProtonTestServer(serverOptions)) {
+            peer.expectSaslOauthBearerConnect(username, password, null);
+            peer.expectOpen().respond();
+            peer.expectClose().respond();
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            final ConnectionOptions clientOptions = connectionOptions();
+
+            clientOptions.user(username);
+            clientOptions.password(password);
+            clientOptions.virtualHost("");
+            clientOptions.sslOptions()
+                         .sslEnabled(true)
+                         .trustStoreLocation(CLIENT_JKS_TRUSTSTORE)
+                         .trustStorePassword(PASSWORD);
+
+            Client container = Client.create();
+            Connection connection = container.connect(remoteURI.getHost(), 
remoteURI.getPort(), clientOptions);
+
+            connection.openFuture().get(10, TimeUnit.SECONDS);
+            connection.close();
+
+            peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
+        }
+    }
+
     @Test
     public void testSaslAnonymousConnection() throws Exception {
         try (ProtonTestServer peer = new ProtonTestServer(serverOptions())) {
diff --git 
a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
 
b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
index 002c6824..52640c41 100644
--- 
a/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
+++ 
b/protonj2-test-driver/src/main/java/org/apache/qpid/protonj2/test/driver/ScriptWriter.java
@@ -465,6 +465,71 @@ public abstract class ScriptWriter {
         expectAMQPHeader().respondWithAMQPHeader();
     }
 
+    /**
+     * Creates all the scripted elements needed for a successful SASL 
OAUTHBEARER
+     * connection. This is generally used with a server type peer which will be
+     * accepting client connections.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to 
with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that 
only
+     * advertises OAUTHBEARER as the mechanism.  It is expected that the 
remote will
+     * send a SASL init with the OAUTHBEARER mechanism selected and the 
outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     * <p>
+     * The host value will be set in the response is checked if the provided 
value is
+     * non-null and is not an empty string.
+     *
+     * @param username
+     *      The user name that is expected in the SASL initial response.
+     * @param token
+     *      The token that is expected in the SASL initial response.
+     * @param host
+     *                 The optional host value to add as the host portion of 
the initial response as the 'host' entry.
+     */
+    public void expectSaslOauthBearerConnect(String username, String token, 
String host) {
+        expectSaslOauthBearerConnect(username, token, host, 0, (String[]) 
null);
+    }
+
+    /**
+     * Creates all the scripted elements needed for a successful SASL 
OAUTHBEARER
+     * connection. This is generally used with a server type peer which will be
+     * accepting client connections.
+     * <p>
+     * For this exchange the SASL header is expected which is responded to 
with the
+     * corresponding SASL header and an immediate SASL mechanisms frame that 
only
+     * advertises OAUTHBEARER as the mechanism.  It is expected that the 
remote will
+     * send a SASL init with the OAUTHBEARER mechanism selected and the 
outcome is
+     * predefined as success.  Once done the expectation is added for the AMQP
+     * header to arrive and a header response will be sent.
+     * <p>
+     * The host value will be set in the response is checked if the provided 
value is
+     * non-null and is not an empty string. The port value is checked if the 
provided
+     * value is greater than zero.
+     *
+     * @param username
+     *      The user name that is expected in the SASL initial response.
+     * @param token
+     *      The token that is expected in the SASL initial response.
+     * @param host
+     *                 The optional host value to add as the host portion of 
the initial response as the 'host' entry.
+     * @param port
+     *                 The optional port value to apply to the initial 
response as the 'port' entry.
+     * @param offeredMechanisms
+     *                 The server offered mechanisms to provide, if null then 
only OAUTHBEARER is offered.
+     */
+    public void expectSaslOauthBearerConnect(String username, String token, 
String host, int port, String... offeredMechanisms) {
+        expectSASLHeader().respondWithSASLHeader();
+        if (offeredMechanisms == null || offeredMechanisms.length == 0) {
+            remoteSaslMechanisms().withMechanisms("OAUTHBEARER").queue();
+        } else {
+            remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
+        }
+        
expectSaslInit().withMechanism("OAUTHBEARER").withInitialResponse(saslOauthBearerInitialResponse(username,
 token, host, port));
+        remoteSaslOutcome().withCode(SaslCode.OK).queue();
+        expectAMQPHeader().respondWithAMQPHeader();
+    }
+
     /**
      * Creates all the scripted elements needed for a failed SASL Plain
      * connection. This is generally used with a server type peer which will be
@@ -643,6 +708,66 @@ public abstract class ScriptWriter {
         remoteSASLHeader().now();
     }
 
+    /**
+     * Used to trigger the sequence of frames that would occur during a 
typical client
+     * connection to a remote peer with SASL OAUTHBEARER. This should be 
called after a
+     * client connects to the remote as the fired frames would fail until 
there is a
+     * connection in place.
+     *
+     * @param username
+     *      The token that is expected in the SASL OAUTHBEARER initial 
response.
+     * @param token
+     *      The password that is expected in the SASL OAUTHBEARER initial 
response.
+     */
+    public void triggerClientSaslOAuthBearerConnect(String username, String 
token) {
+        expectSASLHeader();
+        expectSaslMechanisms().withSaslServerMechanism("OAUTHBEARER");
+        remoteSaslInit().withMechanism("OAUTHBEARER")
+                        
.withInitialResponse(saslOauthBearerInitialResponse(username, token, null, 
0)).queue();
+        expectSaslOutcome().withCode(SaslCode.OK);
+        remoteAMQPHeader().queue();
+        expectAMQPHeader();
+
+        // This trigger the exchange of frames.
+        remoteSASLHeader().now();
+    }
+
+    /**
+     * Used to trigger the sequence of frames that would occur during a 
typical client
+     * connection to a remote peer with SASL OAUTHBEARER. This should be 
called after a
+     * client connects to the remote as the fired frames would fail until 
there is a
+     * connection in place.
+     *
+     * @param username
+     *      The user name that is expected in the SASL OAUTHBEARER initial 
response.
+     * @param token
+     *      The token that is expected in the SASL OAUTHBEARER initial 
response.
+     * @param host
+     *                 The value to write into the 'host' entry in the 
response (if null no host is added).
+     * @param port
+     *                 The value to write into the 'port' entry in the 
response (if is zero or negative no port is added).
+     * @param offeredMechanisms
+     *      The set of mechanisms that the server should offer in the SASL 
Mechanisms frame
+     */
+    public void triggerClientSaslOAuthBearerConnect(String username, String 
token, String host, int port, String... offeredMechanisms) {
+        expectSASLHeader();
+
+        if (offeredMechanisms == null || offeredMechanisms.length == 0) {
+            expectSaslMechanisms().withSaslServerMechanism("OAUTHBEARER");
+        } else {
+            expectSaslMechanisms().withSaslServerMechanisms(offeredMechanisms);
+        }
+
+        remoteSaslInit().withMechanism("OAUTHBEARER")
+                        
.withInitialResponse(saslOauthBearerInitialResponse(username, token, host, 
port)).queue();
+        expectSaslOutcome().withCode(SaslCode.OK);
+        remoteAMQPHeader().queue();
+        expectAMQPHeader();
+
+        // This trigger the exchange of frames.
+        remoteSASLHeader().now();
+    }
+
     //----- Utility methods for tests writing raw scripted SASL tests
 
     public byte[] saslPlainInitialResponse(String username, String password) {
@@ -671,6 +796,29 @@ public abstract class ScriptWriter {
         return initialResponse;
     }
 
+    public byte[] saslOauthBearerInitialResponse(String username, String 
token, String host, int port) {
+        final StringBuilder builder = new StringBuilder();
+        final char SEPARATOR = '\u0001';
+
+        if (username != null && !username.isBlank()) {
+            
builder.append("n,a=").append(username).append(",").append(SEPARATOR);
+        } else {
+            builder.append("n,,").append(SEPARATOR);
+        }
+
+        if (host != null && !host.isBlank()) {
+            builder.append("host=").append(host).append(SEPARATOR);
+        }
+
+        if (port > 0) {
+            builder.append("port=").append(port).append(SEPARATOR);
+        }
+
+        builder.append("auth=Bearer 
").append(token).append(SEPARATOR).append(SEPARATOR);
+
+        return builder.toString().getBytes(StandardCharsets.UTF_8);
+    }
+
     //----- Smart Scripted Response Actions
 
     /**
diff --git 
a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
 
b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
index 659db896..e649cbe9 100644
--- 
a/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
+++ 
b/protonj2-test-driver/src/test/java/org/apache/qpid/protonj2/test/driver/ProtonTestServerTest.java
@@ -122,4 +122,60 @@ public class ProtonTestServerTest extends 
TestPeerTestsBase {
             peer.waitForScriptToComplete();
         }
     }
+
+    @Test
+    public void testServerExpectsSaslOauthBearerConnect() throws Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSaslOauthBearerConnect("user", "pass", null, 0);
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            try (ProtonTestClient client = new ProtonTestClient()) {
+                client.connect(remoteURI.getHost(), remoteURI.getPort());
+                client.expectSASLHeader();
+                
client.expectSaslMechanisms().withSaslServerMechanisms("OAUTHBEARER");
+                client.remoteSaslInit().withMechanism("OAUTHBEARER")
+                                       
.withInitialResponse(peer.saslOauthBearerInitialResponse("user", "pass", null, 
0)).queue();
+                client.expectSaslOutcome().withCode(SaslCode.OK);
+                client.remoteHeader(AMQPHeader.getAMQPHeader()).queue();
+                client.expectAMQPHeader();
+
+                // Start the exchange with the client SASL header
+                client.remoteHeader(AMQPHeader.getSASLHeader()).now();
+
+                client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            }
+
+            peer.waitForScriptToComplete();
+        }
+    }
+
+    @Test
+    public void 
testServerExpectsSaslOauthBearerConnectAndOffersMoreMechanisms() throws 
Exception {
+        try (ProtonTestServer peer = new ProtonTestServer()) {
+            peer.expectSaslOauthBearerConnect("user", "pass", null, 0, 
"OAUTHBEARER", "PLAIN", "ANONYMOUS");
+            peer.start();
+
+            URI remoteURI = peer.getServerURI();
+
+            try (ProtonTestClient client = new ProtonTestClient()) {
+                client.connect(remoteURI.getHost(), remoteURI.getPort());
+                client.expectSASLHeader();
+                
client.expectSaslMechanisms().withSaslServerMechanisms("OAUTHBEARER", "PLAIN", 
"ANONYMOUS");
+                client.remoteSaslInit().withMechanism("OAUTHBEARER")
+                                       
.withInitialResponse(peer.saslOauthBearerInitialResponse("user", "pass", null, 
0)).queue();
+                client.expectSaslOutcome().withCode(SaslCode.OK);
+                client.remoteHeader(AMQPHeader.getAMQPHeader()).queue();
+                client.expectAMQPHeader();
+
+                // Start the exchange with the client SASL header
+                client.remoteHeader(AMQPHeader.getSASLHeader()).now();
+
+                client.waitForScriptToComplete(5, TimeUnit.SECONDS);
+            }
+
+            peer.waitForScriptToComplete();
+        }
+    }
 }
diff --git 
a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanism.java
 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanism.java
new file mode 100644
index 00000000..648a9159
--- /dev/null
+++ 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanism.java
@@ -0,0 +1,122 @@
+/*
+ * 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.protonj2.engine.sasl.client;
+
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.apache.qpid.protonj2.buffer.ProtonBufferAllocator;
+import org.apache.qpid.protonj2.types.Symbol;
+
+/**
+ * Implements the SASL OAUTHBEARER authentication Mechanism .
+ *
+ * User name and Password values are sent without being encrypted.
+ */
+public class OauthBearerMechanism extends AbstractMechanism {
+
+    // RFC 6750 Based pattern match, this protects against invalid tokens
+    private static final Pattern OAUTH_BEARER_TOKEN_PATTERN = 
Pattern.compile("^[a-zA-Z0-9\\-\\._~\\+/]+=*$");
+
+    private static final byte SEPARATOR = 0x01;
+    private static final byte[] USER_PREFIX = 
"n,a=".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] USER_SUFFIX = 
",".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] HOST_PREFIX = 
"host=".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] BEARER_PREFIX = "auth=Bearer 
".getBytes(StandardCharsets.US_ASCII);
+
+    /**
+     * A singleton instance of the symbolic mechanism name.
+     */
+    public static final Symbol OAUTHBEARER = Symbol.valueOf("OAUTHBEARER");
+
+    private String additionalFailureInformation;
+
+    @Override
+    public Symbol getName() {
+        return OAUTHBEARER;
+    }
+
+    @Override
+    public boolean isApplicable(SaslCredentialsProvider credentials) {
+        // Enforce that user-name and password are required and that format 
for a bearer
+        // token as defined in RFC 6750.
+        if (credentials.isSecure() &&
+            credentials.username() != null && 
!credentials.username().isEmpty() &&
+            credentials.password() != null && 
!credentials.password().isEmpty()) {
+
+            return 
OAUTH_BEARER_TOKEN_PATTERN.matcher(credentials.password()).matches();
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public ProtonBuffer getInitialResponse(SaslCredentialsProvider 
credentials) throws SaslException {
+        final byte[] username = 
credentials.username().getBytes(StandardCharsets.UTF_8);
+        final byte[] token = 
credentials.password().getBytes(StandardCharsets.UTF_8);
+        final byte[] vhost;
+
+        if (credentials.vhost() != null && !credentials.vhost().isBlank()) {
+            vhost = credentials.vhost().getBytes(StandardCharsets.US_ASCII);
+        } else {
+            vhost = null;
+        }
+
+        final int encodedSize;
+
+        if (vhost != null) {
+            encodedSize = USER_PREFIX.length + username.length + 
USER_SUFFIX.length + 1 +
+                          HOST_PREFIX.length + vhost.length + 1 +
+                          BEARER_PREFIX.length + token.length + 2;
+        } else {
+            encodedSize = USER_PREFIX.length + username.length + 
USER_SUFFIX.length + 1 +
+                          BEARER_PREFIX.length + token.length + 2;
+        }
+
+        final ProtonBuffer response = 
ProtonBufferAllocator.defaultAllocator().allocate(encodedSize);
+
+        response.writeBytes(USER_PREFIX);
+        response.writeBytes(username);
+        response.writeBytes(USER_SUFFIX);
+        response.writeByte(SEPARATOR);
+
+        if (vhost != null) {
+            response.writeBytes(HOST_PREFIX);
+            response.writeBytes(vhost);
+            response.writeByte(SEPARATOR);
+        }
+
+        response.writeBytes(BEARER_PREFIX);
+        response.writeBytes(token);
+        response.writeByte(SEPARATOR);
+        response.writeByte(SEPARATOR);
+
+        return response;
+    }
+
+    @Override
+    public ProtonBuffer getChallengeResponse(SaslCredentialsProvider 
credentials, ProtonBuffer challenge) throws SaslException {
+        if (challenge != null && challenge.getReadableBytes() > 0 && 
additionalFailureInformation == null) {
+            additionalFailureInformation = 
challenge.toString(StandardCharsets.UTF_8);
+        }
+
+        return EMPTY; // Empty response before remote sends SASL failed outcome
+    }
+}
diff --git 
a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
index c3701c08..7814719f 100644
--- 
a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
+++ 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslCredentialsProvider.java
@@ -41,6 +41,14 @@ public interface SaslCredentialsProvider {
      */
     String password();
 
+    /**
+     * Returns <code>true</code> if the transport IO layer is operating over a 
secure
+     * means such as TLS which provides encryption in flight.
+     *
+     * @return <code>true</code> if the IO layer is secured (encrypted).
+     */
+    boolean isSecure();
+
     /**
      * Returns the local principal for use in SASL authentication which is 
generally
      * provided by the IO layer (TLS). This method can return null if there is 
no
diff --git 
a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
index debc3270..2bb59ede 100644
--- 
a/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
+++ 
b/protonj2/src/main/java/org/apache/qpid/protonj2/engine/sasl/client/SaslMechanisms.java
@@ -38,6 +38,18 @@ public enum SaslMechanisms {
             return INSTANCE;
         }
     },
+    OAUTHBEARER {
+
+        @Override
+        public Symbol getName() {
+            return OauthBearerMechanism.OAUTHBEARER;
+        }
+
+        @Override
+        public Mechanism createMechanism() {
+            return new OauthBearerMechanism();
+        }
+    },
     SCRAM_SHA_512 {
 
         @Override
diff --git 
a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
index 9434d81a..a96505dd 100644
--- 
a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
+++ 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/impl/sasl/ProtonSaslClientTest.java
@@ -275,6 +275,11 @@ public class ProtonSaslClientTest extends 
ProtonEngineTestSupport {
             public Principal localPrincipal() {
                 return null;
             }
+
+            @Override
+            public boolean isSecure() {
+                return false;
+            }
         };
 
         return new SaslAuthenticator(credentials);
diff --git 
a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
index 74787eb8..bb85ee1f 100644
--- 
a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
+++ 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/MechanismTestBase.java
@@ -34,19 +34,35 @@ public class MechanismTestBase {
         
ProtonBufferAllocator.defaultAllocator().allocate(10).setWriteOffset(10);
 
     protected SaslCredentialsProvider credentials() {
-        return new UserCredentialsProvider(USERNAME, PASSWORD, HOST, true);
+        return new UserCredentialsProvider(USERNAME, PASSWORD, HOST, true, 
false);
     }
 
     protected SaslCredentialsProvider credentials(String user, String 
password) {
-        return new UserCredentialsProvider(user, password, null, false);
+        return new UserCredentialsProvider(user, password, null, false, false);
     }
 
     protected SaslCredentialsProvider credentials(String user, String 
password, boolean principal) {
-        return new UserCredentialsProvider(user, password, null, principal);
+        return new UserCredentialsProvider(user, password, null, principal, 
false);
+    }
+
+    protected SaslCredentialsProvider secureCredentials() {
+        return new UserCredentialsProvider(USERNAME, PASSWORD, HOST, true, 
false);
+    }
+
+    protected SaslCredentialsProvider secureCredentials(String user, String 
password) {
+        return new UserCredentialsProvider(user, password, null, false, true);
+    }
+
+    protected SaslCredentialsProvider secureCredentials(String user, String 
password, boolean principal) {
+        return new UserCredentialsProvider(user, password, null, principal, 
true);
     }
 
     protected SaslCredentialsProvider emptyCredentials() {
-        return new UserCredentialsProvider(null, null, null, false);
+        return new UserCredentialsProvider(null, null, null, false, false);
+    }
+
+    protected SaslCredentialsProvider emptySecureCredentials() {
+        return new UserCredentialsProvider(null, null, null, false, true);
     }
 
     private static class UserCredentialsProvider implements 
SaslCredentialsProvider {
@@ -55,12 +71,14 @@ public class MechanismTestBase {
         private final String password;
         private final String host;
         private final boolean principal;
+        private final boolean secure;
 
-        public UserCredentialsProvider(String username, String password, 
String host, boolean principal) {
+        public UserCredentialsProvider(String username, String password, 
String host, boolean principal, boolean secure) {
             this.username = username;
             this.password = password;
             this.host = host;
             this.principal = principal;
+            this.secure = secure;
         }
 
         @Override
@@ -92,5 +110,10 @@ public class MechanismTestBase {
                 return null;
             }
         }
+
+        @Override
+        public boolean isSecure() {
+            return secure;
+        }
     }
 }
diff --git 
a/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanismTest.java
 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanismTest.java
new file mode 100644
index 00000000..88979908
--- /dev/null
+++ 
b/protonj2/src/test/java/org/apache/qpid/protonj2/engine/sasl/client/OauthBearerMechanismTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.protonj2.engine.sasl.client;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.security.sasl.SaslException;
+
+import org.apache.qpid.protonj2.buffer.ProtonBuffer;
+import org.junit.jupiter.api.Test;
+
+public class OauthBearerMechanismTest extends MechanismTestBase {
+
+    @Test
+    public void testGetInitialResponseWithNullUserAndPassword() throws 
SaslException {
+        OauthBearerMechanism mech = new OauthBearerMechanism();
+
+        ProtonBuffer response = mech.getInitialResponse(secureCredentials());
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() != 0);
+    }
+
+    @Test
+    public void testGetChallengeResponse() throws SaslException {
+        OauthBearerMechanism mech = new OauthBearerMechanism();
+
+        ProtonBuffer response = mech.getChallengeResponse(secureCredentials(), 
TEST_BUFFER);
+        assertNotNull(response);
+        assertTrue(response.getReadableBytes() == 0);
+    }
+
+    @Test
+    public void testIsNotApplicableWithInsecureCredentials() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(credentials("user",
 "token", false)),
+            "Should not be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoCredentials() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials(null,
 null, false)),
+            "Should not be applicable with no credentials");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoUser() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials(null,
 "pass", false)),
+            "Should not be applicable with no username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithNoToken() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("user",
 null, false)),
+            "Should not be applicable with no token");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUser() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("",
 "pass", false)),
+            "Should not be applicable with empty username");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyToken() {
+        
assertFalse(SaslMechanisms.XOAUTH2.createMechanism().isApplicable(secureCredentials("user",
 "", false)),
+            "Should not be applicable with empty token");
+    }
+
+    @Test
+    public void testIsNotApplicableWithIllegalAccessToken() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("user",
 "illegalChar\000", false)),
+            "Should not be applicable with non vschars");
+    }
+
+    @Test
+    public void testIsNotApplicableWithEmtpyUserAndToken() {
+        
assertFalse(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("",
 "", false)),
+            "Should not be applicable with empty user and token");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndToken() {
+        
assertTrue(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("user",
 "2YotnFZFEjr1zCsicMWpAA", false)),
+            "Should be applicable with user and token");
+    }
+
+    @Test
+    public void testIsApplicableWithUserAndPasswordAndPrincipal() {
+        
assertTrue(SaslMechanisms.OAUTHBEARER.createMechanism().isApplicable(secureCredentials("user",
 "2YotnFZFEjr1zCsicMWpAA", true)),
+            "Should be applicable with user and token and principal");
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to