diff --git a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java
index 45a82310d44..7fa1d5e8ea7 100644
--- a/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java
+++ b/core/src/main/java/org/keycloak/sdjwt/IssuerSignedJwtVerificationOpts.java
@@ -22,61 +22,30 @@ package org.keycloak.sdjwt;
*
* @author Ingrid Kamga
*/
-public class IssuerSignedJwtVerificationOpts {
- private final boolean validateIssuedAtClaim;
- private final boolean validateExpirationClaim;
- private final boolean validateNotBeforeClaim;
+public class IssuerSignedJwtVerificationOpts extends TimeClaimVerificationOpts {
- public IssuerSignedJwtVerificationOpts(
+ private IssuerSignedJwtVerificationOpts(
boolean validateIssuedAtClaim,
boolean validateExpirationClaim,
- boolean validateNotBeforeClaim) {
- this.validateIssuedAtClaim = validateIssuedAtClaim;
- this.validateExpirationClaim = validateExpirationClaim;
- this.validateNotBeforeClaim = validateNotBeforeClaim;
+ boolean validateNotBeforeClaim,
+ int leewaySeconds
+ ) {
+ super(validateIssuedAtClaim, validateExpirationClaim, validateNotBeforeClaim, leewaySeconds);
}
- public boolean mustValidateIssuedAtClaim() {
- return validateIssuedAtClaim;
+ public static Builder builder() {
+ return new Builder();
}
- public boolean mustValidateExpirationClaim() {
- return validateExpirationClaim;
- }
-
- public boolean mustValidateNotBeforeClaim() {
- return validateNotBeforeClaim;
- }
-
- public static IssuerSignedJwtVerificationOpts.Builder builder() {
- return new IssuerSignedJwtVerificationOpts.Builder();
- }
-
- public static class Builder {
- private boolean validateIssuedAtClaim;
- private boolean validateExpirationClaim = true;
- private boolean validateNotBeforeClaim = true;
-
- public Builder withValidateIssuedAtClaim(boolean validateIssuedAtClaim) {
- this.validateIssuedAtClaim = validateIssuedAtClaim;
- return this;
- }
-
- public Builder withValidateExpirationClaim(boolean validateExpirationClaim) {
- this.validateExpirationClaim = validateExpirationClaim;
- return this;
- }
-
- public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) {
- this.validateNotBeforeClaim = validateNotBeforeClaim;
- return this;
- }
+ public static class Builder extends TimeClaimVerificationOpts.Builder {
+ @Override
public IssuerSignedJwtVerificationOpts build() {
return new IssuerSignedJwtVerificationOpts(
- validateIssuedAtClaim,
- validateExpirationClaim,
- validateNotBeforeClaim
+ requireIssuedAtClaim,
+ requireExpirationClaim,
+ requireNotBeforeClaim,
+ leewaySeconds
);
}
}
diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJws.java b/core/src/main/java/org/keycloak/sdjwt/SdJws.java
index d7a6ea1b759..f1ced19d128 100644
--- a/core/src/main/java/org/keycloak/sdjwt/SdJws.java
+++ b/core/src/main/java/org/keycloak/sdjwt/SdJws.java
@@ -18,7 +18,6 @@ package org.keycloak.sdjwt;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.time.Instant;
import java.util.List;
import java.util.Objects;
@@ -95,7 +94,7 @@ public abstract class SdJws {
}
}
- private static final JWSInput parse(String jwsString) {
+ private static JWSInput parse(String jwsString) {
try {
return new JWSInput(Objects.requireNonNull(jwsString, "jwsString must not be null"));
} catch (JWSInputException e) {
@@ -103,7 +102,7 @@ public abstract class SdJws {
}
}
- private static final JsonNode readPayload(JWSInput jwsInput) {
+ private static JsonNode readPayload(JWSInput jwsInput) {
try {
return SdJwtUtils.mapper.readTree(jwsInput.getContent());
} catch (IOException e) {
@@ -115,48 +114,6 @@ public abstract class SdJws {
return this.jwsInput.getHeader();
}
- public void verifyIssuedAtClaim() throws VerificationException {
- long now = Instant.now().getEpochSecond();
- long iat = SdJwtUtils.readTimeClaim(payload, "iat");
-
- if (now < iat) {
- throw new VerificationException("jwt issued in the future");
- }
- }
-
- public void verifyExpClaim() throws VerificationException {
- long now = Instant.now().getEpochSecond();
- long exp = SdJwtUtils.readTimeClaim(payload, "exp");
-
- if (now >= exp) {
- throw new VerificationException("jwt has expired");
- }
- }
-
- public void verifyNotBeforeClaim() throws VerificationException {
- long now = Instant.now().getEpochSecond();
- long nbf = SdJwtUtils.readTimeClaim(payload, "nbf");
-
- if (now < nbf) {
- throw new VerificationException("jwt not valid yet");
- }
- }
-
- /**
- * Verifies that the JWS is not too old.
- *
- * @param maxAge Maximum age in seconds
- * @throws VerificationException if too old
- */
- public void verifyAge(int maxAge) throws VerificationException {
- long now = Instant.now().getEpochSecond();
- long iat = SdJwtUtils.readTimeClaim(getPayload(), "iat");
-
- if (now - iat > maxAge) {
- throw new VerificationException("jwt is too old");
- }
- }
-
/**
* Verifies that SD-JWT was issued by one of the provided issuers.
* @param issuers List of trusted issuers
diff --git a/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java
index 857a23feacd..def03df9ce1 100644
--- a/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java
+++ b/core/src/main/java/org/keycloak/sdjwt/SdJwtVerificationContext.java
@@ -27,7 +27,6 @@ import org.keycloak.sdjwt.consumer.PresentationRequirements;
import org.keycloak.sdjwt.vp.KeyBindingJWT;
import org.keycloak.sdjwt.vp.KeyBindingJwtVerificationOpts;
-import java.time.Instant;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
@@ -290,31 +289,22 @@ public class SdJwtVerificationContext {
JsonNode payload,
IssuerSignedJwtVerificationOpts issuerSignedJwtVerificationOpts
) throws VerificationException {
- long now = Instant.now().getEpochSecond();
+ TimeClaimVerifier timeClaimVerifier = new TimeClaimVerifier(issuerSignedJwtVerificationOpts);
try {
- if (issuerSignedJwtVerificationOpts.mustValidateIssuedAtClaim()
- && now < SdJwtUtils.readTimeClaim(payload, "iat")) {
- throw new VerificationException("JWT issued in the future");
- }
+ timeClaimVerifier.verifyIssuedAtClaim(payload);
} catch (VerificationException e) {
throw new VerificationException("Issuer-Signed JWT: Invalid `iat` claim", e);
}
try {
- if (issuerSignedJwtVerificationOpts.mustValidateExpirationClaim()
- && now >= SdJwtUtils.readTimeClaim(payload, "exp")) {
- throw new VerificationException("JWT has expired");
- }
+ timeClaimVerifier.verifyExpirationClaim(payload);
} catch (VerificationException e) {
throw new VerificationException("Issuer-Signed JWT: Invalid `exp` claim", e);
}
try {
- if (issuerSignedJwtVerificationOpts.mustValidateNotBeforeClaim()
- && now < SdJwtUtils.readTimeClaim(payload, "nbf")) {
- throw new VerificationException("JWT is not yet valid");
- }
+ timeClaimVerifier.verifyNotBeforeClaim(payload);
} catch (VerificationException e) {
throw new VerificationException("Issuer-Signed JWT: Invalid `nbf` claim", e);
}
@@ -328,17 +318,20 @@ public class SdJwtVerificationContext {
private void validateKeyBindingJwtTimeClaims(
KeyBindingJwtVerificationOpts keyBindingJwtVerificationOpts
) throws VerificationException {
+ JsonNode kbJwtPayload = keyBindingJwt.getPayload();
+ TimeClaimVerifier timeClaimVerifier = new TimeClaimVerifier(keyBindingJwtVerificationOpts);
+
// Check that the creation time of the Key Binding JWT, as determined by the iat claim,
// is within an acceptable window
try {
- keyBindingJwt.verifyIssuedAtClaim();
+ timeClaimVerifier.verifyIssuedAtClaim(kbJwtPayload);
} catch (VerificationException e) {
throw new VerificationException("Key binding JWT: Invalid `iat` claim", e);
}
try {
- keyBindingJwt.verifyAge(keyBindingJwtVerificationOpts.getAllowedMaxAge());
+ timeClaimVerifier.verifyAge(kbJwtPayload, keyBindingJwtVerificationOpts.getAllowedMaxAge());
} catch (VerificationException e) {
throw new VerificationException("Key binding JWT is too old");
}
@@ -346,17 +339,13 @@ public class SdJwtVerificationContext {
// Check other time claims
try {
- if (keyBindingJwtVerificationOpts.mustValidateExpirationClaim()) {
- keyBindingJwt.verifyExpClaim();
- }
+ timeClaimVerifier.verifyExpirationClaim(kbJwtPayload);
} catch (VerificationException e) {
throw new VerificationException("Key binding JWT: Invalid `exp` claim", e);
}
try {
- if (keyBindingJwtVerificationOpts.mustValidateNotBeforeClaim()) {
- keyBindingJwt.verifyNotBeforeClaim();
- }
+ timeClaimVerifier.verifyNotBeforeClaim(kbJwtPayload);
} catch (VerificationException e) {
throw new VerificationException("Key binding JWT: Invalid `nbf` claim", e);
}
diff --git a/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerificationOpts.java
new file mode 100644
index 00000000000..3080f163c02
--- /dev/null
+++ b/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerificationOpts.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2025 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.sdjwt;
+
+/**
+ * Options for validating common time claims during SD-JWT verification.
+ *
+ * @author Ingrid Kamga
+ */
+public class TimeClaimVerificationOpts {
+
+ public static final int DEFAULT_LEEWAY_SECONDS = 10;
+
+ // These options configure whether the respective time claims must be present
+ // during validation. They will always be validated if present.
+
+ private final boolean requireIssuedAtClaim;
+ private final boolean requireExpirationClaim;
+ private final boolean requireNotBeforeClaim;
+
+ /**
+ * Tolerance window to account for clock skew when checking time claims
+ */
+ private final int leewaySeconds;
+
+ protected TimeClaimVerificationOpts(
+ boolean requireIssuedAtClaim,
+ boolean requireExpirationClaim,
+ boolean validateNotBeforeClaim,
+ int leewaySeconds
+ ) {
+ this.requireIssuedAtClaim = requireIssuedAtClaim;
+ this.requireExpirationClaim = requireExpirationClaim;
+ this.requireNotBeforeClaim = validateNotBeforeClaim;
+ this.leewaySeconds = leewaySeconds;
+ }
+
+ public boolean mustRequireIssuedAtClaim() {
+ return requireIssuedAtClaim;
+ }
+
+ public boolean mustRequireExpirationClaim() {
+ return requireExpirationClaim;
+ }
+
+ public boolean mustRequireNotBeforeClaim() {
+ return requireNotBeforeClaim;
+ }
+
+ public int getLeewaySeconds() {
+ return leewaySeconds;
+ }
+
+ public static > Builder builder() {
+ return new Builder<>();
+ }
+
+ public static class Builder> {
+
+ protected boolean requireIssuedAtClaim = true;
+ protected boolean requireExpirationClaim = true;
+ protected boolean requireNotBeforeClaim = true;
+ protected int leewaySeconds = DEFAULT_LEEWAY_SECONDS;
+
+ @SuppressWarnings("unchecked")
+ public T withRequireIssuedAtClaim(boolean requireIssuedAtClaim) {
+ this.requireIssuedAtClaim = requireIssuedAtClaim;
+ return (T) this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T withRequireExpirationClaim(boolean requireExpirationClaim) {
+ this.requireExpirationClaim = requireExpirationClaim;
+ return (T) this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T withRequireNotBeforeClaim(boolean requireNotBeforeClaim) {
+ this.requireNotBeforeClaim = requireNotBeforeClaim;
+ return (T) this;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T withLeewaySeconds(int leewaySeconds) {
+ this.leewaySeconds = leewaySeconds;
+ return (T) this;
+ }
+
+ public TimeClaimVerificationOpts build() {
+ return new TimeClaimVerificationOpts(
+ requireIssuedAtClaim,
+ requireExpirationClaim,
+ requireNotBeforeClaim,
+ leewaySeconds
+ );
+ }
+ }
+}
diff --git a/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerifier.java b/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerifier.java
new file mode 100644
index 00000000000..02431c729e8
--- /dev/null
+++ b/core/src/main/java/org/keycloak/sdjwt/TimeClaimVerifier.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2025 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.sdjwt;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.common.VerificationException;
+
+import java.time.Instant;
+
+/**
+ * Module for checking the validity of JWT time claims.
+ * All checks account for a leeway window to accommodate clock skew.
+ *
+ * @author Ingrid Kamga
+ */
+public class TimeClaimVerifier {
+
+ public static final String CLAIM_NAME_IAT = "iat";
+ public static final String CLAIM_NAME_EXP = "exp";
+ public static final String CLAIM_NAME_NBF = "nbf";
+
+ private final TimeClaimVerificationOpts opts;
+
+ public TimeClaimVerifier(TimeClaimVerificationOpts opts) {
+ if (opts.getLeewaySeconds() < 0) {
+ throw new IllegalArgumentException("Leeway seconds cannot be negative");
+ }
+
+ this.opts = opts;
+ }
+
+ /**
+ * Validates that JWT was not issued in the future
+ *
+ * @param jwtPayload the JWT's payload
+ */
+ public void verifyIssuedAtClaim(JsonNode jwtPayload) throws VerificationException {
+ if (!jwtPayload.hasNonNull(CLAIM_NAME_IAT)) {
+ if (opts.mustRequireIssuedAtClaim()) {
+ throw new VerificationException("Missing 'iat' claim or null");
+ }
+
+ return; // Not required, skipping check
+ }
+
+ long iat = SdJwtUtils.readTimeClaim(jwtPayload, CLAIM_NAME_IAT);
+
+ if ((currentTimestamp() + opts.getLeewaySeconds()) < iat) {
+ throw new VerificationException("JWT was issued in the future");
+ }
+ }
+
+ /**
+ * Validates that JWT has not expired
+ *
+ * @param jwtPayload the JWT's payload
+ */
+ public void verifyExpirationClaim(JsonNode jwtPayload) throws VerificationException {
+ if (!jwtPayload.hasNonNull(CLAIM_NAME_EXP)) {
+ if (opts.mustRequireExpirationClaim()) {
+ throw new VerificationException("Missing 'exp' claim or null");
+ }
+
+ return; // Not required, skipping check
+ }
+
+ long exp = SdJwtUtils.readTimeClaim(jwtPayload, CLAIM_NAME_EXP);
+
+ if ((currentTimestamp() - opts.getLeewaySeconds()) >= exp) {
+ throw new VerificationException("JWT has expired");
+ }
+ }
+
+ /**
+ * Validates that JWT can yet be processed
+ *
+ * @param jwtPayload the JWT's payload
+ */
+ public void verifyNotBeforeClaim(JsonNode jwtPayload) throws VerificationException {
+ if (!jwtPayload.hasNonNull(CLAIM_NAME_NBF)) {
+ if (opts.mustRequireNotBeforeClaim()) {
+ throw new VerificationException("Missing 'nbf' claim or null");
+ }
+
+ return; // Not required, skipping check
+ }
+
+ long nbf = SdJwtUtils.readTimeClaim(jwtPayload, CLAIM_NAME_NBF);
+
+ if ((currentTimestamp() + opts.getLeewaySeconds()) < nbf) {
+ throw new VerificationException("JWT is not yet valid");
+ }
+ }
+
+ /**
+ * Validates that JWT is not too old
+ *
+ * @param jwtPayload the JWT's payload
+ * @param maxAge maximum allowed age in seconds
+ */
+ public void verifyAge(JsonNode jwtPayload, int maxAge) throws VerificationException {
+ long iat = SdJwtUtils.readTimeClaim(jwtPayload, CLAIM_NAME_IAT);
+
+ if ((currentTimestamp() - iat - opts.getLeewaySeconds()) > maxAge) {
+ throw new VerificationException("JWT is too old");
+ }
+ }
+
+ /**
+ * Returns current timestamp in seconds.
+ */
+ public long currentTimestamp() {
+ return Instant.now().getEpochSecond();
+ }
+}
diff --git a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java
index 6f43f91a38b..897397e43d0 100644
--- a/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java
+++ b/core/src/main/java/org/keycloak/sdjwt/vp/KeyBindingJwtVerificationOpts.java
@@ -17,12 +17,17 @@
package org.keycloak.sdjwt.vp;
+import org.keycloak.sdjwt.TimeClaimVerificationOpts;
+
/**
* Options for Key Binding JWT verification.
*
* @author Ingrid Kamga
*/
-public class KeyBindingJwtVerificationOpts {
+public class KeyBindingJwtVerificationOpts extends TimeClaimVerificationOpts {
+
+ public static final int DEFAULT_ALLOWED_MAX_AGE = 5 * 60; // 5 minutes
+
/**
* Specifies the Verifier's policy whether to check Key Binding
*/
@@ -36,22 +41,20 @@ public class KeyBindingJwtVerificationOpts {
private final String nonce;
private final String aud;
- private final boolean validateExpirationClaim;
- private final boolean validateNotBeforeClaim;
-
- public KeyBindingJwtVerificationOpts(
+ private KeyBindingJwtVerificationOpts(
boolean keyBindingRequired,
int allowedMaxAge,
String nonce,
String aud,
boolean validateExpirationClaim,
- boolean validateNotBeforeClaim) {
+ boolean validateNotBeforeClaim,
+ int leewaySeconds
+ ) {
+ super(true, validateExpirationClaim, validateNotBeforeClaim, leewaySeconds);
this.keyBindingRequired = keyBindingRequired;
this.allowedMaxAge = allowedMaxAge;
this.nonce = nonce;
this.aud = aud;
- this.validateExpirationClaim = validateExpirationClaim;
- this.validateNotBeforeClaim = validateNotBeforeClaim;
}
public boolean isKeyBindingRequired() {
@@ -70,25 +73,17 @@ public class KeyBindingJwtVerificationOpts {
return aud;
}
- public boolean mustValidateExpirationClaim() {
- return validateExpirationClaim;
+ public static Builder builder() {
+ return new Builder();
}
- public boolean mustValidateNotBeforeClaim() {
- return validateNotBeforeClaim;
- }
+ public static class Builder extends TimeClaimVerificationOpts.Builder {
- public static KeyBindingJwtVerificationOpts.Builder builder() {
- return new KeyBindingJwtVerificationOpts.Builder();
- }
-
- public static class Builder {
private boolean keyBindingRequired = true;
- private int allowedMaxAge = 5 * 60;
+ protected boolean validateIssuedAtClaim = true;
+ private int allowedMaxAge = DEFAULT_ALLOWED_MAX_AGE;
private String nonce;
private String aud;
- private boolean validateExpirationClaim = true;
- private boolean validateNotBeforeClaim = true;
public Builder withKeyBindingRequired(boolean keyBindingRequired) {
this.keyBindingRequired = keyBindingRequired;
@@ -110,17 +105,14 @@ public class KeyBindingJwtVerificationOpts {
return this;
}
- public Builder withValidateExpirationClaim(boolean validateExpirationClaim) {
- this.validateExpirationClaim = validateExpirationClaim;
- return this;
- }
-
- public Builder withValidateNotBeforeClaim(boolean validateNotBeforeClaim) {
- this.validateNotBeforeClaim = validateNotBeforeClaim;
- return this;
- }
-
+ @Override
public KeyBindingJwtVerificationOpts build() {
+ if (!validateIssuedAtClaim) {
+ throw new IllegalArgumentException(
+ "Validating `iat` claim cannot be disabled for KB-JWT verification because mandated"
+ );
+ }
+
if (keyBindingRequired && (aud == null || nonce == null || nonce.isEmpty())) {
throw new IllegalArgumentException(
"Missing `nonce` and `aud` claims for replay protection"
@@ -132,8 +124,9 @@ public class KeyBindingJwtVerificationOpts {
allowedMaxAge,
nonce,
aud,
- validateExpirationClaim,
- validateNotBeforeClaim
+ requireExpirationClaim,
+ requireNotBeforeClaim,
+ leewaySeconds
);
}
}
diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java
index 575d1f0422b..501fcf05757 100644
--- a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java
+++ b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java
@@ -65,42 +65,6 @@ public abstract class SdJwsTest {
assertThrows(VerificationException.class, () -> sdJws.verifySignature(testSettings.issuerVerifierContext));
}
- @Test
- public void testVerifyExpClaim_ExpiredJWT() {
- JsonNode payload = createPayload();
- ((ObjectNode) payload).put("exp", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond());
- SdJws sdJws = new SdJws(payload) {
- };
- assertThrows(VerificationException.class, sdJws::verifyExpClaim);
- }
-
- @Test
- public void testVerifyExpClaim_Positive() throws Exception {
- JsonNode payload = createPayload();
- ((ObjectNode) payload).put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
- SdJws sdJws = new SdJws(payload) {
- };
- sdJws.verifyExpClaim();
- }
-
- @Test
- public void testVerifyNotBeforeClaim_Negative() {
- JsonNode payload = createPayload();
- ((ObjectNode) payload).put("nbf", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond());
- SdJws sdJws = new SdJws(payload) {
- };
- assertThrows(VerificationException.class, sdJws::verifyNotBeforeClaim);
- }
-
- @Test
- public void testVerifyNotBeforeClaim_Positive() throws Exception {
- JsonNode payload = createPayload();
- ((ObjectNode) payload).put("nbf", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond());
- SdJws sdJws = new SdJws(payload) {
- };
- sdJws.verifyNotBeforeClaim();
- }
-
@Test
public void testPayloadJwsConstruction() {
SdJws sdJws = new SdJws(createPayload()) {
@@ -159,27 +123,4 @@ public abstract class SdJwsTest {
SdJws sdJws = new SdJws(payload) {};
sdJws.verifyVctClaim(Collections.singletonList("IdentityCredential"));
}
-
- @Test
- public void shouldValidateAgeSinceIssued() throws VerificationException {
- long now = Instant.now().getEpochSecond();
- SdJws sdJws = exampleSdJws(now);
- sdJws.verifyAge(180);
- }
-
- @Test
- public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() {
- long now = Instant.now().getEpochSecond();
- SdJws sdJws = exampleSdJws(now - 1000); // that will be too old
- VerificationException exception = assertThrows(VerificationException.class, () -> sdJws.verifyAge(180));
- assertEquals("jwt is too old", exception.getMessage());
- }
-
- private SdJws exampleSdJws(long iat) {
- ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
- payload.set("iat", SdJwtUtils.mapper.valueToTree(iat));
-
- return new SdJws(payload) {
- };
- }
}
diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java
index b1ce1fe8fdc..180e2e07553 100644
--- a/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java
+++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtFacadeTest.java
@@ -139,6 +139,10 @@ public abstract class SdJwtFacadeTest {
}
private IssuerSignedJwtVerificationOpts createVerificationOptions() {
- return new IssuerSignedJwtVerificationOpts(true, true, false);
+ return IssuerSignedJwtVerificationOpts.builder()
+ .withRequireIssuedAtClaim(false)
+ .withRequireExpirationClaim(false)
+ .withRequireNotBeforeClaim(false)
+ .build();
}
}
diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java
index 283f6ea59eb..3ced44b7d92 100644
--- a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java
+++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java
@@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.common.VerificationException;
+import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
import org.keycloak.rule.CryptoInitRule;
@@ -31,8 +32,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import org.keycloak.crypto.SignatureSignerContext;
-
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
@@ -168,9 +167,7 @@ public abstract class SdJwtVerificationTest {
VerificationException.class,
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
- defaultIssuerSignedJwtVerificationOpts()
- .withValidateExpirationClaim(true)
- .build()
+ defaultIssuerSignedJwtVerificationOpts().build()
)
);
@@ -188,7 +185,7 @@ public abstract class SdJwtVerificationTest {
// exp: invalid
ObjectNode claimSet2 = mapper.createObjectNode();
claimSet1.put("given_name", "John");
- claimSet1.put("exp", "should-not-be-a-string");
+ claimSet1.set("exp", null);
DisclosureSpec disclosureSpec = DisclosureSpec.builder()
.withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A")
@@ -203,13 +200,13 @@ public abstract class SdJwtVerificationTest {
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
defaultIssuerSignedJwtVerificationOpts()
- .withValidateExpirationClaim(true)
+ .withRequireExpirationClaim(true)
.build()
)
);
assertEquals("Issuer-Signed JWT: Invalid `exp` claim", exception.getMessage());
- assertEquals("Missing or invalid 'exp' claim", exception.getCause().getMessage());
+ assertEquals("Missing 'exp' claim or null", exception.getCause().getMessage());
}
}
@@ -234,14 +231,12 @@ public abstract class SdJwtVerificationTest {
VerificationException.class,
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
- defaultIssuerSignedJwtVerificationOpts()
- .withValidateIssuedAtClaim(true)
- .build()
+ defaultIssuerSignedJwtVerificationOpts().build()
)
);
assertEquals("Issuer-Signed JWT: Invalid `iat` claim", exception.getMessage());
- assertEquals("JWT issued in the future", exception.getCause().getMessage());
+ assertEquals("JWT was issued in the future", exception.getCause().getMessage());
}
}
@@ -266,9 +261,7 @@ public abstract class SdJwtVerificationTest {
VerificationException.class,
() -> sdJwt.verify(
defaultIssuerVerifyingKeys(),
- defaultIssuerSignedJwtVerificationOpts()
- .withValidateNotBeforeClaim(true)
- .build()
+ defaultIssuerSignedJwtVerificationOpts().build()
)
);
@@ -370,9 +363,9 @@ public abstract class SdJwtVerificationTest {
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
return IssuerSignedJwtVerificationOpts.builder()
- .withValidateIssuedAtClaim(false)
- .withValidateExpirationClaim(false)
- .withValidateNotBeforeClaim(false);
+ .withRequireIssuedAtClaim(false)
+ .withRequireExpirationClaim(false)
+ .withRequireNotBeforeClaim(false);
}
private SdJwt.Builder exampleFlatSdJwtV1() {
diff --git a/core/src/test/java/org/keycloak/sdjwt/TimeClaimVerifierTest.java b/core/src/test/java/org/keycloak/sdjwt/TimeClaimVerifierTest.java
new file mode 100644
index 00000000000..64beaa7aa9f
--- /dev/null
+++ b/core/src/test/java/org/keycloak/sdjwt/TimeClaimVerifierTest.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2025 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.sdjwt;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.junit.Test;
+import org.keycloak.common.VerificationException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_EXP;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_IAT;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_NBF;
+
+/**
+ * @author Ingrid Kamga
+ */
+public class TimeClaimVerifierTest {
+
+ private static final long CURRENT_TIMESTAMP = 1609459200L; // Fixed timestamp: 2021-01-01 00:00:00 UTC
+ private static final int DEFAULT_LEEWAY_SECONDS = 20;
+
+ private final TimeClaimVerifier timeClaimVerifier = new FixedTimeClaimVerifier(DEFAULT_LEEWAY_SECONDS, false);
+ private final TimeClaimVerifier strictTimeClaimVerifier = new FixedTimeClaimVerifier(DEFAULT_LEEWAY_SECONDS, true);
+
+ static class FixedTimeClaimVerifier extends TimeClaimVerifier {
+
+ public FixedTimeClaimVerifier(int leewaySeconds, boolean requireClaims) {
+ super(createOptsWithLeeway(leewaySeconds, requireClaims));
+ }
+
+ @Override
+ public long currentTimestamp() {
+ return CURRENT_TIMESTAMP;
+ }
+ }
+
+ @Test
+ public void testVerifyIatClaimInTheFuture() {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP + 100); // 100 seconds in the future
+
+ VerificationException exception = assertThrows(VerificationException.class,
+ () -> timeClaimVerifier.verifyIssuedAtClaim(payload));
+
+ assertEquals("JWT was issued in the future", exception.getMessage());
+ }
+
+ @Test
+ public void testVerifyIatClaimValid() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP - 1); // Issued 1 second ago, in the past
+
+ timeClaimVerifier.verifyIssuedAtClaim(payload);
+ }
+
+ @Test
+ public void testVerifyIatClaimEdge() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP + 19); // Issued 19 seconds in the future, within the 20 second leeway
+
+ timeClaimVerifier.verifyIssuedAtClaim(payload);
+ }
+
+ @Test
+ public void testVerifyExpClaimExpired() {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_EXP, CURRENT_TIMESTAMP - 100); // Expired 100 seconds ago
+
+ VerificationException exception = assertThrows(VerificationException.class,
+ () -> timeClaimVerifier.verifyExpirationClaim(payload));
+
+ assertEquals("JWT has expired", exception.getMessage());
+ }
+
+ @Test
+ public void testVerifyExpClaimValid() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_EXP, CURRENT_TIMESTAMP + 100); // Expires 100 seconds in the future
+
+ timeClaimVerifier.verifyExpirationClaim(payload);
+ }
+
+ @Test
+ public void testVerifyExpClaimEdge() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_EXP, CURRENT_TIMESTAMP - 19); // 19 seconds ago, within the 20 second leeway
+
+ // No exception expected for JWT expiring within leeway
+ timeClaimVerifier.verifyExpirationClaim(payload);
+ }
+
+ @Test
+ public void testVerifyNotBeforeClaimNotYetValid() {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_NBF, CURRENT_TIMESTAMP + 100); // Not valid for another 100 seconds
+
+ VerificationException exception = assertThrows(VerificationException.class,
+ () -> timeClaimVerifier.verifyNotBeforeClaim(payload));
+
+ assertEquals("JWT is not yet valid", exception.getMessage());
+ }
+
+ @Test
+ public void testVerifyNotBeforeClaimValid() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_NBF, CURRENT_TIMESTAMP - 100); // Valid since 100 seconds ago
+
+ timeClaimVerifier.verifyNotBeforeClaim(payload);
+ }
+
+ // Test for verifyNotBeforeClaim (edge case: valid exactly at current time with leeway)
+ @Test
+ public void testVerifyNotBeforeClaimEdge() throws VerificationException {
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_NBF, CURRENT_TIMESTAMP + 19); // 19 seconds in the future, within the 20 second leeway
+
+ // No exception expected for JWT becoming valid within leeway
+ timeClaimVerifier.verifyNotBeforeClaim(payload);
+ }
+
+ @Test
+ public void testVerifyAgeJwtTooOld() {
+ int maxAgeAllowed = 300; // 5 minutes
+
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP - 361); // 361 seconds old
+
+ VerificationException exception = assertThrows(VerificationException.class,
+ () -> timeClaimVerifier.verifyAge(payload, maxAgeAllowed));
+
+ assertEquals("JWT is too old", exception.getMessage());
+ }
+
+ @Test
+ public void testVerifyAgeValid() throws VerificationException {
+ int maxAgeAllowed = 300; // 5 minutes
+
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP - 100); // Only 100 seconds old
+
+ timeClaimVerifier.verifyAge(payload, maxAgeAllowed);
+ }
+
+ @Test
+ public void testVerifyAgeValidEdge() throws VerificationException {
+ int maxAgeAllowed = 300; // 5 minutes
+
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+ payload.put(CLAIM_NAME_IAT, CURRENT_TIMESTAMP - 320); // 320 seconds old, within the 20 second leeway
+
+ timeClaimVerifier.verifyAge(payload, maxAgeAllowed);
+ }
+
+ @Test
+ public void instantiationShouldFailIfLeewayNegative() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new TimeClaimVerifier(createOptsWithLeeway(-1, false)));
+
+ assertEquals("Leeway seconds cannot be negative", exception.getMessage());
+ }
+
+ @Test
+ public void testPermissiveVerifierMissingClaims() throws VerificationException {
+ // No time claims added
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+
+ // No exception expected as claims are not required
+ timeClaimVerifier.verifyIssuedAtClaim(payload);
+ timeClaimVerifier.verifyExpirationClaim(payload);
+ timeClaimVerifier.verifyNotBeforeClaim(payload);
+ }
+
+ @Test
+ public void testStrictVerifierMissingClaims() {
+ // No time claims added
+ ObjectNode payload = SdJwtUtils.mapper.createObjectNode();
+
+ VerificationException exceptionIat = assertThrows(VerificationException.class,
+ () -> strictTimeClaimVerifier.verifyIssuedAtClaim(payload));
+ assertEquals("Missing 'iat' claim or null", exceptionIat.getMessage());
+
+ VerificationException exceptionExp = assertThrows(VerificationException.class,
+ () -> strictTimeClaimVerifier.verifyExpirationClaim(payload));
+ assertEquals("Missing 'exp' claim or null", exceptionExp.getMessage());
+
+ VerificationException exceptionNbf = assertThrows(VerificationException.class,
+ () -> strictTimeClaimVerifier.verifyNotBeforeClaim(payload));
+ assertEquals("Missing 'nbf' claim or null", exceptionNbf.getMessage());
+ }
+
+ private static TimeClaimVerificationOpts createOptsWithLeeway(int leewaySeconds, boolean requireClaims) {
+ return TimeClaimVerificationOpts.builder()
+ .withLeewaySeconds(leewaySeconds)
+ .withRequireIssuedAtClaim(requireClaims)
+ .withRequireExpirationClaim(requireClaims)
+ .withRequireNotBeforeClaim(requireClaims)
+ .build();
+ }
+}
diff --git a/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java b/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java
index b3829c77daa..6d402c5c7cd 100644
--- a/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java
+++ b/core/src/test/java/org/keycloak/sdjwt/consumer/SdJwtPresentationConsumerTest.java
@@ -101,8 +101,8 @@ public abstract class SdJwtPresentationConsumerTest {
private IssuerSignedJwtVerificationOpts defaultIssuerSignedJwtVerificationOpts() {
return IssuerSignedJwtVerificationOpts.builder()
- .withValidateIssuedAtClaim(false)
- .withValidateNotBeforeClaim(false)
+ .withRequireIssuedAtClaim(false)
+ .withRequireNotBeforeClaim(false)
.build();
}
@@ -112,8 +112,8 @@ public abstract class SdJwtPresentationConsumerTest {
.withAllowedMaxAge(Integer.MAX_VALUE)
.withNonce("1234567890")
.withAud("https://verifier.example.org")
- .withValidateExpirationClaim(false)
- .withValidateNotBeforeClaim(false)
+ .withRequireExpirationClaim(false)
+ .withRequireNotBeforeClaim(false)
.build();
}
}
diff --git a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java
index 5a3a4cb1d7c..ebbd4e9aae8 100644
--- a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java
+++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java
@@ -42,6 +42,9 @@ import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_EXP;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_IAT;
+import static org.keycloak.sdjwt.TimeClaimVerifier.CLAIM_NAME_NBF;
/**
* @author Ingrid Kamga
@@ -263,13 +266,31 @@ public abstract class SdJwtVPVerificationTest {
long now = Instant.now().getEpochSecond();
ObjectNode kbPayload = exampleKbPayload();
- kbPayload.set("iat", mapper.valueToTree(now + 1000));
+ kbPayload.set(CLAIM_NAME_IAT, mapper.valueToTree(now + 1000));
testShouldFailGeneric2(
kbPayload,
defaultKeyBindingJwtVerificationOpts().build(),
"Key binding JWT: Invalid `iat` claim",
- "jwt issued in the future"
+ "JWT was issued in the future"
+ );
+ }
+
+ @Test
+ public void testShouldTolerateKbIssuedInTheFutureWithinLeeway() throws VerificationException {
+ long now = Instant.now().getEpochSecond();
+
+ ObjectNode kbPayload = exampleKbPayload();
+ // Issued just 5 seconds in the future. Should pass with a leeway of 10 seconds.
+ kbPayload.set(CLAIM_NAME_IAT, mapper.valueToTree(now + 5));
+ SdJwtVP sdJwtVP = exampleSdJwtWithCustomKbPayload(kbPayload);
+
+ sdJwtVP.verify(
+ defaultIssuerVerifyingKeys(),
+ defaultIssuerSignedJwtVerificationOpts().build(),
+ defaultKeyBindingJwtVerificationOpts()
+ .withLeewaySeconds(10)
+ .build()
);
}
@@ -279,7 +300,7 @@ public abstract class SdJwtVPVerificationTest {
ObjectNode kbPayload = exampleKbPayload();
// This KB-JWT is then issued more than 60s ago
- kbPayload.set("iat", mapper.valueToTree(issuerSignedJwtIat - 120));
+ kbPayload.set(CLAIM_NAME_IAT, mapper.valueToTree(issuerSignedJwtIat - 120));
testShouldFailGeneric2(
kbPayload,
@@ -296,15 +317,32 @@ public abstract class SdJwtVPVerificationTest {
long now = Instant.now().getEpochSecond();
ObjectNode kbPayload = exampleKbPayload();
- kbPayload.set("exp", mapper.valueToTree(now - 1000));
+ kbPayload.set(CLAIM_NAME_EXP, mapper.valueToTree(now - 1000));
testShouldFailGeneric2(
kbPayload,
- defaultKeyBindingJwtVerificationOpts()
- .withValidateExpirationClaim(true)
- .build(),
+ defaultKeyBindingJwtVerificationOpts().build(),
"Key binding JWT: Invalid `exp` claim",
- "jwt has expired"
+ "JWT has expired"
+ );
+ }
+
+ @Test
+ public void testShouldTolerateExpiredKbWithinLeeway() throws VerificationException {
+ long now = Instant.now().getEpochSecond();
+
+ ObjectNode kbPayload = exampleKbPayload();
+ // Expires just 5 seconds ago. Should pass with a leeway of 10 seconds.
+ kbPayload.set(CLAIM_NAME_EXP, mapper.valueToTree(now - 5));
+ SdJwtVP sdJwtVP = exampleSdJwtWithCustomKbPayload(kbPayload);
+
+ sdJwtVP.verify(
+ defaultIssuerVerifyingKeys(),
+ defaultIssuerSignedJwtVerificationOpts().build(),
+ defaultKeyBindingJwtVerificationOpts()
+ .withRequireExpirationClaim(true)
+ .withLeewaySeconds(10)
+ .build()
);
}
@@ -313,15 +351,13 @@ public abstract class SdJwtVPVerificationTest {
long now = Instant.now().getEpochSecond();
ObjectNode kbPayload = exampleKbPayload();
- kbPayload.set("nbf", mapper.valueToTree(now + 1000));
+ kbPayload.set(CLAIM_NAME_NBF, mapper.valueToTree(now + 1000));
testShouldFailGeneric2(
kbPayload,
- defaultKeyBindingJwtVerificationOpts()
- .withValidateNotBeforeClaim(true)
- .build(),
+ defaultKeyBindingJwtVerificationOpts().build(),
"Key binding JWT: Invalid `nbf` claim",
- "jwt not valid yet"
+ "JWT is not yet valid"
);
}
@@ -399,17 +435,7 @@ public abstract class SdJwtVPVerificationTest {
String exceptionMessage,
String exceptionCauseMessage
) {
- KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(
- kbPayloadSubstitute,
- testSettings.holderSigContext,
- KeyBindingJWT.TYP
- );
-
- String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
- SdJwtVP sdJwtVP = SdJwtVP.of(
- sdJwtVPString.substring(0, sdJwtVPString.lastIndexOf(SdJwt.DELIMITER) + 1)
- + keyBindingJWT.toJws()
- );
+ SdJwtVP sdJwtVP = exampleSdJwtWithCustomKbPayload(kbPayloadSubstitute);
VerificationException exception = assertThrows(
VerificationException.class,
@@ -432,8 +458,8 @@ public abstract class SdJwtVPVerificationTest {
private IssuerSignedJwtVerificationOpts.Builder defaultIssuerSignedJwtVerificationOpts() {
return IssuerSignedJwtVerificationOpts.builder()
- .withValidateIssuedAtClaim(false)
- .withValidateNotBeforeClaim(false);
+ .withRequireIssuedAtClaim(false)
+ .withRequireNotBeforeClaim(false);
}
private KeyBindingJwtVerificationOpts.Builder defaultKeyBindingJwtVerificationOpts() {
@@ -442,8 +468,8 @@ public abstract class SdJwtVPVerificationTest {
.withAllowedMaxAge(Integer.MAX_VALUE)
.withNonce("1234567890")
.withAud("https://verifier.example.org")
- .withValidateExpirationClaim(false)
- .withValidateNotBeforeClaim(false);
+ .withRequireExpirationClaim(false)
+ .withRequireNotBeforeClaim(false);
}
private ObjectNode exampleKbPayload() {
@@ -455,4 +481,17 @@ public abstract class SdJwtVPVerificationTest {
return payload;
}
+
+ private SdJwtVP exampleSdJwtWithCustomKbPayload(JsonNode kbPayloadSubstitute) {
+ KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(
+ kbPayloadSubstitute,
+ testSettings.holderSigContext,
+ KeyBindingJWT.TYP
+ );
+
+ String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s20.1-sdjwt+kb.txt");
+ String sdJwtWithoutKb = sdJwtVPString.substring(0, sdJwtVPString.lastIndexOf(SdJwt.DELIMITER) + 1);
+
+ return SdJwtVP.of(sdJwtWithoutKb + keyBindingJWT.toJws());
+ }
}
diff --git a/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json b/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json
index 27e72e26ada..e705d0ca9fc 100644
--- a/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json
+++ b/core/src/test/resources/sdjwt/a1.example2-issuer-claims.json
@@ -1,5 +1,4 @@
{
"iss": "https://issuer.example.com",
- "iat": 1683000000,
- "exp": 1883000000
+ "aud": "https://api.example.com"
}
\ No newline at end of file
diff --git a/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json b/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json
index 1ae5d7e1557..32f21645586 100644
--- a/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json
+++ b/core/src/test/resources/sdjwt/a1.example2-issuer-payload.json
@@ -10,8 +10,7 @@
"vg70gfzXO8HR7ERDkL46S6Ior1ey0DvZoEUHupJwoxc"
],
"iss": "https://issuer.example.com",
- "iat": 1683000000,
- "exp": 1883000000,
+ "aud": "https://api.example.com",
"address": {
"_sd": [
"IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/credentialbuilder/SdJwtCredentialBuilderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/credentialbuilder/SdJwtCredentialBuilderTest.java
index 30ded9064fc..bb867461ee1 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/credentialbuilder/SdJwtCredentialBuilderTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/credentialbuilder/SdJwtCredentialBuilderTest.java
@@ -138,9 +138,9 @@ public class SdJwtCredentialBuilderTest extends CredentialBuilderTest {
sdJwt.getSdJwtVerificationContext().verifyIssuance(
List.of(exampleVerifier()),
IssuerSignedJwtVerificationOpts.builder()
- .withValidateIssuedAtClaim(false)
- .withValidateNotBeforeClaim(false)
- .withValidateExpirationClaim(false)
+ .withRequireIssuedAtClaim(false)
+ .withRequireNotBeforeClaim(false)
+ .withRequireExpirationClaim(false)
.build(),
null
);