fromString(@NonNull String value) {
- return Stream.of(values()).filter(v -> v.value.equals(value)).findAny();
- }
-
@JsonCreator
private static AuthenticatorAttachment fromJsonString(@NonNull String value) {
- return fromString(value)
- .orElseThrow(
- () ->
- new IllegalArgumentException(
- String.format(
- "Unknown %s value: %s",
- AuthenticatorAttachment.class.getSimpleName(), value)));
+ return Stream.of(values()).filter(v -> v.value.equals(value)).findAny().orElse(null);
}
}
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 3141912ca..ee0b5a38e 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
@@ -25,11 +25,16 @@
package com.yubico.webauthn.data;
import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.yubico.internal.util.JacksonCodecs;
+import com.yubico.webauthn.AssertionResult;
+import com.yubico.webauthn.FinishAssertionOptions;
+import com.yubico.webauthn.FinishRegistrationOptions;
+import com.yubico.webauthn.RegistrationResult;
+import com.yubico.webauthn.RelyingParty;
import java.io.IOException;
+import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NonNull;
@@ -46,7 +51,6 @@
*/
@Value
@Builder(toBuilder = true)
-@JsonIgnoreProperties({"authenticatorAttachment"})
public class PublicKeyCredential<
A extends AuthenticatorResponse, B extends ClientExtensionOutputs> {
@@ -68,6 +72,8 @@ public class PublicKeyCredential<
*/
@NonNull private final A response;
+ private final AuthenticatorAttachment authenticatorAttachment;
+
/**
* A map containing extension identifier → client extension output entries produced by the
* extension’s client extension processing.
@@ -83,6 +89,7 @@ private PublicKeyCredential(
@JsonProperty("id") ByteArray id,
@JsonProperty("rawId") ByteArray rawId,
@NonNull @JsonProperty("response") A response,
+ @JsonProperty("authenticatorAttachment") AuthenticatorAttachment authenticatorAttachment,
@NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults,
@NonNull @JsonProperty("type") PublicKeyCredentialType type) {
if (id == null && rawId == null) {
@@ -95,6 +102,7 @@ private PublicKeyCredential(
this.id = id == null ? rawId : id;
this.response = response;
+ this.authenticatorAttachment = authenticatorAttachment;
this.clientExtensionResults = clientExtensionResults;
this.type = type;
}
@@ -102,9 +110,33 @@ private PublicKeyCredential(
private PublicKeyCredential(
ByteArray id,
@NonNull A response,
+ AuthenticatorAttachment authenticatorAttachment,
@NonNull B clientExtensionResults,
@NonNull PublicKeyCredentialType type) {
- this(id, null, response, clientExtensionResults, type);
+ this(id, null, response, authenticatorAttachment, clientExtensionResults, type);
+ }
+
+ /**
+ * The authenticator
+ * attachment modality in effect at the time the credential was created or used.
+ *
+ * If parsed from JSON, this will be present if and only if the input was a valid value of
+ * {@link AuthenticatorAttachment}.
+ *
+ *
The same value will also be available via {@link
+ * RegistrationResult#getAuthenticatorAttachment()} or {@link
+ * AssertionResult#getAuthenticatorAttachment()} on the result from {@link
+ * RelyingParty#finishRegistration(FinishRegistrationOptions)} or {@link
+ * RelyingParty#finishAssertion(FinishAssertionOptions)}.
+ *
+ * @see RegistrationResult#getAuthenticatorAttachment()
+ * @see AssertionResult#getAuthenticatorAttachment()
+ * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
+ * the standard matures.
+ */
+ @Deprecated
+ public Optional getAuthenticatorAttachment() {
+ return Optional.ofNullable(authenticatorAttachment);
}
public static
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
index 8f9e309c7..f817ad746 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala
@@ -31,6 +31,7 @@ import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.JacksonCodecs
import com.yubico.webauthn.data.AssertionExtensionInputs
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
+import com.yubico.webauthn.data.AuthenticatorAttachment
import com.yubico.webauthn.data.AuthenticatorDataFlags
import com.yubico.webauthn.data.AuthenticatorTransport
import com.yubico.webauthn.data.ByteArray
@@ -2592,6 +2593,36 @@ class RelyingPartyAssertionSpec
resultWithBeOnly.isBackedUp should be(false)
resultWithBackup.isBackedUp should be(true)
}
+
+ it(
+ "exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
+ ) {
+ val pkcTemplate =
+ TestAuthenticator.createAssertion(
+ challenge =
+ request.getPublicKeyCredentialRequestOptions.getChallenge,
+ credentialKey = credentialKeypair,
+ credentialId = credential.getId,
+ )
+
+ forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
+ val pkc = pkcTemplate.toBuilder
+ .authenticatorAttachment(authenticatorAttachment.orNull)
+ .build()
+
+ val result = rp.finishAssertion(
+ FinishAssertionOptions
+ .builder()
+ .request(request)
+ .response(pkc)
+ .build()
+ )
+
+ result.getAuthenticatorAttachment should equal(
+ pkc.getAuthenticatorAttachment
+ )
+ }
+ }
}
}
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
index 983cd281d..a737f23b4 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala
@@ -42,6 +42,7 @@ import com.yubico.webauthn.attestation.AttestationTrustSource
import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult
import com.yubico.webauthn.data.AttestationObject
import com.yubico.webauthn.data.AttestationType
+import com.yubico.webauthn.data.AuthenticatorAttachment
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.AuthenticatorData
import com.yubico.webauthn.data.AuthenticatorDataFlags
@@ -4619,6 +4620,33 @@ class RelyingPartyRegistrationSpec
resultWithBeOnly.isBackedUp should be(false)
resultWithBackup.isBackedUp should be(true)
}
+
+ it(
+ "exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
+ ) {
+ val (pkcTemplate, _, _) =
+ TestAuthenticator.createUnattestedCredential(challenge =
+ request.getChallenge
+ )
+
+ forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
+ val pkc = pkcTemplate.toBuilder
+ .authenticatorAttachment(authenticatorAttachment.orNull)
+ .build()
+
+ val result = rp.finishRegistration(
+ FinishRegistrationOptions
+ .builder()
+ .request(request)
+ .response(pkc)
+ .build()
+ )
+
+ result.getAuthenticatorAttachment should equal(
+ pkc.getAuthenticatorAttachment
+ )
+ }
+ }
}
}
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala
index 8e35e8558..dbe5ca609 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala
@@ -61,11 +61,10 @@ class EnumsSpec
describe("AuthenticatorAttachment") {
describe("can be parsed from JSON") {
- it("but throws IllegalArgumentException for unknown values.") {
- val result = Try(
+ it("and ignores for unknown values.") {
+ val result =
json.readValue("\"foo\"", classOf[AuthenticatorAttachment])
- )
- result.failed.get.getCause shouldBe an[IllegalArgumentException]
+ result should be(null)
}
}
}
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 29cfa491f..0fd026f9b 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
@@ -41,12 +41,13 @@ 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
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
+import scala.jdk.OptionConverters.RichOptional
+
@RunWith(classOf[JUnitRunner])
class JsonIoSpec
extends AnyFunSpec
@@ -351,15 +352,16 @@ class JsonIoSpec
)
}
- it("allows and ignores an authenticatorAttachment attribute.") {
+ it(
+ "allows an authenticatorAttachment attribute, but ignores unknown values."
+ ) {
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit
a: Arbitrary[P]
): Unit = {
forAll(
a.arbitrary,
- Gen.oneOf(
- arbitrary[AuthenticatorAttachment].map(_.getValue),
- arbitrary[String],
+ arbitrary[String].suchThat(s =>
+ !AuthenticatorAttachment.values.map(_.getValue).contains(s)
),
) { (value: P, authenticatorAttachment: String) =>
val tree: ObjectNode = json.valueToTree(value)
@@ -370,8 +372,37 @@ class JsonIoSpec
val encoded = json.writeValueAsString(tree)
println(authenticatorAttachment)
val decoded = json.readValue(encoded, tpe)
+ decoded.getAuthenticatorAttachment.asScala should be(None)
+ }
+
+ forAll(
+ a.arbitrary,
+ arbitrary[AuthenticatorAttachment],
+ ) { (value: P, authenticatorAttachment: AuthenticatorAttachment) =>
+ val tree: ObjectNode = json.valueToTree(value)
+ tree.set(
+ "authenticatorAttachment",
+ new TextNode(authenticatorAttachment.getValue),
+ )
+ val encoded = json.writeValueAsString(tree)
+ println(authenticatorAttachment)
+ val decoded = json.readValue(encoded, tpe)
+
+ decoded.getAuthenticatorAttachment.asScala should equal(
+ Some(authenticatorAttachment)
+ )
+ }
+
+ forAll(
+ a.arbitrary
+ ) { (value: P) =>
+ val tree: ObjectNode = json.valueToTree(
+ value.toBuilder.authenticatorAttachment(null).build()
+ )
+ val encoded = json.writeValueAsString(tree)
+ val decoded = json.readValue(encoded, tpe)
- decoded should equal(value)
+ decoded.getAuthenticatorAttachment.asScala should be(None)
}
}