From 0b7f9e0d741e32dd2c7db2b72a6e1325121343b8 Mon Sep 17 00:00:00 2001
From: Daniel DeGroff
Date: Thu, 21 May 2026 13:08:53 -0600
Subject: [PATCH 1/2] sign test
From ae785b4244379b8e82ad1b21e6bf22a3c2529a91 Mon Sep 17 00:00:00 2001
From: Daniel DeGroff
Date: Thu, 21 May 2026 15:53:07 -0600
Subject: [PATCH 2/2] Remove JWTDecoder.Builder.fixedTime convenience
clock(Clock) already exists as the primary API for overriding the
decoder's notion of "now". Call sites use
clock(Clock.fixed(instant, ZoneOffset.UTC)) directly, matching the
JDK's Clock vocabulary.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
README.md | 6 +++---
.../lattejwtjackson/LatteJWTJacksonAdapter.java | 10 ++++++----
.../jwt/benchmarks/lattejwt/LatteJWTAdapter.java | 10 ++++++----
src/main/java/org/lattejava/jwt/JWT.java | 4 ++--
src/main/java/org/lattejava/jwt/JWTDecoder.java | 11 +----------
src/test/java/org/lattejava/jwt/JWTTest.java | 6 +++---
.../java/org/lattejava/jwt/RFC7515VectorsTest.java | 3 ++-
7 files changed, 23 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index cec6370..72f3c14 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ The recommended entry points are the `Signers` and `Verifiers` factories. They t
JWTs are built with an immutable fluent builder and serialized by a `JWTEncoder`. Decoding is done by a `JWTDecoder`, which takes a `VerifierResolver` — use `VerifierResolver.byKid(Map)` for a `kid`-indexed keyring, or `VerifierResolver.of(verifier)` to wrap a single verifier.
-For the happy path, `JWT.decode(encodedJWT, verifier)` (and an `(encodedJWT, verifier, validator)` overload) routes through a shared default `JWTDecoder` so you don't have to construct one. The same overloads accept a `VerifierResolver` when you have a keyring. Build your own `JWTDecoder` with `JWTDecoder.builder()` when you need non-default settings — a custom `JSONProcessor`, `clockSkew`, allowed algorithms, `fixedTime`, etc. — and pass it to the `JWT.decode(encodedJWT, decoder, verifier)` overload (or call `decoder.decode(...)` directly).
+For the happy path, `JWT.decode(encodedJWT, verifier)` (and an `(encodedJWT, verifier, validator)` overload) routes through a shared default `JWTDecoder` so you don't have to construct one. The same overloads accept a `VerifierResolver` when you have a keyring. Build your own `JWTDecoder` with `JWTDecoder.builder()` when you need non-default settings — a custom `JSONProcessor`, `clockSkew`, allowed algorithms, a custom `Clock`, etc. — and pass it to the `JWT.decode(encodedJWT, decoder, verifier)` overload (or call `decoder.decode(...)` directly).
#### Sign and encode a JWT using HMAC
```java
@@ -175,7 +175,7 @@ Alternate: `ECVerifier.newVerifier(Paths.get("public_key.pem"))`.
#### Build your own JWTDecoder
-The `JWT.decode(...)` static helpers route through a shared default decoder that uses the bundled zero-dependency `LatteJSONProcessor`. Build your own when you need a different `JSONProcessor` or any other non-default setting — `clockSkew`, `expectedAlgorithms`, `expectedType`, `criticalHeaders`, `maxInputBytes`, `fixedTime`, etc. The clock-skew and time-pinning examples below use the same builder.
+The `JWT.decode(...)` static helpers route through a shared default decoder that uses the bundled zero-dependency `LatteJSONProcessor`. Build your own when you need a different `JSONProcessor` or any other non-default setting — `clockSkew`, `expectedAlgorithms`, `expectedType`, `criticalHeaders`, `maxInputBytes`, a custom `Clock`, etc. The clock-skew and time-pinning examples below use the same builder.
`JSONProcessor` is a strategy interface — `serialize(Map)` / `deserialize(byte[])` — and **the library does not bundle a Jackson, Gson, or other third-party adapter**. To use one, write a small class that implements `JSONProcessor` and delegates to your JSON library of choice. Implementations must be stateless and thread-safe; the encoder and decoder may call them concurrently.
@@ -291,7 +291,7 @@ Verifier verifier = Verifiers.forAsymmetric(Algorithm.ES256,
// Pin the decoder to a specific instant. Use ONLY in tests — never in production.
Instant thePast = Instant.parse("2019-01-01T00:00:00Z");
JWTDecoder timeMachine = JWTDecoder.builder()
- .fixedTime(thePast)
+ .clock(Clock.fixed(thePast, ZoneOffset.UTC))
.build();
JWT jwt = timeMachine.decode(encodedJWT, VerifierResolver.of(verifier));
diff --git a/benchmarks/vendors/latte-jwt-jackson/src/main/java/org/lattejava/jwt/benchmarks/lattejwtjackson/LatteJWTJacksonAdapter.java b/benchmarks/vendors/latte-jwt-jackson/src/main/java/org/lattejava/jwt/benchmarks/lattejwtjackson/LatteJWTJacksonAdapter.java
index ea3f957..293a895 100644
--- a/benchmarks/vendors/latte-jwt-jackson/src/main/java/org/lattejava/jwt/benchmarks/lattejwtjackson/LatteJWTJacksonAdapter.java
+++ b/benchmarks/vendors/latte-jwt-jackson/src/main/java/org/lattejava/jwt/benchmarks/lattejwtjackson/LatteJWTJacksonAdapter.java
@@ -4,7 +4,9 @@
*/
package org.lattejava.jwt.benchmarks.lattejwtjackson;
+import java.time.Clock;
import java.time.Instant;
+import java.time.ZoneOffset;
import org.lattejava.jwt.Algorithm;
import org.lattejava.jwt.JSONProcessor;
@@ -55,10 +57,10 @@ public void prepare(Fixtures fixtures) throws Exception {
encoder = new JWTEncoder(json);
- Instant fixedNow = Instant.ofEpochSecond(1761408000L + 1800L);
- es256Decoder = JWTDecoder.builder().fixedTime(fixedNow).jsonProcessor(json).build();
- hs256Decoder = JWTDecoder.builder().fixedTime(fixedNow).jsonProcessor(json).build();
- rs256Decoder = JWTDecoder.builder().fixedTime(fixedNow).jsonProcessor(json).build();
+ Clock fixedClock = Clock.fixed(Instant.ofEpochSecond(1761408000L + 1800L), ZoneOffset.UTC);
+ es256Decoder = JWTDecoder.builder().clock(fixedClock).jsonProcessor(json).build();
+ hs256Decoder = JWTDecoder.builder().clock(fixedClock).jsonProcessor(json).build();
+ rs256Decoder = JWTDecoder.builder().clock(fixedClock).jsonProcessor(json).build();
unsafeDecoder = JWTDecoder.builder().jsonProcessor(json).build();
canonicalJWT = JWT.builder()
diff --git a/benchmarks/vendors/latte-jwt/src/main/java/org/lattejava/jwt/benchmarks/lattejwt/LatteJWTAdapter.java b/benchmarks/vendors/latte-jwt/src/main/java/org/lattejava/jwt/benchmarks/lattejwt/LatteJWTAdapter.java
index 41714b6..0b3ccda 100644
--- a/benchmarks/vendors/latte-jwt/src/main/java/org/lattejava/jwt/benchmarks/lattejwt/LatteJWTAdapter.java
+++ b/benchmarks/vendors/latte-jwt/src/main/java/org/lattejava/jwt/benchmarks/lattejwt/LatteJWTAdapter.java
@@ -4,7 +4,9 @@
*/
package org.lattejava.jwt.benchmarks.lattejwt;
+import java.time.Clock;
import java.time.Instant;
+import java.time.ZoneOffset;
import org.lattejava.jwt.Algorithm;
import org.lattejava.jwt.JWT;
@@ -46,10 +48,10 @@ public void prepare(Fixtures fixtures) throws Exception {
encoder = new JWTEncoder();
- Instant fixedNow = Instant.ofEpochSecond(1761408000L + 1800L);
- es256Decoder = JWTDecoder.builder().fixedTime(fixedNow).build();
- hs256Decoder = JWTDecoder.builder().fixedTime(fixedNow).build();
- rs256Decoder = JWTDecoder.builder().fixedTime(fixedNow).build();
+ Clock fixedClock = Clock.fixed(Instant.ofEpochSecond(1761408000L + 1800L), ZoneOffset.UTC);
+ es256Decoder = JWTDecoder.builder().clock(fixedClock).build();
+ hs256Decoder = JWTDecoder.builder().clock(fixedClock).build();
+ rs256Decoder = JWTDecoder.builder().clock(fixedClock).build();
unsafeDecoder = JWTDecoder.builder().build();
canonicalJWT = JWT.builder()
diff --git a/src/main/java/org/lattejava/jwt/JWT.java b/src/main/java/org/lattejava/jwt/JWT.java
index 479d59d..b5eb564 100644
--- a/src/main/java/org/lattejava/jwt/JWT.java
+++ b/src/main/java/org/lattejava/jwt/JWT.java
@@ -98,7 +98,7 @@ public static Builder builder() {
* be observed until the signature has been validated.
*
* Build your own decoder with {@link JWTDecoder#builder()} when you need non-default settings (custom
- * {@link JSONProcessor}, {@code clockSkew}, allowed algorithms, {@code fixedTime}, etc.).
+ * {@link JSONProcessor}, {@code clockSkew}, allowed algorithms, custom {@link java.time.Clock}, etc.).
*
* @param encodedJWT the compact JWS string; must be non-null
* @param resolver the verifier resolver; must be non-null
@@ -151,7 +151,7 @@ public static JWT decode(String encodedJWT, JWTDecoder decoder, Verifier verifie
* subclass to reject the token.
*
* Build your own decoder with {@link JWTDecoder#builder()} when you need non-default settings (custom
- * {@link JSONProcessor}, {@code clockSkew}, allowed algorithms, {@code fixedTime}, etc.).
+ * {@link JSONProcessor}, {@code clockSkew}, allowed algorithms, custom {@link java.time.Clock}, etc.).
*
* @param encodedJWT the compact JWS string; must be non-null
* @param resolver the verifier resolver; must be non-null
diff --git a/src/main/java/org/lattejava/jwt/JWTDecoder.java b/src/main/java/org/lattejava/jwt/JWTDecoder.java
index a1b8e8c..321f135 100644
--- a/src/main/java/org/lattejava/jwt/JWTDecoder.java
+++ b/src/main/java/org/lattejava/jwt/JWTDecoder.java
@@ -139,7 +139,7 @@ public static Builder builder() {
/**
* Returns the shared default {@link JWTDecoder} used by {@link JWT#decode(String, VerifierResolver)} and its
* overloads. Build your own with {@link #builder()} when you need non-default settings (custom {@link JSONProcessor},
- * {@code clockSkew}, allowed algorithms, {@code fixedTime}, etc.).
+ * {@code clockSkew}, allowed algorithms, custom {@link Clock}, etc.).
*
* @return the shared default instance; never {@code null}
*/
@@ -550,15 +550,6 @@ public Builder expectedType(String expectedType) {
return this;
}
- /**
- * Convenience for {@code clock(Clock.fixed(instant, ZoneOffset.UTC))}.
- */
- public Builder fixedTime(Instant instant) {
- Objects.requireNonNull(instant, "instant");
- this.clock = Clock.fixed(instant, ZoneOffset.UTC);
- return this;
- }
-
public Builder jsonProcessor(JSONProcessor jsonProcessor) {
this.jsonProcessor = Objects.requireNonNull(jsonProcessor, "jsonProcessor");
this.jsonProcessorExplicit = true;
diff --git a/src/test/java/org/lattejava/jwt/JWTTest.java b/src/test/java/org/lattejava/jwt/JWTTest.java
index 7e33a88..7e476a3 100644
--- a/src/test/java/org/lattejava/jwt/JWTTest.java
+++ b/src/test/java/org/lattejava/jwt/JWTTest.java
@@ -308,9 +308,9 @@ public void decode_single_verifier_shortcut_with_validator() {
@Test
public void decode_uses_supplied_decoder() {
- // Use case: JWT.decode(encodedJWT, decoder, resolver) routes through the supplied decoder. We confirm by
- // pinning fixedTime far in the future and asserting the call would fail without our supplied clock skew --
- // proving the supplied decoder (not the default) was consulted.
+ // Use case: JWT.decode(encodedJWT, decoder, resolver) routes through the supplied decoder. We confirm by pinning
+ // the clock far in the future and asserting the call would fail without our supplied clock skew -- proving the
+ // supplied decoder (not the default) was consulted.
JWT jwt = JWT.builder()
.subject("supplied-decoder")
.expiresAt(Instant.parse("2026-04-26T12:00:00Z"))
diff --git a/src/test/java/org/lattejava/jwt/RFC7515VectorsTest.java b/src/test/java/org/lattejava/jwt/RFC7515VectorsTest.java
index 9f614c4..e088b37 100644
--- a/src/test/java/org/lattejava/jwt/RFC7515VectorsTest.java
+++ b/src/test/java/org/lattejava/jwt/RFC7515VectorsTest.java
@@ -8,6 +8,7 @@
import java.math.*;
import java.security.*;
import java.security.spec.*;
+import java.time.*;
import java.util.*;
import org.lattejava.jwt.algorithm.ec.*;
@@ -97,7 +98,7 @@ private static PublicKey rsaPublic(BigInteger n, BigInteger e) {
*/
private static JWTDecoder vectorClockDecoder() {
return JWTDecoder.builder()
- .fixedTime(java.time.Instant.ofEpochSecond(1300819300L))
+ .clock(Clock.fixed(Instant.ofEpochSecond(1300819300L), ZoneOffset.UTC))
.build();
}