Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d1023f4
First crack at SASL2 (XEP-0388)
May 19, 2017
b63bbdf
Bit more SASL2 WIP
dwd Jun 2, 2025
8ba37af
SASL2 WIP - compiling/passing existing tests
dwd Jun 2, 2025
a7d75ab
Add basic tests
dwd Jun 19, 2025
58144a0
Features tests
dwd Jun 19, 2025
bcb12be
Auth tests (failing)
dwd Jun 19, 2025
492503b
Auth tests (passing)
dwd Jun 19, 2025
edceb2d
Add tests for additional data, initial responses, and multistep
dwd Jun 19, 2025
fb88fec
Add back null vs empty processing
dwd Jun 26, 2025
135a6b7
Typoish in comment
dwd Jun 26, 2025
15b5279
Disable SASL2 by default, support it on S2S
dwd Jun 26, 2025
17e99a9
Base UserAgentInfo class
dwd Jul 2, 2025
51b6e79
UserAgentInfo tests
dwd Jul 2, 2025
857f9a5
Wire in UserAgentInfo extraction and storage
dwd Jul 2, 2025
9eebbe3
Tests for wiring in UserAgentInfo extraction and storage
dwd Jul 2, 2025
906a979
Move the tests
dwd Jul 2, 2025
891ec87
Linty winty
dwd Jul 2, 2025
cb8ba05
Fix feature name
dwd Jul 3, 2025
d16c803
SASL2 working, Bind2 ignored.
dwd Oct 10, 2025
e40f725
Address copilot review points
dwd Oct 10, 2025
97a14f9
Rework addSASLMechanisms to getSASLMechanisms and add more javadoc
dwd Jun 12, 2026
ada6969
Guus comments
dwd Jun 12, 2026
247d93f
Coderabbit comments
dwd Jun 12, 2026
fe34e73
Post-rebase deconficting
dwd Jun 12, 2026
383a716
Conflict: Don't add mechanisms twice!
dwd Jun 12, 2026
b044354
Potential fix for pull request finding
dwd Jun 26, 2026
620bc04
Copilot Things
dwd Jun 26, 2026
b4a969b
Add DOAP support for XEP-0388
dwd Jun 26, 2026
b57da8f
Refactor features a bit, ensure non-PLUS are added
dwd Jun 27, 2026
03bfa8d
Move channel binding filter into getAvailableMechanismsForClientSession
dwd Jun 30, 2026
d173088
Introduce requiresChannelBinding() helper to replace endsWith("-PLUS"…
dwd Jun 30, 2026
5b46cb7
Fix SASLAuthenticationTest: mock Connection for channel binding
dwd Jul 3, 2026
b6d4ddb
Add SASL2 Conversations test
Fishbowler Jun 28, 2026
f639cda
Add openfire logs as artifacts for debugging
Fishbowler Jun 28, 2026
df8d8bc
Enable SASL2 in the test-specific Openfire config
Fishbowler Jun 28, 2026
ed098be
Expand SASL2 test to include Channel Binding
Fishbowler Jun 28, 2026
5a3407c
Initial bind2 request
dwd Jul 3, 2025
d1ee007
Wire in Bind2 (no tests)
dwd Jul 7, 2025
9a0248d
Hide client id, refactor
dwd Jul 8, 2025
f7c6293
Add tests. Make tests pass.
dwd Jul 8, 2025
b07a85d
Add SASL2/Bind2 test
dwd Jul 8, 2025
920b135
Fix SASL2/Bind2 test
dwd Jul 8, 2025
9c01433
Advertise Bind2
dwd Jul 8, 2025
eca1e77
Add bound element
dwd Jul 18, 2025
0dd7fa8
Bind2 subfeature support
dwd Aug 15, 2025
26e492d
Bind2 handler tests
dwd Sep 19, 2025
25ecb36
Make SCRAM generic
dwd Oct 10, 2025
a5bf142
SASL2 working, Bind2 ignored.
dwd Oct 10, 2025
2930ca4
Bind2 working, including carbons
dwd Oct 11, 2025
a97635b
CSI Bind2 support
dwd Oct 13, 2025
fa49f18
Fix tests
dwd Jun 12, 2026
34deb85
Fix carbons bind element name, add more tests
dwd Jun 12, 2026
05e366d
Fix processFeatureRequests call and update Bind2 test for async Sessi…
dwd Jul 2, 2026
3d8b5f6
Integrate XEP-0198 Stream Management with SASL2 (XEP-0388) and Bind2 …
dwd Jul 3, 2026
96766cd
Defer SM stanza redelivery until after stream features on SASL2 resume
dwd Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/conversationstest-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ runs:
sudo udevadm trigger --name-match=kvm

- name: Cache Conversations APK
uses: actions/cache@v5
uses: actions/cache@v6
with:
path: conversations.apk
key: conversations-apk-4217303 # Update this number at the same time as the version in run-tests.sh
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/continuous-integration-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,9 @@ jobs:
- name: demoboot
maestro-tags: demoboot
config-file: '' # Use default demoboot config
#- name: sasl2
# maestro-tags: sasl2
# config-file: build/ci/conversations/configs/sasl2.xml
- name: sasl2
maestro-tags: sasl2
config-file: build/ci/conversations/configs/sasl2.xml

steps:
- name: Checkout local actions and test flows # Do this _before_ untarring the distribution, as the checkout will empty the directory prior to the checkout!
Expand All @@ -367,12 +367,19 @@ jobs:
with:
logLevel: debug
- name: Run Conversations tests (${{ matrix.name }})
id: runConversationsTests
uses: ./.github/actions/conversationstest-action
with:
includeTags: ${{ matrix.maestro-tags }}
- name: Stop CI server
if: ${{ always() && steps.startCIServer.conclusion == 'success' }}
uses: ./.github/actions/stopserver-action
- name: Upload Openfire logs on test failure
if: ${{ failure() && steps.runConversationsTests.conclusion == 'failure' }}
uses: actions/upload-artifact@v7
with:
name: Conversations Test (${{ matrix.name }}) Openfire logs
path: distribution/target/distribution-base/logs/openfire.log


should-do-database-tests:
Expand Down
10 changes: 6 additions & 4 deletions build/ci/conversations/configs/sasl2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@
</user3>
</users>
</autosetup>
<properties>
<!-- TODO: Add SASL2-enabling system properties here. -->
<!-- These are likely defined in the SASL2 plugin or as xmpp.auth.* core properties. -->
</properties>
<!-- Enable SASL2 (xmpp.auth.sasl2). TLS is required too, but is satisfied by the CI server's self-signed certificate. -->
<xmpp>
<auth>
<sasl2>true</sasl2>
</auth>
</xmpp>
</jive>
13 changes: 13 additions & 0 deletions build/ci/conversations/flows/sasl2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,20 @@ onFlowStart:
- assertVisible: "Let app always run in background?"
- tapOn: "Allow"

# Using SASL2
- runScript:
file: scripts/checkForLogs.js
env:
PATTERN: .*SASL 2.0 authorization identifier was jane@example.org

# Using Channel Binding
- runScript:
file: scripts/checkForLogs.js
env:
PATTERN: 'jane@example\.org.*Authenticating with SASL_2\/[A-Z0-9-]+-PLUS'

# Authentication successful
- runScript:
file: scripts/checkForLogs.js
env:
PATTERN: 'jane@example\.org.*logged in \(using SASL_2\)'
5 changes: 5 additions & 0 deletions documentation/openfire.doap
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@
<xmpp:version>1.0.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0388.html"/>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0410.html"/>
Expand Down
2 changes: 2 additions & 0 deletions i18n/src/main/resources/openfire_i18n.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,8 @@ system_property.xmpp.auth.anonymous=Set to true to allow anonymous login, otherw
system_property.xmpp.auth.external.client.skip-cert-revalidation=Set to true to avoid validation of the client-provided PKIX certificate (for mutual authentication) other than the validation that happens when the TLS session is established.
system_property.xmpp.auth.external.server.require-authzid=Require the peer to provide an authorization identity through SASL (typically in the Initial Response) when authenticating an inbound S2S connection that uses the EXTERNAL SASL mechanism. This is not required by the XMPP protocol specification, but it was required by Openfire versions prior to release 4.8.0. This configuration option is added to allow for backwards compatibility.
system_property.xmpp.auth.external.server.skip-sending-authzid=Send an authorization identity in the Initial Response when attempting to authenticate using the SASL EXTERNAL mechanism with a remote XMPP domain. Sending the authzid in this manner is not required by the XMPP protocol specification, but is recommended in XEP-0178 for compatibility with older server implementations.
system_property.xmpp.auth.sasl2=Enables support for SASL authentication (XEP-0388)
system_property.xmpp.auth.sasl2.require-tls=Require TLS in order to authenticate with SASL2
system_property.xmpp.auth.ssl.default-trustmanager-impl=The class to use as the default TLS TrustManager (which checks certificates from peers).
system_property.xmpp.client.csi.enabled=Controls if Client State Indication (XEP-0352) functionality is supported by Openfire.
system_property.xmpp.client.csi.delay.enabled=Determines if 'unimportant' stanzas are delayed for a client that is inactive.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
import org.jivesoftware.openfire.session.*;
import org.jivesoftware.openfire.spi.BasicStreamIDFactory;
import org.jivesoftware.openfire.spi.ConnectionType;
import org.jivesoftware.openfire.handler.Bind2StreamManagementHandler;
import org.jivesoftware.openfire.net.Bind2Request;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.openfire.streammanagement.TerminationDelegate;
import org.jivesoftware.util.*;
import org.jivesoftware.util.cache.*;
Expand Down Expand Up @@ -1901,6 +1904,11 @@ public void start() throws IllegalStateException {
super.start();
localSessionManager.start();

// Register the XEP-0198 Stream Management handler for SASL2 Bind2 inline feature processing.
if (StreamManager.isStreamManagementActive()) {
Bind2Request.registerElementHandler(new Bind2StreamManagementHandler());
}

// Run through the server sessions every 10% of the time of the maximum time that a session is allowed to be
// detached, or every 3 minutes if the max time is outside the default boundaries.
// TODO Reschedule task if getSessionDetachTime value changes.
Expand All @@ -1917,6 +1925,7 @@ public void start() throws IllegalStateException {
@Override
public void stop() {
Log.debug("SessionManager: Stopping server");
Bind2Request.unregisterElementHandler(StreamManager.NAMESPACE_V3);
// Stop threads that are sending packets to remote servers
OutgoingSessionPromise.getInstance().shutdown();
if (JiveGlobals.getBooleanProperty("shutdownMessage.enabled")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public void route(Element wrappedElement)
throws UnknownStanzaException {
String tag = wrappedElement.getName();
if ("auth".equals(tag) || "response".equals(tag)) {
SASLAuthentication.handle(session, wrappedElement);
SASLAuthentication.handle(session, wrappedElement, false);
}
else if ("iq".equals(tag)) {
route(getIQ(wrappedElement));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.jivesoftware.openfire.container.AdminConsolePlugin;
import org.jivesoftware.openfire.container.Module;
import org.jivesoftware.openfire.container.PluginManager;
import org.jivesoftware.openfire.csi.CsiModule;
import org.jivesoftware.openfire.disco.*;
import org.jivesoftware.openfire.entitycaps.EntityCapabilitiesManager;
import org.jivesoftware.openfire.filetransfer.DefaultFileTransferManager;
Expand Down Expand Up @@ -769,6 +770,7 @@ private void loadModules() {
loadModule(OfflineMessageStore.class.getName());
loadModule(VCardManager.class.getName());
// Load standard modules
loadModule(CsiModule.class.getName());
loadModule(IQBindHandler.class.getName());
loadModule(IQSessionEstablishmentHandler.class.getName());
loadModule(IQPingHandler.class.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public class ScramUtils {

private ScramUtils() {}

public static byte[] createSaltedPassword(byte[] salt, String password, int iters) throws SaslException {
Mac mac = createSha1Hmac(password.getBytes(StandardCharsets.UTF_8));
public static byte[] createSaltedPassword(byte[] salt, String password, int iters, String algorithm) throws SaslException {
Mac mac = createHmac(password.getBytes(StandardCharsets.UTF_8), algorithm);
mac.update(salt);
mac.update(new byte[]{0, 0, 0, 1});
byte[] result = mac.doFinal();
Expand All @@ -54,22 +54,40 @@ public static byte[] createSaltedPassword(byte[] salt, String password, int iter
return result;
}

public static byte[] computeHmac(final byte[] key, final String string)
public static byte[] computeHmac(final byte[] key, final String string, String algorithm)
throws SaslException {
Mac mac = createSha1Hmac(key);
Mac mac = createHmac(key, algorithm);
mac.update(string.getBytes(StandardCharsets.UTF_8));
return mac.doFinal();
}

public static Mac createSha1Hmac(final byte[] keyBytes)
public static Mac createHmac(final byte[] keyBytes, String algorithm)
throws SaslException {
try {
SecretKeySpec key = new SecretKeySpec(keyBytes, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
String hmacAlgorithm = getHmacAlgorithm(algorithm);
SecretKeySpec key = new SecretKeySpec(keyBytes, hmacAlgorithm);
Mac mac = Mac.getInstance(hmacAlgorithm);
mac.init(key);
return mac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new SaslException(e.getMessage(), e);
}
}

private static String getHmacAlgorithm(String hashAlgorithm) {
return "Hmac" + hashAlgorithm.toUpperCase().replace("-", "");
}

// Keep backward compatibility methods for existing SHA-1 usage
public static byte[] createSaltedPassword(byte[] salt, String password, int iters) throws SaslException {
return createSaltedPassword(salt, password, iters, "SHA-1");
}

public static byte[] computeHmac(final byte[] key, final String string) throws SaslException {
return computeHmac(key, string, "SHA-1");
}

public static Mac createSha1Hmac(final byte[] keyBytes) throws SaslException {
return createHmac(keyBytes, "SHA-1");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.jivesoftware.openfire.csi;

import org.dom4j.Element;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.net.Bind2InlineHandler;
import org.jivesoftware.openfire.net.Bind2Request;
import org.jivesoftware.openfire.session.LocalClientSession;

public class CsiModule extends BasicModule {
static class Bind2CSIHandler implements Bind2InlineHandler {

@Override
public String getNamespace() {
return CsiManager.NAMESPACE;
}

@Override
public boolean handleElement(LocalClientSession session, Element bound, Element element) {
if (element.getName().equals("active")) {
session.getCsiManager().activate();
}
return true;
}
}
private static final Bind2CSIHandler handler = new Bind2CSIHandler();
/**
* <p>Create a basic module with the given name.</p>
*
* @param moduleName The name for the module or null to use the default
*/
public CsiModule(String moduleName) {
super(moduleName);
}

@Override
public void start() throws IllegalStateException {
super.start();
Bind2Request.registerElementHandler(handler);
}

@Override
public void stop() {
Bind2Request.unregisterElementHandler(handler.getNamespace());
super.stop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.jivesoftware.openfire.handler;

import org.dom4j.Element;
import org.jivesoftware.openfire.net.Bind2InlineHandler;
import org.jivesoftware.openfire.session.LocalClientSession;

public class Bind2CarbonsHandler implements Bind2InlineHandler {
@Override
public String getNamespace() {
return "urn:xmpp:carbons:2";
}

@Override
public boolean handleElement(LocalClientSession session, Element bound, Element element) {
session.setMessageCarbonsEnabled(element.getName().equals("enable"));
return true;
}
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate element name instead of treating anything non-"enable" as "disable".

Unlike IQMessageCarbonsHandler.handleIQ, which rejects unrecognized element names with bad_request, this inline handler silently maps any element other than <enable/> to "disable" and always reports success. Malformed/unexpected client input goes unnoticed.

🛡️ Proposed fix
     public boolean handleElement(LocalClientSession session, Element bound, Element element) {
-        session.setMessageCarbonsEnabled(element.getName().equals("enable"));
-        return true;
+        final String name = element.getName();
+        if (!"enable".equals(name) && !"disable".equals(name)) {
+            return false;
+        }
+        session.setMessageCarbonsEnabled("enable".equals(name));
+        return true;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public boolean handleElement(LocalClientSession session, Element bound, Element element) {
session.setMessageCarbonsEnabled(element.getName().equals("enable"));
return true;
}
public boolean handleElement(LocalClientSession session, Element bound, Element element) {
final String name = element.getName();
if (!"enable".equals(name) && !"disable".equals(name)) {
return false;
}
session.setMessageCarbonsEnabled("enable".equals(name));
return true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@xmppserver/src/main/java/org/jivesoftware/openfire/handler/Bind2CarbonsHandler.java`
around lines 14 - 17, The Bind2CarbonsHandler.handleElement logic currently
treats any element name other than "enable" as a disable request and always
succeeds, which lets malformed input slip through. Update handleElement to
explicitly validate the incoming Element name against the allowed carbons tags
(using the same rule set as IQMessageCarbonsHandler.handleIQ), only toggling
session.setMessageCarbonsEnabled for recognized values and rejecting unknown
names with a bad_request-style failure instead of returning success.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2024-2026 Ignite Realtime Foundation. All rights reserved.
*
* Licensed 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.jivesoftware.openfire.handler;

import org.dom4j.Element;
import org.jivesoftware.openfire.net.Bind2InlineHandler;
import org.jivesoftware.openfire.session.LocalClientSession;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A {@link Bind2InlineHandler} that processes XEP-0198 Stream Management {@code <enable/>} elements
* sent inline within a SASL2 Bind2 request (XEP-0388 / XEP-0386).
*
* <p>When a client includes an {@code <enable/>} element in the {@code urn:xmpp:sm:3} namespace
* inside its Bind2 {@code <bind/>} element, this handler delegates to the session's
* {@link StreamManager} to enable stream management (and optionally resumption) immediately
* after resource binding, without requiring a separate round-trip.</p>
*
* <p>The {@code <enabled/>} response from the server is added as a child of the {@code <bound/>}
* element in the SASL2 {@code <success/>} stanza.</p>
*
* @see <a href="https://xmpp.org/extensions/xep-0198.html">XEP-0198: Stream Management</a>
* @see <a href="https://xmpp.org/extensions/xep-0388.html">XEP-0388: Extensible SASL Profile</a>
*/
public class Bind2StreamManagementHandler implements Bind2InlineHandler {

private static final Logger Log = LoggerFactory.getLogger(Bind2StreamManagementHandler.class);

@Override
public String getNamespace() {
return StreamManager.NAMESPACE_V3;
}

/**
* Handles an {@code <enable/>} element from a Bind2 inline feature request by enabling
* XEP-0198 stream management on the session. The {@code <enabled/>} response element
* produced by the stream manager is added as a child of the provided {@code bound} element.
*
* <p>Only {@code <enable/>} elements are processed; any other element name is ignored.</p>
*
* @param session the client session on which stream management should be enabled
* @param bound the {@code <bound/>} element to which the {@code <enabled/>} response is added
* @param element the inline element from the Bind2 request (expected to be {@code <enable/>})
* @return {@code true} if the element was an {@code <enable/>} and was processed;
* {@code false} if the element was not an {@code <enable/>} or processing failed
*/
@Override
public boolean handleElement(LocalClientSession session, Element bound, Element element) {
if (!"enable".equals(element.getName())) {
Log.debug("Bind2StreamManagementHandler received unexpected element '{}'; ignoring.", element.getName());
return false;
}
Log.debug("Processing inline SM <enable/> for session {}", session.getAddress());
final String namespace = element.getNamespaceURI();
final String resumeAttr = element.attributeValue("resume");
final boolean resume = "true".equalsIgnoreCase(resumeAttr) || "1".equals(resumeAttr) || "yes".equalsIgnoreCase(resumeAttr);
final Element enabled = session.getStreamManager().enableAndBuildElement(namespace, resume);
if (enabled != null) {
bound.add(enabled);
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
import org.jivesoftware.openfire.net.Bind2Request;
import org.jivesoftware.openfire.session.ClientSession;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
Expand Down Expand Up @@ -78,4 +79,16 @@ public IQHandlerInfo getInfo() {
public Iterator<String> getFeatures() {
return Collections.singleton(NAMESPACE).iterator();
}

@Override
public void start() throws IllegalStateException {
super.start();
Bind2Request.registerElementHandler(new Bind2CarbonsHandler());
}

@Override
public void stop() {
super.stop();
Bind2Request.unregisterElementHandler("urn:xmpp:carbons:2");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ public List<Element> getAvailableStreamFeatures() {

// If authentication has not happened yet, include available authentication mechanisms.
if (getAuthToken() == null) {
final Element sasl = SASLAuthentication.getSASLMechanismsElement(this);
if (sasl != null) {
elements.add(sasl);
final List<Element> mechanisms = SASLAuthentication.getSASLMechanisms(this);
for (Element mechanism : mechanisms) {
elements.add(mechanism);
}
}

Expand Down
Loading
Loading