This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch mpjwt-negative-clock-skew in repository https://gitbox.apache.org/repos/asf/tomee.git
commit fde3ddb53c5b348aa441a9ff219cb84ab7051e80 Author: Richard Zowalla <[email protected]> AuthorDate: Mon Oct 5 19:32:23 2026 +0200 reject negative mp.jwt clock skew instead of disabling exp checks 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)); + } + } +}
