Skip to content

Conversation

@dwd
Copy link
Member

@dwd dwd commented Dec 18, 2025

This is a "clean" branch of SASL2 only (no Bind2 etc).

It includes automated testing covering the essentials.

Manual testing (should be) done with Conversations and Gajim.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements SASL 2 (Extensible SASL Profile, XEP-0388) support for Openfire, providing a modernized SASL authentication mechanism alongside the existing SASL 1 implementation.

  • Adds SASL2 namespace support with <authenticate> element handling
  • Introduces user agent information capture during SASL2 authentication
  • Includes comprehensive test coverage for both SASL1 and SASL2 flows

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java Core implementation adding SASL2 support, including new addSASLMechanisms methods, AUTHENTICATE element handling, and user agent extraction
xmppserver/src/main/java/org/jivesoftware/openfire/net/UserAgentInfo.java New class for parsing and validating user agent information from SASL2 authentication requests
xmppserver/src/main/java/org/jivesoftware/openfire/net/StanzaHandler.java Updates to handle SASL2 <authenticate> elements and manage SASL2 state flags
xmppserver/src/main/java/org/jivesoftware/openfire/net/SocketReadingMode.java Socket connection handler updates for SASL2 support
xmppserver/src/main/java/org/jivesoftware/openfire/session/LocalClientSession.java Migrates from deprecated getSASLMechanisms to new addSASLMechanisms method
xmppserver/src/main/java/org/jivesoftware/openfire/session/LocalIncomingServerSession.java Migrates from deprecated getSASLMechanisms to new addSASLMechanisms method
xmppserver/src/main/java/org/jivesoftware/openfire/websocket/WebSocketClientStanzaHandler.java Updates WebSocket handler to use new SASL mechanisms API
xmppserver/src/main/java/org/jivesoftware/openfire/http/HttpSession.java Updates HTTP/BOSH handler to use new SASL mechanisms API
xmppserver/src/main/java/org/jivesoftware/openfire/SessionPacketRouter.java Adds usingSASL2 parameter to handle method call
xmppserver/src/test/java/org/jivesoftware/openfire/sasl/TestSaslMechanism.java New test helper providing a controllable SASL mechanism for testing
xmppserver/src/test/java/org/jivesoftware/openfire/sasl/SASLAuthenticationTest.java Comprehensive test suite covering SASL1, SASL2, authentication flows, user agent capture, and edge cases
xmppserver/src/test/java/org/jivesoftware/openfire/net/UserAgentInfoTest.java Test suite for user agent information extraction and validation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* A test SASL Server factory that creates our test mechanism
*/
public static class TestSaslServerFactory implements SaslServerFactory {
private static TestSaslServer saslServer;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The static field saslServer in TestSaslServerFactory is accessed without synchronization. If tests run concurrently, this could lead to race conditions where one test's saslServer overwrites another's. Consider using ThreadLocal or synchronizing access to this field.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +74
//
// @AfterAll
// public static void tearDownClass() {
// CacheFactory.shutdown();
// }
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out @afterall method should either be uncommented if needed or removed to maintain cleaner code. Leaving it commented suggests uncertainty about whether cleanup is needed.

Suggested change
//
// @AfterAll
// public static void tearDownClass() {
// CacheFactory.shutdown();
// }

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +143
long start = System.currentTimeMillis();
System.out.println("Starting setUp");

// Setup XMPPServer mock
XMPPServer.setInstance(xmppServer);
when(xmppServer.getServerInfo()).thenReturn(serverInfo);
when(xmppServer.createJID(anyString(), anyString())).thenReturn(new JID("foo@bar"));
when(xmppServer.createJID(anyString(), isNull())).thenReturn(new JID("foo@bar"));
when(xmppServer.createJID(anyString(), anyString(), anyBoolean())).thenReturn(new JID("foo@bar"));

// Setup ServerInfo mock
when(serverInfo.getXMPPDomain()).thenReturn("example.com");
when(serverInfo.getHostname()).thenReturn("openfire.example.com");

features = DocumentHelper.createElement("features");

// Create our test SASL server
testSaslServer = TestSaslMechanism.registerTestMechanism(clientSession);

// Enable our test mechanism
SASLAuthentication.addSupportedMechanism("TEST-MECHANISM");


sessionDataMap = new HashMap<>();
// Mock both get and set to use the real map
when(clientSession.getSessionData(anyString())).thenAnswer(inv ->
sessionDataMap.get(inv.getArgument(0)));

doAnswer(inv -> {
sessionDataMap.put(inv.getArgument(0), inv.getArgument(1));
return null;
}).when(clientSession).setSessionData(anyString(), any());


// Instead of setting property, directly set the provider through reflection
try {
Field providerField = LockOutManager.class.getDeclaredField("provider");
providerField.setAccessible(true);

// Create anonymous implementation
LockOutProvider mockProvider = new LockOutProvider() {
@Override
public LockOutFlag getDisabledStatus(String username) {
return null;
}
@Override
public void setDisabledStatus(LockOutFlag flag) {}
@Override
public void unsetDisabledStatus(String username) {}
@Override
public boolean isReadOnly() { return false; }
@Override
public boolean isDelayedStartSupported() { return false; }
@Override
public boolean isTimeoutSupported() { return false; }
@Override
public boolean shouldNotBeCached() { return true; }
};

providerField.set(null, mockProvider);
} catch (Exception e) {
fail("Could not set mock provider: " + e.getMessage());
}

long end = System.currentTimeMillis();
System.out.println("Finished setUp in " + (end-start) + "ms");
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug print statements (lines 78-79, 142-143, 213-234) should be removed or replaced with proper logging. These appear to be leftover debugging code that doesn't add value to the tests.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +142
if (Security.getProvider("Test Provider") == null) {
TestSaslServer testSaslServer = new TestSaslServer(clientSession);

// Set the server instance before registering the provider
TestSaslServerFactory.setSaslServer(testSaslServer);

// Register the provider if not already registered
Security.addProvider(new Provider("Test Provider", "1.0", "Test Provider") {{
put("SaslServerFactory.TEST-MECHANISM", TestSaslServerFactory.class.getName());
}});
}

return TestSaslServerFactory.saslServer;
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method registerTestMechanism always returns the static TestSaslServerFactory.saslServer even when a provider already exists. If called multiple times with different sessions, the first session's TestSaslServer will be returned, not the newly created one. This can cause tests to fail or behave unexpectedly because the returned server instance won't match the clientSession parameter passed in.

Suggested change
if (Security.getProvider("Test Provider") == null) {
TestSaslServer testSaslServer = new TestSaslServer(clientSession);
// Set the server instance before registering the provider
TestSaslServerFactory.setSaslServer(testSaslServer);
// Register the provider if not already registered
Security.addProvider(new Provider("Test Provider", "1.0", "Test Provider") {{
put("SaslServerFactory.TEST-MECHANISM", TestSaslServerFactory.class.getName());
}});
}
return TestSaslServerFactory.saslServer;
// Always create a new TestSaslServer for the provided client session
TestSaslServer testSaslServer = new TestSaslServer(clientSession);
// Ensure the factory returns the server instance associated with this session
TestSaslServerFactory.setSaslServer(testSaslServer);
// Register the provider if not already registered
if (Security.getProvider("Test Provider") == null) {
Security.addProvider(new Provider("Test Provider", "1.0", "Test Provider") {{
put("SaslServerFactory.TEST-MECHANISM", TestSaslServerFactory.class.getName());
}});
}
// Return the server instance tied to the given client session
return testSaslServer;

Copilot uses AI. Check for mistakes.

if (elementType == ElementType.AUTHENTICATE) {
if (!usingSASL2) {
throw new IllegalStateException("Unexpected data received while negotiating SASL2 authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI());
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message for ElementType.AUTHENTICATE incorrectly states "negotiating SASL2 authentication" when it should say "negotiating SASL authentication" (not SASL2), since this branch is triggered when receiving an AUTHENTICATE element while NOT using SASL2.

Suggested change
throw new IllegalStateException("Unexpected data received while negotiating SASL2 authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI());
throw new IllegalStateException("Unexpected data received while negotiating SASL authentication. Name of the offending root element: " + doc.getName() + " Namespace: " + doc.getNamespaceURI());

Copilot uses AI. Check for mistakes.
@BeforeAll
public static void setUpClass() throws Exception {
CacheFactory.initialize();
// Set this or I can't set anythign else.
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error in comment: "anythign" should be "anything".

Suggested change
// Set this or I can't set anythign else.
// Set this or I can't set anything else.

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +234
long t0 = System.currentTimeMillis();
System.out.println("Test starting: " + System.currentTimeMillis());

Set<String> implemented = SASLAuthentication.getImplementedMechanisms();
System.out.println("After getImplementedMechanisms: " + (System.currentTimeMillis() - t0));
Set<String> mechanisms = SASLAuthentication.getSupportedMechanisms();
System.out.println("After getSupportedMechanisms: " + (System.currentTimeMillis() - t0));
assertNotNull(mechanisms);
System.out.println("After assertNotNull: " + (System.currentTimeMillis() - t0));

// Add multiple mechanisms and verify they're all present
SASLAuthentication.addSupportedMechanism("PLAIN");
System.out.println("After add PLAIN: " + (System.currentTimeMillis() - t0));
SASLAuthentication.addSupportedMechanism("DIGEST-MD5");
System.out.println("After add DIGEST-MD5: " + (System.currentTimeMillis() - t0));

mechanisms = SASLAuthentication.getSupportedMechanisms();
System.out.println("After second getSupportedMechanisms: " + (System.currentTimeMillis() - t0));

assertTrue(mechanisms.contains("PLAIN"));
assertTrue(mechanisms.contains("DIGEST-MD5"));
System.out.println("Test ending: " + (System.currentTimeMillis() - t0));
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug print statements (lines 213-234) should be removed or replaced with proper logging. These appear to be leftover debugging code that doesn't add value to the test.

Suggested change
long t0 = System.currentTimeMillis();
System.out.println("Test starting: " + System.currentTimeMillis());
Set<String> implemented = SASLAuthentication.getImplementedMechanisms();
System.out.println("After getImplementedMechanisms: " + (System.currentTimeMillis() - t0));
Set<String> mechanisms = SASLAuthentication.getSupportedMechanisms();
System.out.println("After getSupportedMechanisms: " + (System.currentTimeMillis() - t0));
assertNotNull(mechanisms);
System.out.println("After assertNotNull: " + (System.currentTimeMillis() - t0));
// Add multiple mechanisms and verify they're all present
SASLAuthentication.addSupportedMechanism("PLAIN");
System.out.println("After add PLAIN: " + (System.currentTimeMillis() - t0));
SASLAuthentication.addSupportedMechanism("DIGEST-MD5");
System.out.println("After add DIGEST-MD5: " + (System.currentTimeMillis() - t0));
mechanisms = SASLAuthentication.getSupportedMechanisms();
System.out.println("After second getSupportedMechanisms: " + (System.currentTimeMillis() - t0));
assertTrue(mechanisms.contains("PLAIN"));
assertTrue(mechanisms.contains("DIGEST-MD5"));
System.out.println("Test ending: " + (System.currentTimeMillis() - t0));
SASLAuthentication.getImplementedMechanisms();
Set<String> mechanisms = SASLAuthentication.getSupportedMechanisms();
assertNotNull(mechanisms);
// Add multiple mechanisms and verify they're all present
SASLAuthentication.addSupportedMechanism("PLAIN");
SASLAuthentication.addSupportedMechanism("DIGEST-MD5");
mechanisms = SASLAuthentication.getSupportedMechanisms();
assertTrue(mechanisms.contains("PLAIN"));
assertTrue(mechanisms.contains("DIGEST-MD5"));

Copilot uses AI. Check for mistakes.
startedSASL = true;
usingSASL2 = true;
saslStatus = SASLAuthentication.handle(session, doc, usingSASL2);
} else if (startedSASL && "response".equals(tag) || "abort".equals(tag)) {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 212 has incorrect operator precedence. It will evaluate as (startedSASL && "response".equals(tag)) || "abort".equals(tag), meaning "abort" will be processed even when startedSASL is false. This should be startedSASL && ("response".equals(tag) || "abort".equals(tag)).

Suggested change
} else if (startedSASL && "response".equals(tag) || "abort".equals(tag)) {
} else if (startedSASL && ("response".equals(tag) || "abort".equals(tag))) {

Copilot uses AI. Check for mistakes.
Comment on lines +208 to +211
// User is trying to authenticate using SASL2.
startedSASL = true;
usingSASL2 = true;
saslStatus = SASLAuthentication.handle(session, doc, usingSASL2);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: the "authenticate" block uses extra indentation (lines 208-211) compared to the surrounding code blocks. This should use the same indentation level as the "auth" block above it.

Suggested change
// User is trying to authenticate using SASL2.
startedSASL = true;
usingSASL2 = true;
saslStatus = SASLAuthentication.handle(session, doc, usingSASL2);
// User is trying to authenticate using SASL2.
startedSASL = true;
usingSASL2 = true;
saslStatus = SASLAuthentication.handle(session, doc, usingSASL2);

Copilot uses AI. Check for mistakes.
}
if (saslStatus == SASLAuthentication.Status.authenticated && usingSASL2) {
Element features = generateFeatures();
session.deliverRawText(features.asXML());
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable session may be null at this access as suggested by this null guard.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant