1
0
mirror of synced 2026-08-23 02:27:44 +00:00

Improve Equivalence Tests

This commit adds equals and hashCode implementations as well
as a readResolve implementation to ensure that deserialization
mechanisms can correctly assess the equality of a constnat
and a corresponding deserialized instance. For defense-in-depth
reasons, this commit also favors .equals over == for these
constants.

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings
2026-07-10 17:41:21 -06:00
parent 81d17a55a7
commit a447020c92
11 changed files with 448 additions and 6 deletions
@@ -16,6 +16,7 @@
package org.springframework.security.web.webauthn.api;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;
@@ -121,4 +122,25 @@ public final class AuthenticatorTransport implements Serializable {
return new AuthenticatorTransport[] { USB, NFC, BLE, HYBRID, INTERNAL };
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AuthenticatorTransport other)) {
return false;
}
return this.value.equals(other.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Serial
private Object readResolve() throws ObjectStreamException {
return valueOf(this.value);
}
}
@@ -16,6 +16,7 @@
package org.springframework.security.web.webauthn.api;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;
@@ -58,4 +59,25 @@ public final class PublicKeyCredentialType implements Serializable {
return new PublicKeyCredentialType(value);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PublicKeyCredentialType other)) {
return false;
}
return this.value.equals(other.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Serial
private Object readResolve() throws ObjectStreamException {
return valueOf(this.value);
}
}
@@ -16,9 +16,12 @@
package org.springframework.security.web.webauthn.api;
import java.io.ObjectStreamException;
import java.io.Serial;
import java.io.Serializable;
import org.jspecify.annotations.Nullable;
/**
* <a href=
* "https://www.w3.org/TR/webauthn-3/#enumdef-userverificationrequirement">UserVerificationRequirement</a>
@@ -72,4 +75,34 @@ public final class UserVerificationRequirement implements Serializable {
return this.value;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof UserVerificationRequirement other)) {
return false;
}
return this.value.equals(other.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Serial
private Object readResolve() throws ObjectStreamException {
switch (this.value) {
case "required":
return REQUIRED;
case "preferred":
return PREFERRED;
case "discouraged":
return DISCOURAGED;
default:
return this;
}
}
}
@@ -257,8 +257,8 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
byte[] clientDataJSON = response.getClientDataJSON().getBytes();
Challenge challenge = new DefaultChallenge(base64Challenge);
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge);
boolean userVerificationRequired = creationOptions.getAuthenticatorSelection()
.getUserVerification() == UserVerificationRequirement.REQUIRED;
boolean userVerificationRequired = UserVerificationRequirement.REQUIRED
.equals(creationOptions.getAuthenticatorSelection().getUserVerification());
// requireUserPresence The constant Boolean value true
// https://www.w3.org/TR/webauthn-3/#sctn-op-make-cred
boolean userPresenceRequired = true;
@@ -317,8 +317,7 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
private com.webauthn4j.data.PublicKeyCredentialParameters convertParamToWebauthn4j(
PublicKeyCredentialParameters parameter) {
PublicKeyCredentialType credentialType = PublicKeyCredentialType.valueOf(parameter.getType().getValue());
if (credentialType != PublicKeyCredentialType.PUBLIC_KEY) {
if (!PublicKeyCredentialType.PUBLIC_KEY.equals(parameter.getType())) {
throw new IllegalArgumentException(
"Cannot convert unknown credential type " + parameter.getType() + " to webauthn4j");
}
@@ -394,8 +393,8 @@ public class Webauthn4JRelyingPartyOperations implements WebAuthnRelyingPartyOpe
String rpId = requestOptions.getRpId();
Assert.notNull(rpId, "rpId cannot be null");
ServerProperty serverProperty = new ServerProperty(origins, rpId, challenge);
boolean userVerificationRequired = request.getRequestOptions()
.getUserVerification() == UserVerificationRequirement.REQUIRED;
boolean userVerificationRequired = UserVerificationRequirement.REQUIRED
.equals(request.getRequestOptions().getUserVerification());
com.webauthn4j.data.AuthenticationRequest authenticationRequest = new com.webauthn4j.data.AuthenticationRequest(
request.getPublicKey().getRawId().getBytes(), assertionResponse.getAuthenticatorData().getBytes(),
@@ -0,0 +1,42 @@
/*
* 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.api;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Verifies that {@link AuthenticatorAttachment} correctly restores singleton identity
* after Java deserialization. This class already implements {@code readResolve()} and
* serves as the reference for the pattern required by the other pseudo-enum types.
*/
class AuthenticatorAttachmentTests {
@Test
void platformWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorAttachment.PLATFORM))
.isSameAs(AuthenticatorAttachment.PLATFORM);
}
@Test
void crossPlatformWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorAttachment.CROSS_PLATFORM))
.isSameAs(AuthenticatorAttachment.CROSS_PLATFORM);
}
}
@@ -0,0 +1,76 @@
/*
* 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.api;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class AuthenticatorTransportTests {
@Test
void usbWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.USB))
.isSameAs(AuthenticatorTransport.USB);
}
@Test
void nfcWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.NFC))
.isSameAs(AuthenticatorTransport.NFC);
}
@Test
void bleWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.BLE))
.isSameAs(AuthenticatorTransport.BLE);
}
@Test
void smartCardWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.SMART_CARD))
.isSameAs(AuthenticatorTransport.SMART_CARD);
}
@Test
void hybridWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.HYBRID))
.isSameAs(AuthenticatorTransport.HYBRID);
}
@Test
void internalWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(AuthenticatorTransport.INTERNAL))
.isSameAs(AuthenticatorTransport.INTERNAL);
}
@Test
void usbWhenSameValueThenEquals() {
assertThat(new AuthenticatorTransport("usb")).isEqualTo(AuthenticatorTransport.USB);
}
@Test
void usbWhenSameValueThenHashCodeMatches() {
assertThat(new AuthenticatorTransport("usb")).hasSameHashCodeAs(AuthenticatorTransport.USB);
}
@Test
void usbWhenDifferentValueThenNotEquals() {
assertThat(AuthenticatorTransport.USB).isNotEqualTo(AuthenticatorTransport.NFC);
}
}
@@ -0,0 +1,49 @@
/*
* 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.api;
import java.util.Set;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PublicKeyCredentialDescriptorTests {
@Test
void typeWhenDeserializedThenSameAsConstant() {
PublicKeyCredentialDescriptor descriptor = PublicKeyCredentialDescriptor.builder()
.id(TestBytes.get())
.type(PublicKeyCredentialType.PUBLIC_KEY)
.build();
PublicKeyCredentialDescriptor deserialized = SerializationTestUtils.serializeAndDeserialize(descriptor);
assertThat(deserialized.getType()).isSameAs(PublicKeyCredentialType.PUBLIC_KEY);
}
@Test
void transportsWhenDeserializedThenSameAsConstants() {
PublicKeyCredentialDescriptor descriptor = PublicKeyCredentialDescriptor.builder()
.id(TestBytes.get())
.type(PublicKeyCredentialType.PUBLIC_KEY)
.transports(Set.of(AuthenticatorTransport.USB, AuthenticatorTransport.HYBRID))
.build();
PublicKeyCredentialDescriptor deserialized = SerializationTestUtils.serializeAndDeserialize(descriptor);
assertThat(deserialized.getTransports()).containsExactlyInAnyOrder(AuthenticatorTransport.USB,
AuthenticatorTransport.HYBRID);
}
}
@@ -0,0 +1,43 @@
/*
* 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.api;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PublicKeyCredentialRequestOptionsTests {
@Test
void userVerificationRequiredWhenDeserializedThenSameAsConstant() {
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create()
.userVerification(UserVerificationRequirement.REQUIRED)
.build();
PublicKeyCredentialRequestOptions deserialized = SerializationTestUtils.serializeAndDeserialize(options);
assertThat(deserialized.getUserVerification()).isSameAs(UserVerificationRequirement.REQUIRED);
}
@Test
void userVerificationPreferredWhenDeserializedThenSameAsConstant() {
PublicKeyCredentialRequestOptions options = TestPublicKeyCredentialRequestOptions.create()
.userVerification(UserVerificationRequirement.PREFERRED)
.build();
PublicKeyCredentialRequestOptions deserialized = SerializationTestUtils.serializeAndDeserialize(options);
assertThat(deserialized.getUserVerification()).isSameAs(UserVerificationRequirement.PREFERRED);
}
}
@@ -0,0 +1,51 @@
/*
* 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.api;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class PublicKeyCredentialTypeTests {
@Test
void publicKeyWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(PublicKeyCredentialType.PUBLIC_KEY))
.isSameAs(PublicKeyCredentialType.PUBLIC_KEY);
}
@Test
void publicKeyWhenSameValueThenEquals() {
PublicKeyCredentialType first = PublicKeyCredentialType.valueOf("custom");
PublicKeyCredentialType second = PublicKeyCredentialType.valueOf("custom");
assertThat(first).isEqualTo(second);
}
@Test
void publicKeyWhenSameValueThenHashCodeMatches() {
PublicKeyCredentialType first = PublicKeyCredentialType.valueOf("custom");
PublicKeyCredentialType second = PublicKeyCredentialType.valueOf("custom");
assertThat(first).hasSameHashCodeAs(second);
}
@Test
void publicKeyWhenDifferentValueThenNotEquals() {
assertThat(PublicKeyCredentialType.valueOf("custom-1"))
.isNotEqualTo(PublicKeyCredentialType.valueOf("custom-2"));
}
}
@@ -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.web.webauthn.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public final class SerializationTestUtils {
private SerializationTestUtils() {
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T serializeAndDeserialize(T object) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
return (T) ois.readObject();
}
}
catch (Exception ex) {
throw new RuntimeException("Serialization round-trip failed", ex);
}
}
}
@@ -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.web.webauthn.api;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class UserVerificationRequirementTests {
@Test
void requiredWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(UserVerificationRequirement.REQUIRED))
.isSameAs(UserVerificationRequirement.REQUIRED);
}
@Test
void preferredWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(UserVerificationRequirement.PREFERRED))
.isSameAs(UserVerificationRequirement.PREFERRED);
}
@Test
void discouragedWhenDeserializedThenSameAsConstant() {
assertThat(SerializationTestUtils.serializeAndDeserialize(UserVerificationRequirement.DISCOURAGED))
.isSameAs(UserVerificationRequirement.DISCOURAGED);
}
@Test
void requiredWhenSameValueThenEquals() {
assertThat(new UserVerificationRequirement("required")).isEqualTo(UserVerificationRequirement.REQUIRED);
}
@Test
void requiredWhenSameValueThenHashCodeMatches() {
assertThat(new UserVerificationRequirement("required")).hasSameHashCodeAs(UserVerificationRequirement.REQUIRED);
}
@Test
void requiredWhenDifferentValueThenNotEquals() {
assertThat(UserVerificationRequirement.REQUIRED).isNotEqualTo(UserVerificationRequirement.PREFERRED);
}
}