From 9bdbb85d70dfa7baf422fb9db8f710a289f9780d Mon Sep 17 00:00:00 2001 From: pveeckhout Date: Mon, 25 Dec 2023 21:46:33 +0100 Subject: [PATCH 1/6] #235 Replace usage of java.util.Date with java.time.Instant in JWT claims This commit replaces the usage of java.util.Date with java.time.Instant in handling JWT claims. This means upgrading from JDK7 to JDK8. This is done for better adherence to ISO 8601 standards and better precision in time-related operations. Classes working with claims have been updated to use the newer Instant class, providing better precision and cross-timezone compatibility. Additionally, overloading convenience methods for setting OffsetDateTime and ZonedDateTime on top of Instant have been added on the JwtBuilder and DefaultJwtBuilder. --- README.md | 4 +- api/src/main/java/io/jsonwebtoken/Claims.java | 10 +- .../java/io/jsonwebtoken/ClaimsMutator.java | 20 ++-- api/src/main/java/io/jsonwebtoken/Clock.java | 4 +- .../main/java/io/jsonwebtoken/JwtBuilder.java | 112 +++++++++++++++-- .../io/jsonwebtoken/JwtParserBuilder.java | 8 +- .../io/jsonwebtoken/lang/DateFormats.java | 69 +++++------ .../jsonwebtoken/lang/DateFormatsTest.groovy | 87 ++++++++++++-- extensions/jackson/pom.xml | 4 + .../jackson/io/JacksonSerializer.java | 3 +- .../orgjson/io/OrgJsonSerializer.java | 20 +++- .../orgjson/io/OrgJsonSerializerTest.groovy | 34 +++++- .../io/jsonwebtoken/impl/DefaultClaims.java | 28 ++--- .../io/jsonwebtoken/impl/DefaultClock.java | 10 +- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 71 +++++++---- .../jsonwebtoken/impl/DefaultJwtParser.java | 35 +++--- .../impl/DefaultJwtParserBuilder.java | 20 ++-- .../impl/DelegatingClaimsMutator.java | 14 +-- .../java/io/jsonwebtoken/impl/FixedClock.java | 14 +-- .../impl/lang/JwtDateConverter.java | 83 +++++++------ .../io/jsonwebtoken/impl/lang/Parameters.java | 6 +- .../io/jsonwebtoken/JwtParserTest.groovy | 103 ++++++++-------- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 22 +++- .../impl/DefaultClaimsTest.groovy | 113 +++++++++++------- .../impl/DefaultJwtParserBuilderTest.groovy | 1 + .../impl/DefaultJwtParserTest.groovy | 10 +- .../impl/lang/JwtDateConverterTest.groovy | 2 +- pom.xml | 8 +- 28 files changed, 591 insertions(+), 324 deletions(-) diff --git a/README.md b/README.md index 79bc6cc1b..98edcac5e 100644 --- a/README.md +++ b/README.md @@ -991,8 +991,8 @@ String jws = Jwts.builder() .issuer("me") .subject("Bob") .audience("you") - .expiration(expiration) //a java.util.Date - .notBefore(notBefore) //a java.util.Date + .expiration(expiration) //a java.time.Instant + .notBefore(notBefore) //a java.time.Instant .issuedAt(new Date()) // for example, now .id(UUID.randomUUID().toString()) //just an example id diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 4f8589fa3..cd1a79dfd 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -15,7 +15,7 @@ */ package io.jsonwebtoken; -import java.util.Date; +import java.time.Instant; import java.util.Map; import java.util.Set; @@ -106,7 +106,7 @@ public interface Claims extends Map, Identifiable { * * @return the JWT {@code exp} value or {@code null} if not present. */ - Date getExpiration(); + Instant getExpiration(); /** * Returns the JWT @@ -116,7 +116,7 @@ public interface Claims extends Map, Identifiable { * * @return the JWT {@code nbf} value or {@code null} if not present. */ - Date getNotBefore(); + Instant getNotBefore(); /** * Returns the JWT @@ -126,7 +126,7 @@ public interface Claims extends Map, Identifiable { * * @return the JWT {@code iat} value or {@code null} if not present. */ - Date getIssuedAt(); + Instant getIssuedAt(); /** * Returns the JWTs @@ -147,7 +147,7 @@ public interface Claims extends Map, Identifiable { * Returns the JWTs claim ({@code claimName}) value as a {@code requiredType} instance, or {@code null} if not * present. * - *

JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more + *

JJWT only converts simple String, Date, OffsetDateTime, ZonedDateTime, Long, Integer, Short and Byte types automatically. Anything more * complex is expected to be already converted to your desired type by the JSON parser. You may specify a custom * JSON processor using the {@code JwtParserBuilder}'s * {@link JwtParserBuilder#json(io.jsonwebtoken.io.Deserializer) json(Deserializer)} method. See the JJWT diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 4db0950a5..008d5dab5 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -17,8 +17,8 @@ import io.jsonwebtoken.lang.NestedCollection; +import java.time.Instant; import java.util.Collection; -import java.util.Date; /** * Mutation (modifications) to a {@link io.jsonwebtoken.Claims Claims} instance. @@ -113,10 +113,10 @@ public interface ClaimsMutator> { * @param exp the JWT {@code exp} value or {@code null} to remove the property from the JSON map. * @return the {@code Claims} instance for method chaining. * @deprecated since 0.12.0 in favor of the shorter and more modern builder-style named - * {@link #expiration(Date)}. This method will be removed before the JJWT 1.0 release. + * {@link #expiration(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setExpiration(Date exp); + T setExpiration(Instant exp); /** * Sets the JWT @@ -129,7 +129,7 @@ public interface ClaimsMutator> { * @return the {@code Claims} instance for method chaining. * @since 0.12.0 */ - T expiration(Date exp); + T expiration(Instant exp); /** * Sets the JWT @@ -141,10 +141,10 @@ public interface ClaimsMutator> { * @param nbf the JWT {@code nbf} value or {@code null} to remove the property from the JSON map. * @return the {@code Claims} instance for method chaining. * @deprecated since 0.12.0 in favor of the shorter and more modern builder-style named - * {@link #notBefore(Date)}. This method will be removed before the JJWT 1.0 release. + * {@link #notBefore(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setNotBefore(Date nbf); + T setNotBefore(Instant nbf); /** * Sets the JWT @@ -157,7 +157,7 @@ public interface ClaimsMutator> { * @return the {@code Claims} instance for method chaining. * @since 0.12.0 */ - T notBefore(Date nbf); + T notBefore(Instant nbf); /** * Sets the JWT @@ -169,10 +169,10 @@ public interface ClaimsMutator> { * @param iat the JWT {@code iat} value or {@code null} to remove the property from the JSON map. * @return the {@code Claims} instance for method chaining. * @deprecated since 0.12.0 in favor of the shorter and more modern builder-style named - * {@link #issuedAt(Date)}. This method will be removed before the JJWT 1.0 release. + * {@link #issuedAt(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setIssuedAt(Date iat); + T setIssuedAt(Instant iat); /** * Sets the JWT @@ -185,7 +185,7 @@ public interface ClaimsMutator> { * @return the {@code Claims} instance for method chaining. * @since 0.12.0 */ - T issuedAt(Date iat); + T issuedAt(Instant iat); /** * Sets the JWT diff --git a/api/src/main/java/io/jsonwebtoken/Clock.java b/api/src/main/java/io/jsonwebtoken/Clock.java index 584dd605f..672f5e615 100644 --- a/api/src/main/java/io/jsonwebtoken/Clock.java +++ b/api/src/main/java/io/jsonwebtoken/Clock.java @@ -15,7 +15,7 @@ */ package io.jsonwebtoken; -import java.util.Date; +import java.time.Instant; /** * A clock represents a time source that can be used when creating and verifying JWTs. @@ -29,5 +29,5 @@ public interface Clock { * * @return the clock's current timestamp at the instant the method is invoked. */ - Date now(); + Instant now(); } diff --git a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java index 634800851..879e086bf 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -40,7 +40,9 @@ import java.security.SecureRandom; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; -import java.util.Date; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Map; /** @@ -523,14 +525,46 @@ public interface JwtBuilder extends ClaimsMutator { * *

This is a convenience wrapper for:

*
-     * {@link #claims()}.{@link ClaimsMutator#expiration(Date) expiration(exp)}.{@link BuilderClaims#and() and()}
+ * {@link #claims()}.{@link ClaimsMutator#expiration(Instant) expiration(exp)}.{@link BuilderClaims#and() and()} * * @param exp the JWT {@code exp} value or {@code null} to remove the property from the Claims map. * @return the builder instance for method chaining. */ @Override // for better/targeted JavaDoc - JwtBuilder expiration(Date exp); + JwtBuilder expiration(Instant exp); + + /** + * Sets the JWT Claims
+ * exp (expiration) claim. A {@code null} value will remove the property from the Claims. + * + *

A JWT obtained after this timestamp should not be used.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #expiration(Instant) expiration(exp)}.{@link BuilderClaims#and() and()}
+ * + * @param exp the JWT {@code exp} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder expiration(OffsetDateTime exp); + + /** + * Sets the JWT Claims + * exp (expiration) claim. A {@code null} value will remove the property from the Claims. + * + *

A JWT obtained after this timestamp should not be used.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #expiration(Instant) expiration(exp)}.{@link BuilderClaims#and() and()}
+ * + * @param exp the JWT {@code exp} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder expiration(ZonedDateTime exp); /** * Sets the JWT Claims @@ -540,14 +574,46 @@ public interface JwtBuilder extends ClaimsMutator { * *

This is a convenience wrapper for:

*
-     * {@link #claims()}.{@link ClaimsMutator#notBefore(Date) notBefore(nbf)}.{@link BuilderClaims#and() and()}
+ * {@link #claims()}.{@link ClaimsMutator#notBefore(Instant) notBefore(nbf)}.{@link BuilderClaims#and() and()} * * @param nbf the JWT {@code nbf} value or {@code null} to remove the property from the Claims map. * @return the builder instance for method chaining. */ @Override // for better/targeted JavaDoc - JwtBuilder notBefore(Date nbf); + JwtBuilder notBefore(Instant nbf); + + /** + * Sets the JWT Claims
+ * nbf (not before) claim. A {@code null} value will remove the property from the Claims. + * + *

A JWT obtained before this timestamp should not be used.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #notBefore(Instant) notBefore(nbf)}.{@link BuilderClaims#and() and()}
+ * + * @param nbf the JWT {@code nbf} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder notBefore(OffsetDateTime nbf); + + /** + * Sets the JWT Claims + * nbf (not before) claim. A {@code null} value will remove the property from the Claims. + * + *

A JWT obtained before this timestamp should not be used.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #notBefore(Instant) notBefore(nbf)}.{@link BuilderClaims#and() and()}
+ * + * @param nbf the JWT {@code nbf} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder notBefore(ZonedDateTime nbf); /** * Sets the JWT Claims @@ -557,14 +623,46 @@ public interface JwtBuilder extends ClaimsMutator { * *

This is a convenience wrapper for:

*
-     * {@link #claims()}.{@link ClaimsMutator#issuedAt(Date) issuedAt(iat)}.{@link BuilderClaims#and() and()}
+ * {@link #claims()}.{@link ClaimsMutator#issuedAt(Instant) issuedAt(iat)}.{@link BuilderClaims#and() and()} * * @param iat the JWT {@code iat} value or {@code null} to remove the property from the Claims map. * @return the builder instance for method chaining. */ @Override // for better/targeted JavaDoc - JwtBuilder issuedAt(Date iat); + JwtBuilder issuedAt(Instant iat); + + /** + * Sets the JWT Claims
+ * iat (issued at) claim. A {@code null} value will remove the property from the Claims. + * + *

The value is the timestamp when the JWT was created.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #issuedAt(Instant) issuedAt(iat)}.{@link BuilderClaims#and() and()}
+ * + * @param iat the JWT {@code iat} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder issuedAt(OffsetDateTime iat); + + /** + * Sets the JWT Claims + * iat (issued at) claim. A {@code null} value will remove the property from the Claims. + * + *

The value is the timestamp when the JWT was created.

+ * + *

This is a convenience wrapper for:

+ *
+     * {@link #claims()}.{@link #issuedAt(Instant) issuedAt(iat)}.{@link BuilderClaims#and() and()}
+ * + * @param iat the JWT {@code iat} value or {@code null} to remove the property from the Claims map. + * @return the builder instance for method chaining. + */ + // for better/targeted JavaDoc + JwtBuilder issuedAt(ZonedDateTime iat); /** * Sets the JWT Claims diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index d1a4755d5..d72adda60 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -31,7 +31,7 @@ import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; -import java.util.Date; +import java.time.Instant; import java.util.Map; /** @@ -182,7 +182,7 @@ public interface JwtParserBuilder extends Builder { * @see MissingClaimException * @see IncorrectClaimException */ - JwtParserBuilder requireIssuedAt(Date issuedAt); + JwtParserBuilder requireIssuedAt(Instant issuedAt); /** * Ensures that the specified {@code exp} exists in the parsed JWT. If missing or if the parsed @@ -194,7 +194,7 @@ public interface JwtParserBuilder extends Builder { * @see MissingClaimException * @see IncorrectClaimException */ - JwtParserBuilder requireExpiration(Date expiration); + JwtParserBuilder requireExpiration(Instant expiration); /** * Ensures that the specified {@code nbf} exists in the parsed JWT. If missing or if the parsed @@ -206,7 +206,7 @@ public interface JwtParserBuilder extends Builder { * @see MissingClaimException * @see IncorrectClaimException */ - JwtParserBuilder requireNotBefore(Date notBefore); + JwtParserBuilder requireNotBefore(Instant notBefore); /** * Ensures that the specified {@code claimName} exists in the parsed JWT. If missing or if the parsed diff --git a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java index 6a3b501b2..380c387b9 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java +++ b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java @@ -15,11 +15,11 @@ */ package io.jsonwebtoken.lang; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; /** * Utility methods to format and parse date strings. @@ -31,68 +31,55 @@ public final class DateFormats { private DateFormats() { } // prevent instantiation - private static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ssXXX"; - private static final String ISO_8601_MILLIS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + private static final String ISO_8601_MILLIS_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; - private static final ThreadLocal ISO_8601 = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - SimpleDateFormat format = new SimpleDateFormat(ISO_8601_PATTERN); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format; - } - }; + private static final ThreadLocal ISO_8601 = ThreadLocal.withInitial(() -> DateTimeFormatter.ofPattern(ISO_8601_PATTERN)); - private static final ThreadLocal ISO_8601_MILLIS = new ThreadLocal() { - @Override - protected DateFormat initialValue() { - SimpleDateFormat format = new SimpleDateFormat(ISO_8601_MILLIS_PATTERN); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format; - } - }; + private static final ThreadLocal ISO_8601_MILLIS = ThreadLocal.withInitial(() -> DateTimeFormatter.ofPattern(ISO_8601_MILLIS_PATTERN)); /** * Return an ISO-8601-formatted string with millisecond precision representing the - * specified {@code date}. + * specified {@code instant}. Will always convert to UTC timezone. * - * @param date the date for which to create an ISO-8601-formatted string - * @return the date represented as an ISO-8601-formatted string with millisecond precision. + * @param instant the date for which to create an ISO-8601-formatted string + * @return the date represented as an ISO-8601-formatted string in UTC timezone with millisecond precision. */ - public static String formatIso8601(Date date) { - return formatIso8601(date, true); + public static String formatIso8601(Instant instant) { + return formatIso8601(instant, true); } /** * Returns an ISO-8601-formatted string with optional millisecond precision for the specified - * {@code date}. + * {@code instant}. Will always convert to UTC timezone. * - * @param date the date for which to create an ISO-8601-formatted string - * @param includeMillis whether to include millisecond notation within the string. - * @return the date represented as an ISO-8601-formatted string with optional millisecond precision. + * @param instant the instant for which to create an ISO-8601-formatted string + * @param includeMillis whether to include millisecond notation within the string. + * @return the date represented as an ISO-8601-formatted string in UTC timezone with optional millisecond precision. */ - public static String formatIso8601(Date date, boolean includeMillis) { + public static String formatIso8601(Instant instant, boolean includeMillis) { + Assert.notNull(instant, "Instant argument cannot be null."); if (includeMillis) { - return ISO_8601_MILLIS.get().format(date); + return ISO_8601_MILLIS.get().format(instant.atZone(ZoneOffset.UTC)); } - return ISO_8601.get().format(date); + return ISO_8601.get().format(instant.atZone(ZoneOffset.UTC)); } /** - * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Date} instance. The + * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Instant} instance. The * date string may optionally contain millisecond notation, and those milliseconds will be represented accordingly. * * @param s the ISO-8601-formatted string to parse - * @return the string's corresponding {@link Date} instance. - * @throws ParseException if the specified date string is not a validly-formatted ISO-8601 string. + * @return the string's corresponding {@link Instant} instance. + * @throws DateTimeParseException if the specified date string is not a validly-formatted ISO-8601 string. */ - public static Date parseIso8601Date(String s) throws ParseException { + public static Instant parseIso8601Date(String s) throws DateTimeParseException { Assert.notNull(s, "String argument cannot be null."); if (s.lastIndexOf('.') > -1) { //assume ISO-8601 with milliseconds - return ISO_8601_MILLIS.get().parse(s); + return OffsetDateTime.parse(s, ISO_8601_MILLIS.get()).toInstant(); } else { //assume ISO-8601 without millis: - return ISO_8601.get().parse(s); + return OffsetDateTime.parse(s, ISO_8601.get()).toInstant(); } } } diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy index 39123bd5f..4f4a03d6f 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy @@ -17,24 +17,91 @@ package io.jsonwebtoken.lang import org.junit.Test -import java.text.SimpleDateFormat +import java.time.Instant +import java.time.ZoneOffset +import java.time.OffsetDateTime +import java.time.format.DateTimeParseException import static org.junit.Assert.* class DateFormatsTest { - @Test //https://github.com/jwtk/jjwt/issues/291 - void testUtcTimezone() { + @Test + void testFormatIso8601WithMillisZuluOffset() { + final instant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.UTC).toInstant() + String formattedDate = DateFormats.formatIso8601(instant) + assertEquals "2023-12-25T15:30:00.123Z", formattedDate + } + + @Test + void testFormatIso8601WithMillisNonZuluOffset() { + final instant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.ofHours(-4)).toInstant() + String formattedDate = DateFormats.formatIso8601(instant) + assertEquals "2023-12-25T19:30:00.123Z", formattedDate + } + + @Test + void testFormatIso8601WithoutMillisZuluOffset() { + Instant instant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 0, ZoneOffset.UTC).toInstant() + String formattedDate = DateFormats.formatIso8601(instant, false) + assertEquals "2023-12-25T15:30:00Z", formattedDate + } + + @Test + void testFormatIso8601WithoutMillisNonZuluOffset() { + Instant instant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 0, ZoneOffset.ofHours(2)).toInstant() + String formattedDate = DateFormats.formatIso8601(instant, false) + assertEquals "2023-12-25T13:30:00Z", formattedDate + } + + @Test(expected = IllegalArgumentException.class) + void testFormatIso8601NullInput() { + DateFormats.formatIso8601(null) + } + + @Test + void testParseIso8601DateWithMillisZuluOffset() { + String dateString = "2023-12-25T15:30:00.123Z" + Instant parsedDate = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedDate) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.UTC).toInstant() + assertEquals expectedInstant, parsedDate + } + + @Test + void testParseIso8601DateWithMillisNonZuluOffset() { + String dateString = "2023-12-25T15:30:00.123-01:00" + Instant parsedDate = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedDate) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.ofHours(-1)).toInstant() + assertEquals expectedInstant, parsedDate + } - def iso8601 = DateFormats.ISO_8601.get() - def iso8601Millis = DateFormats.ISO_8601_MILLIS.get() + @Test + void testParseIso8601DateWithoutMillisZuluOffset() { + String dateString = "2023-12-25T15:30:00Z" + Instant parsedDate = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedDate) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 0, ZoneOffset.UTC).toInstant() + assertEquals expectedInstant, parsedDate + } - assertTrue iso8601 instanceof SimpleDateFormat - assertTrue iso8601Millis instanceof SimpleDateFormat + @Test + void testParseIso8601DateWithoutMillisNonZuluOffset() { + String dateString = "2023-12-25T15:30:00+01:00" + Instant parsedDate = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedDate) + assertEquals OffsetDateTime.of(2023,12,25,15,30,0, 0, ZoneOffset.ofHours(1)).toInstant(), parsedDate + } - def utc = TimeZone.getTimeZone("UTC") + @Test(expected = DateTimeParseException) + void testParseIso8601DateInvalidFormat() { + String invalidDateString = "2023-12-25 15:30" + DateFormats.parseIso8601Date(invalidDateString) + } - assertEquals utc, iso8601.getTimeZone() - assertEquals utc, iso8601Millis.getTimeZone() + @Test(expected = IllegalArgumentException.class) + void testParseIso8601DateNullInput() { + DateFormats.parseIso8601Date(null) } } diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 658ebd679..3c7b542e3 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -42,6 +42,10 @@ com.fasterxml.jackson.core jackson-databind + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java index 31c5ae00f..0e96edb0e 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonSerializer.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.jsonwebtoken.io.AbstractSerializer; import io.jsonwebtoken.lang.Assert; @@ -41,7 +42,7 @@ public class JacksonSerializer extends AbstractSerializer { MODULE = module; } - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); + static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE).registerModule(new JavaTimeModule()); protected final ObjectMapper objectMapper; diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index 0c81f5ce4..79b5d5ac3 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -30,6 +30,9 @@ import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -93,13 +96,24 @@ private Object toJSONInstance(Object object) throws IOException { return object; } + if(object instanceof Instant) { + return DateFormats.formatIso8601((Instant) object); + } + + if(object instanceof OffsetDateTime) { + return DateFormats.formatIso8601(((OffsetDateTime) object).toInstant()); + } + + if(object instanceof ZonedDateTime) { + return DateFormats.formatIso8601(((ZonedDateTime) object).toInstant()); + } + if (object instanceof Calendar) { - object = ((Calendar) object).getTime(); //sets object to date, will be converted in next if-statement: + return DateFormats.formatIso8601(((Calendar) object).getTime().toInstant()); } if (object instanceof Date) { - Date date = (Date) object; - return DateFormats.formatIso8601(date); + return DateFormats.formatIso8601(((Date) object).toInstant()); } if (object instanceof byte[]) { diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy index 382c5bb14..ad7a6f458 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy @@ -27,6 +27,10 @@ import org.json.JSONString import org.junit.Before import org.junit.Test +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZonedDateTime + import static org.junit.Assert.* class OrgJsonSerializerTest { @@ -190,17 +194,41 @@ class OrgJsonSerializerTest { assertEquals '"hello 世界"', ser('hello 世界') } + @Test + void testInstant() { + Instant instant = Instant.now() + String formatted = DateFormats.formatIso8601(instant) + assertEquals "\"$formatted\"" as String, ser(instant) + } + + @Test + void testOffsetDateTime() { + OffsetDateTime offsetDateTime = OffsetDateTime.now() + def now = offsetDateTime.toInstant() + String formatted = DateFormats.formatIso8601(now) + assertEquals "\"$formatted\"" as String, ser(offsetDateTime) + } + + @Test + void testZonedDateTime() { + ZonedDateTime zonedDateTime = ZonedDateTime.now() + def now = zonedDateTime.toInstant() + String formatted = DateFormats.formatIso8601(now) + assertEquals "\"$formatted\"" as String, ser(zonedDateTime) + } + @Test void testDate() { - Date now = new Date() + Date date = new Date() + def now = date.toInstant() String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(now) + assertEquals "\"$formatted\"" as String, ser(date) } @Test void testCalendar() { def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def now = cal.getTime() + def now = cal.toInstant() String formatted = DateFormats.formatIso8601(now) assertEquals "\"$formatted\"" as String, ser(cal) } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 70a6a5a42..fdb5b5e90 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -23,26 +23,26 @@ import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Registry; -import java.util.Date; +import java.time.Instant; import java.util.Map; import java.util.Set; public class DefaultClaims extends ParameterMap implements Claims { private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " + - "'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " + - "Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " + - "implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " + - "configuration via the JwtParserBuilder.deserializer() method. " + + "'%s'. JJWT only converts simple String, Date, Instant, OffsetDateTime, ZonedDateTime, Long, Integer, Short " + + "and Byte types automatically. Anything more complex is expected to be already converted to your desired type " + + "by the JSON Deserializer implementation. You may specify a custom Deserializer for a JwtParser with the " + + "desired conversion configuration via the JwtParserBuilder.deserializer() method. " + "See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " + "specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types"; static final Parameter ISSUER = Parameters.string(Claims.ISSUER, "Issuer"); static final Parameter SUBJECT = Parameters.string(Claims.SUBJECT, "Subject"); static final Parameter> AUDIENCE = Parameters.stringSet(Claims.AUDIENCE, "Audience"); - static final Parameter EXPIRATION = Parameters.rfcDate(Claims.EXPIRATION, "Expiration Time"); - static final Parameter NOT_BEFORE = Parameters.rfcDate(Claims.NOT_BEFORE, "Not Before"); - static final Parameter ISSUED_AT = Parameters.rfcDate(Claims.ISSUED_AT, "Issued At"); + static final Parameter EXPIRATION = Parameters.rfcDate(Claims.EXPIRATION, "Expiration Time"); + static final Parameter NOT_BEFORE = Parameters.rfcDate(Claims.NOT_BEFORE, "Not Before"); + static final Parameter ISSUED_AT = Parameters.rfcDate(Claims.ISSUED_AT, "Issued At"); static final Parameter JTI = Parameters.string(Claims.ID, "JWT ID"); static final Registry> PARAMS = @@ -81,17 +81,17 @@ public Set getAudience() { } @Override - public Date getExpiration() { + public Instant getExpiration() { return get(EXPIRATION); } @Override - public Date getNotBefore() { + public Instant getNotBefore() { return get(NOT_BEFORE); } @Override - public Date getIssuedAt() { + public Instant getIssuedAt() { return get(ISSUED_AT); } @@ -114,11 +114,11 @@ public T get(String claimName, Class requiredType) { return null; } - if (Date.class.equals(requiredType)) { + if (Instant.class.equals(requiredType)) { try { - value = JwtDateConverter.toDate(value); // NOT specDate logic + value = JwtDateConverter.toInstant(value); // NOT specDate logic } catch (Exception e) { - String msg = "Cannot create Date from '" + claimName + "' value '" + value + "'. Cause: " + e.getMessage(); + String msg = "Cannot create Instant from '" + claimName + "' value '" + value + "'. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClock.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClock.java index bd9d4ecbc..0c07f9613 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClock.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClock.java @@ -17,7 +17,7 @@ import io.jsonwebtoken.Clock; -import java.util.Date; +import java.time.Instant; /** * Default {@link Clock} implementation. @@ -32,12 +32,12 @@ public class DefaultClock implements Clock { public static final Clock INSTANCE = new DefaultClock(); /** - * Simply returns new {@link Date}(). + * Simply returns {@link Instant}.now(). * - * @return a new {@link Date} instance. + * @return a new {@link Instant} instance. */ @Override - public Date now() { - return new Date(); + public Instant now() { + return Instant.now(); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index b6b178c46..354272535 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -73,7 +73,9 @@ import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; -import java.util.Date; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -239,12 +241,7 @@ public JwtBuilder signWith(K key, final SecureDigestAlgorithm) alg; - this.signFunction = Functions.wrap(new Function, byte[]>() { - @Override - public byte[] apply(SecureRequest request) { - return sigAlg.digest(request); - } - }, SignatureException.class, "Unable to compute %s signature.", id); + this.signFunction = Functions.wrap(request -> sigAlg.digest(request), SignatureException.class, "Unable to compute %s signature.", id); return this; } @@ -309,12 +306,7 @@ public JwtBuilder encryptWith(final K key, final KeyAlgorithm alg = this.keyAlg; final String cekMsg = "Unable to obtain content encryption key from key management algorithm '%s'."; - this.keyAlgFunction = Functions.wrap(new Function, KeyResult>() { - @Override - public KeyResult apply(KeyRequest request) { - return alg.getEncryptionKey(request); - } - }, SecurityException.class, cekMsg, algId); + this.keyAlgFunction = Functions.wrap(alg::getEncryptionKey, SecurityException.class, cekMsg, algId); return this; } @@ -434,39 +426,69 @@ public JwtBuilder setAudience(String aud) { @Override public AudienceCollection audience() { - return new DelegateAudienceCollection<>((JwtBuilder) this, claims().audience()); + return new DelegateAudienceCollection<>(this, claims().audience()); } @Override - public JwtBuilder setExpiration(Date exp) { + public JwtBuilder setExpiration(Instant exp) { return expiration(exp); } @Override - public JwtBuilder expiration(Date exp) { + public JwtBuilder expiration(Instant exp) { return claims().expiration(exp).and(); } @Override - public JwtBuilder setNotBefore(Date nbf) { + public JwtBuilder expiration(OffsetDateTime exp) { + return this.expiration(exp.toInstant()); + } + + @Override + public JwtBuilder expiration(ZonedDateTime exp) { + return this.expiration(exp.toInstant()); + } + + @Override + public JwtBuilder setNotBefore(Instant nbf) { return notBefore(nbf); } @Override - public JwtBuilder notBefore(Date nbf) { + public JwtBuilder notBefore(Instant nbf) { return claims().notBefore(nbf).and(); } @Override - public JwtBuilder setIssuedAt(Date iat) { + public JwtBuilder notBefore(OffsetDateTime nbf) { + return this.notBefore(nbf.toInstant()); + } + + @Override + public JwtBuilder notBefore(ZonedDateTime nbf) { + return this.notBefore(nbf.toInstant()); + } + + @Override + public JwtBuilder setIssuedAt(Instant iat) { return issuedAt(iat); } @Override - public JwtBuilder issuedAt(Date iat) { + public JwtBuilder issuedAt(Instant iat) { return claims().issuedAt(iat).and(); } + @Override + public JwtBuilder issuedAt(OffsetDateTime iat) { + return this.issuedAt(iat.toInstant()); + } + + @Override + public JwtBuilder issuedAt(ZonedDateTime iat) { + return this.issuedAt(iat.toInstant()); + } + @Override public JwtBuilder setId(String jti) { return id(jti); @@ -674,12 +696,9 @@ private String unprotected(final Payload content) { } private void encrypt(final AeadRequest req, final AeadResult res) throws SecurityException { - Function fn = Functions.wrap(new Function() { - @Override - public Object apply(Object o) { - enc.encrypt(req, res); - return null; - } + Function fn = Functions.wrap(o -> { + enc.encrypt(req, res); + return null; }, SecurityException.class, "%s encryption failed.", enc.getId()); fn.apply(null); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index beb36f4bb..86b4c65a2 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -85,8 +85,9 @@ import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; +import java.time.Instant; +import java.time.temporal.ChronoField; import java.util.Collection; -import java.util.Date; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -659,22 +660,18 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe //since 0.3: if (claims != null) { - - final Date now = this.clock.now(); - long nowTime = now.getTime(); + final Instant now = this.clock.now(); // https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4 // token MUST NOT be accepted on or after any specified exp time: - Date exp = claims.getExpiration(); + Instant exp = claims.getExpiration(); if (exp != null) { - - long maxTime = nowTime - this.allowedClockSkewMillis; - Date max = allowSkew ? new Date(maxTime) : now; - if (max.after(exp)) { + Instant max = allowSkew ? now.minus(this.allowedClockSkewMillis, ChronoField.MILLI_OF_SECOND.getBaseUnit()) : now; + if (max.isAfter(exp)) { String expVal = DateFormats.formatIso8601(exp, true); String nowVal = DateFormats.formatIso8601(now, true); - long differenceMillis = nowTime - exp.getTime(); + long differenceMillis = now.toEpochMilli() - exp.toEpochMilli(); String msg = "JWT expired " + differenceMillis + " milliseconds ago at " + expVal + ". " + "Current time: " + nowVal + ". Allowed clock skew: " + @@ -685,16 +682,14 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe // https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5 // token MUST NOT be accepted before any specified nbf time: - Date nbf = claims.getNotBefore(); + Instant nbf = claims.getNotBefore(); if (nbf != null) { - - long minTime = nowTime + this.allowedClockSkewMillis; - Date min = allowSkew ? new Date(minTime) : now; - if (min.before(nbf)) { + Instant min = allowSkew ? now.plus(this.allowedClockSkewMillis, ChronoField.MILLI_OF_SECOND.getBaseUnit()) : now; + if (min.isBefore(nbf)) { String nbfVal = DateFormats.formatIso8601(nbf, true); String nowVal = DateFormats.formatIso8601(now, true); - long differenceMillis = nbf.getTime() - nowTime; + long differenceMillis = nbf.toEpochMilli() - now.toEpochMilli(); String msg = "JWT early by " + differenceMillis + " milliseconds before " + nbfVal + ". Current time: " + nowVal + ". Allowed clock skew: " + @@ -728,12 +723,12 @@ private void validateExpectedClaims(Header header, Claims claims) { Object expectedClaimValue = normalize(expected.get(expectedClaimName)); Object actualClaimValue = normalize(claims.get(expectedClaimName)); - if (expectedClaimValue instanceof Date) { + if (expectedClaimValue instanceof Instant) { try { - actualClaimValue = claims.get(expectedClaimName, Date.class); + actualClaimValue = claims.get(expectedClaimName, Instant.class); } catch (Exception e) { - String msg = "JWT Claim '" + expectedClaimName + "' was expected to be a Date, but its value " + - "cannot be converted to a Date using current heuristics. Value: " + actualClaimValue; + String msg = "JWT Claim '" + expectedClaimName + "' was expected to be an Instant, but its value " + + "cannot be converted to an Instant using current heuristics. Value: " + actualClaimValue; throw new IncorrectClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 9b57af0c4..46aeb8228 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -52,7 +52,7 @@ import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; -import java.util.Date; +import java.time.Instant; import java.util.Map; import java.util.Set; @@ -155,14 +155,14 @@ public JwtParserBuilder b64Url(Decoder decoder) { } @Override - public JwtParserBuilder requireIssuedAt(Date issuedAt) { - expectedClaims.setIssuedAt(issuedAt); + public JwtParserBuilder requireIssuedAt(Instant issuedAt) { + expectedClaims.issuedAt(issuedAt); return this; } @Override public JwtParserBuilder requireIssuer(String issuer) { - expectedClaims.setIssuer(issuer); + expectedClaims.issuer(issuer); return this; } @@ -174,25 +174,25 @@ public JwtParserBuilder requireAudience(String audience) { @Override public JwtParserBuilder requireSubject(String subject) { - expectedClaims.setSubject(subject); + expectedClaims.subject(subject); return this; } @Override public JwtParserBuilder requireId(String id) { - expectedClaims.setId(id); + expectedClaims.id(id); return this; } @Override - public JwtParserBuilder requireExpiration(Date expiration) { - expectedClaims.setExpiration(expiration); + public JwtParserBuilder requireExpiration(Instant expiration) { + expectedClaims.expiration(expiration); return this; } @Override - public JwtParserBuilder requireNotBefore(Date notBefore) { - expectedClaims.setNotBefore(notBefore); + public JwtParserBuilder requireNotBefore(Instant notBefore) { + expectedClaims.notBefore(notBefore); return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 8636b5351..04b1b027b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -23,7 +23,7 @@ import io.jsonwebtoken.lang.MapMutator; import io.jsonwebtoken.lang.Strings; -import java.util.Date; +import java.time.Instant; import java.util.Set; /** @@ -115,32 +115,32 @@ public T and() { } @Override - public T setExpiration(Date exp) { + public T setExpiration(Instant exp) { return expiration(exp); } @Override - public T expiration(Date exp) { + public T expiration(Instant exp) { return put(DefaultClaims.EXPIRATION, exp); } @Override - public T setNotBefore(Date nbf) { + public T setNotBefore(Instant nbf) { return notBefore(nbf); } @Override - public T notBefore(Date nbf) { + public T notBefore(Instant nbf) { return put(DefaultClaims.NOT_BEFORE, nbf); } @Override - public T setIssuedAt(Date iat) { + public T setIssuedAt(Instant iat) { return issuedAt(iat); } @Override - public T issuedAt(Date iat) { + public T issuedAt(Instant iat) { return put(DefaultClaims.ISSUED_AT, iat); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java index cecae75ee..44de17454 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java @@ -17,7 +17,7 @@ import io.jsonwebtoken.Clock; -import java.util.Date; +import java.time.Instant; /** * A {@code Clock} implementation that is constructed with a seed timestamp and always reports that same @@ -27,14 +27,14 @@ */ public class FixedClock implements Clock { - private final Date now; + private final Instant now; /** - * Creates a new fixed clock using new {@link Date Date}() as the seed timestamp. All calls to + * Creates a new fixed clock using new {@link Instant instant}() as the seed timestamp. All calls to * {@link #now now()} will always return this seed Date. */ public FixedClock() { - this(new Date()); + this(Instant.now()); } /** @@ -43,7 +43,7 @@ public FixedClock() { * * @param now the specified Date to always return from all calls to {@link #now now()}. */ - public FixedClock(Date now) { + public FixedClock(Instant now) { this.now = now; } @@ -54,11 +54,11 @@ public FixedClock(Date now) { * @param timeInMillis the specified Date in milliseconds to always return from all calls to {@link #now now()}. */ public FixedClock(long timeInMillis) { - this(new Date(timeInMillis)); + this(Instant.ofEpochMilli(timeInMillis)); } @Override - public Date now() { + public Instant now() { return this.now; } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index 2dc15c251..7098c2259 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -17,36 +17,25 @@ import io.jsonwebtoken.lang.DateFormats; -import java.text.ParseException; -import java.util.Calendar; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeParseException; import java.util.Date; +import java.util.Calendar; -public class JwtDateConverter implements Converter { +public class JwtDateConverter implements Converter { public static final JwtDateConverter INSTANCE = new JwtDateConverter(); - @Override - public Object applyTo(Date date) { - if (date == null) { - return null; - } - // https://www.rfc-editor.org/rfc/rfc7519.html#section-2, 'Numeric Date' definition: - return date.getTime() / 1000L; - } - - @Override - public Date applyFrom(Object o) { - return toSpecDate(o); - } - /** - * Returns an RFC-compatible {@link Date} equivalent of the specified object value using heuristics. + * Returns an RFC-compatible {@link Instant} equivalent of the specified object value using heuristics. * - * @param value object to convert to a {@code Date} using heuristics. - * @return an RFC-compatible {@link Date} equivalent of the specified object value using heuristics. + * @param value object to convert to a {@code Instant} using heuristics. + * @return an RFC-compatible {@link Instant} equivalent of the specified object value using heuristics. * @since 0.10.0 */ - public static Date toSpecDate(Object value) { + public static Instant toSpecInstant(Object value) { if (value == null) { return null; } @@ -59,53 +48,75 @@ public static Date toSpecDate(Object value) { if (value instanceof Number) { // https://github.com/jwtk/jjwt/issues/122: // The JWT RFC *mandates* NumericDate values are represented as seconds. - // Because java.util.Date requires milliseconds, we need to multiply by 1000: long seconds = ((Number) value).longValue(); - value = seconds * 1000; + value = Instant.ofEpochSecond(seconds); } - //v would have been normalized to milliseconds if it was a number value, so perform normal date conversion: - return toDate(value); + // would have been normalized to Instant if it was a number value, so perform normal date conversion: + return toInstant(value); } /** - * Returns a {@link Date} equivalent of the specified object value using heuristics. + * Returns a {@link Instant} equivalent of the specified object value using heuristics. * * @param v the object value to represent as a Date. - * @return a {@link Date} equivalent of the specified object value using heuristics. + * @return a {@link Instant} equivalent of the specified object value using heuristics. */ - public static Date toDate(Object v) { + public static Instant toInstant(Object v) { if (v == null) { return null; + } else if (v instanceof Instant) { + return (Instant) v; + } else if (v instanceof ZonedDateTime) { + return ((ZonedDateTime) v).toInstant(); + }else if (v instanceof OffsetDateTime) { + return ((OffsetDateTime) v).toInstant(); } else if (v instanceof Date) { - return (Date) v; + //assume UTC + return ((Date) v).toInstant(); } else if (v instanceof Calendar) { //since 0.10.0 - return ((Calendar) v).getTime(); + //assume UTC + return ((Calendar) v).getTime().toInstant(); } else if (v instanceof Number) { //assume millis: long millis = ((Number) v).longValue(); - return new Date(millis); + //assume UTC + return Instant.ofEpochMilli(millis); } else if (v instanceof String) { return parseIso8601Date((String) v); //ISO-8601 parsing since 0.10.0 } else { - String msg = "Cannot create Date from object of type " + v.getClass().getName() + "."; + String msg = "Cannot create Instant from object of type " + v.getClass().getName() + "."; throw new IllegalArgumentException(msg); } } /** - * Parses the specified ISO-8601-formatted string and returns the corresponding {@link Date} instance. + * Parses the specified ISO-8601-formatted string and returns the corresponding {@link Instant} instance. * * @param value an ISO-8601-formatted string. - * @return a {@link Date} instance reflecting the specified ISO-8601-formatted string. + * @return a {@link Instant} instance reflecting the specified ISO-8601-formatted string. * @since 0.10.0 */ - private static Date parseIso8601Date(String value) throws IllegalArgumentException { + private static Instant parseIso8601Date(String value) throws IllegalArgumentException { try { return DateFormats.parseIso8601Date(value); - } catch (ParseException e) { + } catch (DateTimeParseException e) { String msg = "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. " + "All heuristics exhausted. Cause: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } + + @Override + public Object applyTo(Instant instant) { + if (instant == null) { + return null; + } + // https://www.rfc-editor.org/rfc/rfc7519.html#section-2, 'Numeric Date' definition: + return instant.getEpochSecond(); + } + + @Override + public Instant applyFrom(Object o) { + return toSpecInstant(o); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Parameters.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Parameters.java index 36db08193..612bf1336 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Parameters.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Parameters.java @@ -24,8 +24,8 @@ import java.net.URI; import java.security.MessageDigest; import java.security.cert.X509Certificate; +import java.time.Instant; import java.util.Collection; -import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -41,8 +41,8 @@ public static Parameter string(String id, String name) { return builder(String.class).setId(id).setName(name).build(); } - public static Parameter rfcDate(String id, String name) { - return builder(Date.class).setConverter(JwtDateConverter.INSTANCE).setId(id).setName(name).build(); + public static Parameter rfcDate(String id, String name) { + return builder(Instant.class).setConverter(JwtDateConverter.INSTANCE).setId(id).setName(name).build(); } public static Parameter> x509Chain(String id, String name) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index ec98dc18b..5430ce4bc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -29,8 +29,8 @@ import org.junit.Test import javax.crypto.SecretKey import java.nio.charset.StandardCharsets import java.security.SecureRandom +import java.time.Instant -import static io.jsonwebtoken.DateTestUtils.truncateMillis import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE import static org.junit.Assert.* @@ -228,7 +228,7 @@ class JwtParserTest { long testTime = 1657552537573L Clock fixedClock = new FixedClock(testTime) - Date exp = new Date(testTime - 1000) + Instant exp = Instant.ofEpochMilli(testTime - 1000) String compact = Jwts.builder().setSubject('Joe').setExpiration(exp).compact() @@ -249,7 +249,7 @@ class JwtParserTest { long differenceMillis = 100000 // arbitrary, anything > 0 is fine def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def earlier = new Date(nbf.getTime() - differenceMillis) + def earlier = nbf.minusMillis(differenceMillis) String compact = Jwts.builder().subject('Joe').notBefore(nbf).compact() @@ -260,7 +260,7 @@ class JwtParserTest { def nbf8601 = DateFormats.formatIso8601(nbf, true) def earlier8601 = DateFormats.formatIso8601(earlier, true) String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " + - "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds."; + "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds." assertEquals msg, e.message //https://github.com/jwtk/jjwt/issues/107 (the Z designator at the end of the timestamp): @@ -278,8 +278,8 @@ class JwtParserTest { // otherwise we'll get nondeterministic tests: long seconds = (millis / 1000L).longValue() millis = seconds * 1000L - def exp = new Date(millis) - def later = new Date(exp.getTime() + differenceMillis) + def exp = Instant.ofEpochMilli(millis) + def later = exp.plusMillis(differenceMillis) def s = Jwts.builder().expiration(exp).compact() String subject = 'Joe' @@ -296,7 +296,7 @@ class JwtParserTest { long differenceMillis = 3000 // arbitrary, anything > 0 is fine def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def later = new Date(exp.getTime() + differenceMillis) + def later = exp.plusMillis(differenceMillis) def s = Jwts.builder().expiration(exp).compact() @@ -310,14 +310,14 @@ class JwtParserTest { def exp8601 = DateFormats.formatIso8601(exp, true) def later8601 = DateFormats.formatIso8601(later, true) String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " + - "Current time: ${later8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds."; + "Current time: ${later8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds." assertEquals msg, e.message } } @Test void testParseWithPrematureJwtWithinAllowedClockSkew() { - Date exp = new Date(System.currentTimeMillis() + 3000) + def exp = Instant.now().plusMillis(3000) String subject = 'Joe' String compact = Jwts.builder().setSubject(subject).setNotBefore(exp).compact() @@ -332,7 +332,7 @@ class JwtParserTest { long differenceMillis = 3000 // arbitrary, anything > 0 is fine def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def earlier = new Date(nbf.getTime() - differenceMillis) + def earlier = nbf.minusMillis(differenceMillis) String compact = Jwts.builder().subject('Joe').notBefore(nbf).compact() @@ -347,7 +347,7 @@ class JwtParserTest { def nbf8601 = DateFormats.formatIso8601(nbf, true) def earlier8601 = DateFormats.formatIso8601(earlier, true) String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " + - "Current time: ${earlier8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds."; + "Current time: ${earlier8601}. Allowed clock skew: ${skewSeconds * 1000} milliseconds." assertEquals msg, e.message } } @@ -565,7 +565,7 @@ class JwtParserTest { long differenceMillis = 843 // arbitrary, anything > 0 is fine def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def later = new Date(exp.getTime() + differenceMillis) + def later = exp.plusMillis(differenceMillis) String sub = 'Joe' byte[] key = randomKey() @@ -578,7 +578,7 @@ class JwtParserTest { def exp8601 = DateFormats.formatIso8601(exp, true) def later8601 = DateFormats.formatIso8601(later, true) String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " + - "Current time: ${later8601}. Allowed clock skew: 0 milliseconds."; + "Current time: ${later8601}. Allowed clock skew: 0 milliseconds." assertEquals msg, e.message assertEquals e.getClaims().getSubject(), sub assertEquals e.getHeader().getAlgorithm(), "HS256" @@ -590,7 +590,7 @@ class JwtParserTest { long differenceMillis = 3842 // arbitrary, anything > 0 is fine def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def earlier = new Date(nbf.getTime() - differenceMillis) + def earlier = nbf.minusMillis(differenceMillis) String sub = 'Joe' byte[] key = randomKey() @@ -603,7 +603,7 @@ class JwtParserTest { def nbf8601 = DateFormats.formatIso8601(nbf, true) def earlier8601 = DateFormats.formatIso8601(earlier, true) String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " + - "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds."; + "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds." assertEquals msg, e.message assertEquals e.getClaims().getSubject(), sub @@ -994,7 +994,7 @@ class JwtParserTest { @Test void testParseRequireIssuedAt_Success() { - def issuedAt = new Date(System.currentTimeMillis()) + def issuedAt = Instant.now() byte[] key = randomKey() @@ -1007,13 +1007,13 @@ class JwtParserTest { build(). parseSignedClaims(compact) - assertEquals jwt.getPayload().getIssuedAt().getTime(), truncateMillis(issuedAt), 0 + assertEquals jwt.getPayload().getIssuedAt().getEpochSecond(), issuedAt.epochSecond, 0 } @Test(expected = IncorrectClaimException) void testParseRequireIssuedAt_Incorrect_Fail() { - def goodIssuedAt = new Date(System.currentTimeMillis()) - def badIssuedAt = new Date(System.currentTimeMillis() - 10000) + def goodIssuedAt = Instant.now() + def badIssuedAt = Instant.now().minusMillis(10000) byte[] key = randomKey() @@ -1029,7 +1029,7 @@ class JwtParserTest { @Test(expected = MissingClaimException) void testParseRequireIssuedAt_Missing_Fail() { - def issuedAt = new Date(System.currentTimeMillis() - 10000) + def issuedAt = Instant.now().minusMillis(10000) byte[] key = randomKey() @@ -1358,7 +1358,7 @@ class JwtParserTest { @Test void testParseRequireExpiration_Success() { // expire in the future - def expiration = new Date(System.currentTimeMillis() + 10000) + def expiration = Instant.now().plusMillis(10000) byte[] key = randomKey() @@ -1371,13 +1371,13 @@ class JwtParserTest { build(). parseSignedClaims(compact) - assertEquals jwt.getPayload().getExpiration().getTime(), truncateMillis(expiration) + assertEquals jwt.getPayload().getExpiration().getEpochSecond(), expiration.getEpochSecond(), 0 } @Test(expected = IncorrectClaimException) void testParseRequireExpirationAt_Incorrect_Fail() { - def goodExpiration = new Date(System.currentTimeMillis() + 20000) - def badExpiration = new Date(System.currentTimeMillis() + 10000) + def goodExpiration = Instant.now().plusMillis(20000) + def badExpiration = Instant.now().plusMillis(10000) byte[] key = randomKey() @@ -1393,7 +1393,7 @@ class JwtParserTest { @Test(expected = MissingClaimException) void testParseRequireExpiration_Missing_Fail() { - def expiration = new Date(System.currentTimeMillis() + 10000) + def expiration = Instant.now().plusMillis(10000) byte[] key = randomKey() @@ -1410,7 +1410,7 @@ class JwtParserTest { @Test void testParseRequireNotBefore_Success() { // expire in the future - def notBefore = new Date(System.currentTimeMillis() - 10000) + def notBefore = Instant.now().minusMillis(10000) byte[] key = randomKey() @@ -1423,13 +1423,13 @@ class JwtParserTest { build(). parseSignedClaims(compact) - assertEquals jwt.getPayload().getNotBefore().getTime(), truncateMillis(notBefore) + assertEquals jwt.getPayload().getNotBefore().getEpochSecond(), notBefore.epochSecond, 0 } @Test(expected = IncorrectClaimException) void testParseRequireNotBefore_Incorrect_Fail() { - def goodNotBefore = new Date(System.currentTimeMillis() - 20000) - def badNotBefore = new Date(System.currentTimeMillis() - 10000) + def goodNotBefore = Instant.now().minusMillis(20000) + def badNotBefore = Instant.now().minusMillis(10000) byte[] key = randomKey() @@ -1445,7 +1445,7 @@ class JwtParserTest { @Test(expected = MissingClaimException) void testParseRequireNotBefore_Missing_Fail() { - def notBefore = new Date(System.currentTimeMillis() - 10000) + def notBefore = Instant.now().minusMillis(10000) byte[] key = randomKey() @@ -1462,7 +1462,7 @@ class JwtParserTest { @Test void testParseRequireCustomDate_Success() { - def aDate = new Date(System.currentTimeMillis()) + def aDate = Instant.now() byte[] key = randomKey() @@ -1475,31 +1475,31 @@ class JwtParserTest { build(). parseSignedClaims(compact) - assertEquals jwt.getPayload().get("aDate", Date.class), aDate + assertEquals jwt.getPayload().get("aDate", Instant.class), aDate } @Test //since 0.10.0 - void testParseRequireCustomDateWhenClaimIsNotADate() { + void testParseRequireCustomInstantWhenClaimIsNotAnInstant() { - def goodDate = new Date(System.currentTimeMillis()) - def badDate = 'hello' + def goodInstant = Instant.now() + def badInstant = 'hello' byte[] key = randomKey() String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). - claim("aDate", badDate). + claim("anInstant", badInstant). compact() try { Jwts.parser().setSigningKey(key). - require("aDate", goodDate). + require("anInstant", goodInstant). build(). parseSignedClaims(compact) fail() } catch (IncorrectClaimException e) { - String expected = 'JWT Claim \'aDate\' was expected to be a Date, but its value cannot be converted to a ' + - 'Date using current heuristics. Value: hello' + String expected = 'JWT Claim \'anInstant\' was expected to be an Instant, but its value cannot be converted to an ' + + 'Instant using current heuristics. Value: hello' assertEquals expected, e.getMessage() } } @@ -1507,24 +1507,24 @@ class JwtParserTest { @Test void testParseRequireCustomDate_Incorrect_Fail() { - def goodDate = new Date(System.currentTimeMillis()) - def badDate = new Date(System.currentTimeMillis() - 10000) + def goodInstant = Instant.now() + def badInstant = Instant.now().minusMillis(10000) byte[] key = randomKey() String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). - claim("aDate", badDate). + claim("anInstant", badInstant). compact() try { Jwts.parser().setSigningKey(key). - require("aDate", goodDate). + require("anInstant", goodInstant). build(). parseSignedClaims(compact) fail() } catch (IncorrectClaimException e) { assertEquals( - String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", goodDate, badDate), + String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "anInstant", goodInstant, badInstant), e.getMessage() ) } @@ -1554,23 +1554,18 @@ class JwtParserTest { @Test void testParseClockManipulationWithFixedClock() { - def then = System.currentTimeMillis() - 1000 - Date expiry = new Date(then) - Date beforeExpiry = new Date(then - 1000) + Instant expiry = Instant.now().minusMillis(10000) + Instant beforeExpiry = expiry.minusMillis(1000) String compact = Jwts.builder().setSubject('Joe').setExpiration(expiry).compact() Jwts.parser().unsecured().setClock(new FixedClock(beforeExpiry)).build().parse(compact) } - @Test + @Test(expected = IllegalArgumentException) void testParseClockManipulationWithNullClock() { - JwtParserBuilder parser = Jwts.parser(); - try { - parser.setClock(null) - fail() - } catch (IllegalArgumentException expected) { - } + JwtParserBuilder parser = Jwts.parser() + parser.setClock(null) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 5ded80389..b79f5ff0e 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -40,6 +40,8 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey +import java.time.Instant +import java.time.temporal.ChronoUnit import static org.junit.Assert.* @@ -51,6 +53,14 @@ class JwtsTest { return new Date(secondOnlyPrecisionMillis) } + private static Instant instantWithOnlySecondPrecision(long millis) { + return instantWithOnlySecondPrecision(Instant.ofEpochMilli(millis)) + } + + private static Instant instantWithOnlySecondPrecision(Instant instant) { + return instant.truncatedTo(ChronoUnit.SECONDS) + } + private static Date now() { Date date = dateWithOnlySecondPrecision(System.currentTimeMillis()) return date @@ -68,6 +78,10 @@ class JwtsTest { return dateWithOnlySecondPrecision(time) } + private static Instant laterInstant(int seconds) { + return instantWithOnlySecondPrecision(Instant.now().plusSeconds(seconds)) + } + protected static String base64Url(String s) { byte[] bytes = s.getBytes(Strings.UTF_8) return Encoders.BASE64URL.encode(bytes) @@ -153,7 +167,7 @@ class JwtsTest { } catch (MalformedJwtException e) { String expected = 'Invalid claims: Invalid JWT Claims \'exp\' (Expiration Time) value: -42-. ' + 'String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics exhausted. ' + - 'Cause: Unparseable date: "-42-"' + 'Cause: Text \'-42-\' could not be parsed at index 1' assertEquals expected, e.getMessage() } } @@ -409,7 +423,7 @@ class JwtsTest { @Test void testConvenienceExpiration() { - Date then = laterDate(10000) + Instant then = laterInstant(10000) String compact = Jwts.builder().setExpiration(then).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims def claimedDate = claims.getExpiration() @@ -426,7 +440,7 @@ class JwtsTest { @Test void testConvenienceNotBefore() { - Date now = now() //jwt exp only supports *seconds* since epoch: + Instant now = instantWithOnlySecondPrecision(Instant.now()) //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setNotBefore(now).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims def claimedDate = claims.getNotBefore() @@ -443,7 +457,7 @@ class JwtsTest { @Test void testConvenienceIssuedAt() { - Date now = now() //jwt exp only supports *seconds* since epoch: + Instant now = instantWithOnlySecondPrecision(Instant.now()) //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setIssuedAt(now).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims def claimedDate = claims.getIssuedAt() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index ae987402d..a1961d1d4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -22,6 +22,13 @@ import io.jsonwebtoken.lang.DateFormats import org.junit.Before import org.junit.Test +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalField +import java.time.temporal.TemporalUnit + import static org.junit.Assert.* class DefaultClaimsTest { @@ -177,6 +184,30 @@ class DefaultClaimsTest { assertNull date } + @Test + void testGetRequiredDateFromInstant() { + def expected = Instant.now() + claims.put("anInstant", expected) + Instant result = claims.get("anInstant", Instant.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromOffsetDateTime() { + def expected = OffsetDateTime.now() + claims.put("anOffsetDateTime", expected) + OffsetDateTime result = claims.get("anOffsetDateTime", OffsetDateTime.class) + assertEquals expected, result + } + + @Test + void testGetRequiredDateFromZonedDateTime() { + def expected = ZonedDateTime.now() + claims.put("aZonedDateTime", expected) + ZonedDateTime result = claims.get("aZonedDateTime", ZonedDateTime.class) + assertEquals expected, result + } + @Test void testGetRequiredDateFromDate() { def expected = new Date() @@ -188,34 +219,34 @@ class DefaultClaimsTest { @Test void testGetRequiredDateFromCalendar() { def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def expected = c.getTime() - claims.put("aDate", c) - Date result = claims.get('aDate', Date.class) + def expected = c.toInstant() + claims.put("aCalender", c) + Instant result = claims.get('aCalender', Instant.class) assertEquals expected, result } @Test void testGetRequiredDateFromLong() { - def expected = new Date() + def expected = Instant.now() // note that Long is stored in claim - claims.put("aDate", expected.getTime()) - Date result = claims.get("aDate", Date.class) + claims.put("aLong", expected.toEpochMilli()) + Instant result = claims.get("aLong", Instant.class) assertEquals expected, result } @Test void testGetRequiredDateFromIso8601String() { - def expected = new Date() - claims.put("aDate", DateFormats.formatIso8601(expected)) - Date result = claims.get("aDate", Date.class) + def expected = Instant.now() + claims.put("aString", DateFormats.formatIso8601(expected)) + Instant result = claims.get("aString", Instant.class) assertEquals expected, result } @Test void testGetRequiredDateFromIso8601MillisString() { - def expected = new Date() - claims.put("aDate", DateFormats.formatIso8601(expected, true)) - Date result = claims.get("aDate", Date.class) + def expected = Instant.now() + claims.put("aString", DateFormats.formatIso8601(expected, true)) + Instant result = claims.get("aString", Instant.class) assertEquals expected, result } @@ -225,12 +256,13 @@ class DefaultClaimsTest { String s = d.toString() claims.put('aDate', s) try { - claims.get('aDate', Date.class) + claims.get('aDate', Instant.class) fail() } catch (IllegalArgumentException expected) { - String expectedMsg = "Cannot create Date from 'aDate' value '$s'. Cause: " + + + String expectedMsg = "Cannot create Instant from 'aDate' value '$s'. Cause: " + "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics " + - "exhausted. Cause: Unparseable date: \"$s\"" + "exhausted. Cause: Text \'$s\' could not be parsed at index 0" assertEquals expectedMsg, expected.getMessage() } } @@ -247,10 +279,9 @@ class DefaultClaimsTest { @Test void testGetSpecDateWithLongString() { - Date orig = new Date() - long millis = orig.getTime() - long seconds = millis / 1000L as long - Date expected = new Date(seconds * 1000L) + Instant orig = Instant.now() + long seconds = orig.getEpochSecond() + Instant expected = orig.truncatedTo(ChronoUnit.SECONDS) String secondsString = '' + seconds claims.put(Claims.EXPIRATION, secondsString) claims.put(Claims.ISSUED_AT, secondsString) @@ -265,10 +296,9 @@ class DefaultClaimsTest { @Test void testGetSpecDateWithLong() { - Date orig = new Date() - long millis = orig.getTime() - long seconds = millis / 1000L as long - Date expected = new Date(seconds * 1000L) + Instant orig = Instant.now() + long seconds = orig.getEpochSecond() + Instant expected = orig.truncatedTo(ChronoUnit.SECONDS) claims.put(Claims.EXPIRATION, seconds) claims.put(Claims.ISSUED_AT, seconds) claims.put(Claims.NOT_BEFORE, seconds) @@ -282,9 +312,8 @@ class DefaultClaimsTest { @Test void testGetSpecDateWithIso8601String() { - Date orig = new Date() - long millis = orig.getTime() - long seconds = millis / 1000L as long + Instant orig = Instant.now() + long seconds = orig.getEpochSecond() String s = DateFormats.formatIso8601(orig) claims.put(Claims.EXPIRATION, s) claims.put(Claims.ISSUED_AT, s) @@ -299,9 +328,8 @@ class DefaultClaimsTest { @Test void testGetSpecDateWithDate() { - Date orig = new Date() - long millis = orig.getTime() - long seconds = millis / 1000L as long + Instant orig = Instant.now() + long seconds = orig.getEpochSecond() claims.put(Claims.EXPIRATION, orig) claims.put(Claims.ISSUED_AT, orig) claims.put(Claims.NOT_BEFORE, orig) @@ -316,15 +344,14 @@ class DefaultClaimsTest { @Test void testGetSpecDateWithCalendar() { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - Date date = cal.getTime() - long millis = date.getTime() - long seconds = millis / 1000L as long + Instant instant = cal.toInstant() + long seconds = instant.getEpochSecond() claims.put(Claims.EXPIRATION, cal) claims.put(Claims.ISSUED_AT, cal) claims.put(Claims.NOT_BEFORE, cal) - assertEquals date, claims.getExpiration() - assertEquals date, claims.getIssuedAt() - assertEquals date, claims.getNotBefore() + assertEquals instant, claims.getExpiration() + assertEquals instant, claims.getIssuedAt() + assertEquals instant, claims.getNotBefore() assertEquals seconds, claims.get(Claims.EXPIRATION) assertEquals seconds, claims.get(Claims.ISSUED_AT) assertEquals seconds, claims.get(Claims.NOT_BEFORE) @@ -335,7 +362,7 @@ class DefaultClaimsTest { long millis = System.currentTimeMillis() Date d = new Date(millis) claims.put('exp', d) - assertEquals d, claims.getExpiration() + assertEquals d.toInstant(), claims.getExpiration() } void trySpecDateNonDate(Parameter param) { @@ -344,7 +371,7 @@ class DefaultClaimsTest { claims.put(param.getId(), val) fail() } catch (IllegalArgumentException iae) { - String msg = "Invalid JWT Claims $param value: hi. Cannot create Date from object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1." + String msg = "Invalid JWT Claims $param value: hi. Cannot create Instant from object of type io.jsonwebtoken.impl.DefaultClaimsTest\$1." assertEquals msg, iae.getMessage() } } @@ -358,25 +385,25 @@ class DefaultClaimsTest { @Test void testGetClaimExpiration_Success() { - def now = new Date(System.currentTimeMillis()) + def now = Instant.now() claims.put('exp', now) - Date expected = claims.get("exp", Date.class) + Instant expected = claims.get("exp", Instant.class) assertEquals(expected, claims.getExpiration()) } @Test void testGetClaimIssuedAt_Success() { - def now = new Date(System.currentTimeMillis()) + def now = Instant.now() claims.put('iat', now) - Date expected = claims.get("iat", Date.class) + Instant expected = claims.get("iat", Instant.class) assertEquals(expected, claims.getIssuedAt()) } @Test void testGetClaimNotBefore_Success() { - def now = new Date(System.currentTimeMillis()) + def now = Instant.now() claims.put('nbf', now) - Date expected = claims.get("nbf", Date.class) + Instant expected = claims.get("nbf", Instant.class) assertEquals(expected, claims.getNotBefore()) } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 71479b363..54cd3af2a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import io.jsonwebtoken.* import io.jsonwebtoken.impl.security.* import io.jsonwebtoken.io.* diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 8084b32f2..6a4a0917f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -44,7 +44,7 @@ class DefaultJwtParserTest { // all whitespace chars as defined by Character.isWhitespace: static final String WHITESPACE_STR = ' \u0020 \u2028 \u2029 \t \n \u000B \f \r \u001C \u001D \u001E \u001F ' - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() private DefaultJwtParser parser @@ -263,7 +263,7 @@ class DefaultJwtParserTest { long differenceMillis = 843 // arbitrary, anything > 0 is fine def exp = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def later = new Date(exp.getTime() + differenceMillis) + def later = exp.plusMillis(differenceMillis) def s = Jwts.builder().expiration(exp).compact() try { @@ -272,7 +272,7 @@ class DefaultJwtParserTest { def exp8601 = DateFormats.formatIso8601(exp, true) def later8601 = DateFormats.formatIso8601(later, true) String msg = "JWT expired ${differenceMillis} milliseconds ago at ${exp8601}. " + - "Current time: ${later8601}. Allowed clock skew: 0 milliseconds."; + "Current time: ${later8601}. Allowed clock skew: 0 milliseconds." assertEquals msg, expected.message } } @@ -282,7 +282,7 @@ class DefaultJwtParserTest { long differenceMillis = 3842 // arbitrary, anything > 0 is fine def nbf = JwtDateConverter.INSTANCE.applyFrom(System.currentTimeMillis() / 1000L) - def earlier = new Date(nbf.getTime() - differenceMillis) + def earlier = nbf.minusMillis(differenceMillis) def s = Jwts.builder().notBefore(nbf).compact() try { @@ -291,7 +291,7 @@ class DefaultJwtParserTest { def nbf8601 = DateFormats.formatIso8601(nbf, true) def earlier8601 = DateFormats.formatIso8601(earlier, true) String msg = "JWT early by ${differenceMillis} milliseconds before ${nbf8601}. " + - "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds."; + "Current time: ${earlier8601}. Allowed clock skew: 0 milliseconds." assertEquals msg, expected.message } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy index bd25bdcb3..4f83c900a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/lang/JwtDateConverterTest.groovy @@ -33,6 +33,6 @@ class JwtDateConverterTest { @Test void testToDateWithNull() { - assertNull JwtDateConverter.toDate(null) + assertNull JwtDateConverter.toInstant(null) } } diff --git a/pom.xml b/pom.xml index 69e29a101..4c2c1c586 100644 --- a/pom.xml +++ b/pom.xml @@ -106,10 +106,11 @@ 4.2.rc3 true - 7 + 8 ${user.name}-${maven.build.timestamp} 2.12.7.1 + 2.12.7 20231013 2.9.0 @@ -180,6 +181,11 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.jsr310.version} + org.json json From 0921e29e3ea09679d7446bc17956d95c5ec8fc6b Mon Sep 17 00:00:00 2001 From: Pieter Van Eeckhout Date: Tue, 26 Dec 2023 10:14:24 +0100 Subject: [PATCH 2/6] #235 Update JWT for default Instant usage The JWT handling has been updated to default to Java's Instant class. This change removed support for various date and time classes such as ZonedDateTime, OffsetDateTime, Date, and Calendar in favor of an Instant-based approach throughout the codebase. The appropriate tests and documentation were updated to reflect this change. --- README.md | 2 +- api/src/main/java/io/jsonwebtoken/Claims.java | 2 +- .../orgjson/io/OrgJsonSerializer.java | 22 +----- .../orgjson/io/OrgJsonSerializerTest.groovy | 26 ------- .../io/jsonwebtoken/impl/DefaultClaims.java | 2 +- .../impl/lang/JwtDateConverter.java | 17 +---- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 9 --- .../impl/DefaultClaimsTest.groovy | 75 +++++-------------- 8 files changed, 23 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 98edcac5e..5b44cab87 100644 --- a/README.md +++ b/README.md @@ -2992,7 +2992,7 @@ Jwts.parser() #### Parsing of Custom Claim Types -By default JJWT will only convert simple claim types: String, Date, Long, Integer, Short and Byte. If you need to +By default JJWT will only convert simple claim types: String, Instant, Long, Integer, Short and Byte. If you need to deserialize other types you can configure the `JacksonDeserializer` by passing a `Map` of claim names to types in through a constructor. For example: diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index cd1a79dfd..b6e528696 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -147,7 +147,7 @@ public interface Claims extends Map, Identifiable { * Returns the JWTs claim ({@code claimName}) value as a {@code requiredType} instance, or {@code null} if not * present. * - *

JJWT only converts simple String, Date, OffsetDateTime, ZonedDateTime, Long, Integer, Short and Byte types automatically. Anything more + *

JJWT only converts simple String, Instant, Long, Integer, Short and Byte types automatically. Anything more * complex is expected to be already converted to your desired type by the JSON parser. You may specify a custom * JSON processor using the {@code JwtParserBuilder}'s * {@link JwtParserBuilder#json(io.jsonwebtoken.io.Deserializer) json(Deserializer)} method. See the JJWT diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java index 79b5d5ac3..eb99adcb1 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonSerializer.java @@ -31,11 +31,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.util.Calendar; import java.util.Collection; -import java.util.Date; import java.util.Map; /** @@ -96,26 +92,10 @@ private Object toJSONInstance(Object object) throws IOException { return object; } - if(object instanceof Instant) { + if (object instanceof Instant) { return DateFormats.formatIso8601((Instant) object); } - if(object instanceof OffsetDateTime) { - return DateFormats.formatIso8601(((OffsetDateTime) object).toInstant()); - } - - if(object instanceof ZonedDateTime) { - return DateFormats.formatIso8601(((ZonedDateTime) object).toInstant()); - } - - if (object instanceof Calendar) { - return DateFormats.formatIso8601(((Calendar) object).getTime().toInstant()); - } - - if (object instanceof Date) { - return DateFormats.formatIso8601(((Date) object).toInstant()); - } - if (object instanceof byte[]) { return Encoders.BASE64.encode((byte[]) object); } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy index ad7a6f458..092c78651 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonSerializerTest.groovy @@ -28,8 +28,6 @@ import org.junit.Before import org.junit.Test import java.time.Instant -import java.time.OffsetDateTime -import java.time.ZonedDateTime import static org.junit.Assert.* @@ -201,22 +199,6 @@ class OrgJsonSerializerTest { assertEquals "\"$formatted\"" as String, ser(instant) } - @Test - void testOffsetDateTime() { - OffsetDateTime offsetDateTime = OffsetDateTime.now() - def now = offsetDateTime.toInstant() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(offsetDateTime) - } - - @Test - void testZonedDateTime() { - ZonedDateTime zonedDateTime = ZonedDateTime.now() - def now = zonedDateTime.toInstant() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(zonedDateTime) - } - @Test void testDate() { Date date = new Date() @@ -225,14 +207,6 @@ class OrgJsonSerializerTest { assertEquals "\"$formatted\"" as String, ser(date) } - @Test - void testCalendar() { - def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def now = cal.toInstant() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(cal) - } - @Test void testSimpleIntArray() { assertEquals '[1,2]', ser([1, 2] as int[]) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index fdb5b5e90..08a19f607 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -30,7 +30,7 @@ public class DefaultClaims extends ParameterMap implements Claims { private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " + - "'%s'. JJWT only converts simple String, Date, Instant, OffsetDateTime, ZonedDateTime, Long, Integer, Short " + + "'%s'. JJWT only converts simple String, Instant, Long, Integer, Short " + "and Byte types automatically. Anything more complex is expected to be already converted to your desired type " + "by the JSON Deserializer implementation. You may specify a custom Deserializer for a JwtParser with the " + "desired conversion configuration via the JwtParserBuilder.deserializer() method. " + diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index 7098c2259..c60120b52 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -18,11 +18,7 @@ import io.jsonwebtoken.lang.DateFormats; import java.time.Instant; -import java.time.ZonedDateTime; -import java.time.OffsetDateTime; import java.time.format.DateTimeParseException; -import java.util.Date; -import java.util.Calendar; public class JwtDateConverter implements Converter { @@ -51,7 +47,7 @@ public static Instant toSpecInstant(Object value) { long seconds = ((Number) value).longValue(); value = Instant.ofEpochSecond(seconds); } - // would have been normalized to Instant if it was a number value, so perform normal date conversion: + // would have been normalized to Instant if it was a number value, so perform normal instant conversion: return toInstant(value); } @@ -66,20 +62,9 @@ public static Instant toInstant(Object v) { return null; } else if (v instanceof Instant) { return (Instant) v; - } else if (v instanceof ZonedDateTime) { - return ((ZonedDateTime) v).toInstant(); - }else if (v instanceof OffsetDateTime) { - return ((OffsetDateTime) v).toInstant(); - } else if (v instanceof Date) { - //assume UTC - return ((Date) v).toInstant(); - } else if (v instanceof Calendar) { //since 0.10.0 - //assume UTC - return ((Calendar) v).getTime().toInstant(); } else if (v instanceof Number) { //assume millis: long millis = ((Number) v).longValue(); - //assume UTC return Instant.ofEpochMilli(millis); } else if (v instanceof String) { return parseIso8601Date((String) v); //ISO-8601 parsing since 0.10.0 diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index b79f5ff0e..44daee654 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -53,19 +53,10 @@ class JwtsTest { return new Date(secondOnlyPrecisionMillis) } - private static Instant instantWithOnlySecondPrecision(long millis) { - return instantWithOnlySecondPrecision(Instant.ofEpochMilli(millis)) - } - private static Instant instantWithOnlySecondPrecision(Instant instant) { return instant.truncatedTo(ChronoUnit.SECONDS) } - private static Date now() { - Date date = dateWithOnlySecondPrecision(System.currentTimeMillis()) - return date - } - private static int later() { def date = laterDate(10000) def seconds = date.getTime() / 1000 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index a1961d1d4..d29162f7d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -23,11 +23,7 @@ import org.junit.Before import org.junit.Test import java.time.Instant -import java.time.OffsetDateTime -import java.time.ZonedDateTime import java.time.temporal.ChronoUnit -import java.time.temporal.TemporalField -import java.time.temporal.TemporalUnit import static org.junit.Assert.* @@ -192,39 +188,6 @@ class DefaultClaimsTest { assertEquals expected, result } - @Test - void testGetRequiredDateFromOffsetDateTime() { - def expected = OffsetDateTime.now() - claims.put("anOffsetDateTime", expected) - OffsetDateTime result = claims.get("anOffsetDateTime", OffsetDateTime.class) - assertEquals expected, result - } - - @Test - void testGetRequiredDateFromZonedDateTime() { - def expected = ZonedDateTime.now() - claims.put("aZonedDateTime", expected) - ZonedDateTime result = claims.get("aZonedDateTime", ZonedDateTime.class) - assertEquals expected, result - } - - @Test - void testGetRequiredDateFromDate() { - def expected = new Date() - claims.put("aDate", expected) - Date result = claims.get("aDate", Date.class) - assertEquals expected, result - } - - @Test - void testGetRequiredDateFromCalendar() { - def c = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def expected = c.toInstant() - claims.put("aCalender", c) - Instant result = claims.get('aCalender', Instant.class) - assertEquals expected, result - } - @Test void testGetRequiredDateFromLong() { def expected = Instant.now() @@ -342,13 +305,12 @@ class DefaultClaimsTest { } @Test - void testGetSpecDateWithCalendar() { - Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - Instant instant = cal.toInstant() + void testGetSpecDateWithInstant() { + Instant instant = Instant.now() long seconds = instant.getEpochSecond() - claims.put(Claims.EXPIRATION, cal) - claims.put(Claims.ISSUED_AT, cal) - claims.put(Claims.NOT_BEFORE, cal) + claims.put(Claims.EXPIRATION, instant) + claims.put(Claims.ISSUED_AT, instant) + claims.put(Claims.NOT_BEFORE, instant) assertEquals instant, claims.getExpiration() assertEquals instant, claims.getIssuedAt() assertEquals instant, claims.getNotBefore() @@ -358,11 +320,10 @@ class DefaultClaimsTest { } @Test - void testToSpecDateWithDate() { - long millis = System.currentTimeMillis() - Date d = new Date(millis) - claims.put('exp', d) - assertEquals d.toInstant(), claims.getExpiration() + void testToSpecDateWithInstant() { + Instant i = Instant.now() + claims.put('exp', i) + assertEquals i, claims.getExpiration() } void trySpecDateNonDate(Parameter param) { @@ -411,7 +372,7 @@ class DefaultClaimsTest { void testPutWithIat() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.put('iat', now) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('iat') //conversion should have happened } @@ -420,7 +381,7 @@ class DefaultClaimsTest { void testPutAllWithIat() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.putAll([iat: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('iat') //conversion should have happened } @@ -429,7 +390,7 @@ class DefaultClaimsTest { void testConstructorWithIat() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) this.claims = new DefaultClaims([iat: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('iat') //conversion should have happened } @@ -438,7 +399,7 @@ class DefaultClaimsTest { void testPutWithNbf() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.put('nbf', now) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('nbf') //conversion should have happened } @@ -447,7 +408,7 @@ class DefaultClaimsTest { void testPutAllWithNbf() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.putAll([nbf: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('nbf') //conversion should have happened } @@ -456,7 +417,7 @@ class DefaultClaimsTest { void testConstructorWithNbf() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) this.claims = new DefaultClaims([nbf: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('nbf') //conversion should have happened } @@ -465,7 +426,7 @@ class DefaultClaimsTest { void testPutWithExp() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.put('exp', now) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('exp') //conversion should have happened } @@ -474,7 +435,7 @@ class DefaultClaimsTest { void testPutAllWithExp() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) claims.putAll([exp: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('exp') //conversion should have happened } @@ -483,7 +444,7 @@ class DefaultClaimsTest { void testConstructorWithExp() { long millis = System.currentTimeMillis() long seconds = millis / 1000 as long - Date now = new Date(millis) + Instant now = Instant.ofEpochMilli(millis) this.claims = new DefaultClaims([exp: now]) //this should convert 'now' to seconds since epoch assertEquals seconds, claims.get('exp') //conversion should have happened } From 54f3a196cfd433dee22d6027595a5c17e5da76bb Mon Sep 17 00:00:00 2001 From: pveeckhout Date: Wed, 27 Dec 2023 10:36:59 +0100 Subject: [PATCH 3/6] #235 Replace 'Date' with 'Instant' in JwtParserTest Replaced all instances of 'Date' with 'Instant' in JwtParserTest to more accurately reflect variable usage. Also added a TODO in JwtDateConverter.java to clarify the handling of epochMillis vs epochSeconds. --- .../io/jsonwebtoken/impl/lang/JwtDateConverter.java | 2 +- .../test/groovy/io/jsonwebtoken/JwtParserTest.groovy | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index c60120b52..cb8f6dd5c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -63,7 +63,7 @@ public static Instant toInstant(Object v) { } else if (v instanceof Instant) { return (Instant) v; } else if (v instanceof Number) { - //assume millis: + // TODO millis are assume or expected but instant is in json as epochSeconds NOT epochMillis long millis = ((Number) v).longValue(); return Instant.ofEpochMilli(millis); } else if (v instanceof String) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 5430ce4bc..c76d9e9c0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1460,22 +1460,22 @@ class JwtParserTest { } @Test - void testParseRequireCustomDate_Success() { + void testParseRequireCustomInstant_Success() { - def aDate = Instant.now() + def anInstant = Instant.now() byte[] key = randomKey() String compact = Jwts.builder().signWith(SignatureAlgorithm.HS256, key). - claim("aDate", aDate). + claim("anInstant", anInstant). compact() Jwt jwt = Jwts.parser().setSigningKey(key). - require("aDate", aDate). + require("anInstant", anInstant). build(). parseSignedClaims(compact) - assertEquals jwt.getPayload().get("aDate", Instant.class), aDate + assertEquals jwt.getPayload().get("anInstant", Instant.class), anInstant } @Test @@ -1505,7 +1505,7 @@ class JwtParserTest { } @Test - void testParseRequireCustomDate_Incorrect_Fail() { + void testParseRequireCustomInstant_Incorrect_Fail() { def goodInstant = Instant.now() def badInstant = Instant.now().minusMillis(10000) From fd1728ccd51b8eae1295dc6c6156d6a84f8e936e Mon Sep 17 00:00:00 2001 From: pveeckhout Date: Wed, 27 Dec 2023 10:45:49 +0100 Subject: [PATCH 4/6] #235 Update project version to 1.0.0-SNAPSHOT This commit updates the version of the project across all pom.xml files from 0.12.4-SNAPSHOT to 1.0.0-SNAPSHOT as it prepares for a major release. Changes cover the core project and various extensions, signaling a coordinated upgrade across the codebase. --- api/pom.xml | 2 +- extensions/gson/pom.xml | 2 +- extensions/jackson/pom.xml | 2 +- extensions/orgjson/pom.xml | 2 +- extensions/pom.xml | 2 +- impl/pom.xml | 2 +- pom.xml | 2 +- tdjar/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index 80a49db9b..2d106b7c8 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../pom.xml diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index fb17afece..84972064a 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../../pom.xml diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 3c7b542e3..9107c069b 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../../pom.xml diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index 0936fe406..a2411941c 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../../pom.xml diff --git a/extensions/pom.xml b/extensions/pom.xml index e04955207..972050bf7 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../pom.xml diff --git a/impl/pom.xml b/impl/pom.xml index bb7fb61aa..0682240ef 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 4c2c1c586..97e0769b3 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT JJWT JSON Web Token support for the JVM and Android pom diff --git a/tdjar/pom.xml b/tdjar/pom.xml index 52cbbc2af..9ab645801 100644 --- a/tdjar/pom.xml +++ b/tdjar/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.4-SNAPSHOT + 1.0.0-SNAPSHOT ../pom.xml From 13a83b6629b97b69da1cbcfb93f7e4a26df1339a Mon Sep 17 00:00:00 2001 From: pveeckhout Date: Wed, 27 Dec 2023 13:32:35 +0100 Subject: [PATCH 5/6] #235 Replace usage of Date with Instant All instances of java.util.Date in code have been replaced with java.time.Instant for better time precision and timezone handling. This includes changes in variable names, method names, test cases, comments and documentation. The java.util.Date-based utilities have also been removed. --- CHANGELOG.md | 2 +- README.md | 4 +-- .../io/jsonwebtoken/JwtParserBuilder.java | 4 +-- .../io/jsonwebtoken/lang/DateFormats.java | 10 +++--- .../jsonwebtoken/lang/DateFormatsTest.groovy | 24 +++++++------- .../jackson/io/JacksonDeserializerTest.groovy | 6 ++-- .../jackson/io/stubs/CustomBean.groovy | 18 ++++++----- .../java/io/jsonwebtoken/impl/FixedClock.java | 10 +++--- .../impl/lang/JwtDateConverter.java | 4 +-- .../io/jsonwebtoken/DateTestUtils.groovy | 31 ------------------ .../io/jsonwebtoken/JwtParserTest.groovy | 6 ++-- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 32 ++++++------------- .../impl/DefaultClaimsTest.groovy | 15 ++++----- .../jsonwebtoken/impl/FixedClockTest.groovy | 6 ++-- 14 files changed, 66 insertions(+), 106 deletions(-) delete mode 100644 impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c5bdbae..db16c2b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -624,7 +624,7 @@ would be very strange if a machine's clock was more than 5 minutes difference fr #### Custom Clock Support Timestamps created during parsing can now be obtained via a custom time source via an implementation of - the new `io.jsonwebtoken.Clock` interface. The default implementation simply returns `new Date()` to reflect the time + the new `io.jsonwebtoken.Clock` interface. The default implementation simply returns `new Instant()` to reflect the time when parsing occurs, as most would expect. However, supplying your own clock could be useful, especially during test cases to guarantee deterministic behavior. diff --git a/README.md b/README.md index 5b44cab87..a8f9e3279 100644 --- a/README.md +++ b/README.md @@ -993,7 +993,7 @@ String jws = Jwts.builder() .audience("you") .expiration(expiration) //a java.time.Instant .notBefore(notBefore) //a java.time.Instant - .issuedAt(new Date()) // for example, now + .issuedAt(Instant.now) // for example, now .id(UUID.randomUUID().toString()) //just an example id /// ... etc ... @@ -1416,7 +1416,7 @@ Clock clock = new MyClock(); Jwts.parser().clock(myClock) //... etc ... ``` -The `JwtParser`'s default `Clock` implementation simply returns `new Date()` to reflect the time when parsing occurs, +The `JwtParser`'s default `Clock` implementation simply returns `Instant.now()` to reflect the time when parsing occurs, as most would expect. However, supplying your own clock could be useful, especially when writing test cases to guarantee deterministic behavior. diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index d72adda60..103e9f737 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -223,7 +223,7 @@ public interface JwtParserBuilder extends Builder { /** * Sets the {@link Clock} that determines the timestamp to use when validating the parsed JWT. - * The parser uses a default Clock implementation that simply returns {@code new Date()} when called. + * The parser uses a default Clock implementation that simply returns {@code new Instant()} when called. * * @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT. * @return the parser builder for method chaining. @@ -235,7 +235,7 @@ public interface JwtParserBuilder extends Builder { /** * Sets the {@link Clock} that determines the timestamp to use when validating the parsed JWT. - * The parser uses a default Clock implementation that simply returns {@code new Date()} when called. + * The parser uses a default Clock implementation that simply returns {@code new Instant()} when called. * * @param clock a {@code Clock} object to return the timestamp to use when validating the parsed JWT. * @return the parser builder for method chaining. diff --git a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java index 380c387b9..82bd2642d 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java +++ b/api/src/main/java/io/jsonwebtoken/lang/DateFormats.java @@ -43,8 +43,8 @@ private DateFormats() { * Return an ISO-8601-formatted string with millisecond precision representing the * specified {@code instant}. Will always convert to UTC timezone. * - * @param instant the date for which to create an ISO-8601-formatted string - * @return the date represented as an ISO-8601-formatted string in UTC timezone with millisecond precision. + * @param instant the instant for which to create an ISO-8601-formatted string + * @return the instant represented as an ISO-8601-formatted string in UTC timezone with millisecond precision. */ public static String formatIso8601(Instant instant) { return formatIso8601(instant, true); @@ -56,7 +56,7 @@ public static String formatIso8601(Instant instant) { * * @param instant the instant for which to create an ISO-8601-formatted string * @param includeMillis whether to include millisecond notation within the string. - * @return the date represented as an ISO-8601-formatted string in UTC timezone with optional millisecond precision. + * @return the instant represented as an ISO-8601-formatted string in UTC timezone with optional millisecond precision. */ public static String formatIso8601(Instant instant, boolean includeMillis) { Assert.notNull(instant, "Instant argument cannot be null."); @@ -67,8 +67,8 @@ public static String formatIso8601(Instant instant, boolean includeMillis) { } /** - * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Instant} instance. The - * date string may optionally contain millisecond notation, and those milliseconds will be represented accordingly. + * Parse the specified ISO-8601-formatted date string and return the corresponding {@link Instant} instance. + * The date string may optionally contain millisecond notation, and those milliseconds will be represented accordingly. * * @param s the ISO-8601-formatted string to parse * @return the string's corresponding {@link Instant} instance. diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy index 4f4a03d6f..f5dabe07d 100644 --- a/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/lang/DateFormatsTest.groovy @@ -62,36 +62,36 @@ class DateFormatsTest { @Test void testParseIso8601DateWithMillisZuluOffset() { String dateString = "2023-12-25T15:30:00.123Z" - Instant parsedDate = DateFormats.parseIso8601Date(dateString) - assertNotNull(parsedDate) + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.UTC).toInstant() - assertEquals expectedInstant, parsedDate + assertEquals expectedInstant, parsedInstant } @Test void testParseIso8601DateWithMillisNonZuluOffset() { String dateString = "2023-12-25T15:30:00.123-01:00" - Instant parsedDate = DateFormats.parseIso8601Date(dateString) - assertNotNull(parsedDate) + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.ofHours(-1)).toInstant() - assertEquals expectedInstant, parsedDate + assertEquals expectedInstant, parsedInstant } @Test void testParseIso8601DateWithoutMillisZuluOffset() { String dateString = "2023-12-25T15:30:00Z" - Instant parsedDate = DateFormats.parseIso8601Date(dateString) - assertNotNull(parsedDate) + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 0, ZoneOffset.UTC).toInstant() - assertEquals expectedInstant, parsedDate + assertEquals expectedInstant, parsedInstant } @Test void testParseIso8601DateWithoutMillisNonZuluOffset() { String dateString = "2023-12-25T15:30:00+01:00" - Instant parsedDate = DateFormats.parseIso8601Date(dateString) - assertNotNull(parsedDate) - assertEquals OffsetDateTime.of(2023,12,25,15,30,0, 0, ZoneOffset.ofHours(1)).toInstant(), parsedDate + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) + assertEquals OffsetDateTime.of(2023,12,25,15,30,0, 0, ZoneOffset.ofHours(1)).toInstant(), parsedInstant } @Test(expected = DateTimeParseException) diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index 25e27a360..7bc9eedf0 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -25,6 +25,8 @@ import io.jsonwebtoken.lang.Maps import org.junit.Before import org.junit.Test +import java.time.Instant + import static org.junit.Assert.* class JacksonDeserializerTest { @@ -99,7 +101,7 @@ class JacksonDeserializerTest { CustomBean expectedCustomBean = new CustomBean() .setByteArrayValue("bytes".getBytes("UTF-8")) .setByteValue(0xF as byte) - .setDateValue(new Date(currentTime)) + .setInstantValue(Instant.ofEpochMilli(currentTime)) .setIntValue(11) .setShortValue(22 as short) .setLongValue(33L) @@ -107,7 +109,7 @@ class JacksonDeserializerTest { .setNestedValue(new CustomBean() .setByteArrayValue("bytes2".getBytes("UTF-8")) .setByteValue(0xA as byte) - .setDateValue(new Date(currentTime + 1)) + .setInstantValue(Instant.ofEpochMilli(currentTime + 1)) .setIntValue(111) .setShortValue(222 as short) .setLongValue(333L) diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy index f046ec7d0..e102c84bc 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/stubs/CustomBean.groovy @@ -15,11 +15,13 @@ */ package io.jsonwebtoken.jackson.io.stubs +import java.time.Instant + class CustomBean { private String stringValue private int intValue - private Date dateValue + private Instant instantValue private short shortValue private long longValue private byte byteValue @@ -44,12 +46,12 @@ class CustomBean { return this } - Date getDateValue() { - return dateValue + Instant getInstantValue() { + return instantValue } - CustomBean setDateValue(Date dateValue) { - this.dateValue = dateValue + CustomBean setInstantValue(Instant instantValue) { + this.instantValue = instantValue return this } @@ -109,7 +111,7 @@ class CustomBean { if (longValue != that.longValue) return false if (shortValue != that.shortValue) return false if (!Arrays.equals(byteArrayValue, that.byteArrayValue)) return false - if (dateValue != that.dateValue) return false + if (instantValue != that.instantValue) return false if (nestedValue != that.nestedValue) return false if (stringValue != that.stringValue) return false @@ -120,7 +122,7 @@ class CustomBean { int result result = stringValue.hashCode() result = 31 * result + intValue - result = 31 * result + dateValue.hashCode() + result = 31 * result + instantValue.hashCode() result = 31 * result + (int) shortValue result = 31 * result + (int) (longValue ^ (longValue >>> 32)) result = 31 * result + (int) byteValue @@ -135,7 +137,7 @@ class CustomBean { return "CustomBean{" + "stringValue='" + stringValue + '\'' + ", intValue=" + intValue + - ", dateValue=" + dateValue?.time + + ", instantValue=" + instantValue?.toEpochMilli() + ", shortValue=" + shortValue + ", longValue=" + longValue + ", byteValue=" + byteValue + diff --git a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java index 44de17454..6f087e92c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/FixedClock.java @@ -31,7 +31,7 @@ public class FixedClock implements Clock { /** * Creates a new fixed clock using new {@link Instant instant}() as the seed timestamp. All calls to - * {@link #now now()} will always return this seed Date. + * {@link #now now()} will always return this seed Instant. */ public FixedClock() { this(Instant.now()); @@ -39,9 +39,9 @@ public FixedClock() { /** * Creates a new fixed clock using the specified seed timestamp. All calls to - * {@link #now now()} will always return this seed Date. + * {@link #now now()} will always return this seed Instant. * - * @param now the specified Date to always return from all calls to {@link #now now()}. + * @param now the specified Instant to always return from all calls to {@link #now now()}. */ public FixedClock(Instant now) { this.now = now; @@ -49,9 +49,9 @@ public FixedClock(Instant now) { /** * Creates a new fixed clock using the specified seed timestamp. All calls to - * {@link #now now()} will always return this seed Date. + * {@link #now now()} will always return this seed Instant. * - * @param timeInMillis the specified Date in milliseconds to always return from all calls to {@link #now now()}. + * @param timeInMillis the specified Instant in milliseconds to always return from all calls to {@link #now now()}. */ public FixedClock(long timeInMillis) { this(Instant.ofEpochMilli(timeInMillis)); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java index cb8f6dd5c..bb85b8b3e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -38,7 +38,7 @@ public static Instant toSpecInstant(Object value) { if (value instanceof String) { try { value = Long.parseLong((String) value); - } catch (NumberFormatException ignored) { // will try in the fallback toDate method call below + } catch (NumberFormatException ignored) { // will try in the fallback toInstant method call below } } if (value instanceof Number) { @@ -54,7 +54,7 @@ public static Instant toSpecInstant(Object value) { /** * Returns a {@link Instant} equivalent of the specified object value using heuristics. * - * @param v the object value to represent as a Date. + * @param v the object value to represent as an Instant. * @return a {@link Instant} equivalent of the specified object value using heuristics. */ public static Instant toInstant(Object v) { diff --git a/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy b/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy deleted file mode 100644 index e8c996752..000000000 --- a/impl/src/test/groovy/io/jsonwebtoken/DateTestUtils.groovy +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2019 jsonwebtoken.io - * - * 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 io.jsonwebtoken - -final class DateTestUtils { - - /** - * Date util method for lopping truncate the millis from a date. - * @param date input date - * @return The date time in millis with the precision of seconds - */ - static long truncateMillis(Date date) { - Calendar cal = Calendar.getInstance() - cal.setTime(date) - cal.set(Calendar.MILLISECOND, 0) - return cal.getTimeInMillis() - } -} diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index c76d9e9c0..0d80663b8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -1532,7 +1532,7 @@ class JwtParserTest { @Test void testParseRequireCustomDate_Missing_Fail() { - def aDate = new Date(System.currentTimeMillis()) + def anInstant = Instant.now() byte[] key = randomKey() @@ -1542,12 +1542,12 @@ class JwtParserTest { try { Jwts.parser().setSigningKey(key). - require("aDate", aDate). + require("anInstant", anInstant). build(). parseSignedClaims(compact) fail() } catch (MissingClaimException e) { - String msg = "Missing 'aDate' claim. Expected value: $aDate" + String msg = "Missing 'anInstant' claim. Expected value: $anInstant" assertEquals msg, e.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 44daee654..0e47e11cc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -47,29 +47,17 @@ import static org.junit.Assert.* class JwtsTest { - private static Date dateWithOnlySecondPrecision(long millis) { - long seconds = (millis / 1000) as long - long secondOnlyPrecisionMillis = seconds * 1000 - return new Date(secondOnlyPrecisionMillis) - } private static Instant instantWithOnlySecondPrecision(Instant instant) { return instant.truncatedTo(ChronoUnit.SECONDS) } - private static int later() { - def date = laterDate(10000) - def seconds = date.getTime() / 1000 - return seconds as int - } - - private static Date laterDate(int seconds) { - def millis = seconds * 1000L - def time = System.currentTimeMillis() + millis - return dateWithOnlySecondPrecision(time) + private static long later() { + def instant = laterInstant(10000L) + return instant.getEpochSecond(); } - private static Instant laterInstant(int seconds) { + private static Instant laterInstant(long seconds) { return instantWithOnlySecondPrecision(Instant.now().plusSeconds(seconds)) } @@ -417,8 +405,8 @@ class JwtsTest { Instant then = laterInstant(10000) String compact = Jwts.builder().setExpiration(then).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims - def claimedDate = claims.getExpiration() - assertEquals then, claimedDate + def claimedInstant = claims.getExpiration() + assertEquals then, claimedInstant compact = Jwts.builder().setIssuer("Me") .setExpiration(then) //set it @@ -434,8 +422,8 @@ class JwtsTest { Instant now = instantWithOnlySecondPrecision(Instant.now()) //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setNotBefore(now).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims - def claimedDate = claims.getNotBefore() - assertEquals now, claimedDate + def claimedInstant = claims.getNotBefore() + assertEquals now, claimedInstant compact = Jwts.builder().setIssuer("Me") .setNotBefore(now) //set it @@ -451,8 +439,8 @@ class JwtsTest { Instant now = instantWithOnlySecondPrecision(Instant.now()) //jwt exp only supports *seconds* since epoch: String compact = Jwts.builder().setIssuedAt(now).compact() Claims claims = Jwts.parser().unsecured().build().parse(compact).payload as Claims - def claimedDate = claims.getIssuedAt() - assertEquals now, claimedDate + def claimedInstant = claims.getIssuedAt() + assertEquals now, claimedInstant compact = Jwts.builder().setIssuer("Me") .setIssuedAt(now) //set it diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy index d29162f7d..48c0bed16 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -175,9 +175,9 @@ class DefaultClaimsTest { } @Test - void testGetRequiredDateFromNull() { - Date date = claims.get("aDate", Date.class) - assertNull date + void testGetRequiredInstantFromNull() { + Instant instant = claims.get("anInstant", Instant.class) + assertNull instant } @Test @@ -215,15 +215,14 @@ class DefaultClaimsTest { @Test void testGetRequiredDateFromInvalidIso8601String() { - Date d = new Date() - String s = d.toString() - claims.put('aDate', s) + def s = "23-12-27T11:36:31Z" + claims.put('anInstant', s) try { - claims.get('aDate', Instant.class) + claims.get('anInstant', Instant.class) fail() } catch (IllegalArgumentException expected) { - String expectedMsg = "Cannot create Instant from 'aDate' value '$s'. Cause: " + + String expectedMsg = "Cannot create Instant from 'anInstant' value '$s'. Cause: " + "String value is not a JWT NumericDate, nor is it ISO-8601-formatted. All heuristics " + "exhausted. Cause: Text \'$s\' could not be parsed at index 0" assertEquals expectedMsg, expected.getMessage() diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy index 3e39f03fd..c350b53ed 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/FixedClockTest.groovy @@ -25,10 +25,10 @@ class FixedClockTest { def clock = new FixedClock() - def date1 = clock.now() + def instant1 = clock.now() Thread.sleep(100) - def date2 = clock.now() + def instant2 = clock.now() - assertSame date1, date2 + assertSame instant1, instant2 } } From a9403839608cdc5b8c60de05d739a8addb37cf92 Mon Sep 17 00:00:00 2001 From: Pieter Van Eeckhout Date: Wed, 11 Sep 2024 21:28:12 +0200 Subject: [PATCH 6/6] Update ClaimsMutator.java --- api/src/main/java/io/jsonwebtoken/ClaimsMutator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 45e2f4e46..fe47863ef 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -19,6 +19,7 @@ import java.time.Instant; import java.util.Collection; +import java.util.Date; /** * Mutation (modifications) to a {@link io.jsonwebtoken.Claims Claims} instance. @@ -127,7 +128,7 @@ public interface ClaimsMutator> { * {@link #expiration(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setExpiration(Instant exp); + T setExpiration(Date exp); /** * Sets the JWT @@ -155,7 +156,7 @@ public interface ClaimsMutator> { * {@link #notBefore(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setNotBefore(Instant nbf); + T setNotBefore(Date nbf); /** * Sets the JWT @@ -183,7 +184,7 @@ public interface ClaimsMutator> { * {@link #issuedAt(Instant)}. This method will be removed before the JJWT 1.0 release. */ @Deprecated - T setIssuedAt(Instant iat); + T setIssuedAt(Date iat); /** * Sets the JWT