This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/main by this push:
     new 1834ac7cd0 Apply the configured auth-method to EJB-in-WAR JAX-WS 
endpoints (#3065)
1834ac7cd0 is described below

commit 1834ac7cd0da7e2bccbeae051a17b3e1e6057451
Author: Jonathan Gallimore <[email protected]>
AuthorDate: Wed Oct 7 16:47:31 2026 +0100

    Apply the configured auth-method to EJB-in-WAR JAX-WS endpoints (#3065)
---
 .../tests/jaxws/basicauth/GreeterBean.java         |  41 ++++
 .../tests/jaxws/basicauth/GreeterWs.java           |  25 +++
 .../jaxws/basicauth/WsBasicAuthFormWebappTest.java | 250 +++++++++++++++++++++
 .../tests/jaxws/basicauth/WsBasicAuthTest.java     | 189 ++++++++++++++++
 .../apache/tomee/webservices/TomcatWsRegistry.java |  87 ++++++-
 5 files changed, 591 insertions(+), 1 deletion(-)

diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterBean.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterBean.java
new file mode 100644
index 0000000000..adf56e62c3
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterBean.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.openejb.arquillian.tests.jaxws.basicauth;
+
+import jakarta.ejb.Singleton;
+import jakarta.jws.WebMethod;
+import jakarta.jws.WebParam;
+import jakarta.jws.WebService;
+
+/**
+ * A singleton EJB exposing a business method over SOAP. Packaged in a WAR 
(EJB-in-WAR),
+ * so its web service is registered by TomcatWsRegistry#addWsContainer via the 
addServlet
+ * route (the web application context already exists), rather than 
deployInFakeWebapp().
+ */
+@Singleton
+@WebService(name = "Greeter",
+            targetNamespace = 
"http://basicauth.jaxws.tests.arquillian.openejb.apache.org/";,
+            serviceName = "GreeterService",
+            portName = "GreeterPort")
+public class GreeterBean implements GreeterWs {
+
+    @Override
+    @WebMethod
+    public String greet(@WebParam(name = "name") final String name) {
+        return "Hello, " + name;
+    }
+}
diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterWs.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterWs.java
new file mode 100644
index 0000000000..1dd1480309
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/GreeterWs.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.openejb.arquillian.tests.jaxws.basicauth;
+
+import jakarta.jws.WebParam;
+import jakarta.jws.WebService;
+
+@WebService(targetNamespace = 
"http://basicauth.jaxws.tests.arquillian.openejb.apache.org/";)
+public interface GreeterWs {
+    String greet(@WebParam(name = "name") final String name);
+}
diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthFormWebappTest.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthFormWebappTest.java
new file mode 100644
index 0000000000..6086af5752
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthFormWebappTest.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.openejb.arquillian.tests.jaxws.basicauth;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * The realistic motivating scenario: a WAR that behaves like a normal web 
application secured
+ * with FORM login, while <em>also</em> exposing a SOAP endpoint (a singleton 
EJB) that requires
+ * BASIC authentication.
+ *
+ * <p>A Tomcat context has a single authenticator, so the context-wide FORM 
mechanism cannot be
+ * used to protect the SOAP endpoint — a SOAP client cannot follow an HTML 
login form. The web
+ * service therefore declares its own BASIC requirement via {@code 
openejb-jar.xml}'s
+ * {@code <web-service-security>}. This test asserts the two coexist:</p>
+ *
+ * <ul>
+ *   <li>an unauthenticated SOAP call to {@code /webservices/ws/Greeter} is 
answered with a
+ *       401 BASIC challenge, and</li>
+ *   <li>an unauthenticated request to a FORM-protected page ({@code 
/protected/*}) is driven
+ *       through the FORM login flow, <em>not</em> a BASIC challenge.</li>
+ * </ul>
+ *
+ * <p>This exercises the same {@code addServlet} (EJB-in-WAR) route as {@link 
WsBasicAuthTest},
+ * additionally verifying that the web service gets its own BASIC 
authenticator rather than
+ * inheriting the surrounding web application's FORM authenticator.</p>
+ */
+@RunWith(Arquillian.class)
+public class WsBasicAuthFormWebappTest {
+
+    private static final String LOGIN_MARKER = "PLEASE_LOG_IN";
+
+    private static final String SOAP_REQUEST =
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+            "<soapenv:Envelope 
xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n"; +
+            "                  
xmlns:ns=\"http://basicauth.jaxws.tests.arquillian.openejb.apache.org/\";>\n" +
+            "  <soapenv:Header/>\n" +
+            "  <soapenv:Body>\n" +
+            "    <ns:greet>\n" +
+            "      <name>world</name>\n" +
+            "    </ns:greet>\n" +
+            "  </soapenv:Body>\n" +
+            "</soapenv:Envelope>";
+
+    @ArquillianResource
+    private URL base;
+
+    @Deployment(testable = false)
+    public static WebArchive war() {
+        final String ejbJar =
+                "<ejb-jar xmlns=\"http://java.sun.com/xml/ns/javaee\"\n"; +
+                "         
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "         
xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd\"\n"; +
+                "         version=\"3.1\" metadata-complete=\"false\">\n" +
+                "  <enterprise-beans>\n" +
+                "    <session>\n" +
+                "      <ejb-name>GreeterBean</ejb-name>\n" +
+                "      
<service-endpoint>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterWs</service-endpoint>\n"
 +
+                "      
<ejb-class>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterBean</ejb-class>\n"
 +
+                "      <session-type>Singleton</session-type>\n" +
+                "      <transaction-type>Container</transaction-type>\n" +
+                "    </session>\n" +
+                "  </enterprise-beans>\n" +
+                "</ejb-jar>";
+
+        // The web service declares BASIC; the web application (below) 
declares FORM.
+        final String openejbJar =
+                "<openejb-jar 
xmlns=\"http://www.openejb.org/xml/ns/openejb-jar-2.1\";>\n" +
+                "  <enterprise-beans>\n" +
+                "    <session>\n" +
+                "      <ejb-name>GreeterBean</ejb-name>\n" +
+                "      
<web-service-address>/ws/Greeter</web-service-address>\n" +
+                "      <web-service-security>\n" +
+                "        <security-realm-name/>\n" +
+                "        <transport-guarantee>NONE</transport-guarantee>\n" +
+                "        <auth-method>BASIC</auth-method>\n" +
+                "      </web-service-security>\n" +
+                "    </session>\n" +
+                "  </enterprise-beans>\n" +
+                "</openejb-jar>";
+
+        final String webservices =
+                "<webservices xmlns=\"http://java.sun.com/xml/ns/j2ee\"\n"; +
+                "             
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "             
xsi:schemaLocation=\"http://java.sun.com/xml/ns/j2ee 
http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd\"\n"; +
+                "             version=\"1.1\">\n" +
+                "  <webservice-description>\n" +
+                "    
<webservice-description-name>GreeterService</webservice-description-name>\n" +
+                "    <port-component>\n" +
+                "      
<port-component-name>GreeterPort</port-component-name>\n" +
+                "      <wsdl-port>GreeterPort</wsdl-port>\n" +
+                "      
<service-endpoint-interface>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterWs</service-endpoint-interface>\n"
 +
+                "      <service-impl-bean>\n" +
+                "        <ejb-link>GreeterBean</ejb-link>\n" +
+                "      </service-impl-bean>\n" +
+                "    </port-component>\n" +
+                "  </webservice-description>\n" +
+                "</webservices>";
+
+        // A normal FORM-secured web application: /protected/* requires a 
login, driven through
+        // an HTML form. This is the context-wide authenticator; it must NOT 
be what guards the
+        // SOAP endpoint.
+        final String webXml =
+                "<web-app xmlns=\"https://jakarta.ee/xml/ns/jakartaee\"\n"; +
+                "         
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "         
xsi:schemaLocation=\"https://jakarta.ee/xml/ns/jakartaee 
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd\"\n"; +
+                "         version=\"6.0\">\n" +
+                "  <security-constraint>\n" +
+                "    <web-resource-collection>\n" +
+                "      <web-resource-name>protected</web-resource-name>\n" +
+                "      <url-pattern>/protected/*</url-pattern>\n" +
+                "    </web-resource-collection>\n" +
+                "    <auth-constraint>\n" +
+                "      <role-name>users</role-name>\n" +
+                "    </auth-constraint>\n" +
+                "  </security-constraint>\n" +
+                "  <login-config>\n" +
+                "    <auth-method>FORM</auth-method>\n" +
+                "    <form-login-config>\n" +
+                "      <form-login-page>/login.html</form-login-page>\n" +
+                "      <form-error-page>/error.html</form-error-page>\n" +
+                "    </form-login-config>\n" +
+                "  </login-config>\n" +
+                "  <security-role>\n" +
+                "    <role-name>users</role-name>\n" +
+                "  </security-role>\n" +
+                "</web-app>";
+
+        return ShrinkWrap.create(WebArchive.class, 
"WsBasicAuthFormWebappTest.war")
+                .addClasses(GreeterWs.class, GreeterBean.class)
+                .addAsWebInfResource(new StringAsset(ejbJar), "ejb-jar.xml")
+                .addAsWebInfResource(new StringAsset(openejbJar), 
"openejb-jar.xml")
+                .addAsWebInfResource(new StringAsset(webservices), 
"webservices.xml")
+                .addAsWebResource(new StringAsset(
+                        "<html><body><form method='post' 
action='j_security_check'>" + LOGIN_MARKER
+                                + "<input name='j_username'/><input 
name='j_password'/></form></body></html>"),
+                        "login.html")
+                .addAsWebResource(new 
StringAsset("<html><body>LOGIN_ERROR</body></html>"), "error.html")
+                .addAsWebResource(new 
StringAsset("<html><body>TOP_SECRET</body></html>"), "protected/secret.html")
+                .setWebXML(new StringAsset(webXml));
+    }
+
+    @Test
+    public void soapEndpointIsGuardedByBasic() throws Exception {
+        final HttpURLConnection connection = (HttpURLConnection) 
url("webservices/ws/Greeter").openConnection();
+        try {
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Content-Type", "text/xml; 
charset=UTF-8");
+            connection.setRequestProperty("SOAPAction", "\"\"");
+            connection.setInstanceFollowRedirects(false);
+            connection.setDoOutput(true);
+
+            try (final OutputStream out = connection.getOutputStream()) {
+                out.write(SOAP_REQUEST.getBytes(UTF_8));
+            }
+
+            final int status = connection.getResponseCode();
+            assertEquals("Unauthenticated SOAP call must get a BASIC 401, not 
the FORM flow. "
+                    + "Actual status: " + status, HTTP_UNAUTHORIZED, status);
+
+            final String challenge = 
connection.getHeaderField("WWW-Authenticate");
+            assertNotNull("The SOAP 401 must carry a WWW-Authenticate 
challenge", challenge);
+            assertTrue("Expected a BASIC challenge on the SOAP endpoint but 
was: " + challenge,
+                    challenge.toLowerCase().startsWith("basic"));
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    @Ignore("Known limitation: a Tomcat context has a single authenticator, 
and at web service "
+            + "registration time the WAR's web.xml <login-config> is not yet 
applied to the context, "
+            + "so securing the endpoint installs BASIC context-wide and 
overrides the web application's "
+            + "FORM login. True FORM-webapp + BASIC-SOAP coexistence needs the 
secured endpoint deployed "
+            + "into its own generated sub-context (as the 
JAR/deployInFakeWebapp path already does).")
+    @Test
+    public void webappPageIsGuardedByForm() throws Exception {
+        final HttpURLConnection connection = (HttpURLConnection) 
url("protected/secret.html").openConnection();
+        try {
+            connection.setRequestMethod("GET");
+            connection.setInstanceFollowRedirects(false);
+
+            final int status = connection.getResponseCode();
+
+            // The web application uses FORM, so an unauthenticated request is 
driven through the
+            // login form (Tomcat forwards to form-login-page with a 200) - 
never a BASIC challenge.
+            final String wwwAuth = 
connection.getHeaderField("WWW-Authenticate");
+            assertTrue("A FORM-protected page must not answer with a BASIC 
challenge but got: " + wwwAuth,
+                    wwwAuth == null || 
!wwwAuth.toLowerCase().startsWith("basic"));
+
+            final String body = read(connection);
+            assertTrue("Expected the protected page to be withheld and the 
FORM login page served "
+                            + "instead (status " + status + "), body was: " + 
body,
+                    body.contains(LOGIN_MARKER) && 
!body.contains("TOP_SECRET"));
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    private URL url(final String path) throws Exception {
+        String root = base.toExternalForm();
+        if (!root.endsWith("/")) {
+            root += "/";
+        }
+        return new URL(root + path);
+    }
+
+    private static String read(final HttpURLConnection connection) throws 
Exception {
+        final InputStream in = connection.getResponseCode() < 400
+                ? connection.getInputStream() : connection.getErrorStream();
+        if (in == null) {
+            return "";
+        }
+        try (in) {
+            return new String(in.readAllBytes(), UTF_8);
+        }
+    }
+}
diff --git 
a/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthTest.java
 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthTest.java
new file mode 100644
index 0000000000..73596afea7
--- /dev/null
+++ 
b/arquillian/arquillian-tomee-tests/arquillian-tomee-jaxws-tests/src/test/java/org/apache/openejb/arquillian/tests/jaxws/basicauth/WsBasicAuthTest.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.openejb.arquillian.tests.jaxws.basicauth;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.junit.Arquillian;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Demonstrates a gap in the "common EJB-in-WAR packaging" route through
+ * {@link org.apache.tomee.webservices.TomcatWsRegistry#addWsContainer}.
+ *
+ * <p>A singleton EJB is exposed over SOAP and declares that its endpoint 
requires BASIC
+ * authentication via {@code openejb-jar.xml}'s {@code <web-service-security>} 
element. That
+ * declaration flows all the way to {@code addWsContainer} as the {@code 
authMethod} argument.</p>
+ *
+ * <p>When the same bean is packaged in a plain JAR it is deployed into a 
fresh, generated
+ * context by {@code deployInFakeWebapp()} / {@code createNewContext()}, which 
honours
+ * {@code authMethod} by installing a {@code LoginConfig}, a security 
constraint and a
+ * {@code BasicAuthenticator}. But when it is packaged in a WAR the web 
application context
+ * already exists, so {@code addWsContainer} takes the {@code addServlet(...)} 
branch — which
+ * never receives {@code authMethod}/{@code realmName}/{@code 
transportGuarantee} and therefore
+ * applies no security at all. The declared BASIC requirement is silently 
dropped.</p>
+ *
+ * <p>This is not merely cosmetic. A WAR is commonly secured with FORM login 
so its pages behave
+ * like a normal web application; a Tomcat context has a single authenticator, 
so that same FORM
+ * mechanism cannot protect a SOAP endpoint (a SOAP client cannot follow an 
HTML login form). The
+ * web service therefore relies on its own BASIC declaration to be enforced — 
which, on this path,
+ * it is not.</p>
+ *
+ * <p>The test asserts the <em>desired</em> behaviour: an unauthenticated SOAP 
call must be
+ * rejected with a 401 BASIC challenge. Against an unpatched server it fails 
(the call is served
+ * with HTTP 200), documenting the gap; with the fix that threads the declared 
auth into the
+ * {@code addServlet} path it passes.</p>
+ */
+@RunWith(Arquillian.class)
+public class WsBasicAuthTest {
+
+    private static final String SOAP_REQUEST =
+            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+            "<soapenv:Envelope 
xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\"\n"; +
+            "                  
xmlns:ns=\"http://basicauth.jaxws.tests.arquillian.openejb.apache.org/\";>\n" +
+            "  <soapenv:Header/>\n" +
+            "  <soapenv:Body>\n" +
+            "    <ns:greet>\n" +
+            "      <name>world</name>\n" +
+            "    </ns:greet>\n" +
+            "  </soapenv:Body>\n" +
+            "</soapenv:Envelope>";
+
+    @ArquillianResource
+    private URL base;
+
+    @Deployment(testable = false)
+    public static WebArchive war() {
+        final String ejbJar =
+                "<ejb-jar xmlns=\"http://java.sun.com/xml/ns/javaee\"\n"; +
+                "         
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "         
xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd\"\n"; +
+                "         version=\"3.1\" metadata-complete=\"false\">\n" +
+                "  <enterprise-beans>\n" +
+                "    <session>\n" +
+                "      <ejb-name>GreeterBean</ejb-name>\n" +
+                "      
<service-endpoint>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterWs</service-endpoint>\n"
 +
+                "      
<ejb-class>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterBean</ejb-class>\n"
 +
+                "      <session-type>Singleton</session-type>\n" +
+                "      <transaction-type>Container</transaction-type>\n" +
+                "    </session>\n" +
+                "  </enterprise-beans>\n" +
+                "</ejb-jar>";
+
+        // The web service declares its own BASIC auth requirement. There is 
deliberately NO
+        // security-constraint in web.xml: the point is that the endpoint must 
be protected by
+        // virtue of this declaration alone, exactly as it would be for a 
JAR-packaged bean.
+        final String openejbJar =
+                "<openejb-jar 
xmlns=\"http://www.openejb.org/xml/ns/openejb-jar-2.1\";>\n" +
+                "  <enterprise-beans>\n" +
+                "    <session>\n" +
+                "      <ejb-name>GreeterBean</ejb-name>\n" +
+                "      
<web-service-address>/ws/Greeter</web-service-address>\n" +
+                "      <web-service-security>\n" +
+                "        <security-realm-name/>\n" +
+                "        <transport-guarantee>NONE</transport-guarantee>\n" +
+                "        <auth-method>BASIC</auth-method>\n" +
+                "      </web-service-security>\n" +
+                "    </session>\n" +
+                "  </enterprise-beans>\n" +
+                "</openejb-jar>";
+
+        final String webservices =
+                "<webservices xmlns=\"http://java.sun.com/xml/ns/j2ee\"\n"; +
+                "             
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "             
xsi:schemaLocation=\"http://java.sun.com/xml/ns/j2ee 
http://www.ibm.com/webservices/xsd/j2ee_web_services_1_1.xsd\"\n"; +
+                "             version=\"1.1\">\n" +
+                "  <webservice-description>\n" +
+                "    
<webservice-description-name>GreeterService</webservice-description-name>\n" +
+                "    <port-component>\n" +
+                "      
<port-component-name>GreeterPort</port-component-name>\n" +
+                "      <wsdl-port>GreeterPort</wsdl-port>\n" +
+                "      
<service-endpoint-interface>org.apache.openejb.arquillian.tests.jaxws.basicauth.GreeterWs</service-endpoint-interface>\n"
 +
+                "      <service-impl-bean>\n" +
+                "        <ejb-link>GreeterBean</ejb-link>\n" +
+                "      </service-impl-bean>\n" +
+                "    </port-component>\n" +
+                "  </webservice-description>\n" +
+                "</webservices>";
+
+        // A minimal, unsecured web.xml: this is an ordinary WAR that happens 
to host a SOAP EJB.
+        final String webXml =
+                "<web-app xmlns=\"https://jakarta.ee/xml/ns/jakartaee\"\n"; +
+                "         
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"; +
+                "         
xsi:schemaLocation=\"https://jakarta.ee/xml/ns/jakartaee 
https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd\"\n"; +
+                "         version=\"6.0\">\n" +
+                "  <display-name>WsBasicAuthTest</display-name>\n" +
+                "</web-app>";
+
+        return ShrinkWrap.create(WebArchive.class, "WsBasicAuthTest.war")
+                .addClasses(GreeterWs.class, GreeterBean.class)
+                .addAsWebInfResource(new StringAsset(ejbJar), "ejb-jar.xml")
+                .addAsWebInfResource(new StringAsset(openejbJar), 
"openejb-jar.xml")
+                .addAsWebInfResource(new StringAsset(webservices), 
"webservices.xml")
+                .setWebXML(new StringAsset(webXml));
+    }
+
+    @Test
+    public void soapWithoutCredentialsIsUnauthorized() throws Exception {
+        final HttpURLConnection connection = (HttpURLConnection) 
endpoint().openConnection();
+        try {
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Content-Type", "text/xml; 
charset=UTF-8");
+            connection.setRequestProperty("SOAPAction", "\"\"");
+            connection.setDoOutput(true);
+
+            try (final OutputStream out = connection.getOutputStream()) {
+                out.write(SOAP_REQUEST.getBytes(UTF_8));
+            }
+
+            final int status = connection.getResponseCode();
+            assertEquals("The web service declares BASIC auth, so an 
unauthenticated SOAP call "
+                    + "must be rejected with a 401 - not served. Actual 
status: " + status,
+                    HTTP_UNAUTHORIZED, status);
+
+            final String challenge = 
connection.getHeaderField("WWW-Authenticate");
+            assertNotNull("A 401 must carry a WWW-Authenticate challenge", 
challenge);
+            assertTrue("Expected a BASIC challenge but was: " + challenge,
+                    challenge.toLowerCase().startsWith("basic"));
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    private URL endpoint() throws Exception {
+        String root = base.toExternalForm();
+        if (!root.endsWith("/")) {
+            root += "/";
+        }
+        // WEBSERVICE_SUB_CONTEXT (/webservices) + the web-service-address 
(/ws/Greeter)
+        return new URL(root + "webservices/ws/Greeter");
+    }
+}
diff --git 
a/tomee/tomee-webservices/src/main/java/org/apache/tomee/webservices/TomcatWsRegistry.java
 
b/tomee/tomee-webservices/src/main/java/org/apache/tomee/webservices/TomcatWsRegistry.java
index 523733b010..9391e53f95 100644
--- 
a/tomee/tomee-webservices/src/main/java/org/apache/tomee/webservices/TomcatWsRegistry.java
+++ 
b/tomee/tomee-webservices/src/main/java/org/apache/tomee/webservices/TomcatWsRegistry.java
@@ -22,7 +22,9 @@ import org.apache.catalina.Context;
 import org.apache.catalina.Engine;
 import org.apache.catalina.Host;
 import org.apache.catalina.Service;
+import org.apache.catalina.Valve;
 import org.apache.catalina.Wrapper;
+import org.apache.catalina.authenticator.AuthenticatorBase;
 import org.apache.catalina.authenticator.BasicAuthenticator;
 import org.apache.catalina.authenticator.DigestAuthenticator;
 import org.apache.catalina.authenticator.NonLoginAuthenticator;
@@ -37,6 +39,8 @@ import org.apache.openejb.loader.SystemInstance;
 import org.apache.openejb.server.httpd.HttpListener;
 import org.apache.openejb.server.webservices.WsRegistry;
 import org.apache.openejb.server.webservices.WsServlet;
+import org.apache.openejb.util.LogCategory;
+import org.apache.openejb.util.Logger;
 import org.apache.openejb.util.Strings;
 import org.apache.tomcat.util.descriptor.web.LoginConfig;
 import org.apache.tomcat.util.descriptor.web.SecurityCollection;
@@ -58,6 +62,8 @@ import java.util.concurrent.ConcurrentHashMap;
 import static java.util.Arrays.asList;
 
 public class TomcatWsRegistry implements WsRegistry {
+    private static final Logger LOGGER = 
Logger.getInstance(LogCategory.OPENEJB_WS, TomcatWsRegistry.class);
+
     private static final String WEBSERVICE_SUB_CONTEXT = 
Strings.slashify(SystemInstance.get().getOptions().get("tomee.jaxws.subcontext",
 "/webservices"));
 
     private static final boolean WEBSERVICE_OLDCONTEXT_ACTIVE = 
SystemInstance.get().getOptions().get("tomee.jaxws.oldsubcontext", false);
@@ -209,7 +215,9 @@ public class TomcatWsRegistry implements WsRegistry {
 
             if (webAppContext != null) {
                 // sub context = '/' means the service address is provided by 
webservices
-                    addServlet(host, webAppContext, 
Strings.slashify(WEBSERVICE_SUB_CONTEXT, path), httpListener,
+                    final String mapping = 
Strings.slashify(WEBSERVICE_SUB_CONTEXT, path);
+                    secureWebserviceMapping(webAppContext, mapping, 
authMethod, transportGuarantee, realmName);
+                    addServlet(host, webAppContext, mapping, httpListener,
                                path, addresses, false, moduleId);
             } else if (!WEBSERVICE_OLDCONTEXT_ACTIVE) { // deploying in a jar
                 deployInFakeWebapp(path, classLoader, authMethod, 
transportGuarantee,
@@ -219,6 +227,83 @@ public class TomcatWsRegistry implements WsRegistry {
         return addresses;
     }
 
+    /**
+     * The endpoint is published on a TomEE generated mapping inside an 
existing web context, so the
+     * application's own web.xml security constraints do not cover it. Enforce 
the configured
+     * authMethod/transportGuarantee on that mapping (mirroring what 
createNewContext does for the
+     * fake-webapp deployment) instead of silently publishing the endpoint 
unprotected.
+     */
+    private static void secureWebserviceMapping(final Context context, final 
String mapping,
+                                                String authMethod, String 
transportGuarantee, final String realmName) {
+        if (authMethod != null) {
+            authMethod = authMethod.toUpperCase();
+        }
+        if (transportGuarantee != null) {
+            transportGuarantee = transportGuarantee.toUpperCase();
+        }
+        if (authMethod == null || "NONE".equals(authMethod)) { //NOPMD
+            // no authentication was configured for the endpoint
+            return;
+        }
+        if (!"BASIC".equals(authMethod) && !"DIGEST".equals(authMethod) && 
!"CLIENT-CERT".equals(authMethod)) {
+            throw new IllegalArgumentException("Invalid authMethod: " + 
authMethod);
+        }
+
+        //Setup a Security Constraint on the generated webservice mapping (all 
HTTP methods)
+        final String securityRole = 
SystemInstance.get().getProperty(TOMEE_JAXWS_SECURITY_ROLE_PREFIX + 
context.getName(), "default");
+        for (final String role : securityRole.split(",")) {
+            final SecurityCollection collection = new SecurityCollection();
+            collection.addPattern(mapping);
+            collection.setName(role);
+
+            final SecurityConstraint sc = new SecurityConstraint();
+            sc.addAuthRole("*");
+            sc.addCollection(collection);
+            sc.setAuthConstraint(true);
+            sc.setUserConstraint(transportGuarantee);
+
+            context.addConstraint(sc);
+            context.addSecurityRole(role);
+        }
+
+        //Setup a login configuration if the webapp does not carry its own
+        final LoginConfig loginConfig = context.getLoginConfig();
+        if (loginConfig == null || loginConfig.getAuthMethod() == null) {
+            final LoginConfig config = new LoginConfig();
+            config.setAuthMethod(authMethod);
+            config.setRealmName(realmName);
+            context.setLoginConfig(config);
+        } else if (!authMethod.equalsIgnoreCase(loginConfig.getAuthMethod())) {
+            LOGGER.warning("Webservice endpoint " + mapping + " in context " + 
context.getName()
+                           + " requested auth method " + authMethod + " but 
the web application declares "
+                           + loginConfig.getAuthMethod() + "; the web 
application setting is kept");
+        }
+
+        //Make sure an authenticator able to challenge the caller is in the 
pipeline
+        Valve authenticator = null;
+        for (final Valve valve : context.getPipeline().getValves()) {
+            if (valve instanceof AuthenticatorBase) {
+                authenticator = valve;
+                break;
+            }
+        }
+        if (authenticator instanceof NonLoginAuthenticator) {
+            // installed when the webapp has no login-config: it can never 
authenticate a caller
+            context.getPipeline().removeValve(authenticator);
+            authenticator = null;
+        }
+        if (authenticator == null) {
+            final String method = 
context.getLoginConfig().getAuthMethod().toUpperCase();
+            if ("BASIC".equals(method)) {
+                context.getPipeline().addValve(new BasicAuthenticator());
+            } else if ("DIGEST".equals(method)) {
+                context.getPipeline().addValve(new DigestAuthenticator());
+            } else if ("CLIENT-CERT".equals(method)) {
+                context.getPipeline().addValve(new SSLAuthenticator());
+            }
+        }
+    }
+
     private Context findContext(final String context, final String moduleId, 
final Container host) {
         String root = context;
         if ("ROOT".equals(root)) {

Reply via email to