spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

Jackson serialization of `DefaultSaml2AuthenticatedPrincipal`: `LinkedMultiValueMap is not in the allowlist`

Open felixscheinost opened this issue 2 years ago • 1 comments

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

felixscheinost avatar Sep 06 '22 13:09 felixscheinost

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()
     );

ugrave avatar Sep 09 '22 09:09 ugrave

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.

akashgupta2703 avatar Dec 07 '22 19:12 akashgupta2703