[java-oidc-common] branch main updated: JCOMOIDC-120 - Improve jti-claim validator's handling of overly long values

Henri Mikkonen henri.mikkonen at iki.fi
Wed Sep 11 10:48:51 UTC 2024


This is an automated email from the git hooks/post-receive script.

hjmikkon pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=18bf0d16ad4055986ee3221815caec45c622489a

The following commit(s) were added to refs/heads/main by this push:
     new 18bf0d1  JCOMOIDC-120 - Improve jti-claim validator's handling of overly long values
18bf0d1 is described below

commit 18bf0d16ad4055986ee3221815caec45c622489a
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed Sep 11 13:48:38 2024 +0300

    JCOMOIDC-120 - Improve jti-claim validator's handling of overly long values
    
    https://shibboleth.atlassian.net/browse/JCOMOIDC-120
    
    - Add optional 'replayCacheKeyCalculationStrategy' hook to JWTIdentifierClaimsValidator
      - If non-null value set, it's used for calculating the storage key
    - New CalculateSha256DigestForLongKeyFunction may be used for calculating hash if key length exceeds 64
      - The value is hex string (64 hexadecimal characters)
---
 .../CalculateSha256DigestForLongKeyFunction.java   |  38 ++++++++
 .../claims/impl/JWTIdentifierClaimsValidator.java  |  40 ++++++--
 .../impl/JWTIdentifierClaimsValidatorTest.java     | 103 ++++++++++++++++++++-
 3 files changed, 173 insertions(+), 8 deletions(-)

diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/CalculateSha256DigestForLongKeyFunction.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/CalculateSha256DigestForLongKeyFunction.java
new file mode 100644
index 0000000..33aeae9
--- /dev/null
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/CalculateSha256DigestForLongKeyFunction.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed 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 net.shibboleth.oidc.security.jwt.claims.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+/**
+ * A function that calculates the SHA-256 digest and returns the value as a hex string, if the input length is more
+ * than 64 characters. Otherwise the input is returned intact.
+ */
+public class CalculateSha256DigestForLongKeyFunction implements Function<String, String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String apply(@Nullable final String input) {
+        if (input != null && input.length() > 64) {
+            return DigestUtils.sha256Hex(input);
+        }
+        return input;
+    }
+
+}
diff --git a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidator.java b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidator.java
index eb84d38..e303bf2 100644
--- a/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidator.java
+++ b/oidc-common-crypto-impl/src/main/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidator.java
@@ -17,6 +17,7 @@ package net.shibboleth.oidc.security.jwt.claims.impl;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
+import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -54,6 +55,11 @@ public class JWTIdentifierClaimsValidator extends AbstractClaimsValidator {
      */
     @Nullable private Duration replayCacheRecordLifetime;
 
+    /**
+     * Optional function to calculate they key used with {@link ReplayCache}.
+     */
+    @Nullable private Function<String, String> replayCacheKeyCalculationStrategy;
+
     /**
      * Constructor.
      */
@@ -62,6 +68,7 @@ public class JWTIdentifierClaimsValidator extends AbstractClaimsValidator {
         assert oneMinute != null;
         clockSkew = oneMinute;
         replayCacheRecordLifetime = null;
+        replayCacheKeyCalculationStrategy = null;
     }
     
     /**
@@ -96,6 +103,18 @@ public class JWTIdentifierClaimsValidator extends AbstractClaimsValidator {
         replayCacheRecordLifetime = lifetime;
     }
 
+    /**
+     * Set the function to calculate they key used with {@link ReplayCache}.
+     * 
+     * @param strategy function to set.
+     * 
+     * @since 3.2.0
+     */
+    public void setReplayCacheKeyCalculationStrategy(@Nullable final Function<String, String> strategy) {
+        ifInitializedThrowUnmodifiabledComponentException();
+        replayCacheKeyCalculationStrategy = strategy;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
@@ -121,14 +140,23 @@ public class JWTIdentifierClaimsValidator extends AbstractClaimsValidator {
         } else {
             expiry = exp.toInstant().plus(clockSkew);
         }
-        final String jit = claims.getJWTID();
-        if (StringSupport.trimOrNull(jit) == null) {
-            throw new JWTValidationException("The claims set is missing required JWT identifier (jit)");
+        final String jti = claims.getJWTID();
+        if (StringSupport.trimOrNull(jti) == null) {
+            throw new JWTValidationException("The claims set is missing required JWT identifier (jti)");
+        }
+        final String replayCacheKey;
+        if (replayCacheKeyCalculationStrategy != null) {
+            replayCacheKey = StringSupport.trimOrNull(replayCacheKeyCalculationStrategy.apply(jti));
+            if (replayCacheKey == null) {
+                throw new JWTValidationException("The replay cache key calculation strategy returned null");
+            }
+        } else {
+            replayCacheKey = jti;
         }
         final String className = getClass().getName();
-        assert className != null && jit != null && expiry != null;
-        if (!replayCache.check(className, jit, expiry)) {
-            throw new JWTValidationException("Replay detected for jit '" + jit + "'");
+        assert className != null && replayCacheKey != null && expiry != null;
+        if (!replayCache.check(className, replayCacheKey, expiry)) {
+            throw new JWTValidationException("Replay detected for key '" + replayCacheKey + "'");
         }
     }
 
diff --git a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidatorTest.java b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidatorTest.java
index b69b138..8e3f60f 100644
--- a/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidatorTest.java
+++ b/oidc-common-crypto-impl/src/test/java/net/shibboleth/oidc/security/jwt/claims/impl/JWTIdentifierClaimsValidatorTest.java
@@ -16,10 +16,15 @@ package net.shibboleth.oidc.security.jwt.claims.impl;
 
 import java.time.Instant;
 import java.util.Date;
+import java.util.function.Function;
 
+import javax.annotation.Nullable;
+
+import org.apache.commons.codec.digest.DigestUtils;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.impl.MemoryStorageService;
 import org.opensaml.storage.impl.StorageServiceReplayCache;
+import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -36,17 +41,24 @@ public class JWTIdentifierClaimsValidatorTest {
     private JWTIdentifierClaimsValidator validator;
     
     private ProfileRequestContext prc;
+    private StorageServiceReplayCache replayCache;
     
     @BeforeMethod
     public void setup() throws ComponentInitializationException {
+        setup(null);
+    }
+
+    public void setup(@Nullable final Function<String, String> keyCalculationStrategy)
+            throws ComponentInitializationException {
         validator = new JWTIdentifierClaimsValidator();
-        final StorageServiceReplayCache replayCache = new StorageServiceReplayCache();
+        replayCache = new StorageServiceReplayCache();
         final MemoryStorageService storageService = new MemoryStorageService();
         storageService.setId("mockId");
         storageService.initialize();
         replayCache.setStorage(storageService);
         validator.setReplayCache(replayCache);
         validator.setId("test-validator");
+        validator.setReplayCacheKeyCalculationStrategy(keyCalculationStrategy);
         validator.initialize();
         prc = new ProfileRequestContext();
     }
@@ -81,7 +93,8 @@ public class JWTIdentifierClaimsValidatorTest {
                 .expirationTime(Date.from(Instant.now().plusSeconds(300)))
                 .build();
         validator.doValidate(claimsSet, prc);
-        
+        Assert.assertFalse(replayCache.check(JWTIdentifierClaimsValidator.class.getName(),
+                "mockId", Instant.now().plusSeconds(300)));
     }
     
     @Test(expectedExceptions = JWTValidationException.class)
@@ -94,4 +107,90 @@ public class JWTIdentifierClaimsValidatorTest {
         validator.doValidate(claimsSet, prc);        
     }
 
+    @Test
+    public void doValidateTest_customKeyCalculation() throws JWTValidationException, ComponentInitializationException {
+        setup((jti) -> "custom" + jti);
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID("mockId")
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+        Assert.assertFalse(replayCache.check(JWTIdentifierClaimsValidator.class.getName(),
+                "custommockId", Instant.now().plusSeconds(300)));
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidateTest_failsWhenReplayedWithCustomKeyCalculation() throws JWTValidationException,
+            ComponentInitializationException {
+        setup((jti) -> "custom" + jti);
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID("mockId")
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+        validator.doValidate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidateTest_failsWhenKeyCalculationReturnsEmpty() throws JWTValidationException,
+        ComponentInitializationException {
+        setup((jti) -> "");
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID("mockId")
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidateTest_failsWhenKeyCalculationReturnsNull() throws JWTValidationException,
+        ComponentInitializationException {
+        setup((jti) -> null);
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID("mockId")
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+    }
+
+    @Test
+    public void doValidateTest_sha256KeyCalculationApplied() throws JWTValidationException,
+            ComponentInitializationException {
+        setup(new CalculateSha256DigestForLongKeyFunction());
+        final String jti = "veryLongJWTTokenIdentifierValueThatWillBeSha256DigestedByTheKeyCalculationFunction";
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID(jti)
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+        Assert.assertFalse(replayCache.check(JWTIdentifierClaimsValidator.class.getName(),
+                DigestUtils.sha256Hex(jti), Instant.now().plusSeconds(300)));
+    }
+
+    @Test
+    public void doValidateTest_sha256KeyCalculationIgnored() throws JWTValidationException,
+            ComponentInitializationException {
+        setup(new CalculateSha256DigestForLongKeyFunction());
+        final String jti = "shortJti";
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID(jti)
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+        Assert.assertFalse(replayCache.check(JWTIdentifierClaimsValidator.class.getName(),
+                jti, Instant.now().plusSeconds(300)));
+    }
+
+    @Test(expectedExceptions = JWTValidationException.class)
+    public void doValidateTest_failsWhenReplayedWithSha256KeyCalculation() throws JWTValidationException,
+            ComponentInitializationException {
+        final String jti = "veryLongJWTTokenIdentifierValueThatWillBeSha256DigestedByTheKeyCalculationFunction";
+        final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+                .jwtID(jti)
+                .expirationTime(Date.from(Instant.now().plusSeconds(300)))
+                .build();
+        validator.doValidate(claimsSet, prc);
+        validator.doValidate(claimsSet, prc);
+    }
+
 }

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list