diff --git a/docs/microprofile/jwt.adoc b/docs/microprofile/jwt.adoc index 270be03e6ed..d1b2e948e00 100644 --- a/docs/microprofile/jwt.adoc +++ b/docs/microprofile/jwt.adoc @@ -81,7 +81,8 @@ In addition to the standard MicroProfile JWT configuration properties above, the | Property | Type | Description -| `mp.jwt.tomee.allow.no-exp` +| `mp.jwt.tomee.allow.no-exp` is deprecated please use `tomee.mp.jwt.allow.no-exp` property instead +| `tomee.mp.jwt.allow.no-exp` | Boolean | Disables enforcing the `exp` time of the JWT. Useful if JWTs are also verified by an API Gateway or proxy before reaching the server. The default value is `false` | `tomee.jwt.verify.publickey.cache` diff --git a/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/AllowNoExpPropertyTest.java b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/AllowNoExpPropertyTest.java new file mode 100644 index 00000000000..467cc17a6de --- /dev/null +++ b/itests/microprofile-jwt-itests/src/test/java/org/apache/tomee/microprofile/jwt/itest/AllowNoExpPropertyTest.java @@ -0,0 +1,253 @@ +/* + * 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.microprofile.jwt.itest; + +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.RequestScoped; +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 java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Base64; +import static java.util.Collections.singletonList; +import java.util.Optional; +import org.apache.cxf.feature.LoggingFeature; +import org.apache.cxf.jaxrs.client.WebClient; +import org.apache.johnzon.jaxrs.JohnzonProvider; +import org.apache.tomee.server.composer.Archive; +import org.apache.tomee.server.composer.TomEE; +import org.eclipse.microprofile.auth.LoginConfig; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + + +public class AllowNoExpPropertyTest { + + @Test + public void testNewPropertyOverridesOld1() throws Exception { + final Tokens tokens = Tokens.rsa(2048, 256); + final File appJar = Archive.archive() + .add(AllowNoExpPropertyTest.class) + .add(ColorService.class) + .add(Api.class) + .add("META-INF/microprofile-config.properties", "#\n" + + "mp.jwt.verify.publickey=" + Base64.getEncoder().encodeToString(tokens.getPublicKey().getEncoded()) + + "\n" + "mp.jwt.tomee.allow.no-exp=false" + + "\n" + "tomee.mp.jwt.allow.no-exp=true") + .asJar(); + + final ArrayList output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.microprofile.jwt.", "\n", output::add) + .build(); + + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + + final String claims = "{" + + " \"sub\":\"Jane Awesome\"" + + "}"; + + {// invalid token + final String token = tokens.asToken(claims); + final Response response = webClient.reset() + .path("/movies") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + token) + .get(); + assertEquals(403, response.getStatus()); + } + + assertPresent(output , "mp.jwt.tomee.allow.no-exp property is deprecated"); + assertNotPresent(output, "rejected due to invalid claims"); + assertNotPresent(output, "No Expiration Time (exp) claim present."); + assertNotPresent(output, "\tat org."); // no stack traces + } + + @Test + public void testNewPropertyOverridesOld2() throws Exception { + final Tokens tokens = Tokens.rsa(2048, 256); + final File appJar = Archive.archive() + .add(AllowNoExpPropertyTest.class) + .add(ColorService.class) + .add(Api.class) + .add("META-INF/microprofile-config.properties", "#\n" + + "mp.jwt.verify.publickey=" + Base64.getEncoder().encodeToString(tokens.getPublicKey().getEncoded()) + + "\n" + "mp.jwt.tomee.allow.no-exp=true" + + "\n" + "tomee.mp.jwt.allow.no-exp=false") + .asJar(); + + final ArrayList output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.microprofile.jwt.", "\n", output::add) + .build(); + + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + + final String claims = "{" + + " \"sub\":\"Jane Awesome\"" + + "}"; + + {// invalid token + final String token = tokens.asToken(claims); + final Response response = webClient.reset() + .path("/movies") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + token) + .get(); + assertEquals(401, response.getStatus()); + } + + assertPresent(output , "mp.jwt.tomee.allow.no-exp property is deprecated"); + assertPresent(output, "rejected due to invalid claims"); + assertPresent(output, "No Expiration Time (exp) claim present."); + assertNotPresent(output, "\tat org."); // no stack traces + } + + @Test + public void testNewProperty() throws Exception { + final Tokens tokens = Tokens.rsa(2048, 256); + final File appJar = Archive.archive() + .add(AllowNoExpPropertyTest.class) + .add(ColorService.class) + .add(Api.class) + .add("META-INF/microprofile-config.properties", "#\n" + + "mp.jwt.verify.publickey=" + Base64.getEncoder().encodeToString(tokens.getPublicKey().getEncoded()) + + "\n" + "tomee.mp.jwt.allow.no-exp=true") + .asJar(); + + final ArrayList output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.microprofile.jwt.", "\n", output::add) + .build(); + + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + + final String claims = "{" + + " \"sub\":\"Jane Awesome\"" + + "}"; + + {// invalid token + final String token = tokens.asToken(claims); + final Response response = webClient.reset() + .path("/movies") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + token) + .get(); + assertEquals(403, response.getStatus()); + } + + assertNotPresent(output , "mp.jwt.tomee.allow.no-exp property is deprecated"); + assertNotPresent(output, "rejected due to invalid claims"); + assertNotPresent(output, "No Expiration Time (exp) claim present."); + assertNotPresent(output, "\tat org."); // no stack traces + } + + @Test + public void testOldProperty() throws Exception { + final Tokens tokens = Tokens.rsa(2048, 256); + final File appJar = Archive.archive() + .add(AllowNoExpPropertyTest.class) + .add(ColorService.class) + .add(Api.class) + .add("META-INF/microprofile-config.properties", "#\n" + + "mp.jwt.verify.publickey=" + Base64.getEncoder().encodeToString(tokens.getPublicKey().getEncoded()) + + "\n" + "mp.jwt.tomee.allow.no-exp=true") + .asJar(); + + final ArrayList output = new ArrayList<>(); + final TomEE tomee = TomEE.microprofile() + .add("webapps/test/WEB-INF/beans.xml", "") + .add("webapps/test/WEB-INF/lib/app.jar", appJar) + .watch("org.apache.tomee.microprofile.jwt.", "\n", output::add) + .build(); + + final WebClient webClient = createWebClient(tomee.toURI().resolve("/test").toURL()); + + final String claims = "{" + + " \"sub\":\"Jane Awesome\"" + + "}"; + + {// invalid token + final String token = tokens.asToken(claims); + final Response response = webClient.reset() + .path("/movies") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + token) + .get(); + assertEquals(403, response.getStatus()); + } + + assertPresent(output , "mp.jwt.tomee.allow.no-exp property is deprecated"); + assertNotPresent(output, "rejected due to invalid claims"); + assertNotPresent(output, "No Expiration Time (exp) claim present."); + assertNotPresent(output, "\tat org."); // no stack traces + } + + public void assertPresent(final ArrayList output, final String s) { + final Optional actual = output.stream() + .filter(line -> line.contains(s)) + .findFirst(); + + assertTrue(actual.isPresent()); + } + public void assertNotPresent(final ArrayList output, final String s) { + final Optional 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") + @LoginConfig(authMethod = "MP-JWT") + public class Api extends Application { + } + + @Path("/movies") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @RequestScoped + public static class ColorService { + + @GET + @RolesAllowed({"manager", "user"}) + public String getAllMovies() { + return "Green"; + } + } + +} diff --git a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java index e2f4823eefb..65fac639957 100644 --- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java +++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationProperties.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import static org.eclipse.microprofile.jwt.config.Names.AUDIENCES; @@ -59,6 +60,7 @@ public class JWTAuthConfigurationProperties { public static final String PUBLIC_KEY_ERROR = "Could not read MicroProfile Public Key"; public static final String PUBLIC_KEY_ERROR_LOCATION = PUBLIC_KEY_ERROR + " from Location: "; + private static final Logger CONFIGURATION = Logger.getInstance(JWTLogCategories.CONFIG, JWTAuthConfigurationProperties.class); private Config config; private JWTAuthConfiguration jwtAuthConfiguration; @@ -104,8 +106,8 @@ private JWTAuthConfiguration createJWTAuthConfiguration() { final Supplier> publicKeys = Keys.VERIFY.configure(config); final Supplier> decryptKeys = Keys.DECRYPT.configure(config); - final Boolean allowNoExp = config.getOptionalValue("mp.jwt.tomee.allow.no-exp", Boolean.class).orElse(false); - + final Boolean allowNoExp = queryAllowExp(); + return new JWTAuthConfiguration( publicKeys, getIssuer().orElse(null), @@ -117,6 +119,15 @@ private JWTAuthConfiguration createJWTAuthConfiguration() { config.getOptionalValue("mp.jwt.decrypt.key.algorithm", String.class).orElse(null), config.getOptionalValue("mp.jwt.verify.publickey.algorithm", String.class).orElse(null)); } + + private Boolean queryAllowExp(){ + AtomicBoolean result = new AtomicBoolean(false); + config.getOptionalValue("mp.jwt.tomee.allow.no-exp", Boolean.class).ifPresent(value -> { + result.set(value); + CONFIGURATION.warning("mp.jwt.tomee.allow.no-exp property is deprecated, use tomee.mp.jwt.allow.no-exp propert instead."); + }); + return config.getOptionalValue("tomee.mp.jwt.allow.no-exp", Boolean.class).orElse(result.get()); + } enum Keys { VERIFY("mp.jwt.verify.publickey", "tomee.jwt.verify.publickey"),