From 85374d8bf1cb112c96562142736a3d952c35a63f Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Wed, 11 Mar 2026 12:56:27 +0900 Subject: [PATCH] Add Jackson Mixin for WebAuthnAuthentication Closes gh-18034 Signed-off-by: Toshiaki Maki --- ...blePublicKeyCredentialUserEntityMixin.java | 36 +++++ .../jackson/WebAuthnAuthenticationMixin.java | 41 ++++++ .../jackson/WebauthnJacksonModule.java | 8 ++ .../WebAuthnAuthenticationMixinTests.java | 133 ++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/ImmutablePublicKeyCredentialUserEntityMixin.java create mode 100644 webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixin.java create mode 100644 webauthn/src/test/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixinTests.java diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/ImmutablePublicKeyCredentialUserEntityMixin.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/ImmutablePublicKeyCredentialUserEntityMixin.java new file mode 100644 index 00000000000..3ab59c43927 --- /dev/null +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/ImmutablePublicKeyCredentialUserEntityMixin.java @@ -0,0 +1,36 @@ +/* + * 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.web.webauthn.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; + +/** + * Jackson mixin for {@link ImmutablePublicKeyCredentialUserEntity} + * + * @author Toshiaki Maki + * @since 7.1 + */ +abstract class ImmutablePublicKeyCredentialUserEntityMixin { + + ImmutablePublicKeyCredentialUserEntityMixin(@JsonProperty("name") String name, @JsonProperty("id") Bytes id, + @JsonProperty("displayName") String displayName) { + } + +} diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixin.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixin.java new file mode 100644 index 00000000000..f9810f8cf65 --- /dev/null +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixin.java @@ -0,0 +1,41 @@ +/* + * 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.web.webauthn.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication; + +/** + * Jackson mixin for {@link WebAuthnAuthentication} + * + * @author Toshiaki Maki + * @since 7.1 + */ +@JsonIgnoreProperties({ "authenticated" }) +abstract class WebAuthnAuthenticationMixin { + + WebAuthnAuthenticationMixin(@JsonProperty("principal") PublicKeyCredentialUserEntity principal, + @JsonProperty("authorities") Collection authorities) { + } + +} diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebauthnJacksonModule.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebauthnJacksonModule.java index 2a6bd62056c..f8820773c30 100644 --- a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebauthnJacksonModule.java +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/WebauthnJacksonModule.java @@ -33,12 +33,14 @@ import org.springframework.security.web.webauthn.api.COSEAlgorithmIdentifier; import org.springframework.security.web.webauthn.api.CredProtectAuthenticationExtensionsClientInput; import org.springframework.security.web.webauthn.api.CredentialPropertiesOutput; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; import org.springframework.security.web.webauthn.api.PublicKeyCredential; import org.springframework.security.web.webauthn.api.PublicKeyCredentialCreationOptions; import org.springframework.security.web.webauthn.api.PublicKeyCredentialRequestOptions; import org.springframework.security.web.webauthn.api.PublicKeyCredentialType; import org.springframework.security.web.webauthn.api.ResidentKeyRequirement; import org.springframework.security.web.webauthn.api.UserVerificationRequirement; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication; import org.springframework.security.web.webauthn.management.RelyingPartyPublicKey; /** @@ -47,6 +49,7 @@ * * @author Sebastien Deleuze * @author Rob Winch + * @author Toshiaki Maki * @since 7.0 */ @SuppressWarnings("serial") @@ -61,6 +64,8 @@ public WebauthnJacksonModule() { @Override public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(WebAuthnAuthentication.class) + .allowIfSubType(ImmutablePublicKeyCredentialUserEntity.class); } @Override @@ -92,6 +97,9 @@ public void setupModule(SetupContext context) { context.setMixIn(RelyingPartyPublicKey.class, RelyingPartyPublicKeyMixin.class); context.setMixIn(ResidentKeyRequirement.class, ResidentKeyRequirementMixin.class); context.setMixIn(UserVerificationRequirement.class, UserVerificationRequirementMixin.class); + context.setMixIn(WebAuthnAuthentication.class, WebAuthnAuthenticationMixin.class); + context.setMixIn(ImmutablePublicKeyCredentialUserEntity.class, + ImmutablePublicKeyCredentialUserEntityMixin.class); } } diff --git a/webauthn/src/test/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixinTests.java b/webauthn/src/test/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixinTests.java new file mode 100644 index 00000000000..426acafd976 --- /dev/null +++ b/webauthn/src/test/java/org/springframework/security/web/webauthn/jackson/WebAuthnAuthenticationMixinTests.java @@ -0,0 +1,133 @@ +/* + * 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.web.webauthn.jackson; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.web.webauthn.api.Bytes; +import org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity; +import org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebAuthnAuthenticationMixin} and + * {@link ImmutablePublicKeyCredentialUserEntityMixin} with polymorphic type handling. + * + *

+ * This test class is separate from {@link JacksonTests} because it requires a + * {@link JsonMapper} configured with {@link SecurityJacksonModules} to enable polymorphic + * type information ({@code @class}). {@link JacksonTests} uses a {@link JsonMapper} + * configured only with {@link WebauthnJacksonModule}, and its existing custom serializers + * are not compatible with the automatic inclusion of type information enabled by + * {@link SecurityJacksonModules}. + * + * @author Toshiaki Maki + * @since 7.1 + */ +class WebAuthnAuthenticationMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setup() { + ClassLoader classLoader = getClass().getClassLoader(); + WebauthnJacksonModule webauthnJacksonModule = new WebauthnJacksonModule(); + BasicPolymorphicTypeValidator.Builder typeValidatorBuilder = BasicPolymorphicTypeValidator.builder(); + webauthnJacksonModule.configurePolymorphicTypeValidator(typeValidatorBuilder); + List modules = SecurityJacksonModules.getModules(classLoader, typeValidatorBuilder); + modules.add(webauthnJacksonModule); + this.mapper = JsonMapper.builder().addModules(modules).build(); + } + + @Test + void writeWebAuthnAuthentication() throws Exception { + ImmutablePublicKeyCredentialUserEntity principal = (ImmutablePublicKeyCredentialUserEntity) ImmutablePublicKeyCredentialUserEntity + .builder() + .name("user@example.localhost") + .id(Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w")) + .displayName("User") + .build(); + WebAuthnAuthentication authentication = new WebAuthnAuthentication(principal, + List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + String json = this.mapper.writeValueAsString(authentication); + + String expected = """ + { + "@class": "org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication", + "principal": { + "@class": "org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity", + "name": "user@example.localhost", + "id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w", + "displayName": "User" + }, + "authorities": ["java.util.Collections$UnmodifiableRandomAccessList", [ + { + "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority", + "authority": "ROLE_USER" + } + ]] + } + """; + JSONAssert.assertEquals(expected, json, false); + assertThat(json).doesNotContain("\"authenticated\""); + } + + @Test + void readWebAuthnAuthentication() throws Exception { + String json = """ + { + "@class": "org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication", + "principal": { + "@class": "org.springframework.security.web.webauthn.api.ImmutablePublicKeyCredentialUserEntity", + "name": "user@example.localhost", + "id": "oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w", + "displayName": "User" + }, + "authorities": ["java.util.Collections$UnmodifiableRandomAccessList", [ + { + "@class": "org.springframework.security.core.authority.SimpleGrantedAuthority", + "authority": "ROLE_USER" + } + ]] + } + """; + ImmutablePublicKeyCredentialUserEntity expectedPrincipal = (ImmutablePublicKeyCredentialUserEntity) ImmutablePublicKeyCredentialUserEntity + .builder() + .name("user@example.localhost") + .id(Bytes.fromBase64("oWJtkJ6vJ_m5b84LB4_K7QKTCTEwLIjCh4tFMCGHO4w")) + .displayName("User") + .build(); + WebAuthnAuthentication expected = new WebAuthnAuthentication(expectedPrincipal, + List.of(new SimpleGrantedAuthority("ROLE_USER"))); + + WebAuthnAuthentication authentication = this.mapper.readValue(json, WebAuthnAuthentication.class); + + assertThat(authentication).usingRecursiveComparison().isEqualTo(expected); + } + +}