From ca617d971156ea3d457b6bbfacf3191030a003c5 Mon Sep 17 00:00:00 2001 From: forkimenjeckayang <104195313+forkimenjeckayang@users.noreply.github.com> Date: Wed, 17 Dec 2025 14:58:01 +0100 Subject: [PATCH] [OID4VCI]: Use Keycloak time utility for OID4VC related timestamps (#44871) Closes: #44235 Signed-off-by: forkimenjeckayang --- .../test/java/org/keycloak/sdjwt/SdJwsTest.java | 15 ++++++++------- .../sdjwt/SdJwtCreationAndSigningTest.java | 15 +++++++++------ .../org/keycloak/sdjwt/SdJwtVerificationTest.java | 10 +++++----- .../sdjwt/sdjwtvp/SdJwtVPVerificationTest.java | 12 ++++++------ .../issuance/keybinding/JwtCNonceHandler.java | 11 +++++------ .../mappers/OID4VCIssuedAtTimeClaimMapper.java | 5 +++-- .../oid4vc/issuance/signing/OID4VCTest.java | 5 ++++- 7 files changed, 40 insertions(+), 33 deletions(-) diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java index 19f272de1f0..7abcd4e66c4 100644 --- a/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwsTest.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Time; import org.keycloak.jose.jws.JWSHeader; import org.keycloak.rule.CryptoInitRule; @@ -46,7 +47,7 @@ public abstract class SdJwsTest { ObjectMapper mapper = new ObjectMapper(); ObjectNode node = mapper.createObjectNode(); node.put("sub", "test"); - node.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + node.put("exp", Instant.ofEpochSecond(Time.currentTime()).plus(1, ChronoUnit.HOURS).getEpochSecond()); node.put("name", "Test User"); return node; } @@ -72,7 +73,7 @@ public abstract class SdJwsTest { @Test public void testVerifyExpClaim_ExpiredJWT() { ObjectNode payload = createPayload(); - payload.put("exp", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()); + payload.put("exp", Instant.ofEpochSecond(Time.currentTime()).minus(1, ChronoUnit.HOURS).getEpochSecond()); assertThrows(VerificationException.class, () -> { new ClaimVerifier.ExpCheck(0, false).test(payload); }); @@ -81,7 +82,7 @@ public abstract class SdJwsTest { @Test public void testVerifyExpClaim_Positive() throws Exception { ObjectNode payload = createPayload(); - payload.put("exp", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + payload.put("exp", Instant.ofEpochSecond(Time.currentTime()).plus(1, ChronoUnit.HOURS).getEpochSecond()); new ClaimVerifier.ExpCheck(0, false).test(payload); } @@ -89,7 +90,7 @@ public abstract class SdJwsTest { @Test public void testVerifyNotBeforeClaim_Negative() { ObjectNode payload = createPayload(); - payload.put("nbf", Instant.now().plus(1, ChronoUnit.HOURS).getEpochSecond()); + payload.put("nbf", Instant.ofEpochSecond(Time.currentTime()).plus(1, ChronoUnit.HOURS).getEpochSecond()); assertThrows(VerificationException.class, () -> { new ClaimVerifier.NbfCheck(0, false).test(payload); }); @@ -98,7 +99,7 @@ public abstract class SdJwsTest { @Test public void testVerifyNotBeforeClaim_Positive() throws Exception { ObjectNode payload = createPayload(); - payload.put("nbf", Instant.now().minus(1, ChronoUnit.HOURS).getEpochSecond()); + payload.put("nbf", Instant.ofEpochSecond(Time.currentTime()).minus(1, ChronoUnit.HOURS).getEpochSecond()); new ClaimVerifier.NbfCheck(0, false).test(payload); } @@ -179,7 +180,7 @@ public abstract class SdJwsTest { @Test public void shouldValidateAgeSinceIssued() throws VerificationException { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); JwsToken sdJws = exampleSdJws(now); new ClaimVerifier.IatLifetimeCheck(0, 180).test(sdJws.getPayload()); @@ -187,7 +188,7 @@ public abstract class SdJwsTest { @Test public void shouldValidateAgeSinceIssued_IfJwtIsTooOld() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); long iat = now - 1000; long maxLifetime = 180; JwsToken sdJws = exampleSdJws(iat); // that will be too old diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java index 4b4c9a6c146..f947530305f 100644 --- a/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtCreationAndSigningTest.java @@ -30,6 +30,7 @@ import java.util.stream.Collectors; import org.keycloak.OID4VCConstants; import org.keycloak.common.VerificationException; import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.Time; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.ECDSASignatureSignerContext; import org.keycloak.crypto.ECDSASignatureVerifierContext; @@ -66,9 +67,10 @@ public abstract class SdJwtCreationAndSigningTest { @Test public void testCreateSdJwtWithoutKeybindingAndNoSignature() throws Exception { - final long iat = Instant.now().minus(10, ChronoUnit.SECONDS).getEpochSecond(); - final long nbf = Instant.now().minus(5, ChronoUnit.SECONDS).getEpochSecond(); - final long exp = Instant.now().plus(60, ChronoUnit.SECONDS).getEpochSecond(); + Instant now = Instant.ofEpochSecond(Time.currentTime()); + final long iat = now.minus(10, ChronoUnit.SECONDS).getEpochSecond(); + final long nbf = now.minus(5, ChronoUnit.SECONDS).getEpochSecond(); + final long exp = now.plus(60, ChronoUnit.SECONDS).getEpochSecond(); String disclosurePayload = "{\n" + " \"given_name\": \"Carlos\",\n" + @@ -193,9 +195,10 @@ public abstract class SdJwtCreationAndSigningTest { SignatureSignerContext issuerSignerContext = new ECDSASignatureSignerContext(issuerKeyPair); SignatureSignerContext holderSignerContext = new ECDSASignatureSignerContext(holderKeyPair); - final long iat = Instant.now().minus(10, ChronoUnit.SECONDS).getEpochSecond(); - final long nbf = Instant.now().minus(5, ChronoUnit.SECONDS).getEpochSecond(); - final long exp = Instant.now().plus(60, ChronoUnit.SECONDS).getEpochSecond(); + Instant now = Instant.ofEpochSecond(Time.currentTime()); + final long iat = now.minus(10, ChronoUnit.SECONDS).getEpochSecond(); + final long nbf = now.minus(5, ChronoUnit.SECONDS).getEpochSecond(); + final long exp = now.plus(60, ChronoUnit.SECONDS).getEpochSecond(); final String nonce = "123456789"; final String audience = String.format("x509_san_dns:%s", authorizationServerUrl); diff --git a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java index 458e2421830..1f365b3fd3b 100644 --- a/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/SdJwtVerificationTest.java @@ -17,7 +17,6 @@ package org.keycloak.sdjwt; -import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -25,6 +24,7 @@ import java.util.function.Function; import org.keycloak.OID4VCConstants; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Time; import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.rule.CryptoInitRule; @@ -169,7 +169,7 @@ public abstract class SdJwtVerificationTest { @Test public void sdJwtVerificationShouldFail_IfExpired() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode claimSet = mapper.createObjectNode(); claimSet.put("given_name", "John"); @@ -220,7 +220,7 @@ public abstract class SdJwtVerificationTest { // exp: null ObjectNode claimSet1 = mapper.createObjectNode(); claimSet1.put("given_name", "John"); - claimSet1.put("exp", Instant.now().getEpochSecond() - (31536000)); + claimSet1.put("exp", Time.currentTime() - (31536000)); // exp: invalid ObjectNode claimSet2 = mapper.createObjectNode(); @@ -268,7 +268,7 @@ public abstract class SdJwtVerificationTest { @Test public void sdJwtVerificationShouldFail_IfIssuedInTheFuture() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode claimSet = mapper.createObjectNode(); claimSet.put("given_name", "John"); @@ -317,7 +317,7 @@ public abstract class SdJwtVerificationTest { @Test public void sdJwtVerificationShouldFail_IfNbfInvalid() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode claimSet = mapper.createObjectNode(); claimSet.put("given_name", "John"); 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 d062df1d40f..aae18aecc28 100644 --- a/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java +++ b/core/src/test/java/org/keycloak/sdjwt/sdjwtvp/SdJwtVPVerificationTest.java @@ -17,13 +17,13 @@ package org.keycloak.sdjwt.sdjwtvp; -import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.keycloak.OID4VCConstants; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Time; import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.rule.CryptoInitRule; import org.keycloak.sdjwt.IssuerSignedJwtVerificationOpts; @@ -265,7 +265,7 @@ public abstract class SdJwtVPVerificationTest { @Test public void testShouldFail_IfKbIssuedInFuture() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode kbPayload = exampleKbPayload(); kbPayload.set(OID4VCConstants.CLAIM_NAME_IAT, mapper.valueToTree(now + 1000)); @@ -280,7 +280,7 @@ public abstract class SdJwtVPVerificationTest { @Test public void testShouldTolerateKbIssuedInTheFutureWithinClockSkew() throws VerificationException { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode kbPayload = exampleKbPayload(); // Issued just 5 seconds in the future. Should pass with a clock skew of 10 seconds. @@ -317,7 +317,7 @@ public abstract class SdJwtVPVerificationTest { @Test public void testShouldFail_IfKbExpired() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode kbPayload = exampleKbPayload(); kbPayload.set(OID4VCConstants.CLAIM_NAME_EXP, mapper.valueToTree(now - 1000)); @@ -332,7 +332,7 @@ public abstract class SdJwtVPVerificationTest { @Test public void testShouldTolerateExpiredKbWithinClockSkew() throws VerificationException { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode kbPayload = exampleKbPayload(); // Expires just 5 seconds ago. Should pass with a clock skew of 10 seconds. @@ -351,7 +351,7 @@ public abstract class SdJwtVPVerificationTest { @Test public void testShouldFail_IfKbNotBeforeTimeYet() { - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); ObjectNode kbPayload = exampleKbPayload(); kbPayload.set(OID4VCConstants.CLAIM_NAME_NBF, mapper.valueToTree(now + 1000)); diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/keybinding/JwtCNonceHandler.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/keybinding/JwtCNonceHandler.java index d29a35889d0..ef856330615 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/keybinding/JwtCNonceHandler.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/keybinding/JwtCNonceHandler.java @@ -18,8 +18,6 @@ package org.keycloak.protocol.oid4vc.issuance.keybinding; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -34,6 +32,7 @@ import jakarta.annotation.Nullable; import org.keycloak.TokenVerifier; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Time; import org.keycloak.constants.OID4VCIConstants; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.KeyUse; @@ -80,10 +79,10 @@ public class JwtCNonceHandler implements CNonceHandler { RealmModel realm = keycloakSession.getContext().getRealm(); final String issuer = OID4VCIssuerWellKnownProvider.getIssuer(keycloakSession.getContext()); // TODO discussion about the attribute name to use - final Integer nonceLifetimeMillis = realm.getAttribute(OID4VCIConstants.C_NONCE_LIFETIME_IN_SECONDS, 60); + final Integer nonceLifetimeSeconds = realm.getAttribute(OID4VCIConstants.C_NONCE_LIFETIME_IN_SECONDS, 60); audiences = Optional.ofNullable(audiences).orElseGet(Collections::emptyList); - final Instant now = Instant.now(); - final long expiresAt = now.plus(nonceLifetimeMillis, ChronoUnit.SECONDS).getEpochSecond(); + final long nowSeconds = Time.currentTime(); + final long expiresAt = nowSeconds + nonceLifetimeSeconds; final int nonceLength = NONCE_DEFAULT_LENGTH + new Random().nextInt(NONCE_LENGTH_RANDOM_OFFSET); // this generated value itself is basically just a salt-value for the generated token, which itself is the nonce. final String strongSalt = Base64.getEncoder().encodeToString(RandomSecret.createRandomSecret(nonceLength)); @@ -144,7 +143,7 @@ public class JwtCNonceHandler implements CNonceHandler { if (exp == null) { throw new VerificationException("c_nonce has no expiration time"); } - long now = Instant.now().getEpochSecond(); + long now = Time.currentTime(); if (exp < now) { String message = String.format( "c_nonce not valid: %s(exp) < %s(now)", diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java index 9105f0748a0..05a34a3475b 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/mappers/OID4VCIssuedAtTimeClaimMapper.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.models.oid4vci.CredentialScopeModel; @@ -121,9 +122,9 @@ public class OID4VCIssuedAtTimeClaimMapper extends OID4VCMapper { Instant iat = Optional.ofNullable(mapperModel.getConfig()) .flatMap(config -> Optional.ofNullable(config.get(VALUE_SOURCE))) .filter(valueSource -> Objects.equals(valueSource, "COMPUTE")) - .map(valueSource -> Instant.now()) + .map(valueSource -> Instant.ofEpochSecond(Time.currentTime())) .orElseGet(() -> Optional.ofNullable(verifiableCredential.getIssuanceDate()) - .orElse(Instant.now())); + .orElse(Instant.ofEpochSecond(Time.currentTime()))); Instant normalizedIat = new TimeClaimNormalizer(userSessionModel.getRealm()) .normalize(iat); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java index 20643398184..0ce6fb75a05 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCTest.java @@ -55,6 +55,7 @@ import org.keycloak.common.util.CertificateUtils; import org.keycloak.common.util.KeyUtils; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.common.util.PemUtils; +import org.keycloak.common.util.Time; import org.keycloak.constants.OID4VCIConstants; import org.keycloak.crypto.ECDSASignatureSignerContext; import org.keycloak.crypto.KeyUse; @@ -119,7 +120,9 @@ public abstract class OID4VCTest extends AbstractTestRealmKeycloakTest { protected static final String CONTEXT_URL = "https://www.w3.org/2018/credentials/v1"; protected static final URI TEST_DID = URI.create("did:web:test.org"); protected static final List TEST_TYPES = List.of("VerifiableCredential"); - protected static final Instant TEST_EXPIRATION_DATE = Instant.now().plus(365, ChronoUnit.DAYS).truncatedTo(ChronoUnit.SECONDS); + protected static final Instant TEST_EXPIRATION_DATE = Instant.ofEpochMilli(Time.currentTimeMillis()) + .plus(365, ChronoUnit.DAYS) + .truncatedTo(ChronoUnit.SECONDS); protected static final Instant TEST_ISSUANCE_DATE = Instant.ofEpochSecond(1000); protected static final KeyWrapper RSA_KEY = getRsaKey();