spring-security
spring-security copied to clipboard
Jackson serialization of `DefaultSaml2AuthenticatedPrincipal`: `LinkedMultiValueMap is not in the allowlist`
Describe the bug
Jackson serialization of DefaultSaml2AuthenticatedPrincipal
doesn't work anymore since Spring Boot 2.7.3.
An exception is thrown:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: The class with org.springframework.util.LinkedMultiValueMap and name of org.springframework.util.LinkedMultiValueMap is not in the allowlist.
To Reproduce
Setup a Jackson object mapper like that
val springSecurityObjectMapper: JsonMapper = jacksonMapperBuilder()
.addModules(SecurityJackson2Modules.getModules(Companion::class.java.classLoader))
Then try to use the mapper to serialize an Authentication
containing a DefaultSaml2AuthenticatedPrincipal
constructed by OpenSaml4AuthenticationProvider
.
Expected behavior
Serialization works.
Probable cause
I think this is the offending commit https://github.com/spring-projects/spring-security/commit/e092ec780f672992a84a9db1bb834fa369f472ea
I get the same problem after updating from spring security 5.7.2 to 5.7.3. Problem is the fixed already mention above. If i copy the attribute map and use a LinkedHashMap againt it works.
Note sure which could be the best fix. Either add the LinkedMultiValueMap to the SecurityJackson2Modules as mixing or copy the attribute Map in the DefaultSaml2AuthenticatedPrincipal constructor to a fix map implementation which is serializable as json (ex . LinkedHashMap)
My current workaround is to copy the principal and overide the attirbute map in a custom ResponseAuthenticationConverter:
Saml2Authentication authentication = ...
DefaultSaml2AuthenticatedPrincipal principal = (DefaultSaml2AuthenticatedPrincipal) authentication.getPrincipal();
DefaultSaml2AuthenticatedPrincipal copy = new DefaultSaml2AuthenticatedPrincipal(
principal.getName(),
new LinkedHashMap<>(principal.getAttributes()),
principal.getSessionIndexes()
);
authentication = new Saml2Authentication(
copy, authentication.getSaml2Response(), authentication.getAuthorities()
);
I ran into same issue post upgrading spring boot from 2.7.0 to 3.0.0. For now, I added a mix-in for LinkedMultiValueMap
as below -
@SuppressWarnings("serial")
public class CustomSaml2Jackson2Module extends SimpleModule {
public CustomSaml2Jackson2Module() {
super(CustomSaml2Jackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
}
@Override
public void setupModule(SetupContext context) {
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
context.setMixInAnnotations(LinkedMultiValueMap.class, LinkedMultiValueMapMixin.class);
}
}
Instead of extending SimpleModule, Saml2Jackson2Module can also be extended.
Mixin class -
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonDeserialize(using = LinkedMultiValueMapDeserializer.class)
public abstract class LinkedMultiValueMapMixin {
@JsonCreator
LinkedMultiValueMapMixin(Map<?, ?> map) {
}
}
Custom Deserializer -
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class LinkedMultiValueMapDeserializer extends JsonDeserializer<MultiValueMap<?, ?>> {
@Override
public MultiValueMap<?, ?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode mapNode = mapper.readTree(parser);
Map<String, List<Object>> result = new LinkedHashMap<>();
if (mapNode != null && mapNode.isObject()) {
Iterable<Map.Entry<String, JsonNode>> fields = mapNode::fields;
for (Map.Entry<String, JsonNode> field : fields) {
result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), new TypeReference<List<Object>>() {}));
}
}
return new LinkedMultiValueMap<>(result);
}
}
I then registered this and everything seems to work fine now. I am still running tests on LinkedMultiValueMapDeserializer
to check whether it is working as expected.