[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