From 2334610fa90783fab8400b5f84d4d9c81068cca0 Mon Sep 17 00:00:00 2001 From: Ulrich Grave Date: Thu, 24 Feb 2022 23:50:09 +0100 Subject: [PATCH] Add Jackson Support for Saml2 Module Closes gh-10905 --- .../security/jackson2/CoreJackson2Module.java | 2 + .../jackson2/SecurityJackson2Modules.java | 6 + .../jackson2/UnmodifiableMapDeserializer.java | 54 +++++ .../jackson2/UnmodifiableMapMixin.java | 48 ++++ .../UnmodifiableMapDeserializerTests.java | 53 +++++ ...ing-security-saml2-service-provider.gradle | 3 + ...ml2AuthenticatedPrincipalDeserializer.java | 66 ++++++ ...faultSaml2AuthenticatedPrincipalMixin.java | 46 ++++ .../saml2/jackson2/JsonNodeUtils.java | 50 ++++ .../Saml2AuthenticationDeserializer.java | 71 ++++++ .../jackson2/Saml2AuthenticationMixin.java | 46 ++++ .../saml2/jackson2/Saml2Jackson2Module.java | 56 +++++ .../jackson2/Saml2LogoutRequestMixin.java | 56 +++++ .../Saml2PostAuthenticationRequestMixin.java | 53 +++++ ...ml2RedirectAuthenticationRequestMixin.java | 54 +++++ ...Saml2AuthenticatedPrincipalMixinTests.java | 105 +++++++++ .../Saml2AuthenticationMixinTests.java | 64 +++++ .../Saml2LogoutRequestMixinTests.java | 73 ++++++ ...l2PostAuthenticationRequestMixinTests.java | 61 +++++ ...directAuthenticationRequestMixinTests.java | 64 +++++ .../saml2/jackson2/TestSaml2JsonPayloads.java | 220 ++++++++++++++++++ 21 files changed, 1251 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java create mode 100644 core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java create mode 100644 core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalDeserializer.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/JsonNodeUtils.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationDeserializer.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java diff --git a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java index 1aa8eb0213..3a91f0c136 100644 --- a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java +++ b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java @@ -64,6 +64,8 @@ public class CoreJackson2Module extends SimpleModule { UnmodifiableSetMixin.class); context.setMixInAnnotations(Collections.unmodifiableList(Collections.emptyList()).getClass(), UnmodifiableListMixin.class); + context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(), + UnmodifiableMapMixin.class); context.setMixInAnnotations(User.class, UserMixin.class); context.setMixInAnnotations(UsernamePasswordAuthenticationToken.class, UsernamePasswordAuthenticationTokenMixin.class); diff --git a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java b/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java index b049032cd2..15493a63b2 100644 --- a/core/src/main/java/org/springframework/security/jackson2/SecurityJackson2Modules.java +++ b/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()); * * * @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; } diff --git a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapDeserializer.java new file mode 100644 index 0000000000..a934565860 --- /dev/null +++ b/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> { + + @Override + public Map deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode node = mapper.readTree(jp); + + Map result = new LinkedHashMap<>(); + if (node != null && node.isObject()) { + Iterable> fields = node::fields; + for (Map.Entry field : fields) { + result.put(field.getKey(), mapper.readValue(field.getValue().traverse(mapper), Object.class)); + } + } + return Collections.unmodifiableMap(result); + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java b/core/src/main/java/org/springframework/security/jackson2/UnmodifiableMapMixin.java new file mode 100644 index 0000000000..802c6d1964 --- /dev/null +++ b/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. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * @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) { + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java b/core/src/test/java/org/springframework/security/jackson2/UnmodifiableMapDeserializerTests.java new file mode 100644 index 0000000000..066ac68bd6 --- /dev/null +++ b/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 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")); + } + +} diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index c4a893aa2d..631a202da5 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -60,8 +60,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" diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalDeserializer.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalDeserializer.java new file mode 100644 index 0000000000..7bc017a972 --- /dev/null +++ b/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 { + + private static final TypeReference> SESSION_INDICES_LIST = new TypeReference>() { + }; + + private static final TypeReference>> ATTRIBUTES_MAP = new TypeReference>>() { + }; + + @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> attributes = JsonNodeUtils.findValue(jsonNode, "attributes", ATTRIBUTES_MAP, mapper); + List 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; + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixin.java new file mode 100644 index 0000000000..80b04e5365 --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @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 { + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/JsonNodeUtils.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/JsonNodeUtils.java new file mode 100644 index 0000000000..e91377bfe6 --- /dev/null +++ b/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 findValue(JsonNode jsonNode, String fieldName, TypeReference 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(); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationDeserializer.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationDeserializer.java new file mode 100644 index 0000000000..6f82fb75e4 --- /dev/null +++ b/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 { + + private static final TypeReference> GRANTED_AUTHORITY_LIST = new TypeReference>() { + }; + + private static final TypeReference OBJECT = new TypeReference() { + }; + + @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 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); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixin.java new file mode 100644 index 0000000000..b74c618d9c --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @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 { + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2Jackson2Module.java new file mode 100644 index 0000000000..0febccb626 --- /dev/null +++ b/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); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixin.java new file mode 100644 index 0000000000..17eb29f492 --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @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 parameters, @JsonProperty("id") String id, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixin.java new file mode 100644 index 0000000000..3f502b61d2 --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @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) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixin.java new file mode 100644 index 0000000000..9af07a7f6b --- /dev/null +++ b/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}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new Saml2Jackson2Module());
+ * 
+ * + * @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) { + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/DefaultSaml2AuthenticatedPrincipalMixinTests.java new file mode 100644 index 0000000000..d2da7dde20 --- /dev/null +++ b/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\", []]"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2AuthenticationMixinTests.java new file mode 100644 index 0000000000..37b38ce102 --- /dev/null +++ b/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); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2LogoutRequestMixinTests.java new file mode 100644 index 0000000000..965b82257b --- /dev/null +++ b/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 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); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2PostAuthenticationRequestMixinTests.java new file mode 100644 index 0000000000..c7bd5f29b9 --- /dev/null +++ b/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); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/Saml2RedirectAuthenticationRequestMixinTests.java new file mode 100644 index 0000000000..9199fb3992 --- /dev/null +++ b/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); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson2/TestSaml2JsonPayloads.java new file mode 100644 index 0000000000..d1e745ba5c --- /dev/null +++ b/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> ATTRIBUTES; + + static { + Map> 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 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 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; + } + +}