This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit ba43471735218c416dbc3592a370a293ce5f6986 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) (cherry picked from commit 1834ac7cd0da7e2bccbeae051a17b3e1e6057451) --- .../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)) {
