diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2InlineHandler.java b/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2InlineHandler.java
index 5c6f7ac08a..850690f9da 100644
--- a/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2InlineHandler.java
+++ b/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2InlineHandler.java
@@ -39,4 +39,30 @@ public interface Bind2InlineHandler {
* @return true if the element was handled successfully, false otherwise
*/
boolean handleElement(LocalClientSession session, Element bound, Element element);
+
+ /**
+ * Indicates whether this handler is currently available for advertisement and request processing.
+ *
+ * A handler whose feature is disabled by configuration is neither advertised in the Bind2 inline feature list nor
+ * invoked for a request that names its namespace, so that a peer is never offered something it cannot use.
+ *
+ * @return {@code true} when the inline feature is available
+ */
+ default boolean isEnabled() {
+ return true;
+ }
+
+ /**
+ * Gives a handler an opportunity to add the protocol-defined failure response after request processing failed.
+ *
+ * Not every inline extension defines one, so the default does nothing.
+ *
+ * @param session the client session
+ * @param bound the Bind2 response element
+ * @param element the request that could not be processed
+ * @param cause the processing exception, or {@code null} when the handler returned {@code false}
+ */
+ default void handleFailure(LocalClientSession session, Element bound, Element element, Exception cause) {
+ // Most inline extensions do not define a failure response.
+ }
}
diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2Request.java b/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2Request.java
index 5539c6fca0..04f0f09fce 100644
--- a/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2Request.java
+++ b/xmppserver/src/main/java/org/jivesoftware/openfire/net/Bind2Request.java
@@ -21,11 +21,12 @@
import org.dom4j.QName;
import org.jivesoftware.openfire.auth.ScramUtils;
import org.jivesoftware.openfire.session.LocalClientSession;
-import org.jivesoftware.openfire.session.LocalSession;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import javax.security.sasl.SaslException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@@ -94,16 +95,18 @@ public Element processFeatureRequests(LocalClientSession clientSession, Element
String namespace = element.getNamespaceURI();
Bind2InlineHandler handler = elementHandlers.get(namespace);
- if (handler != null) {
+ if (handler != null && handler.isEnabled()) {
try {
if (!handler.handleElement(clientSession, bound, element)) {
- Log.warn("Handler for namespace {} failed to process element", namespace);
+ Log.info("Handler for namespace {} failed to process element", namespace);
+ invokeFailureHandler(clientSession, bound, element, null, handler, namespace);
}
} catch (Exception e) {
- Log.error("Error processing element with namespace: " + namespace, e);
+ Log.warn("Error processing element with namespace: {}", namespace, e);
+ invokeFailureHandler(clientSession, bound, element, e, handler, namespace);
}
} else {
- Log.debug("No handler registered for namespace: {}", namespace);
+ Log.debug("No handler registered/enabled for namespace: {}", namespace);
// We don't fail here because there's no obvious way we could fail.
}
}
@@ -111,10 +114,33 @@ public Element processFeatureRequests(LocalClientSession clientSession, Element
return bound;
}
+ /**
+ * Invokes the failure-handler of a Bind2-handler, logging but otherwise suppressing any exception thrown by the
+ * failure-handler.
+ *
+ * @param clientSession the client session.
+ * @param bound the bound element.
+ * @param element the element that failed to be processed.
+ * @param cause the processing exception, or {@code null} when the handler returned {@code false}.
+ * @param handler the Bind2-handler that failed to process the element.
+ * @param namespace the namespace of the element.
+ */
+ private static void invokeFailureHandler(final LocalClientSession clientSession, final Element bound, final Element element, @Nullable final Exception cause, @Nonnull final Bind2InlineHandler handler, final String namespace)
+ {
+ try {
+ handler.handleFailure(clientSession, bound, element, cause);
+ } catch (Exception ex) {
+ Log.warn("Error invoking failure handler after failing to process element with namespace: {}", namespace, ex);
+ }
+ }
+
public static Element featureElement() {
Element bind2 = DocumentHelper.createElement(new QName("bind", new Namespace("", "urn:xmpp:bind:0")));
Element bind2inline = bind2.addElement("inline");
for (Bind2InlineHandler handler : elementHandlers.values()) {
+ if (!handler.isEnabled()) {
+ continue;
+ }
Element var = bind2inline.addElement("feature");
var.addAttribute("var", handler.getNamespace());
}
@@ -221,21 +247,21 @@ public String generateResourceString(UserAgentInfo userAgentInfo) {
// Using a fixed constant here - building a rainbow table here for the case
// where the client supplies no tag is going to be very expensive, so this
// prevents an id recovery attack.
- String valueToHmac = resource.toString() + "OpenfireResourceConstant";
+ String valueToHmac = resource + "OpenfireResourceConstant";
// Compute HMAC
- byte[] hmacResult = ScramUtils.computeHmac(keyBytes, valueToHmac);
+ byte[] hmacResult = ScramUtils.computeHmac(keyBytes, valueToHmac, "HmacSHA1");
// Convert first 8 bytes of HMAC to hex for resource suffix (16 chars)
String hmacHex = StringUtils.encodeHex(Arrays.copyOf(hmacResult, 8));
// Construct final resource string
- return resource.toString() + hmacHex;
+ return resource + hmacHex;
} catch (SaslException e) {
// Fall back to UUID in case of HMAC computation failure
Log.error("Failed to compute HMAC for resource string", e);
- return resource.toString() + UUID.randomUUID().toString();
+ return resource.toString() + UUID.randomUUID();
}
}
}
diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java b/xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java
index 887449ef89..41d6d7d0f6 100644
--- a/xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java
+++ b/xmppserver/src/main/java/org/jivesoftware/openfire/net/SASLAuthentication.java
@@ -820,14 +820,16 @@ else if (session instanceof LocalIncomingServerSession serverSession) {
Log.warn("An exception occurred while binding resource '{}' for session '{}' during SASL2+Bind2 authentication.", resource, clientSession, throwable);
}
final boolean bound = throwable == null && result == SessionManager.BindResult.BOUND;
- final Element success = SaslOutcome.buildSasl2SuccessElement(successData, authorizationIdentity, bound ? resource : null, finalFastToken);
- if (bound) {
- bind2Request.processFeatureRequests(clientSession, success);
- }
- if (bound) {
- clientSession.setStatus(Session.Status.AUTHENTICATED);
- SessionEventDispatcher.dispatchEvent(clientSession, SessionEventDispatcher.EventType.resource_bound);
+ if (!bound) {
+ Log.warn("Unable to bind resource '{}' for session '{}' during SASL2+Bind2 authentication. Bind result: {}", resource, clientSession, result);
+ SaslOutcome.authenticationFailed(clientSession, Failure.TEMPORARY_AUTH_FAILURE, true);
+ return;
}
+ final Element success = SaslOutcome.buildSasl2SuccessElement(successData, authorizationIdentity, resource, finalFastToken);
+ clientSession.setStatus(Session.Status.AUTHENTICATED);
+ bind2Request.processFeatureRequests(clientSession, success);
+ SessionEventDispatcher.dispatchEvent(clientSession, SessionEventDispatcher.EventType.resource_bound);
+
// Deliver stream features now that has been sent.
final Element features = DocumentHelper.createElement(QName.get("features", "stream", "http://etherx.jabber.org/streams"));
final List specificFeatures = clientSession.getAvailableStreamFeatures();
diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/net/Bind2RequestProcessingTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/net/Bind2RequestProcessingTest.java
index 63122872bf..49f3b6c523 100644
--- a/xmppserver/src/test/java/org/jivesoftware/openfire/net/Bind2RequestProcessingTest.java
+++ b/xmppserver/src/test/java/org/jivesoftware/openfire/net/Bind2RequestProcessingTest.java
@@ -29,6 +29,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
@@ -90,6 +91,8 @@ public void setUp() {
when(mockHandler2.getNamespace()).thenReturn("http://test2.namespace");
when(mockHandler1.handleElement(any(), any(), any())).thenReturn(true);
when(mockHandler2.handleElement(any(), any(), any())).thenReturn(true);
+ when(mockHandler1.isEnabled()).thenReturn(true);
+ when(mockHandler2.isEnabled()).thenReturn(true);
}
@AfterEach
@@ -193,6 +196,7 @@ public void testProcessFeatureRequestsWithHandlerException() {
assertNotNull(result);
verify(mockHandler1).handleElement(any(), elementWithNameAndNS("bound", "urn:xmpp:bind:0"), eq(featureElement1));
verify(mockHandler2).handleElement(any(), elementWithNameAndNS("bound", "urn:xmpp:bind:0"), eq(featureElement2));
+ verify(mockHandler1).handleFailure(eq(mockSession), elementWithNameAndNS("bound", "urn:xmpp:bind:0"), eq(featureElement1), any(RuntimeException.class));
}
@Test
@@ -206,6 +210,7 @@ public void testProcessFeatureRequestsWithHandlerReturnsFalse() {
assertNotNull(result);
verify(mockHandler1).handleElement(any(), elementWithNameAndNS("bound", "urn:xmpp:bind:0"), eq(featureElement1));
+ verify(mockHandler1).handleFailure(eq(mockSession), elementWithNameAndNS("bound", "urn:xmpp:bind:0"), eq(featureElement1), isNull());
}
@Test
@@ -244,7 +249,7 @@ public void testFeatureElementWithNoHandlers() {
Element inline = feature.element("inline");
assertNotNull(inline);
- assertTrue(inline.elements("feature").isEmpty(), "Expected no advertised features when no handlers are registered");
+ assertTrue(advertisedFeatures(inline).isEmpty(), "Expected no advertised features when no handlers are registered");
}
@Test
@@ -274,14 +279,10 @@ public void testFeatureElementAdvertisesBothHandlers() {
assertNotNull(feature);
Element inline = feature.element("inline");
assertNotNull(inline);
- List features = inline.elements("feature");
- assertEquals(2, features.size());
-
- List vars = features.stream()
- .map(e -> e.attributeValue("var"))
- .collect(Collectors.toList());
- assertTrue(vars.contains("http://test1.namespace"), "Expected http://test1.namespace to be advertised");
- assertTrue(vars.contains("http://test2.namespace"), "Expected http://test2.namespace to be advertised");
+ Set featureVars = advertisedFeatures(inline);
+ assertEquals(2, featureVars.size());
+ assertTrue(featureVars.contains("http://test1.namespace"), "Expected http://test1.namespace to be advertised");
+ assertTrue(featureVars.contains("http://test2.namespace"), "Expected http://test2.namespace to be advertised");
}
@Test
@@ -294,8 +295,215 @@ public void testFeatureElementAfterUnregisteringHandler() {
Element inline = feature.element("inline");
assertNotNull(inline);
- List features = inline.elements("feature");
- assertEquals(1, features.size());
- assertEquals("http://test2.namespace", features.get(0).attributeValue("var"));
+ Set featureVars = advertisedFeatures(inline);
+ assertEquals(1, featureVars.size());
+ assertTrue(featureVars.contains("http://test2.namespace"), "Expected http://test2.namespace to be advertised");
+ }
+
+ /**
+ * Verifies that a handler reports no failure when its request was processed successfully.
+ *
+ * A failure response that accompanies a successful request would be reported to the peer as though something had
+ * gone wrong, and for XEP-0198 would leave a client believing stream management had not been enabled when it had.
+ */
+ @Test
+ public void testNoFailureIsReportedWhenProcessingSucceeds()
+ {
+ // Setup test fixture.
+ Bind2Request.registerElementHandler(mockHandler1);
+ final Bind2Request request = new Bind2Request("test-client", List.of(featureElement1));
+
+ // Execute system under test.
+ request.processFeatureRequests(mockSession, successElement);
+
+ // Verify result.
+ verify(mockHandler1).handleElement(any(), any(), eq(featureElement1));
+ verify(mockHandler1, never()).handleFailure(any(), any(), any(), any());
+ }
+
+ /**
+ * Verifies that a handler whose feature is unavailable is not invoked for a request that names its namespace.
+ *
+ * A peer can send an inline request for a feature that was advertised earlier in the stream but has since been
+ * disabled by configuration, so availability must be checked when the request is processed and not only when the
+ * feature list is built.
+ */
+ @Test
+ public void testDisabledHandlerIsNotInvoked()
+ {
+ // Setup test fixture.
+ when(mockHandler1.isEnabled()).thenReturn(false);
+ Bind2Request.registerElementHandler(mockHandler1);
+ final Bind2Request request = new Bind2Request("test-client", List.of(featureElement1));
+
+ // Execute system under test.
+ request.processFeatureRequests(mockSession, successElement);
+
+ // Verify result.
+ verify(mockHandler1, never()).handleElement(any(), any(), any());
+ verify(mockHandler1, never()).handleFailure(any(), any(), any(), any());
+ }
+
+ /**
+ * Verifies that one unavailable handler does not prevent the others from processing their requests.
+ */
+ @Test
+ public void testDisabledHandlerDoesNotSuppressOthers()
+ {
+ // Setup test fixture.
+ when(mockHandler1.isEnabled()).thenReturn(false);
+ Bind2Request.registerElementHandler(mockHandler1);
+ Bind2Request.registerElementHandler(mockHandler2);
+ final Bind2Request request = new Bind2Request("test-client", List.of(featureElement1, featureElement2));
+
+ // Execute system under test.
+ request.processFeatureRequests(mockSession, successElement);
+
+ // Verify result.
+ verify(mockHandler1, never()).handleElement(any(), any(), any());
+ verify(mockHandler2).handleElement(any(), any(), eq(featureElement2));
+ }
+
+ /**
+ * Verifies that a handler whose feature is unavailable is not advertised.
+ *
+ * Advertising a feature that would then be ignored invites a peer to send a request that receives no response at
+ * all, which it cannot distinguish from one the server failed to process.
+ */
+ @Test
+ public void testDisabledHandlerIsNotAdvertised()
+ {
+ // Setup test fixture.
+ when(mockHandler1.isEnabled()).thenReturn(false);
+ Bind2Request.registerElementHandler(mockHandler1);
+ Bind2Request.registerElementHandler(mockHandler2);
+
+ // Execute system under test.
+ final Element inline = Bind2Request.featureElement().element("inline");
+
+ // Verify result.
+ assertFalse(advertisedFeatures(inline).contains("http://test1.namespace"),
+ "An unavailable feature must not be advertised.");
+ assertTrue(advertisedFeatures(inline).contains("http://test2.namespace"),
+ "An available feature must still be advertised alongside an unavailable one.");
+ }
+
+ /**
+ * Verifies that availability is evaluated each time the feature list is built, rather than when the handler was
+ * registered.
+ *
+ * Whether a feature is available is typically governed by a dynamic configuration property, and stream features
+ * are regenerated more than once during a stream's lifetime.
+ */
+ @Test
+ public void testAvailabilityIsEvaluatedPerAdvertisement()
+ {
+ // Setup test fixture.
+ Bind2Request.registerElementHandler(mockHandler1);
+
+ // Execute system under test & verify result.
+ when(mockHandler1.isEnabled()).thenReturn(true);
+ assertTrue(advertisedFeatures(Bind2Request.featureElement().element("inline")).contains("http://test1.namespace"),
+ "An available feature must be advertised.");
+
+ when(mockHandler1.isEnabled()).thenReturn(false);
+ assertFalse(advertisedFeatures(Bind2Request.featureElement().element("inline")).contains("http://test1.namespace"),
+ "A feature that has since become unavailable must no longer be advertised.");
+
+ when(mockHandler1.isEnabled()).thenReturn(true);
+ assertTrue(advertisedFeatures(Bind2Request.featureElement().element("inline")).contains("http://test1.namespace"),
+ "A feature that has become available again must be advertised again.");
+ }
+
+ /**
+ * Verifies that a handler which overrides neither of the two new methods behaves exactly as handlers did before
+ * they existed: it is available, and it tolerates being asked to report a failure.
+ *
+ * Handlers are contributed by plugins, which are not necessarily recompiled against a new interface.
+ */
+ @Test
+ public void testHandlerWithoutOverridesRetainsPreviousBehaviour()
+ {
+ // Setup test fixture: a handler implementing only what the interface has always required.
+ final Bind2InlineHandler handler = new Bind2InlineHandler() {
+ @Override
+ public String getNamespace() {
+ return "http://legacy.namespace";
+ }
+
+ @Override
+ public boolean handleElement(LocalClientSession session, Element bound, Element element) {
+ return true;
+ }
+ };
+
+ // Execute system under test & verify result.
+ assertTrue(handler.isEnabled(),
+ "A handler that does not override isEnabled must be treated as available, as handlers were before the method existed.");
+ assertDoesNotThrow(() -> handler.handleFailure(mockSession, DocumentHelper.createElement(QName.get("bound", "urn:xmpp:bind:0")), featureElement1, null),
+ "A handler that does not override handleFailure must tolerate being asked to report one.");
+ }
+
+ /**
+ * Verifies that whatever a handler adds to the response while reporting a failure survives into the result.
+ *
+ * This is what makes a protocol-defined failure response expressible at all; XEP-0198 § 9.1.1, for example,
+ * requires a <failed/> element inside <bound/> when stream management could not be enabled.
+ */
+ @Test
+ public void testFailureResponseIsRetainedInTheBoundElement()
+ {
+ // Setup test fixture.
+ when(mockHandler1.handleElement(any(), any(), any())).thenReturn(false);
+ doAnswer(invocation -> {
+ final Element bound = invocation.getArgument(1);
+ bound.addElement("failed", "http://test1.namespace");
+ return null;
+ }).when(mockHandler1).handleFailure(any(), any(), any(), any());
+ Bind2Request.registerElementHandler(mockHandler1);
+ final Bind2Request request = new Bind2Request("test-client", List.of(featureElement1));
+
+ // Execute system under test.
+ final Element bound = request.processFeatureRequests(mockSession, successElement);
+
+ // Verify result.
+ assertNotNull(bound.element(QName.get("failed", "http://test1.namespace")),
+ "A failure response added by a handler must be retained in the response that is sent to the peer.");
+ }
+
+ /**
+ * Verifies that a handler which throws while reporting a failure does not prevent the remaining requests from
+ * being processed.
+ *
+ * Handlers are contributed by plugins, so a misbehaving one must not be able to fail the authentication that the
+ * bind request is part of.
+ */
+ @Test
+ public void testExceptionWhileReportingFailureDoesNotPropagate()
+ {
+ // Setup test fixture.
+ when(mockHandler1.handleElement(any(), any(), any())).thenReturn(false);
+ doThrow(new RuntimeException("handler is broken")).when(mockHandler1).handleFailure(any(), any(), any(), any());
+ Bind2Request.registerElementHandler(mockHandler1);
+ Bind2Request.registerElementHandler(mockHandler2);
+ final Bind2Request request = new Bind2Request("clientTag", Arrays.asList(featureElement1, featureElement2));
+
+ // Execute system under test.
+ final Element bound = assertDoesNotThrow(() -> request.processFeatureRequests(mockSession, successElement));
+
+ // Verify result.
+ assertNotNull(bound);
+ verify(mockHandler1, times(1)).handleFailure(any(), any(), any(), any());
+ verify(mockHandler2).handleElement(any(), any(), eq(featureElement2));
+ }
+
+ /**
+ * Returns the namespaces advertised as inline features.
+ */
+ private static Set advertisedFeatures(final Element inline)
+ {
+ return inline.elements("feature").stream()
+ .map(feature -> feature.attributeValue("var"))
+ .collect(Collectors.toSet());
}
}
diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/net/SASLAuthenticationTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/net/SASLAuthenticationTest.java
index e17bc5f6ad..5fc741a1a0 100644
--- a/xmppserver/src/test/java/org/jivesoftware/openfire/net/SASLAuthenticationTest.java
+++ b/xmppserver/src/test/java/org/jivesoftware/openfire/net/SASLAuthenticationTest.java
@@ -31,6 +31,7 @@
import org.jivesoftware.openfire.sasl.SaslFailureException;
import org.jivesoftware.openfire.sasl.SaslMechanismCatalog;
import org.jivesoftware.openfire.sasl.TestSaslMechanism;
+import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
@@ -50,12 +51,14 @@
import javax.security.sasl.SaslServer;
import java.util.*;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
import static org.jivesoftware.openfire.net.SASLAuthentication.SASL_NAMESPACE;
import static org.jivesoftware.openfire.net.SASLAuthentication.SASL2_NAMESPACE;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -337,6 +340,107 @@ public void shouldGenerateAnonymousAuthTokenForClientWhenUsernameIsNullWithSasl2
}
}
+ /**
+ * Verifies that a SASL2 authentication whose inline Bind2 request cannot be honoured fails, rather than reporting
+ * success for a session that has no resource bound.
+ *
+ * A client that receives {@code } without {@code } has no way to tell that binding failed: it
+ * believes it is authenticated, and the stream features that follow tell it nothing to the contrary.
+ */
+ @Test
+ public void bind2ConflictFailsSasl2WithoutSuccessOrFeatures() throws Exception
+ {
+ // Setup test fixture.
+ final Connection connection = mock(Connection.class);
+ final LocalClientSession session = new LocalClientSession(Fixtures.XMPP_DOMAIN, connection,
+ new BasicStreamIDFactory().createStreamID(), Locale.ENGLISH);
+ final Bind2Request bind2Request = mock(Bind2Request.class);
+ when(bind2Request.generateResourceString(any())).thenReturn("conflicting-resource");
+ session.setSessionData("bind2-request", bind2Request);
+ when(XMPPServer.getInstance().getSessionManager().bindResource(any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(SessionManager.BindResult.CONFLICT));
+
+ // Execute system under test.
+ SASLAuthentication.authenticationSuccessful(session, "testuser", "PLAIN", new byte[0], true);
+
+ // Verify result.
+ final ArgumentCaptor delivered = ArgumentCaptor.forClass(String.class);
+ verify(connection).deliverRawText(delivered.capture());
+ final Element failure = DocumentHelper.parseText(delivered.getValue()).getRootElement();
+ assertEquals("failure", failure.getName(), "A bind that could not be honoured must fail the authentication.");
+ assertEquals(SASLAuthentication.SASL2_NAMESPACE, failure.getNamespaceURI(), "The failure must be in the SASL2 namespace.");
+ assertNotNull(failure.element(QName.get("temporary-auth-failure", SASLAuthentication.SASL_NAMESPACE)),
+ "A bind conflict is not permanent, so the client must be told it may try again.");
+ verify(bind2Request, never()).processFeatureRequests(any(), any());
+ assertFalse(session.isAuthenticated(), "A session whose resource could not be bound must not be authenticated.");
+ }
+
+ /**
+ * Verifies the same for a resource binding that fails by throwing, rather than by reporting a conflict.
+ */
+ @Test
+ public void bind2ExceptionFailsSasl2WithoutSuccessOrFeatures() throws Exception
+ {
+ // Setup test fixture.
+ final Connection connection = mock(Connection.class);
+ final LocalClientSession session = new LocalClientSession(Fixtures.XMPP_DOMAIN, connection,
+ new BasicStreamIDFactory().createStreamID(), Locale.ENGLISH);
+ final Bind2Request bind2Request = mock(Bind2Request.class);
+ when(bind2Request.generateResourceString(any())).thenReturn("test-resource");
+ session.setSessionData("bind2-request", bind2Request);
+ final CompletableFuture failedBind = new CompletableFuture<>();
+ failedBind.completeExceptionally(new IllegalStateException("test failure"));
+ when(XMPPServer.getInstance().getSessionManager().bindResource(any(), any(), any())).thenReturn(failedBind);
+
+ // Execute system under test.
+ SASLAuthentication.authenticationSuccessful(session, "testuser", "PLAIN", new byte[0], true);
+
+ // Verify result.
+ final ArgumentCaptor delivered = ArgumentCaptor.forClass(String.class);
+ verify(connection).deliverRawText(delivered.capture());
+ final Element failure = DocumentHelper.parseText(delivered.getValue()).getRootElement();
+ assertEquals("failure", failure.getName(), "A bind that threw must fail the authentication.");
+ assertNotNull(failure.element(QName.get("temporary-auth-failure", SASLAuthentication.SASL_NAMESPACE)),
+ "An unexpected failure to bind is not permanent, so the client must be told it may try again.");
+ verify(bind2Request, never()).processFeatureRequests(any(), any());
+ assertFalse(session.isAuthenticated(), "A session whose resource could not be bound must not be authenticated.");
+ }
+
+ /**
+ * Verifies that inline feature handlers run against a session that is already authenticated.
+ *
+ * Some inline features are only permitted once a resource has been bound and the session is authenticated;
+ * XEP-0198 § 3 says as much of stream management, and its handler consults the session's state. Running the
+ * handlers first would make every such feature report an unexpected-request failure.
+ */
+ @Test
+ public void bind2InlineHandlersRunAgainstAnAuthenticatedSession() throws Exception
+ {
+ // Setup test fixture.
+ final Connection connection = mock(Connection.class);
+ final LocalClientSession session = new LocalClientSession(Fixtures.XMPP_DOMAIN, connection,
+ new BasicStreamIDFactory().createStreamID(), Locale.ENGLISH);
+ final Bind2Request bind2Request = mock(Bind2Request.class);
+ when(bind2Request.generateResourceString(any())).thenReturn("test-resource");
+ session.setSessionData("bind2-request", bind2Request);
+ when(XMPPServer.getInstance().getSessionManager().bindResource(any(), any(), any()))
+ .thenReturn(CompletableFuture.completedFuture(SessionManager.BindResult.BOUND));
+
+ final AtomicReference statusWhenHandlersRan = new AtomicReference<>();
+ when(bind2Request.processFeatureRequests(any(), any())).thenAnswer(invocation -> {
+ statusWhenHandlersRan.set(session.getStatus());
+ return DocumentHelper.createElement(QName.get("bound", "urn:xmpp:bind:0"));
+ });
+
+ // Execute system under test.
+ SASLAuthentication.authenticationSuccessful(session, "testuser", "PLAIN", new byte[0], true);
+
+ // Verify result.
+ verify(bind2Request).processFeatureRequests(any(), any());
+ assertEquals(Session.Status.AUTHENTICATED, statusWhenHandlersRan.get(),
+ "An inline feature handler must observe a session that is already authenticated, as some features are only permitted then.");
+ }
+
/**
* Verifies that authenticationSuccessful generates a user auth token for a client with a username.
* For SASL1, the success element has no authorization-identifier.