Skip to content

Commit

Permalink
Merge pull request #104 from Yubico/allow-rawid
Browse files Browse the repository at this point in the history
Allow rawId in PublicKeyCredential deserialization
  • Loading branch information
emlun authored Mar 15, 2021
2 parents dc2bc5e + d2b018c commit 55b69d0
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 3 deletions.
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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 ==

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,34 @@ public class PublicKeyCredential<A extends AuthenticatorResponse, B extends Clie

@JsonCreator
private PublicKeyCredential(
@NonNull @JsonProperty("id") ByteArray id,
@JsonProperty("id") ByteArray id,
@JsonProperty("rawId") ByteArray rawId,
@NonNull @JsonProperty("response") A response,
@NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults,
@NonNull @JsonProperty("type") PublicKeyCredentialType type
) {
this.id = id;
if (id == null && rawId == null) {
throw new NullPointerException("At least one of \"id\" and \"rawId\" must be non-null.");
}
if (id != null && rawId != null && !id.equals(rawId)) {
throw new IllegalArgumentException(String.format("\"id\" and \"rawId\" are not equal: %s != %s", id, rawId));
}

this.id = id == null ? rawId : id;
this.response = response;
this.clientExtensionResults = clientExtensionResults;
this.type = type;
}

private PublicKeyCredential(
ByteArray id,
@NonNull A response,
@NonNull B clientExtensionResults,
@NonNull PublicKeyCredentialType type
) {
this(id, null, response, clientExtensionResults, type);
}

public static <A extends AuthenticatorResponse, B extends ClientExtensionOutputs> PublicKeyCredentialBuilder<A, B>.MandatoryStages builder() {
return new PublicKeyCredentialBuilder<A, B>().start();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ 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._
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
Expand Down Expand Up @@ -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]](){})
}
}

}

0 comments on commit 55b69d0

Please sign in to comment.