Skip to content

SASL 2 #3113

Open
dwd wants to merge 21 commits intoigniterealtime:mainfrom
dwd:sasl-2-clean
Open

SASL 2 #3113
dwd wants to merge 21 commits intoigniterealtime:mainfrom
dwd:sasl-2-clean

Conversation

@dwd
Copy link
Copy Markdown
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
Copy Markdown
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.

Comment thread xmppserver/src/test/java/org/jivesoftware/openfire/sasl/TestSaslMechanism.java Outdated
Comment thread xmppserver/src/test/java/org/jivesoftware/openfire/sasl/TestSaslMechanism.java Outdated
Comment thread xmppserver/src/main/java/org/jivesoftware/openfire/net/StanzaHandler.java Outdated
@dwd dwd marked this pull request as ready for review April 3, 2026 12:00
@dwd dwd requested a review from Fishbowler April 3, 2026 13:28
Copy link
Copy Markdown
Member

@guusdk guusdk left a comment

Choose a reason for hiding this comment

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

Thanks for this! Apologies for taking so long to review it.

I've extensively reviewed the PR, and have left a mix of comments: some relate to code style, others to implementation issues.

}

public static Element getSASLMechanismsElement( ClientSession session )
public static void addSASLMechanisms( @Nonnull List<Element> features, @Nonnull LocalSession session )
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This may be me, but I find this code hard to reason over, because of its mutating design. In general, a method that returns data (which the caller then adds) is usually easier to reason about, test, and reuse.

The mutating design is a awkward here, as the code has a "getter-like" concern ("what mechanisms apply?"), unlike something like a Builder, where I'd expect to see this design.

The code here performs a side effect on a passed-in container (Element / List<Element>). That works, but it makes call sites harder to reason about and is a bit inconsistent with the get... style used elsewhere in this class.

Would it be cleaner to have a pure method that returns the mechanism elements for a session (for example getSASLMechanisms(LocalSession): List<Element>), and let callers add those to their own XML structure? That would make side effects explicit at the call site, reduce duplication between the two overloads, and generally simplify things.

* @param session The current session
*
* @return The valid SASL mechanisms available for the specified session.
* addSASLMechanisms adds suitable mechanisms to either an Element (intended to be the features element)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please avoid writing javadoc (that by nature is tied to exactly one method) that describes several overloaded methods. Just give each method it's own javadoc. I spent a good 5 minutes analyzing usages of this method, trying to figure out how the list of elements came into play, before noticing that there was a different method that simply accept a List instead of a singular Element as an argument.

{
// Add (proprietary) Providers of SASL implementation to the Java security context.
Security.addProvider( new org.jivesoftware.openfire.sasl.SaslProvider() );
if (Security.getProvider( "JiveSoftware" ) == null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Guarding to avoid duplicate registration seems like the right behavior. I'm wondering if this needs to happen in different places, too.

I'm interested in finding out how you ran into this being an issue.

Is it worth pulling this change out into a separate ticket and PR? That both allows us to keep the SASL 2 PR focused, but also give this provider-registration hardening a dedicated review path.

private static final Pattern BASE64_ENCODED = Pattern.compile("^(=|([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==))$");

private static final String SASL_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-sasl";
private static final String SASL2_NAMESPACE = "urn:xmpp:sasl:2";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see why we should keep these namespace declarations private. If anything, the SASLAuthentication class should be the authoritative source for such declarations, right?

* @see <a href="https://xmpp.org/extensions/xep-0388.html">XEP-0388: Extensible SASL Profile</a>
*/
public static final SystemProperty<Boolean> ENABLE_SASL2 = SystemProperty.Builder.ofType(Boolean.class)
.setKey("xmpp.auth.sasl2")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs an entry in openfire_i18n.properties, something like:

system_property.xmpp.auth.sasl2=Enables support for SASL2 authentication (XEP-0388) and advertisement of SASL2 stream features.

if (authenticateClient(doc, true)) {
// SASL authentication was successful so open a new stream and offer
// resource binding and session establishment (to client sessions only)
saslSuccessful();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In this explicit SASL2 branch, saslSuccessful() is not compatible with SASL2. Notably, it sends a <stream:stream which is appropriate for SASL(1) only (SASL2 does not restart the stream upon success).

Also, the code comment is wrong here. It describes the SASL(1) functionality, not the SASL2 functionality, which will cause future-us to scratch behind the ears.

}
if (saslStatus == SASLAuthentication.Status.authenticated && usingSASL2) {
Element features = generateFeatures();
session.deliverRawText(features.asXML());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sending the (post SASL) features here happens after receiving "response" or "abort" from the client. Shouldn't it also happen after receiving "success"?

}
}

public static Element getSASLMechanismsElement( ClientSession session, boolean usingSASL2 )
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SASL2 mechanisms can only be advertised if the session is encrypted. I'm not sure if that's best enforced in this method, or in callers of this method, but if SASL2 is used, the corresponding mechanisms can be returned only if the session has already established TLS / encryption.

For SASL(1) RFC 6120, section 5.2 leaves some wiggle-room:

An initiating entity SHOULD use TLS to secure its stream with the receiving entity before proceeding with SASL authentication.

For SASL2, XEP-0388, section 3 is more strict:

SASL2 MUST only be used by Clients or offered by Servers after TLS negotiation

}

public static Element getSASLMechanismsElement( LocalIncomingServerSession session )
public static Element getSASLMechanismsElement( LocalIncomingServerSession session, boolean usingSASL2 )
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(same code review as the one I left for the method that processes client sessions): SASL2 mechanisms can only be advertised if the session is encrypted. I'm not sure if that's best enforced in this method, or in callers of this method, but if SASL2 is used, the corresponding mechanisms can be returned only if the session has already established TLS / encryption.

For SASL(1) RFC 6120, section 5.2 leaves some wiggle-room:

An initiating entity SHOULD use TLS to secure its stream with the receiving entity before proceeding with SASL authentication.

For SASL2, XEP-0388, section 3 is more strict:

SASL2 MUST only be used by Clients or offered by Servers after TLS negotiation

decoded = new byte[0];
}
else if (encoded.equals("="))
else if ( decoded.length == 0 )
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think something is wrong here. In the decodeData method, the emptyNull argument is used, in part, to determine if the resulting value (decoded) is null or an empty byte array. In the lines just above this, a null value is unconditionally replaced by an empty byte array, which happens before the return value of decodeData was otherwise evaluated. That suggests that either the implementation in decodeData that conditionally returns null is wrong (null values are immediately replaced anyways) or that this code is not correct.

Another reason for me to believe that something is wrong: for the SASL2 flow, the data argument to decodeData() will be null if there's no initial response (and emptyNull will be false), because of this in line 490-493:

if (elementType == ElementType.AUTHENTICATE)
{
    data = doc.element("initial-response");
}

This will cause decodeData() to return an empty byte array (line 375):

 if (doc == null) return new byte[0]; if (doc == null) return new byte[0];

By the time that we get to the code where I'm adding this review, that will cause the SASL_LAST_RESPONSE_WAS_PROVIDED_BUT_EMPTY to be set on the session - while in fact the SASL response did not provide an initial response.

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.

3 participants