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)) {