diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8fb137a..096b090f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -732,7 +732,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.adoc b/README.adoc index a6ddf003c..546796c3c 100644 --- a/README.adoc +++ b/README.adoc @@ -1063,9 +1063,9 @@ String jws = Jwts.builder() .issuer("me") .subject("Bob") .audience().add("you").and() - .expiration(expiration) //a java.util.Date - .notBefore(notBefore) //a java.util.Date - .issuedAt(new Date()) // for example, now + .expiration(expiration) //a java.time.Instant + .notBefore(notBefore) //a java.time.Instant + .issuedAt(Instant.now) // for example, now .id(UUID.randomUUID().toString()) //just an example id /// ... etc ... @@ -1530,7 +1530,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. @@ -3406,7 +3406,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: @@ -3634,7 +3634,7 @@ last char) are significant. The last two bits are not decoded. Thus all of: dGVzdCBzdHJpbmo dGVzdCBzdHJpbmp dGVzdCBzdHJpbmq -dGVzdCBzdHJpbmr +dGVzdCBzdHJpbmr ---- All decode to the same 11 bytes (116, 101, 115, 116, 32, 115, 116, 114, 105, 110, 106). ==== diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index 4f8589fa3..b6e528696 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, 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/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 1fdca1e43..fe47863ef 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -17,6 +17,7 @@ import io.jsonwebtoken.lang.NestedCollection; +import java.time.Instant; import java.util.Collection; import java.util.Date; @@ -124,7 +125,7 @@ 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); @@ -140,7 +141,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 @@ -152,7 +153,7 @@ 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); @@ -168,7 +169,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 @@ -180,7 +181,7 @@ 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); @@ -196,7 +197,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 79936695a..84f5f028c 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; /** @@ -195,7 +195,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 @@ -207,7 +207,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 @@ -219,7 +219,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 @@ -236,7 +236,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. @@ -248,7 +248,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 6a3b501b2..82bd2642d 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 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(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 instant 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 - * 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 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..f5dabe07d 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 parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.UTC).toInstant() + assertEquals expectedInstant, parsedInstant + } + + @Test + void testParseIso8601DateWithMillisNonZuluOffset() { + String dateString = "2023-12-25T15:30:00.123-01:00" + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 123000000, ZoneOffset.ofHours(-1)).toInstant() + assertEquals expectedInstant, parsedInstant + } - def iso8601 = DateFormats.ISO_8601.get() - def iso8601Millis = DateFormats.ISO_8601_MILLIS.get() + @Test + void testParseIso8601DateWithoutMillisZuluOffset() { + String dateString = "2023-12-25T15:30:00Z" + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) + final expectedInstant = OffsetDateTime.of(2023, 12, 25, 15, 30, 0, 0, ZoneOffset.UTC).toInstant() + assertEquals expectedInstant, parsedInstant + } - assertTrue iso8601 instanceof SimpleDateFormat - assertTrue iso8601Millis instanceof SimpleDateFormat + @Test + void testParseIso8601DateWithoutMillisNonZuluOffset() { + String dateString = "2023-12-25T15:30:00+01:00" + Instant parsedInstant = DateFormats.parseIso8601Date(dateString) + assertNotNull(parsedInstant) + assertEquals OffsetDateTime.of(2023,12,25,15,30,0, 0, ZoneOffset.ofHours(1)).toInstant(), parsedInstant + } - 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 469789f61..4e39f20a2 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 a00541b61..4748908f5 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 @@ -22,6 +22,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; @@ -60,6 +61,7 @@ public class JacksonSerializer extends AbstractSerializer { static ObjectMapper newObjectMapper() { return new ObjectMapper() .registerModule(MODULE) + .registerModule(new JavaTimeModule()) .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true) // https://github.com/jwtk/jjwt/issues/877 .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // https://github.com/jwtk/jjwt/issues/893 } 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 b21667441..d415c0363 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 @@ -26,6 +26,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 { @@ -100,7 +102,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) @@ -108,7 +110,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/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..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 @@ -30,9 +30,8 @@ import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; -import java.util.Calendar; +import java.time.Instant; import java.util.Collection; -import java.util.Date; import java.util.Map; /** @@ -93,13 +92,8 @@ private Object toJSONInstance(Object object) throws IOException { return object; } - if (object instanceof Calendar) { - object = ((Calendar) object).getTime(); //sets object to date, will be converted in next if-statement: - } - - if (object instanceof Date) { - Date date = (Date) object; - return DateFormats.formatIso8601(date); + if (object instanceof Instant) { + return DateFormats.formatIso8601((Instant) object); } 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..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 @@ -27,6 +27,8 @@ import org.json.JSONString import org.junit.Before import org.junit.Test +import java.time.Instant + import static org.junit.Assert.* class OrgJsonSerializerTest { @@ -191,18 +193,18 @@ class OrgJsonSerializerTest { } @Test - void testDate() { - Date now = new Date() - String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(now) + void testInstant() { + Instant instant = Instant.now() + String formatted = DateFormats.formatIso8601(instant) + assertEquals "\"$formatted\"" as String, ser(instant) } @Test - void testCalendar() { - def cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - def now = cal.getTime() + void testDate() { + Date date = new Date() + def now = date.toInstant() String formatted = DateFormats.formatIso8601(now) - assertEquals "\"$formatted\"" as String, ser(cal) + assertEquals "\"$formatted\"" as String, ser(date) } @Test diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 70a6a5a42..08a19f607 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, 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. " + "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 afa6a4804..ec20cbd80 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); @@ -668,12 +690,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 75faa419a..ce5059181 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; @@ -668,22 +669,18 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws //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: " + @@ -694,16 +691,14 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws // 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: " + @@ -737,12 +732,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 ddce12b22..eeed918a3 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 fd5c48330..2183ebccd 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.Map; import java.util.Set; @@ -141,32 +141,32 @@ protected void changed() { } @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..6f087e92c 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,38 +27,38 @@ */ 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 - * {@link #now now()} will always return this seed Date. + * 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 Instant. */ public FixedClock() { - this(new Date()); + this(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 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(Date now) { + public FixedClock(Instant now) { this.now = 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(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..bb85b8b3e 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/JwtDateConverter.java @@ -17,95 +17,91 @@ import io.jsonwebtoken.lang.DateFormats; -import java.text.ParseException; -import java.util.Calendar; -import java.util.Date; +import java.time.Instant; +import java.time.format.DateTimeParseException; -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; } 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) { // 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 instant 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. + * @param v the object value to represent as an Instant. + * @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 Date) { - return (Date) v; - } else if (v instanceof Calendar) { //since 0.10.0 - return ((Calendar) v).getTime(); + } 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 new Date(millis); + 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/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 ec98dc18b..0d80663b8 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() @@ -1460,71 +1460,71 @@ class JwtParserTest { } @Test - void testParseRequireCustomDate_Success() { + void testParseRequireCustomInstant_Success() { - def aDate = new Date(System.currentTimeMillis()) + 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", Date.class), aDate + assertEquals jwt.getPayload().get("anInstant", Instant.class), anInstant } @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() } } @Test - void testParseRequireCustomDate_Incorrect_Fail() { + void testParseRequireCustomInstant_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() ) } @@ -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,35 +1542,30 @@ 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() } } @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 5f6b82229..4837b3882 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -42,32 +42,25 @@ 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.* class JwtsTest { - private static Date dateWithOnlySecondPrecision(long millis) { - long seconds = (millis / 1000) as long - long secondOnlyPrecisionMillis = seconds * 1000 - return new Date(secondOnlyPrecisionMillis) - } - private static Date now() { - Date date = dateWithOnlySecondPrecision(System.currentTimeMillis()) - return date + 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 long later() { + def instant = laterInstant(10000L) + return instant.getEpochSecond(); } - private static Date laterDate(int seconds) { - def millis = seconds * 1000L - def time = System.currentTimeMillis() + millis - return dateWithOnlySecondPrecision(time) + private static Instant laterInstant(long seconds) { + return instantWithOnlySecondPrecision(Instant.now().plusSeconds(seconds)) } protected static String base64Url(String s) { @@ -155,7 +148,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() } } @@ -411,11 +404,11 @@ 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() - assertEquals then, claimedDate + def claimedInstant = claims.getExpiration() + assertEquals then, claimedInstant compact = Jwts.builder().setIssuer("Me") .setExpiration(then) //set it @@ -428,11 +421,11 @@ 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() - assertEquals now, claimedDate + def claimedInstant = claims.getNotBefore() + assertEquals now, claimedInstant compact = Jwts.builder().setIssuer("Me") .setNotBefore(now) //set it @@ -445,11 +438,11 @@ 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() - 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 ae987402d..48c0bed16 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultClaimsTest.groovy @@ -22,6 +22,9 @@ import io.jsonwebtoken.lang.DateFormats import org.junit.Before import org.junit.Test +import java.time.Instant +import java.time.temporal.ChronoUnit + import static org.junit.Assert.* class DefaultClaimsTest { @@ -172,65 +175,56 @@ class DefaultClaimsTest { } @Test - void testGetRequiredDateFromNull() { - Date date = claims.get("aDate", Date.class) - assertNull date - } - - @Test - void testGetRequiredDateFromDate() { - def expected = new Date() - claims.put("aDate", expected) - Date result = claims.get("aDate", Date.class) - assertEquals expected, result + void testGetRequiredInstantFromNull() { + Instant instant = claims.get("anInstant", Instant.class) + assertNull instant } @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) + void testGetRequiredDateFromInstant() { + def expected = Instant.now() + claims.put("anInstant", expected) + Instant result = claims.get("anInstant", 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 } @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', Date.class) + claims.get('anInstant', Instant.class) fail() } catch (IllegalArgumentException expected) { - String expectedMsg = "Cannot create Date 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: Unparseable date: \"$s\"" + "exhausted. Cause: Text \'$s\' could not be parsed at index 0" assertEquals expectedMsg, expected.getMessage() } } @@ -247,10 +241,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 +258,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 +274,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 +290,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) @@ -314,28 +304,25 @@ 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 - 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() + void testGetSpecDateWithInstant() { + Instant instant = Instant.now() + long seconds = instant.getEpochSecond() + 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() assertEquals seconds, claims.get(Claims.EXPIRATION) assertEquals seconds, claims.get(Claims.ISSUED_AT) assertEquals seconds, claims.get(Claims.NOT_BEFORE) } @Test - void testToSpecDateWithDate() { - long millis = System.currentTimeMillis() - Date d = new Date(millis) - claims.put('exp', d) - assertEquals d, claims.getExpiration() + void testToSpecDateWithInstant() { + Instant i = Instant.now() + claims.put('exp', i) + assertEquals i, claims.getExpiration() } void trySpecDateNonDate(Parameter param) { @@ -344,7 +331,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 +345,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()) } @@ -384,7 +371,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 } @@ -393,7 +380,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 } @@ -402,7 +389,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 } @@ -411,7 +398,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 } @@ -420,7 +407,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 } @@ -429,7 +416,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 } @@ -438,7 +425,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 } @@ -447,7 +434,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 } @@ -456,7 +443,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 } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index c874b8046..53733a3d2 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 4c41142c2..67a30f97a 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/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 } } 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 9827e61e6..aec615aaf 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