Browse Source

Add Jackson Support for Saml2 Module

Closes gh-10905
Ulrich Grave 3 years ago
parent
commit
df84826c95
21 changed files with 1251 additions and 0 deletions
  1. 2 0
      core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java
  2. 6 0
      core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java
  3. 54 0
      core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java
  4. 48 0
      core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java
  5. 53 0
      core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java
  6. 3 0
      saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle
  7. 66 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalDeserializer.java
  8. 46 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java
  9. 50 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/JsonNodeUtils.java
  10. 71 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationDeserializer.java
  11. 46 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java
  12. 56 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java
  13. 56 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java
  14. 53 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java
  15. 54 0
      saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java
  16. 105 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java
  17. 64 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java
  18. 73 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java
  19. 61 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java
  20. 64 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java
  21. 220 0
      saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java

+ 2 - 0
core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java

@@ -64,6 +64,8 @@ public class CoreJackson2Module extends SimpleModule {
 				UnmodifiableSetMixin.class);
 		context.setMixInAnnotations(Collections.<Object>unmodifiableList(Collections.emptyList()).getClass(),
 				UnmodifiableListMixin.class);
+		context.setMixInAnnotations(Collections.<Object, Object>unmodifiableMap(Collections.emptyMap()).getClass(),
+				UnmodifiableMapMixin.class);
 		context.setMixInAnnotations(User.class, UserMixin.class);
 		context.setMixInAnnotations(UsernamePasswordAuthenticationToken.class,
 				UsernamePasswordAuthenticationTokenMixin.class);

+ 6 - 0
core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java

@@ -63,6 +63,7 @@ import org.springframework.util.ClassUtils;
  *     mapper.registerModule(new WebServletJackson2Module());
  *     mapper.registerModule(new WebServerJackson2Module());
  *     mapper.registerModule(new OAuth2ClientJackson2Module());
+ *     mapper.registerModule(new Saml2Jackson2Module());
  * </pre>
  *
  * @author Jitendra Singh.
@@ -86,6 +87,8 @@ public final class SecurityJackson2Modules {
 
 	private static final String ldapJackson2ModuleClass = "org.springframework.security.ldap.jackson2.LdapJackson2Module";
 
+	private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module";
+
 	private SecurityJackson2Modules() {
 	}
 
@@ -134,6 +137,9 @@ public final class SecurityJackson2Modules {
 		if (ClassUtils.isPresent(ldapJackson2ModuleClass, loader)) {
 			addToModulesList(loader, modules, ldapJackson2ModuleClass);
 		}
+		if (ClassUtils.isPresent(saml2Jackson2ModuleClass, loader)) {
+			addToModulesList(loader, modules, saml2Jackson2ModuleClass);
+		}
 		return modules;
 	}
 

+ 54 - 0
core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.jackson2;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Custom deserializer for {@link UnmodifiableMapMixin}.
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see UnmodifiableMapMixin
+ */
+class UnmodifiableMapDeserializer extends JsonDeserializer<Map<?, ?>> {
+
+	@Override
+	public Map<?, ?> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode node = mapper.readTree(jp);
+
+		Map<String, Object> result = new LinkedHashMap<>();
+		if (node != null && node.isObject()) {
+			Iterable<Map.Entry<String, JsonNode>> fields = node::fields;
+			for (Map.Entry<String, JsonNode> field : fields) {
+				result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class));
+			}
+		}
+		return Collections.unmodifiableMap(result);
+	}
+
+}

+ 48 - 0
core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.jackson2;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+/**
+ * This mixin class used to deserialize java.util.Collections$UnmodifiableMap and used
+ * with various AuthenticationToken implementation's mixin classes.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see UnmodifiableMapDeserializer
+ * @see CoreJackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonDeserialize(using = UnmodifiableMapDeserializer.class)
+class UnmodifiableMapMixin {
+
+	@JsonCreator
+	UnmodifiableMapMixin(Map<?, ?> map) {
+	}
+
+}

+ 53 - 0
core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.jackson2;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UnmodifiableMapDeserializerTests extends AbstractMixinTests {
+
+	// @formatter:off
+	private static final String DEFAULT_MAP_JSON = "{"
+			+ "\"@class\": \"java.util.Collections$UnmodifiableMap\","
+			+ "\"Key\": \"Value\""
+			+ "}";
+	// @formatter:on
+
+	@Test
+	void shouldSerialize() throws Exception {
+		String mapJson = mapper
+				.writeValueAsString(Collections.unmodifiableMap(Collections.singletonMap("Key", "Value")));
+
+		JSONAssert.assertEquals(DEFAULT_MAP_JSON, mapJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		Map<String, String> map = mapper.readValue(DEFAULT_MAP_JSON,
+				Collections.unmodifiableMap(Collections.emptyMap()).getClass());
+
+		assertThat(map).isNotNull().isInstanceOf(Collections.unmodifiableMap(Collections.emptyMap()).getClass())
+				.containsAllEntriesOf(Map.of("Key", "Value"));
+	}
+
+}

+ 3 - 0
saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle

@@ -52,8 +52,11 @@ dependencies {
 
 	provided 'jakarta.servlet:jakarta.servlet-api'
 
+	optional 'com.fasterxml.jackson.core:jackson-databind'
+
 	testImplementation 'com.squareup.okhttp3:mockwebserver'
 	testImplementation "org.assertj:assertj-core"
+	testImplementation "org.skyscreamer:jsonassert"
 	testImplementation "org.junit.jupiter:junit-jupiter-api"
 	testImplementation "org.junit.jupiter:junit-jupiter-params"
 	testImplementation "org.junit.jupiter:junit-jupiter-engine"

+ 66 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalDeserializer.java

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+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;
+
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+
+/**
+ * Custom deserializer for {@link DefaultSaml2AuthenticatedPrincipal}.
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see DefaultSaml2AuthenticatedPrincipalMixin
+ */
+class DefaultSaml2AuthenticatedPrincipalDeserializer extends JsonDeserializer<DefaultSaml2AuthenticatedPrincipal> {
+
+	private static final TypeReference<List<String>> SESSION_INDICES_LIST = new TypeReference<List<String>>() {
+	};
+
+	private static final TypeReference<Map<String, List<Object>>> ATTRIBUTES_MAP = new TypeReference<Map<String, List<Object>>>() {
+	};
+
+	@Override
+	public DefaultSaml2AuthenticatedPrincipal deserialize(JsonParser jp, DeserializationContext ctxt)
+			throws IOException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+
+		String name = JsonNodeUtils.findStringValue(jsonNode, "name");
+		Map<String, List<Object>> attributes = JsonNodeUtils.findValue(jsonNode, "attributes", ATTRIBUTES_MAP, mapper);
+		List<String> sessionIndexes = JsonNodeUtils.findValue(jsonNode, "sessionIndexes", SESSION_INDICES_LIST, mapper);
+		String registrationId = JsonNodeUtils.findStringValue(jsonNode, "registrationId");
+
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(name, attributes,
+				sessionIndexes);
+		if (registrationId != null) {
+			principal.setRelyingPartyRegistrationId(registrationId);
+		}
+		return principal;
+	}
+
+}

+ 46 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize
+ * {@link DefaultSaml2AuthenticatedPrincipal}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see DefaultSaml2AuthenticatedPrincipalDeserializer
+ * @see Saml2Jackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonDeserialize(using = DefaultSaml2AuthenticatedPrincipalDeserializer.class)
+class DefaultSaml2AuthenticatedPrincipalMixin {
+
+}

+ 50 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/JsonNodeUtils.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.MissingNode;
+
+final class JsonNodeUtils {
+
+	private JsonNodeUtils() {
+	}
+
+	static String findStringValue(JsonNode jsonNode, String fieldName) {
+		if (jsonNode == null) {
+			return null;
+		}
+		JsonNode value = jsonNode.findValue(fieldName);
+		return (value != null && value.isTextual()) ? value.asText() : null;
+	}
+
+	static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
+			ObjectMapper mapper) {
+		if (jsonNode == null) {
+			return null;
+		}
+		JsonNode value = jsonNode.findValue(fieldName);
+		return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
+	}
+
+	static JsonNode readJsonNode(JsonNode jsonNode, String field) {
+		return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
+	}
+
+}

+ 71 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationDeserializer.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import java.io.IOException;
+import java.util.List;
+
+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;
+
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+
+/**
+ * Custom deserializer for {@link Saml2Authentication}.
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see Saml2AuthenticationMixin
+ */
+class Saml2AuthenticationDeserializer extends JsonDeserializer<Saml2Authentication> {
+
+	private static final TypeReference<List<GrantedAuthority>> GRANTED_AUTHORITY_LIST = new TypeReference<List<GrantedAuthority>>() {
+	};
+
+	private static final TypeReference<Object> OBJECT = new TypeReference<Object>() {
+	};
+
+	@Override
+	public Saml2Authentication deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+		ObjectMapper mapper = (ObjectMapper) jp.getCodec();
+		JsonNode jsonNode = mapper.readTree(jp);
+
+		boolean authenticated = JsonNodeUtils.readJsonNode(jsonNode, "authenticated").asBoolean();
+		JsonNode principalNode = JsonNodeUtils.readJsonNode(jsonNode, "principal");
+		AuthenticatedPrincipal principal = getPrincipal(mapper, principalNode);
+		String saml2Response = JsonNodeUtils.findStringValue(jsonNode, "saml2Response");
+		List<GrantedAuthority> authorities = JsonNodeUtils.findValue(jsonNode, "authorities", GRANTED_AUTHORITY_LIST,
+				mapper);
+		Object details = JsonNodeUtils.findValue(jsonNode, "details", OBJECT, mapper);
+
+		Saml2Authentication authentication = new Saml2Authentication(principal, saml2Response, authorities);
+		authentication.setAuthenticated(authenticated);
+		authentication.setDetails(details);
+		return authentication;
+	}
+
+	private AuthenticatedPrincipal getPrincipal(ObjectMapper mapper, JsonNode principalNode) throws IOException {
+		return mapper.readValue(principalNode.traverse(mapper), AuthenticatedPrincipal.class);
+	}
+
+}

+ 46 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize {@link Saml2Authentication}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see Saml2AuthenticationDeserializer
+ * @see Saml2Jackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
+		isGetterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonDeserialize(using = Saml2AuthenticationDeserializer.class)
+class Saml2AuthenticationMixin {
+
+}

+ 56 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+
+/**
+ * Jackson module for saml2-service-provider. This module register
+ * {@link Saml2AuthenticationMixin}, {@link DefaultSaml2AuthenticatedPrincipalMixin},
+ * {@link Saml2LogoutRequestMixin}, {@link Saml2RedirectAuthenticationRequestMixin} and
+ * {@link Saml2PostAuthenticationRequestMixin}.
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see SecurityJackson2Modules
+ */
+public class Saml2Jackson2Module extends SimpleModule {
+
+	public Saml2Jackson2Module() {
+		super(Saml2Jackson2Module.class.getName(), new Version(1, 0, 0, null, null, null));
+	}
+
+	@Override
+	public void setupModule(SetupContext context) {
+		context.setMixInAnnotations(Saml2Authentication.class, Saml2AuthenticationMixin.class);
+		context.setMixInAnnotations(DefaultSaml2AuthenticatedPrincipal.class,
+				DefaultSaml2AuthenticatedPrincipalMixin.class);
+		context.setMixInAnnotations(Saml2LogoutRequest.class, Saml2LogoutRequestMixin.class);
+		context.setMixInAnnotations(Saml2RedirectAuthenticationRequest.class,
+				Saml2RedirectAuthenticationRequestMixin.class);
+		context.setMixInAnnotations(Saml2PostAuthenticationRequest.class, Saml2PostAuthenticationRequestMixin.class);
+	}
+
+}

+ 56 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize {@link Saml2LogoutRequest}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see Saml2Jackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+class Saml2LogoutRequestMixin {
+
+	@JsonCreator
+	Saml2LogoutRequestMixin(@JsonProperty("location") String location,
+			@JsonProperty("relayState") Saml2MessageBinding relayState,
+			@JsonProperty("parameters") Map<String, String> parameters, @JsonProperty("id") String id,
+			@JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) {
+	}
+
+}

+ 53 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize
+ * {@link Saml2PostAuthenticationRequest}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see Saml2Jackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+class Saml2PostAuthenticationRequestMixin {
+
+	@JsonCreator
+	Saml2PostAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest,
+			@JsonProperty("relayState") String relayState,
+			@JsonProperty("authenticationRequestUri") String authenticationRequestUri) {
+	}
+
+}

+ 54 - 0
saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
+
+/**
+ * Jackson Mixin class helps in serialize/deserialize
+ * {@link Saml2RedirectAuthenticationRequest}.
+ *
+ * <pre>
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * </pre>
+ *
+ * @author Ulrich Grave
+ * @since 5.7
+ * @see Saml2Jackson2Module
+ * @see SecurityJackson2Modules
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
+@JsonIgnoreProperties(ignoreUnknown = true)
+class Saml2RedirectAuthenticationRequestMixin {
+
+	@JsonCreator
+	Saml2RedirectAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest,
+			@JsonProperty("sigAlg") String sigAlg, @JsonProperty("signature") String signature,
+			@JsonProperty("relayState") String relayState,
+			@JsonProperty("authenticationRequestUri") String authenticationRequestUri) {
+	}
+
+}

+ 105 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DefaultSaml2AuthenticatedPrincipalMixinTests {
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	void setUp() {
+		this.mapper = new ObjectMapper();
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	void shouldSerialize() throws Exception {
+		DefaultSaml2AuthenticatedPrincipal principal = TestSaml2JsonPayloads.createDefaultPrincipal();
+
+		String principalJson = this.mapper.writeValueAsString(principal);
+
+		JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, principalJson, true);
+	}
+
+	@Test
+	void shouldSerializeWithoutRegistrationId() throws Exception {
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(
+				TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES,
+				TestSaml2JsonPayloads.SESSION_INDEXES);
+
+		String principalJson = this.mapper.writeValueAsString(principal);
+
+		JSONAssert.assertEquals(principalWithoutRegId(), principalJson, true);
+	}
+
+	@Test
+	void shouldSerializeWithoutIndices() throws Exception {
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(
+				TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES);
+		principal.setRelyingPartyRegistrationId(TestSaml2JsonPayloads.REG_ID);
+
+		String principalJson = this.mapper.writeValueAsString(principal);
+
+		JSONAssert.assertEquals(principalWithoutIndices(), principalJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue(
+				TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, DefaultSaml2AuthenticatedPrincipal.class);
+
+		assertThat(principal).isNotNull();
+		assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME);
+		assertThat(principal.getRelyingPartyRegistrationId()).isEqualTo(TestSaml2JsonPayloads.REG_ID);
+		assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES);
+		assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES);
+	}
+
+	@Test
+	void shouldDeserializeWithoutRegistrationId() throws Exception {
+		DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue(principalWithoutRegId(),
+				DefaultSaml2AuthenticatedPrincipal.class);
+
+		assertThat(principal).isNotNull();
+		assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME);
+		assertThat(principal.getRelyingPartyRegistrationId()).isNull();
+		assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES);
+		assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES);
+	}
+
+	private static String principalWithoutRegId() {
+		return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON.replace(TestSaml2JsonPayloads.REG_ID_JSON,
+				"null");
+	}
+
+	private static String principalWithoutIndices() {
+		return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON
+				.replace(TestSaml2JsonPayloads.SESSION_INDEXES_JSON, "[\"java.util.Collections$EmptyList\", []]");
+	}
+
+}

+ 64 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Saml2AuthenticationMixinTests {
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	void setUp() {
+		this.mapper = new ObjectMapper();
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	void shouldSerialize() throws Exception {
+		Saml2Authentication authentication = TestSaml2JsonPayloads.createDefaultAuthentication();
+
+		String authenticationJson = this.mapper.writeValueAsString(authentication);
+
+		JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, authenticationJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		Saml2Authentication authentication = this.mapper
+				.readValue(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, Saml2Authentication.class);
+
+		assertThat(authentication).isNotNull();
+		assertThat(authentication.getDetails()).isEqualTo(TestSaml2JsonPayloads.DETAILS);
+		assertThat(authentication.getCredentials()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE);
+		assertThat(authentication.getSaml2Response()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE);
+		assertThat(authentication.getAuthorities()).isEqualTo(TestSaml2JsonPayloads.AUTHORITIES);
+		assertThat(authentication.getPrincipal()).usingRecursiveComparison()
+				.isEqualTo(TestSaml2JsonPayloads.createDefaultPrincipal());
+		assertThat(authentication.getDetails()).usingRecursiveComparison().isEqualTo(TestSaml2JsonPayloads.DETAILS);
+	}
+
+}

+ 73 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Saml2LogoutRequestMixinTests {
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	void setUp() {
+		this.mapper = new ObjectMapper();
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	void shouldSerialize() throws Exception {
+		Saml2LogoutRequest request = TestSaml2JsonPayloads.createDefaultSaml2LogoutRequest();
+
+		String requestJson = this.mapper.writeValueAsString(request);
+
+		JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON, requestJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		Saml2LogoutRequest logoutRequest = this.mapper.readValue(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON,
+				Saml2LogoutRequest.class);
+
+		assertThat(logoutRequest).isNotNull();
+		assertThat(logoutRequest.getId()).isEqualTo(TestSaml2JsonPayloads.ID);
+		assertThat(logoutRequest.getRelyingPartyRegistrationId())
+				.isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID);
+		assertThat(logoutRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST);
+		assertThat(logoutRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE);
+		assertThat(logoutRequest.getLocation()).isEqualTo(TestSaml2JsonPayloads.LOCATION);
+		assertThat(logoutRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT);
+		Map<String, String> expectedParams = new HashMap<>();
+		expectedParams.put("SAMLRequest", TestSaml2JsonPayloads.SAML_REQUEST);
+		expectedParams.put("RelayState", TestSaml2JsonPayloads.RELAY_STATE);
+		expectedParams.put("AdditionalParam", TestSaml2JsonPayloads.ADDITIONAL_PARAM);
+		assertThat(logoutRequest.getParameters()).containsAllEntriesOf(expectedParams);
+	}
+
+}

+ 61 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Saml2PostAuthenticationRequestMixinTests {
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	void setUp() {
+		this.mapper = new ObjectMapper();
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	void shouldSerialize() throws Exception {
+		Saml2PostAuthenticationRequest request = TestSaml2JsonPayloads.createDefaultSaml2PostAuthenticationRequest();
+
+		String requestJson = this.mapper.writeValueAsString(request);
+
+		JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, requestJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		Saml2PostAuthenticationRequest authRequest = this.mapper
+				.readValue(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, Saml2PostAuthenticationRequest.class);
+
+		assertThat(authRequest).isNotNull();
+		assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST);
+		assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE);
+		assertThat(authRequest.getAuthenticationRequestUri())
+				.isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI);
+	}
+
+}

+ 64 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import org.springframework.security.jackson2.SecurityJackson2Modules;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class Saml2RedirectAuthenticationRequestMixinTests {
+
+	private ObjectMapper mapper;
+
+	@BeforeEach
+	void setUp() {
+		this.mapper = new ObjectMapper();
+		ClassLoader loader = getClass().getClassLoader();
+		this.mapper.registerModules(SecurityJackson2Modules.getModules(loader));
+	}
+
+	@Test
+	void shouldSerialize() throws Exception {
+		Saml2RedirectAuthenticationRequest request = TestSaml2JsonPayloads
+				.createDefaultSaml2RedirectAuthenticationRequest();
+
+		String requestJson = this.mapper.writeValueAsString(request);
+
+		JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, requestJson, true);
+	}
+
+	@Test
+	void shouldDeserialize() throws Exception {
+		Saml2RedirectAuthenticationRequest authRequest = this.mapper.readValue(
+				TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, Saml2RedirectAuthenticationRequest.class);
+
+		assertThat(authRequest).isNotNull();
+		assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST);
+		assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE);
+		assertThat(authRequest.getAuthenticationRequestUri())
+				.isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI);
+		assertThat(authRequest.getSigAlg()).isEqualTo(TestSaml2JsonPayloads.SIG_ALG);
+		assertThat(authRequest.getSignature()).isEqualTo(TestSaml2JsonPayloads.SIGNATURE);
+	}
+
+}

+ 220 - 0
saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java

@@ -0,0 +1,220 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * 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
+ *
+ *      https://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 org.springframework.security.saml2.jackson2;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal;
+import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
+import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest;
+import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest;
+import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest;
+import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
+import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations;
+
+final class TestSaml2JsonPayloads {
+
+	private TestSaml2JsonPayloads() {
+	}
+
+	static final Map<String, List<Object>> ATTRIBUTES;
+
+	static {
+		Map<String, List<Object>> tmpAttributes = new HashMap<>();
+		tmpAttributes.put("name", Collections.singletonList("attr_name"));
+		tmpAttributes.put("email", Collections.singletonList("attr_email"));
+		tmpAttributes.put("listOf", Collections.unmodifiableList(Arrays.asList("Element1", "Element2", 4, true)));
+		ATTRIBUTES = Collections.unmodifiableMap(tmpAttributes);
+	}
+
+	static final String REG_ID = "REG_ID_TEST";
+	static final String REG_ID_JSON = "\"" + REG_ID + "\"";
+
+	static final String SESSION_INDEXES_JSON = "[" + "  \"java.util.Collections$UnmodifiableRandomAccessList\","
+			+ "  [ \"Index 1\", \"Index 2\" ]" + "]";
+	static final List<String> SESSION_INDEXES = Collections.unmodifiableList(Arrays.asList("Index 1", "Index 2"));
+
+	static final String PRINCIPAL_NAME = "principalName";
+
+	// @formatter:off
+	static final String DEFAULT_AUTHENTICATED_PRINCIPAL_JSON = "{"
+			+ "  \"@class\": \"org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal\","
+			+ "  \"name\": \"" + PRINCIPAL_NAME + "\","
+			+ "  \"attributes\": {"
+			+ "    \"@class\": \"java.util.Collections$UnmodifiableMap\","
+			+ "    \"listOf\": ["
+			+ "      \"java.util.Collections$UnmodifiableRandomAccessList\","
+			+ "      [ \"Element1\", \"Element2\", 4, true ]"
+			+ "    ],"
+			+ "    \"email\": ["
+			+ "      \"java.util.Collections$SingletonList\","
+			+ "      [ \"attr_email\" ]"
+			+ "    ],"
+			+ "    \"name\": ["
+			+ "      \"java.util.Collections$SingletonList\","
+			+ "      [ \"attr_name\" ]"
+			+ "    ]"
+			+ "  },"
+			+ "  \"sessionIndexes\": " + SESSION_INDEXES_JSON + ","
+			+ "  \"registrationId\": " + REG_ID_JSON + ""
+			+ "}";
+	// @formatter:on
+
+	static DefaultSaml2AuthenticatedPrincipal createDefaultPrincipal() {
+		DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(PRINCIPAL_NAME,
+				ATTRIBUTES, SESSION_INDEXES);
+		principal.setRelyingPartyRegistrationId(REG_ID);
+		return principal;
+	}
+
+	static final String SAML_REQUEST = "samlRequestValue";
+	static final String RELAY_STATE = "relayStateValue";
+	static final String AUTHENTICATION_REQUEST_URI = "authenticationRequestUriValue";
+	static final String SIG_ALG = "sigAlgValue";
+	static final String SIGNATURE = "signatureValue";
+
+	// @formatter:off
+	static final String DEFAULT_REDIRECT_AUTH_REQUEST_JSON = "{"
+			+ " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest\","
+			+ " \"samlRequest\": \"" + SAML_REQUEST + "\","
+			+ " \"relayState\": \"" + RELAY_STATE + "\","
+			+ " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\","
+			+ " \"sigAlg\": \"" + SIG_ALG + "\","
+			+ " \"signature\": \"" + SIGNATURE + "\""
+			+ "}";
+	// @formatter:on
+
+	// @formatter:off
+	static final String DEFAULT_POST_AUTH_REQUEST_JSON = "{"
+			+ " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest\","
+			+ " \"samlRequest\": \"" + SAML_REQUEST + "\","
+			+ " \"relayState\": \"" + RELAY_STATE + "\","
+			+ " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\""
+			+ "}";
+	// @formatter:on
+
+	static final String ID = "idValue";
+	static final String LOCATION = "locationValue";
+	static final String BINDNG = "REDIRECT";
+	static final String RELYINGPARTY_REGISTRATION_ID = "registrationIdValue";
+	static final String ADDITIONAL_PARAM = "additionalParamValue";
+
+	// @formatter:off
+	static final String DEFAULT_LOGOUT_REQUEST_JSON = "{"
+			+ "  \"@class\": \"org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest\","
+			+ "  \"id\": \"" + ID + "\","
+			+ "  \"location\": \"" + LOCATION + "\","
+			+ "  \"binding\": \"" + BINDNG + "\","
+			+ "  \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\","
+			+ "  \"parameters\": { "
+			+ "     \"@class\": \"java.util.Collections$UnmodifiableMap\","
+			+ "     \"SAMLRequest\": \"" + SAML_REQUEST + "\","
+			+ "     \"RelayState\": \"" + RELAY_STATE + "\","
+			+ "     \"AdditionalParam\": \"" + ADDITIONAL_PARAM + "\""
+			+ "  }"
+			+ "}";
+	// @formatter:on
+
+	static Saml2PostAuthenticationRequest createDefaultSaml2PostAuthenticationRequest() {
+		return Saml2PostAuthenticationRequest.withRelyingPartyRegistration(TestRelyingPartyRegistrations.full()
+				.assertingPartyDetails((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI))
+				.build()).samlRequest(SAML_REQUEST).relayState(RELAY_STATE).build();
+	}
+
+	static Saml2RedirectAuthenticationRequest createDefaultSaml2RedirectAuthenticationRequest() {
+		return Saml2RedirectAuthenticationRequest
+				.withRelyingPartyRegistration(TestRelyingPartyRegistrations.full()
+						.assertingPartyDetails((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI))
+						.build())
+				.samlRequest(SAML_REQUEST).relayState(RELAY_STATE).sigAlg(SIG_ALG).signature(SIGNATURE).build();
+	}
+
+	static Saml2LogoutRequest createDefaultSaml2LogoutRequest() {
+		return Saml2LogoutRequest
+				.withRelyingPartyRegistration(
+						TestRelyingPartyRegistrations.full().registrationId(RELYINGPARTY_REGISTRATION_ID)
+								.assertingPartyDetails((party) -> party.singleLogoutServiceLocation(LOCATION)
+										.singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT))
+								.build())
+				.id(ID).samlRequest(SAML_REQUEST).relayState(RELAY_STATE)
+				.parameters((params) -> params.put("AdditionalParam", ADDITIONAL_PARAM)).build();
+	}
+
+	static final Collection<GrantedAuthority> AUTHORITIES = Collections
+			.unmodifiableList(Arrays.asList(new SimpleGrantedAuthority("Role1"), new SimpleGrantedAuthority("Role2")));
+
+	static final Object DETAILS = User.withUsername("username").password("empty").authorities("A", "B").build();
+	static final String SAML_RESPONSE = "samlResponseValue";
+
+	// @formatter:off
+	static final String DEFAULT_SAML2AUTHENTICATION_JSON = "{"
+			+ "	\"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2Authentication\","
+			+ "	\"authorities\": ["
+			+ "		\"java.util.Collections$UnmodifiableRandomAccessList\","
+			+ "		["
+			+ "			{"
+			+ "				\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\","
+			+ "				\"authority\": \"Role1\""
+			+ "			},"
+			+ "			{"
+			+ "				\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\","
+			+ "				\"authority\": \"Role2\""
+			+ "			}"
+			+ "		]"
+			+ " ],"
+			+ "	\"details\": {"
+			+ "		\"@class\": \"org.springframework.security.core.userdetails.User\","
+			+ "		\"password\": \"empty\","
+			+ "		\"username\": \"username\","
+			+ "		\"authorities\": ["
+			+ "			\"java.util.Collections$UnmodifiableSet\", ["
+			+ "				{"
+			+ "					\"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\","
+			+ "					\"authority\":\"A\""
+			+ "				},"
+			+ "				{"
+			+ "					\"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\","
+			+ "					\"authority\":\"B\""
+			+ "				}"
+			+ "		]],"
+			+ "		\"accountNonExpired\": true,"
+			+ "		\"accountNonLocked\": true,"
+			+ "		\"credentialsNonExpired\": true,"
+			+ "		\"enabled\": true"
+			+ "	},"
+			+ "	\"authenticated\": true,"
+			+ "	\"principal\": " + DEFAULT_AUTHENTICATED_PRINCIPAL_JSON + ","
+			+ "	\"saml2Response\": \"" + SAML_RESPONSE + "\""
+			+ "}";
+	// @formatter:on
+
+	static Saml2Authentication createDefaultAuthentication() {
+		DefaultSaml2AuthenticatedPrincipal principal = createDefaultPrincipal();
+		Saml2Authentication authentication = new Saml2Authentication(principal, SAML_RESPONSE, AUTHORITIES);
+		authentication.setDetails(DETAILS);
+		return authentication;
+	}
+
+}