- Ensures that Jackson duplicate property detection/rejection is enabled by default. (#895)

Fixes #877
This commit is contained in:
lhazlewood 2024-01-17 16:45:30 -08:00 committed by GitHub
parent d878404434
commit 86e06559bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 85 additions and 9 deletions

View File

@ -72,14 +72,8 @@ public class JacksonDeserializer<T> extends AbstractDeserializer<T> {
* @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
*/
public JacksonDeserializer(Map<String, Class<?>> 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 class JacksonDeserializer<T> extends AbstractDeserializer<T> {
this(objectMapper, (Class<T>) 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:
* <pre>{@code
* {
* "issuer": "https://issuer.example.com",
* "user": {
* "firstName": "Jill",
* "lastName": "Coder"
* }
* }}</pre>
* 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}.
* <p>
* 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}.
* <p>
* 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<String, Class<?>> 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<T> returnType) {
Assert.notNull(objectMapper, "ObjectMapper cannot be null.");
Assert.notNull(returnType, "Return type cannot be null.");

View File

@ -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<T> extends AbstractSerializer<T> {
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;

View File

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