[OID4VCI]: Use Keycloak time utility for OID4VC related timestamps (#44871)

Closes: #44235


Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
forkimenjeckayang 2025-12-17 14:58:01 +01:00 committed by GitHub
parent 3218cd1847
commit ca617d9711
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 40 additions and 33 deletions

View File

@ -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

View File

@ -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);

View File

@ -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");

View File

@ -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));

View File

@ -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)",

View File

@ -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);

View File

@ -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<String> 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();