diff --git a/docs/src/main/asciidoc/_elytron/Elytron_Subsystem.adoc b/docs/src/main/asciidoc/_elytron/Elytron_Subsystem.adoc
index 02905353fe4d..bce4315b3d2c 100644
--- a/docs/src/main/asciidoc/_elytron/Elytron_Subsystem.adoc
+++ b/docs/src/main/asciidoc/_elytron/Elytron_Subsystem.adoc
@@ -93,8 +93,8 @@ server factories.
the SASL server factory is an aggregation of other SASL server
factories.
-|configurable-http-server-mechanism-factory |A SASL server factory
-definition where the SASL server factory is an aggregation of other SASL
+|configurable-http-server-mechanism-factory |An HTTP server factory
+definition where the HTTP server factory is an aggregation of other HTTP
server factories.
|configurable-sasl-server-factory |A SASL server factory definition
diff --git a/docs/src/main/asciidoc/_elytron/migrate/SSL_with_Client_Cert_Migration.adoc b/docs/src/main/asciidoc/_elytron/migrate/SSL_with_Client_Cert_Migration.adoc
index 5f369b4ad364..b4f12848c9a5 100644
--- a/docs/src/main/asciidoc/_elytron/migrate/SSL_with_Client_Cert_Migration.adoc
+++ b/docs/src/main/asciidoc/_elytron/migrate/SSL_with_Client_Cert_Migration.adoc
@@ -164,6 +164,24 @@ Resulting in: -
----
+If you want to use the HTTP DIGEST authentication mechanism with a load balancer, you can add the following property to the HTTP server mechanism factory:
+
+----
+
+ ...
+
+ ...
+
+
+
+
+
+ ...
+
+ ...
+
+----
+
== SASL Authentication Factory
The architecture of the two authentication factories if very similar so a SASL authentication factory can be defined in the same way as the HTTP equivalent.
diff --git a/testsuite/integration/manualmode/src/test/java/org/wildfly/test/manual/elytron/digest/session/HttpSessionDigestTestCase.java b/testsuite/integration/manualmode/src/test/java/org/wildfly/test/manual/elytron/digest/session/HttpSessionDigestTestCase.java
new file mode 100644
index 000000000000..40bf9291e57f
--- /dev/null
+++ b/testsuite/integration/manualmode/src/test/java/org/wildfly/test/manual/elytron/digest/session/HttpSessionDigestTestCase.java
@@ -0,0 +1,231 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2023, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.wildfly.test.manual.elytron.digest.session;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.jboss.arquillian.container.test.api.ContainerController;
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.container.test.api.RunAsClient;
+import org.jboss.arquillian.container.test.api.TargetsContainer;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.junit.InSequence;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.as.test.integration.management.util.CLIWrapper;
+import org.jboss.shrinkwrap.api.Archive;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Arquillian.class)
+public class HttpSessionDigestTestCase {
+
+ public static final int MGMT_PORT_NODE_1 = 10090;
+ public static final int MGMT_PORT_NODE_2 = 10190;
+ public static final String LOCALHOST = "localhost";
+ @ArquillianResource
+ private static ContainerController serverController;
+ private static final String CONAINER_NODE_1 = "session-digest-node1";
+ private static final String CONAINER_NODE_2 = "session-digest-node2";
+
+ @ArquillianResource
+ private Deployer deployer;
+
+ @Deployment(name = "DEPLOYMENT_NODE_1", managed = false, testable = false)
+ @TargetsContainer(CONAINER_NODE_1)
+ public static Archive> createNode1Deployment() {
+ return getWebArchive();
+ }
+
+ @Deployment(name = "DEPLOYMENT_NODE_2", managed = false, testable = false)
+ @TargetsContainer(CONAINER_NODE_2)
+ public static Archive> createNode2Deployment() {
+ return getWebArchive();
+ }
+
+ @NotNull
+ private static WebArchive getWebArchive() {
+ WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "test.war");
+ webArchive.addAsWebResource(Thread.currentThread().getContextClassLoader().getResource("elytron/digest/index.html"), "index.html");
+ webArchive.addAsWebInfResource("elytron/digest/web.xml", "web.xml");
+ return webArchive;
+ }
+
+ @Test
+ @RunAsClient
+ @InSequence(1)
+ public void setup() throws Exception {
+ if (!serverController.isStarted(CONAINER_NODE_1)) {
+ serverController.start(CONAINER_NODE_1);
+ }
+ if (!serverController.isStarted(CONAINER_NODE_2)) {
+ serverController.start(CONAINER_NODE_2);
+ }
+
+ configureServerWithFSRealmAndSessionDigestProperty(new CLIWrapper(LOCALHOST, MGMT_PORT_NODE_1, true));
+ configureServerWithFSRealmAndSessionDigestProperty(new CLIWrapper(LOCALHOST, MGMT_PORT_NODE_2, true));
+
+ try {
+ deployer.deploy("DEPLOYMENT_NODE_1");
+ deployer.deploy("DEPLOYMENT_NODE_2");
+ } catch (Exception e) {
+ Assert.fail();
+ }
+ }
+
+ private static void configureServerWithFSRealmAndSessionDigestProperty(CLIWrapper cli2) {
+ cli2.sendLine("/subsystem=elytron/filesystem-realm=exampleFsRealm:add(path=fs-realm-users,relative-to=jboss.server.config.dir)");
+ cli2.sendLine("/subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity(identity=jane)");
+ cli2.sendLine("/subsystem=elytron/filesystem-realm=exampleFsRealm:set-password(clear={password=\"passwordJane\"}, identity=jane)");
+ cli2.sendLine("/subsystem=elytron/filesystem-realm=exampleFsRealm:add-identity-attribute(identity=jane, name=Roles, value=[\"Admin\"])");
+ cli2.sendLine("/subsystem=elytron/configurable-http-server-mechanism-factory=configured-http:add(http-server-mechanism-factory=global,properties={org.wildfly.security.http.use-session-based-digest-nonce-manager=true})");
+ cli2.sendLine("/subsystem=elytron/http-authentication-factory=application-http-authentication:write-attribute(name=http-server-mechanism-factory,value=configured-http)");
+ cli2.sendLine("/subsystem=elytron/http-authentication-factory=application-http-authentication:write-attribute(name=mechanism-configurations,value=[{mechanism-name=DIGEST,mechanism-realm-configurations=[{realm-name=exampleFsRealm}]}])");
+ cli2.sendLine("batch");
+ cli2.sendLine("/subsystem=elytron/security-domain=ApplicationDomain:write-attribute(name=realms,value=[{realm=exampleFsRealm}])");
+ cli2.sendLine("/subsystem=elytron/security-domain=ApplicationDomain:write-attribute(name=default-realm,value=exampleFsRealm)");
+ cli2.sendLine("/subsystem=undertow/application-security-domain=other:write-attribute(name=http-authentication-factory,value=application-http-authentication)");
+ cli2.sendLine("/subsystem=undertow/application-security-domain=other:undefine-attribute(name=security-domain)");
+ cli2.sendLine("run-batch");
+ cli2.sendLine("reload");
+ }
+
+ @Test
+ @RunAsClient
+ @InSequence(2)
+ public void testHttpSessionDigestPropertyWithTwoServers() throws Exception {
+ testDigestAuthenticationForTwoServers();
+ }
+
+ @Test
+ @RunAsClient
+ @InSequence(3)
+ public void testHttpSessionDigestSameNonceCannotBeUsedTwice() throws Exception {
+ String server1 = "http://localhost:8180/test/";
+ String server2 = "http://localhost:8280/test/";
+
+ try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
+ HttpGet httpFirstGetRequest = new HttpGet(server1);
+ HttpResponse response = httpclient.execute(httpFirstGetRequest);
+ Map wwwAuth = Arrays.stream(response.getHeaders("WWW-Authenticate")[0].getElements())
+ .collect(Collectors.toMap(HeaderElement::getName, HeaderElement::getValue));
+ String realm = wwwAuth.get("Digest realm");
+ String nonce = wwwAuth.get("nonce");
+ String uri = "/test/";
+
+ // the first call always fails with a 401 and a requested nonce, realm, etc.
+ Assert.assertEquals(response.getStatusLine().getStatusCode(), 401);
+ httpFirstGetRequest.releaseConnection();
+
+ // create response with headers
+ HttpGet request = new HttpGet(server1);
+ addAuthenticateHeader(request, realm, nonce, uri);
+
+ // send a response to the server2 which did not send a challenge
+ // the result is 200 because the nonce manager was configured to be persisted with "org.wildfly.security.http.use-session-based-digest-nonce-manager" option
+ request.setURI(new URI(server2));
+ Assert.assertEquals(200, httpclient.execute(request).getStatusLine().getStatusCode());
+ request.releaseConnection();
+
+ // try to send a same response to the server1 that have sent a challenge
+ // 401 is returned because the same nonce cannot be used twice
+ request.setURI(new URI(server1));
+ response = httpclient.execute(request);
+ Assert.assertEquals(401, response.getStatusLine().getStatusCode());
+ request.releaseConnection();
+ }
+ }
+
+ private void testDigestAuthenticationForTwoServers() throws IOException, NoSuchAlgorithmException, URISyntaxException {
+ String server1 = "http://localhost:8180/test/";
+ String server2 = "http://localhost:8280/test/";
+
+ try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
+ HttpGet httpFirstGetRequest = new HttpGet(server1);
+ HttpResponse response = httpclient.execute(httpFirstGetRequest);
+ Map wwwAuth = Arrays.stream(response.getHeaders("WWW-Authenticate")[0].getElements())
+ .collect(Collectors.toMap(HeaderElement::getName, HeaderElement::getValue));
+ String realm = wwwAuth.get("Digest realm");
+ String nonce = wwwAuth.get("nonce");
+ String uri = "/test/";
+
+ // the first call always fails with a 401 and a requested nonce, realm, etc.
+ Assert.assertEquals(response.getStatusLine().getStatusCode(), 401);
+ httpFirstGetRequest.releaseConnection();
+
+ // create response with headers
+ HttpGet request = new HttpGet(server1);
+ addAuthenticateHeader(request, realm, nonce, uri);
+
+ // send a response to the server2 which did not send a challenge
+ // the result is 200 because the nonce manager was configured to be persisted with "org.wildfly.security.http.use-session-based-digest-nonce-manager" option
+ request.setURI(new URI(server2));
+ Assert.assertEquals(200, httpclient.execute(request).getStatusLine().getStatusCode());
+ request.releaseConnection();
+ }
+ }
+
+ private void addAuthenticateHeader(HttpGet httpGetRequestWithAuthHeader, String realm, String nonce, String uri) throws NoSuchAlgorithmException {
+ httpGetRequestWithAuthHeader.setHeader("Authorization", "Digest " +
+ "username=" + "\"jane\",\n" +
+ "realm=\"" + realm + "\",\n" +
+ "nonce=\"" + nonce + "\",\n" +
+ "uri=\"" + uri + "\",\n" +
+ "algorithm=\"" + "MD5" + "\",\n" +
+ "response=\"" + computeDigest("/test/", nonce, "jane", "passwordJane", "MD5", realm, "GET") +
+ "\"");
+ }
+
+ private String computeDigest(String uri, String nonce, String username, String password, String algorithm, String realm, String method) throws NoSuchAlgorithmException, NoSuchAlgorithmException {
+ String A1, HashA1, A2, HashA2;
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ A1 = username + ":" + realm + ":" + password;
+ HashA1 = getMD5(A1);
+ A2 = method + ":" + uri;
+ HashA2 = getMD5(A2);
+ String combo, finalHash;
+ combo = HashA1 + ":" + nonce + ":" + HashA2;
+ finalHash = DigestUtils.md5Hex(combo);
+ return finalHash;
+ }
+
+ public String getMD5(String value) {
+ return DigestUtils.md5Hex(value);
+ }
+}
diff --git a/testsuite/integration/manualmode/src/test/resources/arquillian.xml b/testsuite/integration/manualmode/src/test/resources/arquillian.xml
index fc036bdff7f9..f47a93f9a05d 100644
--- a/testsuite/integration/manualmode/src/test/resources/arquillian.xml
+++ b/testsuite/integration/manualmode/src/test/resources/arquillian.xml
@@ -508,6 +508,31 @@
${container.java.home}
+
+
+
+ ${basedir}/target/wildfly-session-digest-node1
+ ${server.jvm.args} -Djboss.node.name=session-digest-node1 -Djboss.socket.binding.port-offset=100
+ ${jboss.config.file.name:standalone-ha.xml}
+ ${jboss.args}
+ true
+ ${node0:127.0.0.1}
+ ${as.managementPort:10090}
+
+
+
+
+
+ ${basedir}/target/wildfly-session-digest-node2
+ ${server.jvm.args} -Djboss.node.name=session-digest-node2 -Djboss.socket.binding.port-offset=200
+ ${jboss.config.file.name:standalone-ha.xml}
+ ${jboss.args}
+ true
+ ${node0:127.0.0.1}
+ ${as.managementPort:10190}
+
+
+
diff --git a/testsuite/integration/manualmode/src/test/resources/elytron/digest/index.html b/testsuite/integration/manualmode/src/test/resources/elytron/digest/index.html
new file mode 100644
index 000000000000..42f04cd2f54f
--- /dev/null
+++ b/testsuite/integration/manualmode/src/test/resources/elytron/digest/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Hello World
+
+
+Hello World
+
+
diff --git a/testsuite/integration/manualmode/src/test/resources/elytron/digest/web.xml b/testsuite/integration/manualmode/src/test/resources/elytron/digest/web.xml
new file mode 100644
index 000000000000..5bb1297fb02c
--- /dev/null
+++ b/testsuite/integration/manualmode/src/test/resources/elytron/digest/web.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ /*
+
+
+ User
+ Admin
+
+
+
+
+ abc
+
+
+
+ DIGEST
+ exampleFsRealm
+
+
+
\ No newline at end of file
diff --git a/testsuite/integration/src/test/scripts/manualmode-build.xml b/testsuite/integration/src/test/scripts/manualmode-build.xml
index 0f0b43e8c860..c807923ed70e 100644
--- a/testsuite/integration/src/test/scripts/manualmode-build.xml
+++ b/testsuite/integration/src/test/scripts/manualmode-build.xml
@@ -56,6 +56,16 @@
+
+
+
+
+
+
+
+
+
+