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..f24d38aca15 100644 --- a/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java +++ b/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java @@ -20,8 +20,6 @@ import java.time.Instant; import tools.jackson.core.Version; -import tools.jackson.databind.cfg.DateTimeFeature; -import tools.jackson.databind.cfg.MapperBuilder; import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import org.springframework.security.authentication.AnonymousAuthenticationToken; @@ -101,7 +99,6 @@ public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Buil @Override public void setupModule(SetupContext context) { - ((MapperBuilder) context.getOwner()).enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS); context.setMixIn(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class); context.setMixIn(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class); context.setMixIn(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class); diff --git a/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java b/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java index 3e1db9cc6a7..3a97e80edf5 100644 --- a/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java +++ b/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -44,7 +45,10 @@ abstract class FactorGrantedAuthorityMixin { */ @JsonCreator FactorGrantedAuthorityMixin(@JsonProperty("authority") String authority, - @JsonProperty("issuedAt") Instant issuedAt) { + @JsonProperty("issuedAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant issuedAt) { } + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + abstract Instant getIssuedAt(); + } diff --git a/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java b/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java index 865a1be9a7a..b63d3e26bc5 100644 --- a/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java +++ b/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java @@ -21,6 +21,9 @@ import org.json.JSONException; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.FactorGrantedAuthority; @@ -57,4 +60,41 @@ void deserializeGrantedAuthorityTest() { assertThat(authority.getIssuedAt()).isEqualTo(this.issuedAt); } + @Test + void serializeWhenWriteDatesAsTimestampsDisabledThenStillUsesTimestamps() throws JSONException { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + GrantedAuthority authority = FactorGrantedAuthority.withAuthority("FACTOR_PASSWORD") + .issuedAt(this.issuedAt) + .build(); + String json = customMapper.writeValueAsString(authority); + JSONAssert.assertEquals(AUTHORITY_JSON, json, true); + } + + @Test + void deserializeWhenWriteDatesAsTimestampsDisabledThenStillDeserializesTimestamps() { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + FactorGrantedAuthority authority = (FactorGrantedAuthority) customMapper.readValue(AUTHORITY_JSON, Object.class); + assertThat(authority).isNotNull(); + assertThat(authority.getAuthority()).isEqualTo("FACTOR_PASSWORD"); + assertThat(authority.getIssuedAt()).isEqualTo(this.issuedAt); + } + + @Test + void serializeDoesNotOverrideGlobalDateTimeFeature() { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + assertThat(customMapper.isEnabled(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse(); + } + } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java index 1f91943f5bd..e9e3ab6c1b9 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import tools.jackson.databind.annotation.JsonDeserialize; @@ -45,8 +46,10 @@ abstract class OAuth2AccessTokenMixin { OAuth2AccessTokenMixin( @JsonProperty("tokenType") @JsonDeserialize( converter = StdConverters.AccessTokenTypeConverter.class) OAuth2AccessToken.TokenType tokenType, - @JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt, - @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("scopes") Set scopes) { + @JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant issuedAt, + @JsonProperty("expiresAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant expiresAt, + @JsonProperty("scopes") Set scopes) { } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java index a4a9085ee49..e899888ccc1 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -40,7 +41,8 @@ abstract class OAuth2RefreshTokenMixin { @JsonCreator - OAuth2RefreshTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt) { + OAuth2RefreshTokenMixin(@JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant issuedAt) { } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java index 14ed972340a..90b03e22214 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -41,8 +42,10 @@ abstract class OidcIdTokenMixin { @JsonCreator - OidcIdTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt, - @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("claims") Map claims) { + OidcIdTokenMixin(@JsonProperty("tokenValue") String tokenValue, + @JsonProperty("issuedAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant issuedAt, + @JsonProperty("expiresAt") @JsonFormat(shape = JsonFormat.Shape.NUMBER) Instant expiresAt, + @JsonProperty("claims") Map claims) { } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java index 7d1ff8cf4eb..72b479a70ef 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java @@ -26,7 +26,9 @@ import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import tools.jackson.core.JacksonException; +import tools.jackson.databind.cfg.DateTimeFeature; import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; import org.springframework.security.jackson.SecurityJacksonModules; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -107,6 +109,47 @@ public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception JSONAssert.assertEquals(expectedJson, json, true); } + @Test + public void serializeWhenWriteDatesAsTimestampsDisabledThenTokenDatesStillUseTimestamps() throws Exception { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistrationBuilder.build(), + this.principalName, this.accessToken, this.refreshToken); + String expectedJson = asJson(authorizedClient); + String json = customMapper.writeValueAsString(authorizedClient); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenWriteDatesAsTimestampsDisabledThenTokenDatesStillDeserialize() throws Exception { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient(this.clientRegistrationBuilder.build(), + this.principalName, this.accessToken, this.refreshToken); + String json = asJson(expectedAuthorizedClient); + OAuth2AuthorizedClient authorizedClient = customMapper.readValue(json, OAuth2AuthorizedClient.class); + assertThat(authorizedClient.getAccessToken().getIssuedAt()).isEqualTo(this.accessToken.getIssuedAt()); + assertThat(authorizedClient.getAccessToken().getExpiresAt()).isEqualTo(this.accessToken.getExpiresAt()); + assertThat(authorizedClient.getRefreshToken()).isNotNull(); + assertThat(authorizedClient.getRefreshToken().getIssuedAt()).isEqualTo(this.refreshToken.getIssuedAt()); + } + + @Test + public void setupWhenWriteDatesAsTimestampsDisabledThenSettingIsNotOverridden() { + ClassLoader loader = getClass().getClassLoader(); + JsonMapper customMapper = JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .addModules(SecurityJacksonModules.getModules(loader, BasicPolymorphicTypeValidator.builder())) + .build(); + assertThat(customMapper.isEnabled(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)).isFalse(); + } + @Test public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistrationBuilder.build(),