diff --git a/NEWS b/NEWS index 0746178c5..069bb2c08 100644 --- a/NEWS +++ b/NEWS @@ -17,6 +17,14 @@ New features: around passkey use cases. * Added `Automatic-Module-Name` to jar manifest. +Fixes: + +* `AuthenticatorAttestationResponse` now tolerates and ignores properties + `"publicKey"` and `"publicKeyAlgorithm"` during JSON deserialization. These + properties are emitted by the `PublicKeyCredential.toJSON()` method added in + WebAuthn Level 3. + + `webauthn-server-attestation`: New features: diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java index 0a33448db..29ba7e3ec 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttestationResponse.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.yubico.internal.util.CollectionUtil; import com.yubico.webauthn.data.exception.Base64UrlException; @@ -49,6 +50,7 @@ * Information About Public Key Credential (interface AuthenticatorAttestationResponse) */ @Value +@JsonIgnoreProperties({"publicKey", "publicKeyAlgorithm"}) public class AuthenticatorAttestationResponse implements AuthenticatorResponse { /** diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala index 1bcdb5d37..3fe9a73c5 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala @@ -25,10 +25,12 @@ package com.yubico.webauthn.data import com.fasterxml.jackson.core.`type`.TypeReference +import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.exc.ValueInstantiationException import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.node.BooleanNode +import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.fasterxml.jackson.datatype.jdk8.Jdk8Module @@ -44,6 +46,7 @@ import com.yubico.webauthn.extension.appid.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.junit.JUnitRunner @@ -62,6 +65,7 @@ class JsonIoSpec .builder() .addModule(new Jdk8Module()) .build() + val jf: JsonNodeFactory = JsonNodeFactory.instance describe("The class") { @@ -133,52 +137,6 @@ class JsonIoSpec } describe("The class PublicKeyCredential") { - it( - "has an alternative parseRegistrationResponseJson function as an alias." - ) { - def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) - val decoded: A = json.readValue(encoded, tpe) - val altDecoded = - PublicKeyCredential.parseRegistrationResponseJson(encoded) - val altRecoded: String = json.writeValueAsString(altDecoded) - - altDecoded should equal(decoded) - altRecoded should equal(encoded) - } - } - test( - new TypeReference[PublicKeyCredential[ - AuthenticatorAttestationResponse, - ClientRegistrationExtensionOutputs, - ]]() {} - ) - } - - it( - "has an alternative parseAuthenticationResponseJson function as an alias." - ) { - def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { - forAll { value: A => - val encoded: String = json.writeValueAsString(value) - val decoded: A = json.readValue(encoded, tpe) - val altDecoded = - PublicKeyCredential.parseAssertionResponseJson(encoded) - val altRecoded: String = json.writeValueAsString(altDecoded) - - altDecoded should equal(decoded) - altRecoded should equal(encoded) - } - } - test( - new TypeReference[PublicKeyCredential[ - AuthenticatorAssertionResponse, - ClientAssertionExtensionOutputs, - ]]() {} - ) - } - it("allows rawId to be present without id.") { def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P] @@ -416,6 +374,92 @@ class JsonIoSpec } } + describe("The function PublicKeyCredential.parseRegistrationResponseJson") { + it("can parse registration responses.") { + def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { + forAll { value: A => + val encoded: String = json.writeValueAsString(value) + val decoded: A = json.readValue(encoded, tpe) + val altDecoded = + PublicKeyCredential.parseRegistrationResponseJson(encoded) + val altRecoded: String = json.writeValueAsString(altDecoded) + + altDecoded should equal(decoded) + altRecoded should equal(encoded) + } + } + + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ]]() {} + ) + } + + describe("""tolerates and ignores the "response" sub-attribute:""") { + def test[T <: JsonNode](attrName: String, genAttrValue: Gen[T]): Unit = { + type P = PublicKeyCredential[ + AuthenticatorAttestationResponse, + ClientRegistrationExtensionOutputs, + ] + it(s"${attrName}.") { + forAll( + arbitrary[P], + genAttrValue, + ) { (value: P, attrValue: T) => + val tree: ObjectNode = json.valueToTree(value) + tree + .get("response") + .asInstanceOf[ObjectNode] + .set(attrName, attrValue) + val encoded = json.writeValueAsString(tree) + val decoded = + PublicKeyCredential.parseRegistrationResponseJson(encoded) + val recoded: ObjectNode = json.valueToTree[ObjectNode](decoded) + recoded.has(attrName) should be(false) + } + } + } + + test( + "publicKeyAlgorithm", + arbitraryCOSEAlgorithmIdentifier.arbitrary.map(i => + jf.numberNode(i.getId) + ), + ) + + test( + "publicKey", + arbitrary[String].map(new TextNode(_)), + ) + } + } + + describe("The function PublicKeyCredential.parseAssertionResponseJson") { + it("can parse assertion responses.") { + def test[A](tpe: TypeReference[A])(implicit a: Arbitrary[A]): Unit = { + forAll { value: A => + val encoded: String = json.writeValueAsString(value) + val decoded: A = json.readValue(encoded, tpe) + val altDecoded = + PublicKeyCredential.parseAssertionResponseJson(encoded) + val altRecoded: String = json.writeValueAsString(altDecoded) + + altDecoded should equal(decoded) + altRecoded should equal(encoded) + } + } + + test( + new TypeReference[PublicKeyCredential[ + AuthenticatorAssertionResponse, + ClientAssertionExtensionOutputs, + ]]() {} + ) + } + } + describe("The class PublicKeyCredentialCreationOptions") { it("""has a toCredentialsCreateJson() method which returns a JSON object with the PublicKeyCredentialCreationOptions set as a top-level "publicKey" property.""") { forAll { pkcco: PublicKeyCredentialCreationOptions =>