diff --git a/NEWS b/NEWS
index a42a06d88..9728482a0 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,9 @@ Changes:
Note that `webauthn-server-attestation` still depends on BouncyCastle.
+* Jackson deserializer for `PublicKeyCredential` now allows a `rawId` property
+ to be present if `id` is not present, or if `rawId` equals `id`.
+
== Version 1.7.0 ==
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java
index 426df09b1..f6e8c8f91 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java
@@ -84,17 +84,34 @@ public class PublicKeyCredential PublicKeyCredentialBuilder.MandatoryStages builder() {
return new PublicKeyCredentialBuilder().start();
}
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 27776e020..7e7e22f18 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
@@ -29,10 +29,14 @@ import com.fasterxml.jackson.core.`type`.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.databind.exc.ValueInstantiationException
+import com.fasterxml.jackson.databind.node.ObjectNode
+import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.yubico.webauthn.AssertionRequest
import com.yubico.webauthn.AssertionResult
import com.yubico.webauthn.Generators._
+import com.yubico.webauthn.RegisteredCredential
import com.yubico.webauthn.RegistrationResult
import com.yubico.webauthn.attestation.Attestation
import com.yubico.webauthn.attestation.Generators._
@@ -40,7 +44,6 @@ import com.yubico.webauthn.attestation.Transport
import com.yubico.webauthn.data.Generators._
import com.yubico.webauthn.extension.appid.AppId
import com.yubico.webauthn.extension.appid.Generators._
-import com.yubico.webauthn.RegisteredCredential
import org.junit.runner.RunWith
import org.scalacheck.Arbitrary
import org.scalatest.FunSpec
@@ -161,6 +164,106 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec
}
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]): Unit = {
+ forAll { value: P =>
+ val encoded: String = json.writeValueAsString(value)
+ val decoded = json.readTree(encoded)
+ decoded.asInstanceOf[ObjectNode]
+ .set[ObjectNode]("rawId", new TextNode(value.getId.getBase64Url))
+ .remove("id")
+ val reencoded = json.writeValueAsString(decoded)
+ val restored: P = json.readValue(reencoded, tpe)
+
+ restored.getId should equal (value.getId)
+ restored should equal (value)
+ }
+ }
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
+ }
+
+ it("allows id to be present without rawId.") {
+ def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
+ forAll { value: P =>
+ val encoded: String = json.writeValueAsString(value)
+ val decoded = json.readTree(encoded)
+ decoded.asInstanceOf[ObjectNode]
+ .set[ObjectNode]("id", new TextNode(value.getId.getBase64Url))
+ .remove("rawId")
+ val reencoded = json.writeValueAsString(decoded)
+ val restored: P = json.readValue(reencoded, tpe)
+
+ restored should equal (value)
+ }
+ }
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
+ }
+
+ it("allows both id and rawId to be present if equal.") {
+ def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
+ forAll { value: P =>
+ val encoded: String = json.writeValueAsString(value)
+ val decoded = json.readTree(encoded)
+ decoded.asInstanceOf[ObjectNode].set("id", new TextNode(value.getId.getBase64Url))
+ decoded.asInstanceOf[ObjectNode].set("rawId", new TextNode(value.getId.getBase64Url))
+ val reencoded = json.writeValueAsString(decoded)
+ val restored: P = json.readValue(reencoded, tpe)
+
+ restored should equal (value)
+ }
+ }
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
+ }
+
+ it("does not allow both id and rawId to be absent.") {
+ def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
+ forAll { value: P =>
+ val encoded: String = json.writeValueAsString(value)
+ val decoded = json.readTree(encoded).asInstanceOf[ObjectNode]
+ decoded.remove("id")
+ decoded.remove("rawId")
+ val reencoded = json.writeValueAsString(decoded)
+
+ an [ValueInstantiationException] should be thrownBy {
+ json.readValue(reencoded, tpe)
+ }
+ }
+ }
+
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
+ }
+
+ it("does not allow both id and rawId to be present and not equal.") {
+ def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
+ forAll { value: P =>
+ val modId = new ByteArray(
+ if (value.getId.getBytes.isEmpty)
+ Array(0)
+ else
+ value.getId.getBytes.updated(0, (value.getId.getBytes()(0) + 1 % 127).byteValue)
+ )
+
+ val encoded: String = json.writeValueAsString(value)
+ val decoded = json.readTree(encoded)
+ decoded.asInstanceOf[ObjectNode]
+ .set[ObjectNode]("id", new TextNode(value.getId.getBase64Url))
+ .set[ObjectNode]("rawId", new TextNode(modId.getBase64Url))
+ val reencoded = json.writeValueAsString(decoded)
+
+ an [ValueInstantiationException] should be thrownBy {
+ json.readValue(reencoded, tpe)
+ }
+ }
+ }
+
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
+ test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
+ }
}
}