diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java
index 8d5c70145..00ebe1c0a 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/RegistrationExtensionInputs.java
@@ -25,6 +25,7 @@
package com.yubico.webauthn.data;
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.webauthn.RelyingParty;
@@ -60,15 +61,13 @@ public final class RegistrationExtensionInputs implements ExtensionInputs {
private final Extensions.Prf.PrfRegistrationInput prf;
private final Boolean uvm;
- @JsonCreator
private RegistrationExtensionInputs(
- @JsonProperty("appidExclude") AppId appidExclude,
- @JsonProperty("credProps") Boolean credProps,
- @JsonProperty("credProtect")
- Extensions.CredentialProtection.CredentialProtectionInput credProtect,
- @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob,
- @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf,
- @JsonProperty("uvm") Boolean uvm) {
+ AppId appidExclude,
+ Boolean credProps,
+ Extensions.CredentialProtection.CredentialProtectionInput credProtect,
+ Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob,
+ Extensions.Prf.PrfRegistrationInput prf,
+ Boolean uvm) {
this.appidExclude = appidExclude;
this.credProps = credProps;
this.credProtect = credProtect;
@@ -77,6 +76,32 @@ private RegistrationExtensionInputs(
this.uvm = uvm;
}
+ @JsonCreator
+ private RegistrationExtensionInputs(
+ @JsonProperty("appidExclude") AppId appidExclude,
+ @JsonProperty("credProps") Boolean credProps,
+ @JsonProperty("credentialProtectionPolicy")
+ Extensions.CredentialProtection.CredentialProtectionPolicy credProtectPolicy,
+ @JsonProperty("enforceCredentialProtectionPolicy") Boolean enforceCredProtectPolicy,
+ @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobRegistrationInput largeBlob,
+ @JsonProperty("prf") Extensions.Prf.PrfRegistrationInput prf,
+ @JsonProperty("uvm") Boolean uvm) {
+ this(
+ appidExclude,
+ credProps,
+ Optional.ofNullable(credProtectPolicy)
+ .map(
+ policy -> {
+ return enforceCredProtectPolicy != null && enforceCredProtectPolicy
+ ? Extensions.CredentialProtection.CredentialProtectionInput.require(policy)
+ : Extensions.CredentialProtection.CredentialProtectionInput.prefer(policy);
+ })
+ .orElse(null),
+ largeBlob,
+ prf,
+ uvm);
+ }
+
/**
* Merge other into this. Non-null field values from this
* take precedence.
@@ -118,6 +143,12 @@ public boolean getCredProps() {
return credProps != null && credProps;
}
+ /** For JSON serialization, to omit false values. */
+ @JsonProperty("credProps")
+ private Boolean getCredPropsJson() {
+ return getCredProps() ? true : null;
+ }
+
/**
* @return The Credential Protection (credProtect) extension input, if set.
* @since 2.7.0
@@ -127,14 +158,34 @@ public boolean getCredProps() {
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#sctn-authenticator-credential-properties-extension">ยง10.4.
* Credential Properties Extension (credProps)
*/
+ @JsonIgnore
public Optional getCredProtect() {
return Optional.ofNullable(credProtect);
}
- /** For JSON serialization, to omit false values. */
- @JsonProperty("credProps")
- private Boolean getCredPropsJson() {
- return getCredProps() ? true : null;
+ /**
+ * For JSON serialization, because credProtect does not group all inputs under the "credProtect"
+ * key.
+ */
+ @JsonProperty("credentialProtectionPolicy")
+ private Optional
+ getCredProtectPolicy() {
+ return getCredProtect()
+ .map(
+ Extensions.CredentialProtection.CredentialProtectionInput
+ ::getCredentialProtectionPolicy);
+ }
+
+ /**
+ * For JSON serialization, because credProtect does not group all inputs under the "credProtect"
+ * key.
+ */
+ @JsonProperty("enforceCredentialProtectionPolicy")
+ private Optional getEnforceCredProtectPolicy() {
+ return getCredProtect()
+ .map(
+ Extensions.CredentialProtection.CredentialProtectionInput
+ ::isEnforceCredentialProtectionPolicy);
}
/**
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala
index 30080c42c..4840b9960 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/ExtensionsSpec.scala
@@ -3,11 +3,16 @@ package com.yubico.webauthn.data
import com.fasterxml.jackson.databind.node.ObjectNode
import com.yubico.internal.util.JacksonCodecs
import com.yubico.scalacheck.gen.JacksonGenerators.arbitraryObjectNode
+import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionInput
+import com.yubico.webauthn.data.Extensions.CredentialProtection.CredentialProtectionPolicy
import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput
import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput
import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput
import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationInput.LargeBlobSupport
import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobRegistrationOutput
+import com.yubico.webauthn.data.Extensions.Prf.PrfAuthenticationInput
+import com.yubico.webauthn.data.Extensions.Prf.PrfRegistrationInput
+import com.yubico.webauthn.data.Extensions.Prf.PrfValues
import com.yubico.webauthn.data.Generators.arbitraryAssertionExtensionInputs
import com.yubico.webauthn.data.Generators.arbitraryClientRegistrationExtensionOutputs
import com.yubico.webauthn.data.Generators.arbitraryRegistrationExtensionInputs
@@ -23,6 +28,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import java.nio.charset.StandardCharsets
import scala.jdk.CollectionConverters.IteratorHasAsScala
+import scala.jdk.CollectionConverters.MapHasAsJava
import scala.jdk.CollectionConverters.SetHasAsScala
import scala.jdk.OptionConverters.RichOptional
@@ -34,14 +40,23 @@ class ExtensionsSpec
describe("RegistrationExtensionInputs") {
describe("has a getExtensionIds() method which") {
- it("contains exactly the names of contained extensions.") {
+ it("contains exactly the names of contained extensions, except for credProtect.") {
forAll { input: RegistrationExtensionInputs =>
+ val expectedJsonKeys = input.getExtensionIds.asScala.flatMap(id => {
+ if (id == "credProtect") {
+ // credProtect does not gather all inputs under the extension ID as a map key.
+ List(
+ "credentialProtectionPolicy",
+ "enforceCredentialProtectionPolicy",
+ )
+ } else {
+ List(id)
+ }
+ })
val json = JacksonCodecs.json().valueToTree[ObjectNode](input)
val jsonKeyNames = json.fieldNames.asScala.toList
- val extensionIds = input.getExtensionIds
- jsonKeyNames.length should equal(extensionIds.size)
- jsonKeyNames.toSet should equal(extensionIds.asScala)
+ jsonKeyNames.toSet should equal(expectedJsonKeys)
}
}
}
@@ -70,9 +85,17 @@ class ExtensionsSpec
"""{
|"appidExclude": "https://example.org",
|"credProps": true,
+ |"credentialProtectionPolicy": "userVerificationRequired",
+ |"enforceCredentialProtectionPolicy": true,
|"largeBlob": {
| "support": "required"
|},
+ |"prf": {
+ | "eval": {
+ | "first": "AAAA",
+ | "second": "BBBB"
+ | }
+ |},
|"uvm": true
|}""".stripMargin
@@ -85,14 +108,39 @@ class ExtensionsSpec
decoded should not be null
decoded.getExtensionIds.asScala should equal(
- Set("appidExclude", "credProps", "largeBlob", "uvm")
+ Set(
+ "appidExclude",
+ "credProps",
+ "credProtect",
+ "largeBlob",
+ "prf",
+ "uvm",
+ )
)
decoded.getAppidExclude.toScala should equal(
Some(new AppId("https://example.org"))
)
+ decoded.getCredProps should equal(true)
+ decoded.getCredProtect.toScala should equal(
+ Some(
+ CredentialProtectionInput.require(
+ CredentialProtectionPolicy.UV_REQUIRED
+ )
+ )
+ )
decoded.getLargeBlob.toScala should equal(
Some(new LargeBlobRegistrationInput(LargeBlobSupport.REQUIRED))
)
+ decoded.getPrf.toScala should equal(
+ Some(
+ PrfRegistrationInput.eval(
+ PrfValues.two(
+ ByteArray.fromBase64Url("AAAA"),
+ ByteArray.fromBase64Url("BBBB"),
+ )
+ )
+ )
+ )
decoded.getUvm should be(true)
redecoded should equal(decoded)
@@ -160,6 +208,21 @@ class ExtensionsSpec
|"largeBlob": {
| "read": true
|},
+ |"prf": {
+ | "eval": {
+ | "first": "AAAA",
+ | "second": "BBBB"
+ | },
+ | "evalByCredential": {
+ | "CCCC": {
+ | "first": "DDDD"
+ | },
+ | "EEEE": {
+ | "first": "FFFF",
+ | "second": "GGGG"
+ | }
+ | }
+ |},
|"uvm": true
|}""".stripMargin
@@ -172,7 +235,7 @@ class ExtensionsSpec
decoded should not be null
decoded.getExtensionIds.asScala should equal(
- Set("appid", "largeBlob", "uvm")
+ Set("appid", "largeBlob", "prf", "uvm")
)
decoded.getAppid.toScala should equal(
Some(new AppId("https://example.org"))
@@ -180,6 +243,29 @@ class ExtensionsSpec
decoded.getLargeBlob.toScala should equal(
Some(LargeBlobAuthenticationInput.read())
)
+ decoded.getPrf.toScala should equal(
+ Some(
+ PrfAuthenticationInput.evalByCredentialWithFallback(
+ Map(
+ PublicKeyCredentialDescriptor
+ .builder()
+ .id(ByteArray.fromBase64Url("CCCC"))
+ .build() -> PrfValues.one(ByteArray.fromBase64Url("DDDD")),
+ PublicKeyCredentialDescriptor
+ .builder()
+ .id(ByteArray.fromBase64Url("EEEE"))
+ .build() -> PrfValues.two(
+ ByteArray.fromBase64Url("FFFF"),
+ ByteArray.fromBase64Url("GGGG"),
+ ),
+ ).asJava,
+ PrfValues.two(
+ ByteArray.fromBase64Url("AAAA"),
+ ByteArray.fromBase64Url("BBBB"),
+ ),
+ )
+ )
+ )
decoded.getUvm should be(true)
redecoded should equal(decoded)