This is an automated email from the ASF dual-hosted git repository. jlmonteiro pushed a commit to branch TOMEE-4065_LoginToContinue in repository https://gitbox.apache.org/repos/asf/tomee.git
commit b7f32fa8e2cdd96b264132be88e21949ac0be215 Author: Jean-Louis Monteiro <[email protected]> AuthorDate: Tue Nov 8 18:27:20 2022 +0100 TOMEE-4065 Fix to allow @LoginToContinue to be used directly in the HttpAuthenticationMechanism instead of requiring the interface to be implemented. Thanks Stefan Kalscheuer for reporting and for the fix --- itests/pom.xml | 1 + itests/tomee-security-itests/pom.xml | 111 ++++++++++ .../tomee/security/itest/AnotherAuthMechanism.java | 83 ++++++++ .../apache/tomee/security/itest/AuthMechanism.java | 69 ++++++ .../tomee/security/itest/LoginToContinueTest.java | 235 +++++++++++++++++++++ .../security/cdi/BasicAuthenticationMechanism.java | 1 - .../security/cdi/LoginToContinueInterceptor.java | 6 +- ...curityServletAuthenticationMechanismMapper.java | 4 +- 8 files changed, 506 insertions(+), 4 deletions(-) diff --git a/itests/pom.xml b/itests/pom.xml index 7b8229a9bc..990c1ce2dc 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -49,6 +49,7 @@ <module>jaxrs</module> <module>itest-common</module> <module>itest-util</module> + <module>tomee-security-itests</module> </modules> <build> diff --git a/itests/tomee-security-itests/pom.xml b/itests/tomee-security-itests/pom.xml new file mode 100644 index 0000000000..e62d7d5349 --- /dev/null +++ b/itests/tomee-security-itests/pom.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>itests</artifactId> + <groupId>org.apache.tomee</groupId> + <version>9.0.0-SNAPSHOT</version> + </parent> + + <artifactId>tomee-security-itests</artifactId> + <packaging>jar</packaging> + <name>TomEE :: iTests :: TomEE Security</name> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <systemPropertyVariables> + <version>${project.version}</version> + </systemPropertyVariables> + <!-- + <reuseForks>true</reuseForks> + <forkCount>1C</forkCount> + --> + </configuration> + </plugin> + </plugins> + </build> + + <dependencies> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>tomee-server-composer</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>itest-common</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>itest-util</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>apache-tomee</artifactId> + <version>9.0.0-SNAPSHOT</version> + <type>tar.gz</type> + <classifier>microprofile</classifier> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>jakartaee-api</artifactId> + <version>9.1-M2</version> + <scope>provided</scope> + </dependency> + + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>openejb-cxf-rs</artifactId> + <version>${tomee.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.tomee</groupId> + <artifactId>tomee-security</artifactId> + <version>9.0.0-SNAPSHOT</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + <dependency> + <groupId>net.sourceforge.htmlunit</groupId> + <artifactId>htmlunit</artifactId> + <version>2.33</version> + <scope>test</scope> + </dependency> + </dependencies> +</project> + diff --git a/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AnotherAuthMechanism.java b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AnotherAuthMechanism.java new file mode 100644 index 0000000000..e9bd64ddbb --- /dev/null +++ b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AnotherAuthMechanism.java @@ -0,0 +1,83 @@ +/* + * 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.tomee.security.itest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.security.enterprise.AuthenticationException; +import jakarta.security.enterprise.AuthenticationStatus; +import jakarta.security.enterprise.authentication.mechanism.http.AutoApplySession; +import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; +import jakarta.security.enterprise.authentication.mechanism.http.HttpMessageContext; +import jakarta.security.enterprise.authentication.mechanism.http.LoginToContinue; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.HttpMethod; +import org.apache.tomee.security.http.LoginToContinueMechanism; + +import static java.util.Collections.singleton; + +@ApplicationScoped +@AutoApplySession +@LoginToContinue +public class AnotherAuthMechanism implements HttpAuthenticationMechanism, LoginToContinueMechanism { + + @Override + public AuthenticationStatus validateRequest( + HttpServletRequest request, + HttpServletResponse response, + HttpMessageContext httpMessageContext) throws + AuthenticationException { + final String token = request.getParameter("token"); + + if (validateForm(httpMessageContext.getRequest(), token)) { + + // validating the token would go here. We obviously send only a random string so + // not doing anything + if (!"1234ABCD".equals(token)) { + return httpMessageContext.responseUnauthorized(); + } + + return httpMessageContext.notifyContainerAboutLogin("jwt-token", singleton("tomcat")); + } + + return httpMessageContext.doNothing(); + } + + private boolean validateForm(final HttpServletRequest request, final String token) { + return request.getMethod().equals(HttpMethod.POST) && + request.getRequestURI().endsWith("/login-jwt") && + token != null && !token.isEmpty(); + } + + @Override + public LoginToContinue getLoginToContinue() { + return FakeAnnotationHolder.class.getAnnotation(LoginToContinue.class); + } + + // to make sure we use the method and not the annotation, we use this class instead of adding the annotation on the + // mechanism. If the annotation isn't read from this definition, it will fail because loginPage and errorPage are + // by default different + @LoginToContinue( + loginPage = "/login-app", + errorPage = "/login-error-app" + ) + public static class FakeAnnotationHolder { + + } +} \ No newline at end of file diff --git a/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AuthMechanism.java b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AuthMechanism.java new file mode 100644 index 0000000000..84e213b76c --- /dev/null +++ b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/AuthMechanism.java @@ -0,0 +1,69 @@ +/* + * 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.tomee.security.itest; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.security.enterprise.AuthenticationException; +import jakarta.security.enterprise.AuthenticationStatus; +import jakarta.security.enterprise.authentication.mechanism.http.AutoApplySession; +import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; +import jakarta.security.enterprise.authentication.mechanism.http.HttpMessageContext; +import jakarta.security.enterprise.authentication.mechanism.http.LoginToContinue; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.HttpMethod; + +import static java.util.Collections.singleton; + +@ApplicationScoped +@AutoApplySession +@LoginToContinue( + loginPage = "/login-app", + errorPage = "/login-error-app" +) +public class AuthMechanism implements HttpAuthenticationMechanism { + + @Override + public AuthenticationStatus validateRequest( + HttpServletRequest request, + HttpServletResponse response, + HttpMessageContext httpMessageContext) throws + AuthenticationException { + final String token = request.getParameter("token"); + + if (validateForm(httpMessageContext.getRequest(), token)) { + + // validating the token would go here. We obviously send only a random string so + // not doing anything + if (!"1234ABCD".equals(token)) { + return httpMessageContext.responseUnauthorized(); + } + + return httpMessageContext.notifyContainerAboutLogin("jwt-token", singleton("tomcat")); + } + + return httpMessageContext.doNothing(); + } + + private boolean validateForm(final HttpServletRequest request, final String token) { + return request.getMethod().equals(HttpMethod.POST) && + request.getRequestURI().endsWith("/login-jwt") && + token != null && !token.isEmpty(); + } +} \ No newline at end of file diff --git a/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/LoginToContinueTest.java b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/LoginToContinueTest.java new file mode 100644 index 0000000000..56417b8ac4 --- /dev/null +++ b/itests/tomee-security-itests/src/test/java/org/apache/tomee/security/itest/LoginToContinueTest.java @@ -0,0 +1,235 @@ +/* + * 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.tomee.security.itest; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.apache.cxf.feature.LoggingFeature; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.johnzon.jaxrs.JohnzonProvider; +import org.apache.tomee.security.cdi.TomcatUserIdentityStoreDefinition; +import org.apache.tomee.server.composer.Archive; +import org.apache.tomee.server.composer.TomEE; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Optional; + +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LoginToContinueTest { + + @Test + public void testAnnotation() throws Exception { + final File appJar = Archive.archive() + .add(this.getClass()) + .add(ColorService.class) + .add(Api.class) + .add(LoginServlet.class) + .add(ErrorServlet.class) + .add(TestServlet.class) + .add(AuthMechanism.class) + .asJar(); + + final ArrayList<String> output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + //.debug(5005, true) + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.", "\n", output::add) + .update() + .build(); + + + { // do something + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + final Response response = webClient.reset() + .path("/colors") + .header("Content-Type", "application/json") + .get(); + assertEquals(200, response.getStatus()); + } + + // assert logs + assertNotPresent(output, "\tat org."); // no stack traces + + { + final com.gargoylesoftware.htmlunit.WebClient webClient = new com.gargoylesoftware.htmlunit.WebClient(); + final HtmlPage page = webClient.getPage(tomee.toURI().resolve("/test/auth-app").toURL()); + assertEquals(200, page.getWebResponse().getStatusCode()); + + final HtmlForm login = page.getFormByName("login"); + login.getInputByName("token").setValueAttribute("1234ABCD"); + + final Page result = login.getInputByName("submit").click(); + assertEquals(200, result.getWebResponse().getStatusCode()); + assertEquals("ok!", result.getWebResponse().getContentAsString()); + } + } + + @Test + public void testInterface() throws Exception { + final File appJar = Archive.archive() + .add(this.getClass()) + .add(ColorService.class) + .add(Api.class) + .add(LoginServlet.class) + .add(ErrorServlet.class) + .add(TestServlet.class) + .add(AnotherAuthMechanism.class) + .add(AnotherAuthMechanism.FakeAnnotationHolder.class) + .asJar(); + + final ArrayList<String> output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + //.debug(5005, true) + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.", "\n", output::add) + .update() + .build(); + + + { // do something + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + final Response response = webClient.reset() + .path("/colors") + .header("Content-Type", "application/json") + .get(); + assertEquals(200, response.getStatus()); + } + + // assert logs + assertNotPresent(output, "\tat org."); // no stack traces + + { + final com.gargoylesoftware.htmlunit.WebClient webClient = new com.gargoylesoftware.htmlunit.WebClient(); + final HtmlPage page = webClient.getPage(tomee.toURI().resolve("/test/auth-app").toURL()); + assertEquals(200, page.getWebResponse().getStatusCode()); + + final HtmlForm login = page.getFormByName("login"); + login.getInputByName("token").setValueAttribute("1234ABCD"); + + final Page result = login.getInputByName("submit").click(); + assertEquals(200, result.getWebResponse().getStatusCode()); + assertEquals("ok!", result.getWebResponse().getContentAsString()); + } + } + + public void assertPresent(final ArrayList<String> output, final String s) { + final Optional<String> actual = output.stream() + .filter(line -> line.contains(s)) + .findFirst(); + + assertTrue(actual.isPresent()); + } + public void assertNotPresent(final ArrayList<String> output, final String s) { + final Optional<String> actual = output.stream() + .filter(line -> line.contains(s)) + .findFirst(); + + assertTrue(!actual.isPresent()); + } + + private static WebClient createWebClient(final URL base) { + return WebClient.create(base.toExternalForm(), singletonList(new JohnzonProvider<>()), + singletonList(new LoggingFeature()), null); + } + + @ApplicationPath("/api") + public class Api extends Application { + } + + @Path("/colors") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ApplicationScoped + public static class ColorService { + + @GET + public String getColor() { + return "Green"; + } + } + + @TomcatUserIdentityStoreDefinition + @WebServlet(urlPatterns = "/login-app") + public static class LoginServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + final String loginPage = + "<html>" + + "<body>" + + " <h1>Login Page</h1>" + + " <form name=\"login\" method=post action=\"login-jwt\">\n" + + " <p>JWT Token:</p>" + + " <input type=\"text\" name=\"token\">\n" + + " <input type=\"submit\" name=\"submit\" value=\"Submit\">\n" + + " <input type=\"reset\" value=\"Reset\">" + + " </form>" + + "</body>" + + "</html>"; + resp.getWriter().write(loginPage); + } + } + + @WebServlet(urlPatterns = "/login-error-app") + public static class ErrorServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + + } + } + + @TomcatUserIdentityStoreDefinition + @WebServlet(urlPatterns = "/auth-app") + @ServletSecurity(@HttpConstraint(rolesAllowed = "tomcat")) + public static class TestServlet extends HttpServlet { + @Override + protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) + throws ServletException, IOException { + resp.getWriter().write("ok!"); + } + } + +} \ No newline at end of file diff --git a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/BasicAuthenticationMechanism.java b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/BasicAuthenticationMechanism.java index cba0010bb7..e4bb93f991 100644 --- a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/BasicAuthenticationMechanism.java +++ b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/BasicAuthenticationMechanism.java @@ -23,7 +23,6 @@ import jakarta.security.enterprise.AuthenticationStatus; import jakarta.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition; import jakarta.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; import jakarta.security.enterprise.authentication.mechanism.http.HttpMessageContext; -import jakarta.security.enterprise.authentication.mechanism.http.LoginToContinue; import jakarta.security.enterprise.credential.BasicAuthenticationCredential; import jakarta.security.enterprise.identitystore.CredentialValidationResult; import jakarta.security.enterprise.identitystore.IdentityStoreHandler; diff --git a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/LoginToContinueInterceptor.java b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/LoginToContinueInterceptor.java index a608ef85b2..8ceeb31391 100644 --- a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/LoginToContinueInterceptor.java +++ b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/LoginToContinueInterceptor.java @@ -218,6 +218,10 @@ public class LoginToContinueInterceptor { return ((LoginToContinueMechanism) invocationContext.getTarget()).getLoginToContinue(); } - throw new IllegalArgumentException(); + if (invocationContext.getTarget().getClass().isAnnotationPresent(LoginToContinue.class)) { + return invocationContext.getTarget().getClass().getAnnotation(LoginToContinue.class); + } + + throw new IllegalArgumentException("Missing @LoginToContinue on type " + invocationContext.getTarget().getClass()); } } diff --git a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityServletAuthenticationMechanismMapper.java b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityServletAuthenticationMechanismMapper.java index d46eb907ee..1fc5ddaafc 100644 --- a/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityServletAuthenticationMechanismMapper.java +++ b/tomee/tomee-security/src/main/java/org/apache/tomee/security/cdi/TomEESecurityServletAuthenticationMechanismMapper.java @@ -74,7 +74,7 @@ public class TomEESecurityServletAuthenticationMechanismMapper { final Set<HttpAuthenticationMechanism> availableBeans = authenticationMechanisms.stream().collect(Collectors.toSet()); availableBeans.removeAll(servletAuthenticationMapper.values()); - availableBeans.remove(defaultAuthenticationMechanism); // this our wrapper + availableBeans.remove(defaultAuthenticationMechanism); // this is our wrapper if (availableBeans.size() == 1) { defaultAuthenticationMechanism.setDelegate(availableBeans.iterator().next()); @@ -89,7 +89,7 @@ public class TomEESecurityServletAuthenticationMechanismMapper { "Deploy a single one for the application, or associate it with a @WebServlet."); } else if (servletAuthenticationMapper.size() == 1) { - // don't think it's covered by the spec but sotera seems to support such a case + // don't think it's covered by the spec but soteria seems to support such a case defaultAuthenticationMechanism.setDelegate(servletAuthenticationMapper.values().iterator().next()); }
