diff --git a/NEWS b/NEWS
index add4060bb..6c8660ee9 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,11 @@
+== Version 1.3.0 (unreleased) ==
+
+New features:
+
+* New optional parameter `timeout` added to `StartRegistrationOptions` and
+ `StartAssertionOptions`
+
+
== Version 1.2.0 ==
New features:
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java
index b40c51162..6d521d6ff 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java
@@ -292,6 +292,7 @@ public PublicKeyCredentialCreationOptions startRegistration(StartRegistrationOpt
)
.authenticatorSelection(startRegistrationOptions.getAuthenticatorSelection())
.extensions(startRegistrationOptions.getExtensions())
+ .timeout(startRegistrationOptions.getTimeout())
;
attestationConveyancePreference.ifPresent(builder::attestation);
return builder.build();
@@ -344,6 +345,7 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio
.appid(appId)
.build()
)
+ .timeout(startAssertionOptions.getTimeout())
;
startAssertionOptions.getUserVerification().ifPresent(pkcro::userVerification);
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java
index 5d958f020..e503be09f 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java
@@ -80,9 +80,19 @@ public class StartAssertionOptions {
@NonNull
private final Optional
+ * The default is empty.
+ *
+ * The default is empty. + *
+ */ + public StartAssertionOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); + } } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java index 3660c86c9..f2f87eed3 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java @@ -25,6 +25,7 @@ package com.yubico.webauthn; import com.yubico.webauthn.data.AuthenticatorSelectionCriteria; +import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions; import com.yubico.webauthn.data.RegistrationExtensionInputs; import com.yubico.webauthn.data.UserIdentity; import java.util.Optional; @@ -58,12 +59,22 @@ public class StartRegistrationOptions { @Builder.Default private final RegistrationExtensionInputs extensions = RegistrationExtensionInputs.builder().build(); + /** + * The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration operation. + *+ * The default is empty. + *
+ */ + @NonNull + private final Optional+ * The default is empty. + *
+ */ + public StartRegistrationOptionsBuilder timeout(@NonNull Optional+ * The default is empty. + *
+ */ + public StartRegistrationOptionsBuilder timeout(long timeout) { + return this.timeout(Optional.of(timeout)); + } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index df7339445..5d61940dd 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala @@ -28,6 +28,8 @@ import java.util.Optional import com.yubico.internal.util.scala.JavaConverters._ import com.yubico.scalacheck.gen.JavaGenerators._ +import com.yubico.webauthn.data.AuthenticatorAttachment +import com.yubico.webauthn.data.AuthenticatorSelectionCriteria import com.yubico.webauthn.data.PublicKeyCredentialDescriptor import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.UserIdentity @@ -38,6 +40,7 @@ import com.yubico.webauthn.extension.appid.AppId import com.yubico.webauthn.extension.appid.Generators._ import org.junit.runner.RunWith import org.scalacheck.Arbitrary._ +import org.scalacheck.Gen import org.scalatest.FunSpec import org.scalatest.Matchers import org.scalatest.junit.JUnitRunner @@ -104,6 +107,56 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with Generato request2.getChallenge.size should be >= 32 } + it("allows setting the timeout to empty.") { + val pkcco = relyingParty().startRegistration( + StartRegistrationOptions.builder() + .user(userId) + .timeout(Optional.empty[java.lang.Long]) + .build()) + pkcco.getTimeout.asScala shouldBe 'empty + } + + it("allows setting the timeout to a positive value.") { + val rp = relyingParty() + + forAll(Gen.posNum[Long]) { timeout: Long => + val pkcco = rp.startRegistration( + StartRegistrationOptions.builder() + .user(userId) + .timeout(timeout) + .build()) + + pkcco.getTimeout.asScala should equal (Some(timeout)) + } + } + + it("does not allow setting the timeout to zero or negative.") { + an [IllegalArgumentException] should be thrownBy { + StartRegistrationOptions.builder() + .user(userId) + .timeout(0) + } + + an [IllegalArgumentException] should be thrownBy { + StartRegistrationOptions.builder() + .user(userId) + .timeout(Optional.of[java.lang.Long](0L)) + } + + forAll(Gen.negNum[Long]) { timeout: Long => + an [IllegalArgumentException] should be thrownBy { + StartRegistrationOptions.builder() + .user(userId) + .timeout(timeout) + } + + an [IllegalArgumentException] should be thrownBy { + StartRegistrationOptions.builder() + .user(userId) + .timeout(Optional.of[java.lang.Long](timeout)) + } + } + } } describe("RelyingParty.startAssertion") { @@ -152,6 +205,50 @@ class RelyingPartyStartOperationSpec extends FunSpec with Matchers with Generato } } + it("allows setting the timeout to empty.") { + val req = relyingParty().startAssertion( + StartAssertionOptions.builder() + .timeout(Optional.empty[java.lang.Long]) + .build()) + req.getPublicKeyCredentialRequestOptions.getTimeout.asScala shouldBe 'empty + } + + it("allows setting the timeout to a positive value.") { + val rp = relyingParty() + + forAll(Gen.posNum[Long]) { timeout: Long => + val req = rp.startAssertion( + StartAssertionOptions.builder() + .timeout(timeout) + .build()) + + req.getPublicKeyCredentialRequestOptions.getTimeout.asScala should equal (Some(timeout)) + } + } + + it("does not allow setting the timeout to zero or negative.") { + an [IllegalArgumentException] should be thrownBy { + StartAssertionOptions.builder() + .timeout(0) + } + + an [IllegalArgumentException] should be thrownBy { + StartAssertionOptions.builder() + .timeout(Optional.of[java.lang.Long](0L)) + } + + forAll(Gen.negNum[Long]) { timeout: Long => + an [IllegalArgumentException] should be thrownBy { + StartAssertionOptions.builder() + .timeout(timeout) + } + + an [IllegalArgumentException] should be thrownBy { + StartAssertionOptions.builder() + .timeout(Optional.of[java.lang.Long](timeout)) + } + } + } } }