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));
+        }
+    }
+}

Reply via email to