Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c830e18
First crack at SASL2 (XEP-0388)
May 19, 2017
83cb8a7
Bit more SASL2 WIP
dwd Jun 2, 2025
f65653b
SASL2 WIP - compiling/passing existing tests
dwd Jun 2, 2025
fa009f5
Add basic tests
dwd Jun 19, 2025
f7c22e8
Features tests
dwd Jun 19, 2025
7ae538a
Auth tests (failing)
dwd Jun 19, 2025
3c45235
Auth tests (passing)
dwd Jun 19, 2025
5de7f15
Add tests for additional data, initial responses, and multistep
dwd Jun 19, 2025
bcdec8b
Add back null vs empty processing
dwd Jun 26, 2025
ba969c8
Typoish in comment
dwd Jun 26, 2025
9dddd11
Disable SASL2 by default, support it on S2S
dwd Jun 26, 2025
ac01412
Base UserAgentInfo class
dwd Jul 2, 2025
5cd1c6e
UserAgentInfo tests
dwd Jul 2, 2025
7c5a1ba
Wire in UserAgentInfo extraction and storage
dwd Jul 2, 2025
60ec619
Tests for wiring in UserAgentInfo extraction and storage
dwd Jul 2, 2025
4c9d4e1
Move the tests
dwd Jul 2, 2025
9c98a83
Linty winty
dwd Jul 2, 2025
acee234
Fix feature name
dwd Jul 3, 2025
5e926c9
Extract resource binding.
dwd Jul 3, 2025
b865fec
Initial bind2 request
dwd Jul 3, 2025
f55c56c
Wire in Bind2 (no tests)
dwd Jul 7, 2025
24fe13e
Hide client id, refactor
dwd Jul 8, 2025
5807a40
Add tests. Make tests pass.
dwd Jul 8, 2025
f21ebaa
Add SASL2/Bind2 test
dwd Jul 8, 2025
b05d078
Fix SASL2/Bind2 test
dwd Jul 8, 2025
95826f5
Advertise Bind2
dwd Jul 8, 2025
14066c5
Add bound element
dwd Jul 18, 2025
fb1cd56
Bind2 subfeature support
dwd Aug 15, 2025
27ac170
Bind2 handler tests
dwd Sep 19, 2025
0361815
Make SCRAM generic
dwd Oct 10, 2025
95bf29e
Merge remote-tracking branch 'ignite/main' into sasl2-2025
dwd Oct 10, 2025
189d4d1
Put back changes to bind code
dwd Oct 10, 2025
91c9add
SASL2 working, Bind2 ignored.
dwd Oct 10, 2025
e1664f0
Bind2 working, including carbons
dwd Oct 11, 2025
1c05dc1
CSI Bind2 support
dwd Oct 13, 2025
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
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 @@ -35,6 +35,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 @@ -765,6 +766,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("active"));
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,63 +106,12 @@ public IQ handleIQ(IQ packet) throws UnauthorizedException {
session.process(reply);
return reply;
}
if (authToken.isAnonymous()) {
// User used ANONYMOUS SASL so initialize the session as an anonymous login
session.setAnonymousAuth();
}
else {
String username = authToken.getUsername().toLowerCase();
// If a session already exists with the requested JID, then check to see
// if we should kick it off or refuse the new connection
final JID desiredJid = new JID(username, serverName, resource, true);
ClientSession oldSession = routingTable.getClientRoute(desiredJid);
if (oldSession != null) {
try {
if (oldSession.isClosed()) {
// If there's an old session that's already closed, then this could be a detached session. The
// new session does not conflict with the old one, but the old one needs to be cleaned up to
// prevent data consistency issues (OF-3044).
Log.debug("Instructing all cluster nodes to remove any detached session for '{}' as a new session is binding to that resource.", desiredJid);
CacheFactory.doSynchronousClusterTask(new ClientSessionTask(desiredJid, RemoteSessionTask.Operation.removeDetached), true);
}
else
{
Log.debug("Found a pre-existing, non-closed session for '{}'. Performing resource conflict resolution.", desiredJid);
int conflictLimit = sessionManager.getConflictKickLimit();
if (conflictLimit == SessionManager.NEVER_KICK) {
Log.debug("Conflict resolution configuration is 'NEVER KICK'. Rejecting the bind request with error condition 'conflict'.");
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.conflict);
// Send the error directly since a route does not exist at this point.
session.process(reply);
return null;
}

int conflictCount = oldSession.incrementConflictCount();
if (conflictCount > conflictLimit) {
Log.debug("Kick out an old connection that is conflicting with a new one. Old session: {}", oldSession);
oldSession.close(new StreamError(StreamError.Condition.conflict));

// OF-1923: As the session is now replaced, the old session will never be resumed.
if (oldSession instanceof LocalClientSession) {
sessionManager.removeDetached((LocalClientSession) oldSession);
}
} else {
Log.debug("Conflict resolution configuration does not allow kicking of old session (yet). Conflict count: {}, conflict limit: {}. Rejecting the bind request with error condition 'conflict'.", conflictCount, conflictLimit);
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(PacketError.Condition.conflict);
// Send the error directly since a route does not exist at this point.
session.process(reply);
return null;
}
}
}
catch (Exception e) {
Log.error("Error during login", e);
}
}
// If the connection was not refused due to conflict, log the user in
session.setAuthToken(authToken, resource);
PacketError.Condition error = session.bindResource(resource);
if (error != null) {
reply.setChildElement(packet.getChildElement().createCopy());
reply.setError(error);
session.process(reply);
return null;
}

child.addElement("jid").setText(session.getAddress().toString());
Expand Down
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,10 +227,7 @@ 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);
}
SASLAuthentication.addSASLMechanisms(elements, this);
}

if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.jivesoftware.openfire.net;

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

/**
* Interface for plugins that handle inline elements in SASL2 bind2 requests.
*/
public interface Bind2InlineHandler {

/**
* Gets the namespace this handler processes.
*
* @return The XML namespace URI this handler supports
*/
String getNamespace();

/**
* Process an inline element from a bind2 request.
*
* @param bound The "bound" element to add any output to
* @param element The DOM element to process
* @return true if the element was handled successfully, false otherwise
*/
boolean handleElement(LocalClientSession session, Element bound, Element element);
}
Loading
Loading