From ce05241c7f4c60433f37e8d96cb2f52eb00ce6a3 Mon Sep 17 00:00:00 2001 From: Ingrid Kamga Date: Tue, 11 Nov 2025 09:02:44 +0100 Subject: [PATCH] [OID4VCI] Tolerate clock skew in SD-JWT time checks (#43506) Closes #43456 Signed-off-by: Ingrid Kamga --- .../IssuerSignedJwtVerificationOpts.java | 59 ++--- .../main/java/org/keycloak/sdjwt/SdJws.java | 47 +--- .../sdjwt/SdJwtVerificationContext.java | 33 +-- .../sdjwt/TimeClaimVerificationOpts.java | 113 +++++++++ .../org/keycloak/sdjwt/TimeClaimVerifier.java | 130 +++++++++++ .../vp/KeyBindingJwtVerificationOpts.java | 59 +++-- .../java/org/keycloak/sdjwt/SdJwsTest.java | 59 ----- .../org/keycloak/sdjwt/SdJwtFacadeTest.java | 6 +- .../keycloak/sdjwt/SdJwtVerificationTest.java | 29 +-- .../keycloak/sdjwt/TimeClaimVerifierTest.java | 215 ++++++++++++++++++ .../SdJwtPresentationConsumerTest.java | 8 +- .../sdjwtvp/SdJwtVPVerificationTest.java | 95 +++++--- .../sdjwt/a1.example2-issuer-claims.json | 3 +- .../sdjwt/a1.example2-issuer-payload.json | 3 +- .../SdJwtCredentialBuilderTest.java | 6 +- 15 files changed, 603 insertions(+), 262 deletions(-) create mode 100644 core/src/main/java/org/keycloak/sdjwt/TimeClaimVerificationOpts.java create mode 100644 core/src/main/java/org/keycloak/sdjwt/TimeClaimVerifier.java create mode 100644 core/src/test/java/org/keycloak/sdjwt/TimeClaimVerifierTest.java 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 );