From 6ae26cb3adfe145502f00f3321630f714eacc405 Mon Sep 17 00:00:00 2001 From: seonwoo_jung Date: Mon, 25 May 2026 09:16:45 +0900 Subject: [PATCH] Create Jackson Mixin for InvalidOneTimeTokenException InvalidOneTimeTokenException had no Jackson mixin registered, so storing the session in a serialized backing store (for example, Redis) failed to deserialize when the exception was present in the security context. This commit follows the existing BadCredentialsExceptionMixin pattern, adding mixins in the jackson and jackson2 packages and registering them in CoreJacksonModule and CoreJackson2Module. The Jackson 3 PolymorphicTypeValidator is updated to allow InvalidOneTimeTokenException as a subtype. Closes gh-19203 Signed-off-by: seonwoo_jung --- .../security/jackson/CoreJacksonModule.java | 3 + .../InvalidOneTimeTokenExceptionMixin.java | 47 ++++++++++++++ .../security/jackson2/CoreJackson2Module.java | 2 + .../InvalidOneTimeTokenExceptionMixin.java | 61 +++++++++++++++++++ ...nvalidOneTimeTokenExceptionMixinTests.java | 55 +++++++++++++++++ ...nvalidOneTimeTokenExceptionMixinTests.java | 58 ++++++++++++++++++ 6 files changed, 226 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixin.java create mode 100644 core/src/test/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixinTests.java diff --git a/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java b/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java index 5c6f27ba08e..bebfb361383 100644 --- a/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java +++ b/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java @@ -29,6 +29,7 @@ import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.ott.InvalidOneTimeTokenException; import org.springframework.security.authentication.ott.OneTimeTokenAuthentication; import org.springframework.security.core.authority.FactorGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -83,6 +84,7 @@ public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Buil .allowIfSubType(SecurityContextImpl.class) .allowIfSubType(TestingAuthenticationToken.class) .allowIfSubType(OneTimeTokenAuthentication.class) + .allowIfSubType(InvalidOneTimeTokenException.class) .allowIfSubType("java.util.Collections$UnmodifiableSet") .allowIfSubType("java.util.Collections$UnmodifiableRandomAccessList") .allowIfSubType("java.util.Collections$EmptyList") @@ -111,6 +113,7 @@ public void setupModule(SetupContext context) { context.setMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); context.setMixIn(BadCredentialsException.class, BadCredentialsExceptionMixin.class); context.setMixIn(OneTimeTokenAuthentication.class, OneTimeTokenAuthenticationMixin.class); + context.setMixIn(InvalidOneTimeTokenException.class, InvalidOneTimeTokenExceptionMixin.class); } } diff --git a/core/src/main/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixin.java b/core/src/main/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixin.java new file mode 100644 index 00000000000..38a8351abe3 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present 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.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class helps in serialize/deserialize + * {@link org.springframework.security.authentication.ott.InvalidOneTimeTokenException} + * class. + * + * @author seonwoo-jung + * @since 7.1 + * @see CoreJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties({ "cause", "stackTrace", "authenticationRequest" }) +class InvalidOneTimeTokenExceptionMixin { + + /** + * Constructor used by Jackson to create + * {@link org.springframework.security.authentication.ott.InvalidOneTimeTokenException} + * object. + * @param message the detail message + */ + @JsonCreator + InvalidOneTimeTokenExceptionMixin(@JsonProperty("message") String message) { + } + +} 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 2f8a87c4f41..892df014753 100644 --- a/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java +++ b/core/src/main/java/org/springframework/security/jackson2/CoreJackson2Module.java @@ -25,6 +25,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.ott.InvalidOneTimeTokenException; import org.springframework.security.authentication.ott.OneTimeTokenAuthenticationToken; import org.springframework.security.core.authority.FactorGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -77,6 +78,7 @@ public void setupModule(SetupContext context) { UsernamePasswordAuthenticationTokenMixin.class); context.setMixInAnnotations(BadCredentialsException.class, BadCredentialsExceptionMixin.class); context.setMixInAnnotations(OneTimeTokenAuthenticationToken.class, OneTimeTokenAuthenticationTokenMixin.class); + context.setMixInAnnotations(InvalidOneTimeTokenException.class, InvalidOneTimeTokenExceptionMixin.class); } } diff --git a/core/src/main/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixin.java b/core/src/main/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixin.java new file mode 100644 index 00000000000..04cbf01ab32 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixin.java @@ -0,0 +1,61 @@ +/* + * Copyright 2004-present 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 com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class helps in serialize/deserialize + * {@link org.springframework.security.authentication.ott.InvalidOneTimeTokenException} + * class. To use this class you need to register it with + * {@link com.fasterxml.jackson.databind.ObjectMapper}. + * + *
+ *     ObjectMapper mapper = new ObjectMapper();
+ *     mapper.registerModule(new CoreJackson2Module());
+ * 
+ * + * Note: This class will save TypeInfo (full class name) into a property + * called @class The cause and stackTrace are ignored in the serialization. + * + * @author seonwoo-jung + * @since 7.1 + * @see CoreJackson2Module + * @deprecated as of 7.0 in favor of + * {@code org.springframework.security.jackson.InvalidOneTimeTokenExceptionMixin} based on + * Jackson 3 + */ +@SuppressWarnings("removal") +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "authenticationRequest" }) +@Deprecated(forRemoval = true) +class InvalidOneTimeTokenExceptionMixin { + + /** + * Constructor used by Jackson to create + * {@link org.springframework.security.authentication.ott.InvalidOneTimeTokenException} + * object. + * @param message the detail message + */ + @JsonCreator + InvalidOneTimeTokenExceptionMixin(@JsonProperty("message") String message) { + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixinTests.java b/core/src/test/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixinTests.java new file mode 100644 index 00000000000..d93cccfd99c --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/InvalidOneTimeTokenExceptionMixinTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-present 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.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.authentication.ott.InvalidOneTimeTokenException; + +import static org.assertj.core.api.Assertions.assertThat; + +class InvalidOneTimeTokenExceptionMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String EXCEPTION_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.ott.InvalidOneTimeTokenException\"," + + "\"localizedMessage\": \"message\", " + + "\"message\": \"message\", " + + "\"suppressed\": [\"[Ljava.lang.Throwable;\",[]]" + + "}"; + // @formatter:on + + @Test + void serializeInvalidOneTimeTokenExceptionMixinTest() throws JSONException { + InvalidOneTimeTokenException exception = new InvalidOneTimeTokenException("message"); + String serializedJson = this.mapper.writeValueAsString(exception); + JSONAssert.assertEquals(EXCEPTION_JSON, serializedJson, true); + } + + @Test + void deserializeInvalidOneTimeTokenExceptionMixinTest() { + InvalidOneTimeTokenException exception = this.mapper.readValue(EXCEPTION_JSON, + InvalidOneTimeTokenException.class); + assertThat(exception).isNotNull(); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getMessage()).isEqualTo("message"); + assertThat(exception.getLocalizedMessage()).isEqualTo("message"); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixinTests.java b/core/src/test/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixinTests.java new file mode 100644 index 00000000000..75a807a8912 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson2/InvalidOneTimeTokenExceptionMixinTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2004-present 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 com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.authentication.ott.InvalidOneTimeTokenException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class InvalidOneTimeTokenExceptionMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String EXCEPTION_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.ott.InvalidOneTimeTokenException\"," + + "\"localizedMessage\": \"message\", " + + "\"message\": \"message\", " + + "\"suppressed\": [\"[Ljava.lang.Throwable;\",[]]" + + "}"; + // @formatter:on + + @Test + public void serializeInvalidOneTimeTokenExceptionMixinTest() throws JsonProcessingException, JSONException { + InvalidOneTimeTokenException exception = new InvalidOneTimeTokenException("message"); + String serializedJson = this.mapper.writeValueAsString(exception); + JSONAssert.assertEquals(EXCEPTION_JSON, serializedJson, true); + } + + @Test + public void deserializeInvalidOneTimeTokenExceptionMixinTest() throws IOException { + InvalidOneTimeTokenException exception = this.mapper.readValue(EXCEPTION_JSON, + InvalidOneTimeTokenException.class); + assertThat(exception).isNotNull(); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getMessage()).isEqualTo("message"); + assertThat(exception.getLocalizedMessage()).isEqualTo("message"); + } + +}