diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index 8314f1ef3..afb152918 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -72,14 +72,8 @@ public JacksonDeserializer() { * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type */ public JacksonDeserializer(Map> claimTypeMap) { - // DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer - // between instances - this(new ObjectMapper()); - Assert.notNull(claimTypeMap, "Claim type map cannot be null."); - // register a new Deserializer - SimpleModule module = new SimpleModule(); - module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap))); - objectMapper.registerModule(module); + // DO NOT specify JacksonSerializer.DEFAULT_OBJECT_MAPPER here as that would modify the shared instance + this(JacksonSerializer.newObjectMapper(), claimTypeMap); } /** @@ -92,6 +86,46 @@ public JacksonDeserializer(ObjectMapper objectMapper) { this(objectMapper, (Class) Object.class); } + /** + * Creates a new JacksonDeserializer where the values of the claims can be parsed into given types by registering + * a type-converting {@link com.fasterxml.jackson.databind.Module Module} on the specified {@link ObjectMapper}. + * A common usage example is to parse custom User object out of a claim, for example the claims: + *
{@code
+     * {
+     *     "issuer": "https://issuer.example.com",
+     *     "user": {
+     *         "firstName": "Jill",
+     *         "lastName": "Coder"
+     *     }
+     * }}
+ * Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being + * transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}. + *

+ * Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this + * constructor modifies the specified {@code objectMapper} argument and customizes it to support the + * specified {@code claimTypeMap}. + *

+ * If you do not want your {@code ObjectMapper} instance modified, but also want to support custom types for + * JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering + * your custom types separately and then use the {@link #JacksonDeserializer(ObjectMapper)} constructor instead + * (which does not modify the {@code objectMapper} argument). + * + * @param objectMapper the objectMapper to modify by registering a custom type-converting + * {@link com.fasterxml.jackson.databind.Module Module} + * @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type + * @since 0.12.4 + */ + //TODO: Make this public on a minor release + // (cannot do that on a point release as that would violate semver) + private JacksonDeserializer(ObjectMapper objectMapper, Map> claimTypeMap) { + this(objectMapper); + Assert.notNull(claimTypeMap, "Claim type map cannot be null."); + // register a new Deserializer on the ObjectMapper instance: + SimpleModule module = new SimpleModule(); + module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap))); + objectMapper.registerModule(module); + } + private JacksonDeserializer(ObjectMapper objectMapper, Class returnType) { Assert.notNull(objectMapper, "ObjectMapper cannot be null."); Assert.notNull(returnType, "Return type cannot be null."); 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..2582da507 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 @@ -16,6 +16,7 @@ package io.jsonwebtoken.jackson.io; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; @@ -41,7 +42,22 @@ public class JacksonSerializer extends AbstractSerializer { MODULE = module; } - static final ObjectMapper DEFAULT_OBJECT_MAPPER = new ObjectMapper().registerModule(MODULE); + static final ObjectMapper DEFAULT_OBJECT_MAPPER = newObjectMapper(); + + /** + * Creates and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). + * + * @return and returns a new ObjectMapper with the {@code jjwt-jackson} module registered and + * {@code JsonParser.Feature.STRICT_DUPLICATE_DETECTION} enabled (set to true). + * @since 0.12.4 + */ + // package protected on purpose, do not expose to the public API + static ObjectMapper newObjectMapper() { + return new ObjectMapper() + .registerModule(MODULE) + .configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true); // https://github.com/jwtk/jjwt/issues/877 + } protected final ObjectMapper objectMapper; 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..62f253ec7 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 @@ -16,6 +16,7 @@ //file:noinspection GrDeprecatedAPIUsage package io.jsonwebtoken.jackson.io +import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer @@ -120,6 +121,31 @@ class JacksonDeserializerTest { assertEquals expected, result } + /** + * Asserts https://github.com/jwtk/jjwt/issues/877 + */ + @Test + void testStrictDuplicateDetection() { + // 'bKey' is repeated twice: + String json = """ + { + "aKey":"oneValue", + "bKey": 15, + "bKey": "hello" + } + """ + try { + new JacksonDeserializer<>().deserialize(new StringReader(json)) + fail() + } catch (DeserializationException expected) { + String causeMsg = "Duplicate field 'bKey'\n at [Source: (StringReader); line: 5, column: 23]" + String msg = "Unable to deserialize: $causeMsg" + assertEquals msg, expected.getMessage() + assertTrue expected.getCause() instanceof JsonParseException + assertEquals causeMsg, expected.getCause().getMessage() + } + } + /** * For: https://github.com/jwtk/jjwt/issues/564 */