Skip to content

Commit 55b69d0

Browse files
authored
Merge pull request #104 from Yubico/allow-rawid
Allow rawId in PublicKeyCredential deserialization
2 parents dc2bc5e + d2b018c commit 55b69d0

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Changes:
1111

1212
Note that `webauthn-server-attestation` still depends on BouncyCastle.
1313

14+
* Jackson deserializer for `PublicKeyCredential` now allows a `rawId` property
15+
to be present if `id` is not present, or if `rawId` equals `id`.
16+
1417

1518
== Version 1.7.0 ==
1619

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,34 @@ public class PublicKeyCredential<A extends AuthenticatorResponse, B extends Clie
8484

8585
@JsonCreator
8686
private PublicKeyCredential(
87-
@NonNull @JsonProperty("id") ByteArray id,
87+
@JsonProperty("id") ByteArray id,
88+
@JsonProperty("rawId") ByteArray rawId,
8889
@NonNull @JsonProperty("response") A response,
8990
@NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults,
9091
@NonNull @JsonProperty("type") PublicKeyCredentialType type
9192
) {
92-
this.id = id;
93+
if (id == null && rawId == null) {
94+
throw new NullPointerException("At least one of \"id\" and \"rawId\" must be non-null.");
95+
}
96+
if (id != null && rawId != null && !id.equals(rawId)) {
97+
throw new IllegalArgumentException(String.format("\"id\" and \"rawId\" are not equal: %s != %s", id, rawId));
98+
}
99+
100+
this.id = id == null ? rawId : id;
93101
this.response = response;
94102
this.clientExtensionResults = clientExtensionResults;
95103
this.type = type;
96104
}
97105

106+
private PublicKeyCredential(
107+
ByteArray id,
108+
@NonNull A response,
109+
@NonNull B clientExtensionResults,
110+
@NonNull PublicKeyCredentialType type
111+
) {
112+
this(id, null, response, clientExtensionResults, type);
113+
}
114+
98115
public static <A extends AuthenticatorResponse, B extends ClientExtensionOutputs> PublicKeyCredentialBuilder<A, B>.MandatoryStages builder() {
99116
return new PublicKeyCredentialBuilder<A, B>().start();
100117
}

webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,21 @@ import com.fasterxml.jackson.core.`type`.TypeReference
2929
import com.fasterxml.jackson.databind.DeserializationFeature
3030
import com.fasterxml.jackson.databind.ObjectMapper
3131
import com.fasterxml.jackson.databind.SerializationFeature
32+
import com.fasterxml.jackson.databind.exc.ValueInstantiationException
33+
import com.fasterxml.jackson.databind.node.ObjectNode
34+
import com.fasterxml.jackson.databind.node.TextNode
3235
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
3336
import com.yubico.webauthn.AssertionRequest
3437
import com.yubico.webauthn.AssertionResult
3538
import com.yubico.webauthn.Generators._
39+
import com.yubico.webauthn.RegisteredCredential
3640
import com.yubico.webauthn.RegistrationResult
3741
import com.yubico.webauthn.attestation.Attestation
3842
import com.yubico.webauthn.attestation.Generators._
3943
import com.yubico.webauthn.attestation.Transport
4044
import com.yubico.webauthn.data.Generators._
4145
import com.yubico.webauthn.extension.appid.AppId
4246
import com.yubico.webauthn.extension.appid.Generators._
43-
import com.yubico.webauthn.RegisteredCredential
4447
import org.junit.runner.RunWith
4548
import org.scalacheck.Arbitrary
4649
import org.scalatest.FunSpec
@@ -161,6 +164,106 @@ class JsonIoSpec extends FunSpec with Matchers with ScalaCheckDrivenPropertyChec
161164
}
162165
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
163166
}
167+
168+
it("allows rawId to be present without id.") {
169+
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
170+
forAll { value: P =>
171+
val encoded: String = json.writeValueAsString(value)
172+
val decoded = json.readTree(encoded)
173+
decoded.asInstanceOf[ObjectNode]
174+
.set[ObjectNode]("rawId", new TextNode(value.getId.getBase64Url))
175+
.remove("id")
176+
val reencoded = json.writeValueAsString(decoded)
177+
val restored: P = json.readValue(reencoded, tpe)
178+
179+
restored.getId should equal (value.getId)
180+
restored should equal (value)
181+
}
182+
}
183+
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
184+
test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
185+
}
186+
187+
it("allows id to be present without rawId.") {
188+
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
189+
forAll { value: P =>
190+
val encoded: String = json.writeValueAsString(value)
191+
val decoded = json.readTree(encoded)
192+
decoded.asInstanceOf[ObjectNode]
193+
.set[ObjectNode]("id", new TextNode(value.getId.getBase64Url))
194+
.remove("rawId")
195+
val reencoded = json.writeValueAsString(decoded)
196+
val restored: P = json.readValue(reencoded, tpe)
197+
198+
restored should equal (value)
199+
}
200+
}
201+
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
202+
test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
203+
}
204+
205+
it("allows both id and rawId to be present if equal.") {
206+
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
207+
forAll { value: P =>
208+
val encoded: String = json.writeValueAsString(value)
209+
val decoded = json.readTree(encoded)
210+
decoded.asInstanceOf[ObjectNode].set("id", new TextNode(value.getId.getBase64Url))
211+
decoded.asInstanceOf[ObjectNode].set("rawId", new TextNode(value.getId.getBase64Url))
212+
val reencoded = json.writeValueAsString(decoded)
213+
val restored: P = json.readValue(reencoded, tpe)
214+
215+
restored should equal (value)
216+
}
217+
}
218+
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
219+
test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
220+
}
221+
222+
it("does not allow both id and rawId to be absent.") {
223+
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
224+
forAll { value: P =>
225+
val encoded: String = json.writeValueAsString(value)
226+
val decoded = json.readTree(encoded).asInstanceOf[ObjectNode]
227+
decoded.remove("id")
228+
decoded.remove("rawId")
229+
val reencoded = json.writeValueAsString(decoded)
230+
231+
an [ValueInstantiationException] should be thrownBy {
232+
json.readValue(reencoded, tpe)
233+
}
234+
}
235+
}
236+
237+
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
238+
test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
239+
}
240+
241+
it("does not allow both id and rawId to be present and not equal.") {
242+
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit a: Arbitrary[P]): Unit = {
243+
forAll { value: P =>
244+
val modId = new ByteArray(
245+
if (value.getId.getBytes.isEmpty)
246+
Array(0)
247+
else
248+
value.getId.getBytes.updated(0, (value.getId.getBytes()(0) + 1 % 127).byteValue)
249+
)
250+
251+
val encoded: String = json.writeValueAsString(value)
252+
val decoded = json.readTree(encoded)
253+
decoded.asInstanceOf[ObjectNode]
254+
.set[ObjectNode]("id", new TextNode(value.getId.getBase64Url))
255+
.set[ObjectNode]("rawId", new TextNode(modId.getBase64Url))
256+
val reencoded = json.writeValueAsString(decoded)
257+
258+
an [ValueInstantiationException] should be thrownBy {
259+
json.readValue(reencoded, tpe)
260+
}
261+
}
262+
}
263+
264+
test(new TypeReference[PublicKeyCredential[AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs]](){})
265+
test(new TypeReference[PublicKeyCredential[AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs]](){})
266+
}
164267
}
165268

166269
}

0 commit comments

Comments
 (0)