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 86d5ede3d5 reject negative mp.jwt clock skew instead of disabling exp
checks (#3060)
86d5ede3d5 is described below
commit 86d5ede3d592bf8ea398379a86a809d8e6609718
Author: Richard Zowalla <[email protected]>
AuthorDate: Wed Oct 7 17:33:41 2026 +0200
reject negative mp.jwt clock skew instead of disabling exp checks (#3060)
A negative skew evaluated tokens at epoch 0, accepting expired tokens.
Fail deployment on a negative value and clamp to zero in the filter.
---
.../apache/tomee/microprofile/jwt/MPJWTFilter.java | 9 +-
.../jwt/config/JWTAuthConfigurationProperties.java | 10 +-
.../tomee/microprofile/jwt/ClockSkewTest.java | 102 +++++++++++++++++++++
.../config/JWTAuthConfigurationPropertiesTest.java | 44 +++++++++
4 files changed, 158 insertions(+), 7 deletions(-)
diff --git
a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java
b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java
index 5108c49d48..95a739b0e7 100644
--- a/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java
+++ b/mp-jwt/src/main/java/org/apache/tomee/microprofile/jwt/MPJWTFilter.java
@@ -49,7 +49,6 @@ import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.MalformedClaimException;
-import org.jose4j.jwt.NumericDate;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
@@ -425,11 +424,9 @@ public class MPJWTFilter implements Filter {
if (authContextInfo.getIssuer() != null) {
builder.setExpectedIssuer(authContextInfo.getIssuer());
}
- if (authContextInfo.getClockSkew()>= 0) {
-
builder.setAllowedClockSkewInSeconds(authContextInfo.getClockSkew());
- } else {
- builder.setEvaluationTime(NumericDate.fromSeconds(0));
- }
+ // never let a negative skew weaken or disable the exp/nbf/iat
time checks
+ final Integer clockSkew = authContextInfo.getClockSkew();
+ builder.setAllowedClockSkewInSeconds(clockSkew == null ? 0 :
Math.max(0, clockSkew));
final Map<String, Key> publicKeys;
try {
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 f96aaba1a8..d04bd2ab9d 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
@@ -120,7 +120,15 @@ public class JWTAuthConfigurationProperties {
config.getOptionalValue("mp.jwt.decrypt.key.algorithm",
String.class).orElse(null),
config.getOptionalValue("mp.jwt.verify.publickey.algorithm",
String.class).orElse(null),
config.getOptionalValue(TOKEN_AGE, Integer.class).orElse(null),
- config.getOptionalValue(CLOCK_SKEW, Integer.class).orElse(0));
+ validateClockSkew(config.getOptionalValue(CLOCK_SKEW,
Integer.class).orElse(0)));
+ }
+
+ static Integer validateClockSkew(final Integer clockSkew) {
+ if (clockSkew != null && clockSkew < 0) {
+ throw new DeploymentException("Invalid " + CLOCK_SKEW + " value: "
+ clockSkew +
+ ". The clock skew must be zero or a positive number of
seconds.");
+ }
+ return clockSkew;
}
private Boolean queryAllowExp(){
diff --git
a/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/ClockSkewTest.java
b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/ClockSkewTest.java
new file mode 100644
index 0000000000..51dd309f45
--- /dev/null
+++ b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/ClockSkewTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+import org.apache.tomee.microprofile.jwt.config.JWTAuthConfiguration;
+import org.jose4j.jwk.RsaJsonWebKey;
+import org.jose4j.jwk.RsaJwkGenerator;
+import org.jose4j.jws.AlgorithmIdentifiers;
+import org.jose4j.jws.JsonWebSignature;
+import org.jose4j.jwt.JwtClaims;
+import org.jose4j.jwt.NumericDate;
+import org.junit.Test;
+
+import java.security.Key;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class ClockSkewTest {
+
+ @Test
+ public void expiredTokenIsRejectedWithNegativeClockSkew() throws Exception
{
+ final RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
+ final long now = NumericDate.now().getValue();
+ final String expired = sign(key, now - 7200, now - 3600);
+
+ assertRejected(expired, config(key, -1));
+ assertRejected(expired, config(key, Integer.MIN_VALUE));
+ }
+
+ @Test
+ public void expiredTokenIsRejectedWithNullClockSkew() throws Exception {
+ final RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
+ final long now = NumericDate.now().getValue();
+
+ assertRejected(sign(key, now - 7200, now - 3600), config(key, null));
+ }
+
+ @Test
+ public void validTokenIsAcceptedWithNegativeClockSkew() throws Exception {
+ final RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
+ final long now = NumericDate.now().getValue();
+
+ assertEquals("alice", MPJWTFilter.ValidateJSonWebToken.parse(sign(key,
now - 60, now + 3600), config(key, -1)).getName());
+ }
+
+ @Test
+ public void positiveClockSkewIsHonoured() throws Exception {
+ final RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
+ final long now = NumericDate.now().getValue();
+ final String recentlyExpired = sign(key, now - 600, now - 30);
+
+ assertEquals("alice",
MPJWTFilter.ValidateJSonWebToken.parse(recentlyExpired, config(key,
300)).getName());
+ assertRejected(recentlyExpired, config(key, 0));
+ }
+
+ private static void assertRejected(final String token, final
JWTAuthConfiguration config) {
+ try {
+ MPJWTFilter.ValidateJSonWebToken.parse(token, config);
+ fail("expired token must be rejected");
+ } catch (final ParseException expected) {
+ // ok
+ }
+ }
+
+ private static String sign(final RsaJsonWebKey key, final long issuedAt,
final long expiresAt) throws Exception {
+ final JwtClaims claims = new JwtClaims();
+ claims.setSubject("alice");
+ claims.setIssuer("https://server.example.com");
+ claims.setIssuedAt(NumericDate.fromSeconds(issuedAt));
+ claims.setExpirationTime(NumericDate.fromSeconds(expiresAt));
+
+ final JsonWebSignature jws = new JsonWebSignature();
+ jws.setPayload(claims.toJson());
+ jws.setKey(key.getPrivateKey());
+ jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
+ return jws.getCompactSerialization();
+ }
+
+ private static JWTAuthConfiguration config(final RsaJsonWebKey key, final
Integer clockSkew) {
+ return new JWTAuthConfiguration(
+ () -> Collections.<String,
Key>singletonMap(JWTAuthConfiguration.DEFAULT_KEY, key.getPublicKey()),
+ "https://server.example.com", false, new String[0],
+ LinkedHashMap::new, "Authorization", null, null, null, null,
clockSkew);
+ }
+}
diff --git
a/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationPropertiesTest.java
b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationPropertiesTest.java
new file mode 100644
index 0000000000..842717696c
--- /dev/null
+++
b/mp-jwt/src/test/java/org/apache/tomee/microprofile/jwt/config/JWTAuthConfigurationPropertiesTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.config;
+
+import jakarta.enterprise.inject.spi.DeploymentException;
+import org.junit.Test;
+
+import static org.eclipse.microprofile.jwt.config.Names.CLOCK_SKEW;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class JWTAuthConfigurationPropertiesTest {
+
+ @Test
+ public void nonNegativeClockSkewIsAccepted() {
+ assertEquals(Integer.valueOf(0),
JWTAuthConfigurationProperties.validateClockSkew(0));
+ assertEquals(Integer.valueOf(60),
JWTAuthConfigurationProperties.validateClockSkew(60));
+ }
+
+ @Test
+ public void negativeClockSkewFailsDeployment() {
+ try {
+ JWTAuthConfigurationProperties.validateClockSkew(-1);
+ fail("a negative clock skew must fail the deployment");
+ } catch (final DeploymentException expected) {
+ assertTrue(expected.getMessage().contains(CLOCK_SKEW));
+ }
+ }
+}