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 f8e588eb6..c24ad4e97 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
@@ -493,7 +493,8 @@ public PublicKeyCredentialCreationOptions startRegistration(
.appidExclude(appId)
.credProps()
.build()))
- .timeout(startRegistrationOptions.getTimeout());
+ .timeout(startRegistrationOptions.getTimeout())
+ .hints(startRegistrationOptions.getHints());
attestationConveyancePreference.ifPresent(builder::attestation);
return builder.build();
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java
index 23a71c5bf..495524d12 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java
@@ -452,7 +452,8 @@ public PublicKeyCredentialCreationOptions startRegistration(
.appidExclude(appId)
.credProps()
.build()))
- .timeout(startRegistrationOptions.getTimeout());
+ .timeout(startRegistrationOptions.getTimeout())
+ .hints(startRegistrationOptions.getHints());
attestationConveyancePreference.ifPresent(builder::attestation);
return builder.build();
}
@@ -509,7 +510,8 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio
startAssertionOptions
.getExtensions()
.merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
- .timeout(startAssertionOptions.getTimeout());
+ .timeout(startAssertionOptions.getTimeout())
+ .hints(startAssertionOptions.getHints());
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 461f31228..c02672b8f 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
@@ -26,17 +26,25 @@
import com.yubico.webauthn.data.AssertionExtensionInputs;
import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.PublicKeyCredentialHint;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.UserVerificationRequirement;
-import java.util.Optional;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
-/** Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. */
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}.
+ */
@Value
@Builder(toBuilder = true)
-public class StartAssertionOptions {
+public final class StartAssertionOptions {
private final String username;
@@ -51,7 +59,8 @@ public class StartAssertionOptions {
*
*
The default specifies no extension inputs.
*/
- @NonNull @Builder.Default
+ @NonNull
+ @Builder.Default
private final AssertionExtensionInputs extensions = AssertionExtensionInputs.builder().build();
/**
@@ -79,6 +88,18 @@ public class StartAssertionOptions {
*/
private final Long timeout;
+ private final List hints;
+
+ private StartAssertionOptions(String username, ByteArray userHandle, @NonNull AssertionExtensionInputs extensions, UserVerificationRequirement userVerification, Long timeout, List hints) {
+ this.username = username;
+ this.userHandle = userHandle;
+ this.extensions = extensions;
+ this.userVerification = userVerification;
+ this.timeout = timeout;
+ this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
+ }
+
+
/**
* The username of the user to authenticate, if the user has already been identified.
*
@@ -96,10 +117,10 @@ public class StartAssertionOptions {
* The default is empty (absent).
*
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public Optional getUsername() {
return Optional.ofNullable(username);
@@ -124,10 +145,10 @@ public Optional getUsername() {
* @see #getUsername()
* @see User Handle
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public Optional getUserHandle() {
return Optional.ofNullable(userHandle);
@@ -189,10 +210,10 @@ public static class StartAssertionOptionsBuilder {
* @see #userHandle(Optional)
* @see #userHandle(ByteArray)
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public StartAssertionOptionsBuilder username(@NonNull Optional username) {
this.username = username.orElse(null);
@@ -223,10 +244,10 @@ public StartAssertionOptionsBuilder username(@NonNull Optional username)
* @see #userHandle(Optional)
* @see #userHandle(ByteArray)
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public StartAssertionOptionsBuilder username(String username) {
return this.username(Optional.ofNullable(username));
@@ -253,12 +274,12 @@ public StartAssertionOptionsBuilder username(String username) {
* @see #username(Optional)
* @see #userHandle(ByteArray)
* @see User
- * Handle
+ * Handle
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public StartAssertionOptionsBuilder userHandle(@NonNull Optional userHandle) {
this.userHandle = userHandle.orElse(null);
@@ -289,10 +310,10 @@ public StartAssertionOptionsBuilder userHandle(@NonNull Optional user
* @see #username(Optional)
* @see #userHandle(Optional)
* @see Client-side-discoverable
- * credential
+ * href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-discoverable
+ * credential
* @see Passkey in passkeys.dev reference
+ * href="https://passkeys.dev">passkeys.dev reference
*/
public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) {
return this.userHandle(Optional.ofNullable(userHandle));
@@ -310,7 +331,7 @@ public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) {
* The default is {@link UserVerificationRequirement#PREFERRED}.
*/
public StartAssertionOptionsBuilder userVerification(
- @NonNull Optional userVerification) {
+ @NonNull Optional userVerification) {
this.userVerification = userVerification.orElse(null);
return this;
}
@@ -327,7 +348,7 @@ public StartAssertionOptionsBuilder userVerification(
* The default is {@link UserVerificationRequirement#PREFERRED}.
*/
public StartAssertionOptionsBuilder userVerification(
- UserVerificationRequirement userVerification) {
+ UserVerificationRequirement userVerification) {
return this.userVerification(Optional.ofNullable(userVerification));
}
@@ -370,5 +391,19 @@ public StartAssertionOptionsBuilder timeout(long timeout) {
private StartAssertionOptionsBuilder timeout(Long timeout) {
return this.timeout(Optional.ofNullable(timeout));
}
+
+ public StartAssertionOptionsBuilder hints(@NonNull String... hints) {
+ this.hints = Arrays.asList(hints);
+ return this;
+ }
+
+ public StartAssertionOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
+ return this.hints(Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
+ }
+
+ public StartAssertionOptionsBuilder hints(@NonNull List hints) {
+ this.hints = hints;
+ return this;
+ }
}
}
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 e78184fb5..e64d265da 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
@@ -26,20 +26,30 @@
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
+import com.yubico.webauthn.data.PublicKeyCredentialHint;
import com.yubico.webauthn.data.RegistrationExtensionInputs;
import com.yubico.webauthn.data.UserIdentity;
-import java.util.Optional;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
-/** Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}. */
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}.
+ */
@Value
@Builder(toBuilder = true)
-public class StartRegistrationOptions {
+public final class StartRegistrationOptions {
- /** Identifiers for the user creating a credential. */
- @NonNull private final UserIdentity user;
+ /**
+ * Identifiers for the user creating a credential.
+ */
+ @NonNull
+ private final UserIdentity user;
/**
* Constraints on what kind of authenticator the user is allowed to use to create the credential,
@@ -47,10 +57,13 @@ public class StartRegistrationOptions {
*/
private final AuthenticatorSelectionCriteria authenticatorSelection;
- /** Extension inputs for this registration operation. */
- @NonNull @Builder.Default
+ /**
+ * Extension inputs for this registration operation.
+ */
+ @NonNull
+ @Builder.Default
private final RegistrationExtensionInputs extensions =
- RegistrationExtensionInputs.builder().build();
+ RegistrationExtensionInputs.builder().build();
/**
* The value for {@link PublicKeyCredentialCreationOptions#getTimeout()} for this registration
@@ -64,6 +77,17 @@ public class StartRegistrationOptions {
*/
private final Long timeout;
+ private final List hints;
+
+ private StartRegistrationOptions(@NonNull UserIdentity user, AuthenticatorSelectionCriteria authenticatorSelection, @NonNull RegistrationExtensionInputs extensions, Long timeout, List hints) {
+ this.user = user;
+ this.authenticatorSelection = authenticatorSelection;
+ this.extensions = extensions;
+ this.timeout = timeout;
+ this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
+ }
+
+
/**
* Constraints on what kind of authenticator the user is allowed to use to create the credential,
* and on features that authenticator must or should support.
@@ -112,7 +136,7 @@ public StartRegistrationOptionsBuilder user(UserIdentity user) {
* credential, and on features that authenticator must or should support.
*/
public StartRegistrationOptionsBuilder authenticatorSelection(
- @NonNull Optional authenticatorSelection) {
+ @NonNull Optional authenticatorSelection) {
return this.authenticatorSelection(authenticatorSelection.orElse(null));
}
@@ -121,7 +145,7 @@ public StartRegistrationOptionsBuilder authenticatorSelection(
* credential, and on features that authenticator must or should support.
*/
public StartRegistrationOptionsBuilder authenticatorSelection(
- AuthenticatorSelectionCriteria authenticatorSelection) {
+ AuthenticatorSelectionCriteria authenticatorSelection) {
this.authenticatorSelection = authenticatorSelection;
return this;
}
@@ -157,5 +181,20 @@ public StartRegistrationOptionsBuilder timeout(@NonNull Optional timeout)
public StartRegistrationOptionsBuilder timeout(long timeout) {
return this.timeout(Optional.of(timeout));
}
+
+
+ public StartRegistrationOptionsBuilder hints(@NonNull String... hints) {
+ this.hints = Arrays.asList(hints);
+ return this;
+ }
+
+ public StartRegistrationOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
+ return this.hints(Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
+ }
+
+ public StartRegistrationOptionsBuilder hints(@NonNull List hints) {
+ this.hints = hints;
+ return this;
+ }
}
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java
index 3d2b6033f..e4ff9e4d8 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java
@@ -36,12 +36,14 @@
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
+
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
@@ -94,6 +96,8 @@ public class PublicKeyCredentialCreationOptions {
*/
private final Long timeout;
+ private final List hints;
+
/**
* Intended for use by Relying Parties that wish to limit the creation of multiple credentials for
* the same account on a single authenticator. The client is requested to return an error if the
@@ -136,6 +140,7 @@ private PublicKeyCredentialCreationOptions(
@NonNull @JsonProperty("pubKeyCredParams")
List pubKeyCredParams,
@JsonProperty("timeout") Long timeout,
+ @JsonProperty("hints") List hints,
@JsonProperty("excludeCredentials") Set excludeCredentials,
@JsonProperty("authenticatorSelection") AuthenticatorSelectionCriteria authenticatorSelection,
@JsonProperty("attestation") AttestationConveyancePreference attestation,
@@ -145,6 +150,7 @@ private PublicKeyCredentialCreationOptions(
this.challenge = challenge;
this.pubKeyCredParams = filterAvailableAlgorithms(pubKeyCredParams);
this.timeout = timeout;
+ this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
this.excludeCredentials =
excludeCredentials == null
? null
@@ -317,6 +323,20 @@ public PublicKeyCredentialCreationOptionsBuilder timeout(long timeout) {
return this.timeout(Optional.of(timeout));
}
+ public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull String... hints) {
+ this.hints = Arrays.asList(hints);
+ return this;
+ }
+
+ public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
+ return this.hints(Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
+ }
+
+ public PublicKeyCredentialCreationOptionsBuilder hints(List hints) {
+ this.hints = hints;
+ return this;
+ }
+
/**
* Intended for use by Relying Parties that wish to limit the creation of multiple credentials
* for the same account on a single authenticator. The client is requested to return an error if
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java
new file mode 100644
index 000000000..922de36d3
--- /dev/null
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2018, Yubico AB
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package com.yubico.webauthn.data;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.stream.Stream;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.NonNull;
+import lombok.Value;
+
+import static com.yubico.webauthn.data.AuthenticatorTransport.BLE;
+
+/**
+ * Authenticators may communicate with Clients using a variety of transports. This enumeration
+ * defines a hint as to how Clients might communicate with a particular Authenticator in order to
+ * obtain an assertion for a specific credential. Note that these hints represent the Relying
+ * Party's best belief as to how an Authenticator may be reached. A Relying Party may obtain a list
+ * of transports hints from some attestation statement formats or via some out-of-band mechanism; it
+ * is outside the scope of this specification to define that mechanism.
+ *
+ * Authenticators may implement various transports for communicating with clients. This
+ * enumeration defines hints as to how clients might communicate with a particular authenticator in
+ * order to obtain an assertion for a specific credential. Note that these hints represent the
+ * WebAuthn Relying Party's best belief as to how an authenticator may be reached. A Relying Party
+ * may obtain a list of transports hints from some attestation statement formats or via some
+ * out-of-band mechanism; it is outside the scope of the Web Authentication specification to define
+ * that mechanism.
+ *
+ * @see ยง5.10.4.
+ * Authenticator Transport Enumeration (enum AuthenticatorTransport)
+ */
+@Value
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class PublicKeyCredentialHint {
+
+ @JsonValue @NonNull private final String value;
+
+ /**
+ * Indicates the respective authenticator can be contacted over removable USB.
+ *
+ * @see 5.8.4.
+ * Authenticator Transport Enumeration (enum AuthenticatorTransport)
+ */
+ public static final PublicKeyCredentialHint SECURITY_KEY = new PublicKeyCredentialHint("security-key");
+
+ /**
+ * Indicates the respective authenticator can be contacted over Near Field Communication (NFC).
+ *
+ * @see 5.8.4.
+ * Authenticator Transport Enumeration (enum AuthenticatorTransport)
+ */
+ public static final PublicKeyCredentialHint CLIENT_DEVICE = new PublicKeyCredentialHint("client-device");
+
+ /**
+ * Indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low
+ * Energy / BLE).
+ *
+ * @see 5.8.4.
+ * Authenticator Transport Enumeration (enum AuthenticatorTransport)
+ */
+ public static final PublicKeyCredentialHint HYBRID = new PublicKeyCredentialHint("hybrid");
+
+ /**
+ * @return An array containing all predefined values of {@link AuthenticatorTransport} known by
+ * this implementation.
+ */
+ public static PublicKeyCredentialHint[] values() {
+ return new PublicKeyCredentialHint[] {SECURITY_KEY, CLIENT_DEVICE, HYBRID};
+ }
+
+ /**
+ * @return If id
is the same as that of any of {@link #USB}, {@link #NFC}, {@link
+ * #BLE}, {@link #HYBRID} or {@link #INTERNAL}, returns that constant instance. Otherwise
+ * returns a new instance containing id
.
+ * @see #valueOf(String)
+ */
+ @JsonCreator
+ public static PublicKeyCredentialHint of(@NonNull String value) {
+ return Stream.of(values())
+ .filter(v -> v.getValue().equals(value))
+ .findAny()
+ .orElseGet(() -> new PublicKeyCredentialHint(value));
+ }
+
+ /**
+ * @return If name
equals "USB"
, "NFC"
, "BLE"
,
+ * "HYBRID"
or "INTERNAL"
, returns the constant by that name.
+ * @throws IllegalArgumentException if name
is anything else.
+ * @see #of(String)
+ */
+ public static PublicKeyCredentialHint valueOf(String name) {
+ switch (name) {
+ case "SECURITY_KEY":
+ return SECURITY_KEY;
+ case "CLIENT_DEVICE":
+ return CLIENT_DEVICE;
+ case "HYBRID":
+ return HYBRID;
+ default:
+ throw new IllegalArgumentException(
+ "No constant com.yubico.webauthn.data.PublicKeyCredentialHint." + name);
+ }
+ }
+}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java
index 4834d81a4..78ee88653 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java
@@ -31,6 +31,9 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.yubico.internal.util.CollectionUtil;
import com.yubico.internal.util.JacksonCodecs;
+
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import lombok.Builder;
@@ -66,6 +69,8 @@ public class PublicKeyCredentialRequestOptions {
*/
private final Long timeout;
+ private final List hints;
+
/**
* Specifies the relying party identifier claimed by the caller.
*
@@ -112,12 +117,14 @@ public class PublicKeyCredentialRequestOptions {
private PublicKeyCredentialRequestOptions(
@NonNull @JsonProperty("challenge") ByteArray challenge,
@JsonProperty("timeout") Long timeout,
+ @JsonProperty("hints") List hints,
@JsonProperty("rpId") String rpId,
@JsonProperty("allowCredentials") List allowCredentials,
@JsonProperty("userVerification") UserVerificationRequirement userVerification,
@NonNull @JsonProperty("extensions") AssertionExtensionInputs extensions) {
this.challenge = challenge;
this.timeout = timeout;
+ this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
this.rpId = rpId;
this.allowCredentials =
allowCredentials == null ? null : CollectionUtil.immutableList(allowCredentials);
@@ -213,6 +220,20 @@ public PublicKeyCredentialRequestOptionsBuilder timeout(long timeout) {
return this.timeout(Optional.of(timeout));
}
+ public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull String... hints) {
+ this.hints = Arrays.asList(hints);
+ return this;
+ }
+
+ public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
+ return this.hints(Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
+ }
+
+ public PublicKeyCredentialRequestOptionsBuilder hints(List hints) {
+ this.hints = hints;
+ return this;
+ }
+
/**
* Specifies the relying party identifier claimed by the caller.
*
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
index bcad72216..05cbd33f8 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala
@@ -1,16 +1,8 @@
package com.yubico.webauthn
import com.yubico.scalacheck.gen.GenUtil.halfsized
-import com.yubico.webauthn.data.AssertionExtensionInputs
-import com.yubico.webauthn.data.AttestationType
-import com.yubico.webauthn.data.AuthenticatorAssertionResponse
-import com.yubico.webauthn.data.AuthenticatorAttestationResponse
-import com.yubico.webauthn.data.ByteArray
-import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
-import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs
+import com.yubico.webauthn.data.{AssertionExtensionInputs, AttestationType, AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ByteArray, ClientAssertionExtensionOutputs, ClientRegistrationExtensionOutputs, PublicKeyCredential, PublicKeyCredentialHint, UserVerificationRequirement}
import com.yubico.webauthn.data.Generators._
-import com.yubico.webauthn.data.PublicKeyCredential
-import com.yubico.webauthn.data.UserVerificationRequirement
import org.bouncycastle.asn1.x500.X500Name
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary.arbitrary
@@ -97,12 +89,21 @@ object Generators {
for {
extensions <- arbitrary[Option[AssertionExtensionInputs]]
timeout <- Gen.option(Gen.posNum[Long])
+ hints <- arbitrary[Option[Either[List[String], List[PublicKeyCredentialHint]]]]
usernameOrUserHandle <- arbitrary[Option[Either[String, ByteArray]]]
userVerification <- arbitrary[Option[UserVerificationRequirement]]
} yield {
val b = StartAssertionOptions.builder()
extensions.foreach(b.extensions)
timeout.foreach(b.timeout)
+ hints.foreach {
+ case Left(h) => {
+ b.hints(h.asJava)
+ }
+ case Right(h) => {
+ b.hints(h: _*)
+ }
+ }
usernameOrUserHandle.foreach {
case Left(username) => b.username(username)
case Right(userHandle) => b.userHandle(userHandle)
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 fdec0b5c8..f959129f9 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
@@ -26,21 +26,9 @@ package com.yubico.webauthn
import com.yubico.internal.util.JacksonCodecs
import com.yubico.webauthn.Generators._
-import com.yubico.webauthn.data.AssertionExtensionInputs
-import com.yubico.webauthn.data.AttestationConveyancePreference
-import com.yubico.webauthn.data.AuthenticatorAttachment
-import com.yubico.webauthn.data.AuthenticatorSelectionCriteria
-import com.yubico.webauthn.data.AuthenticatorTransport
-import com.yubico.webauthn.data.ByteArray
+import com.yubico.webauthn.data.{AssertionExtensionInputs, AttestationConveyancePreference, AuthenticatorAttachment, AuthenticatorSelectionCriteria, AuthenticatorTransport, ByteArray, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialHint, PublicKeyCredentialParameters, RegistrationExtensionInputs, RelyingPartyIdentity, ResidentKeyRequirement, UserIdentity}
import com.yubico.webauthn.data.Generators.Extensions.registrationExtensionInputs
import com.yubico.webauthn.data.Generators._
-import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions
-import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
-import com.yubico.webauthn.data.PublicKeyCredentialParameters
-import com.yubico.webauthn.data.RegistrationExtensionInputs
-import com.yubico.webauthn.data.RelyingPartyIdentity
-import com.yubico.webauthn.data.ResidentKeyRequirement
-import com.yubico.webauthn.data.UserIdentity
import com.yubico.webauthn.extension.appid.AppId
import com.yubico.webauthn.extension.appid.Generators._
import com.yubico.webauthn.test.Helpers
@@ -981,6 +969,39 @@ class RelyingPartyStartOperationSpec
}
}
+ it("allows setting the hints to a value not in the spec.") {
+ val pkcco = relyingParty(userId = userId).startRegistration(
+ StartRegistrationOptions
+ .builder()
+ .user(userId)
+ .hints("hej")
+ .build()
+ )
+ pkcco.getHints.asScala should equal(List("hej"))
+ }
+
+ it("allows setting the hints to a value in the spec.") {
+ val pkcco = relyingParty(userId = userId).startRegistration(
+ StartRegistrationOptions
+ .builder()
+ .user(userId)
+ .hints(PublicKeyCredentialHint.SECURITY_KEY)
+ .build()
+ )
+ pkcco.getHints.asScala should equal(List("security-key"))
+ }
+
+ it("allows setting the hints to empty") {
+ val pkcco = relyingParty(userId = userId).startRegistration(
+ StartRegistrationOptions
+ .builder()
+ .user(userId)
+ .hints("")
+ .build()
+ )
+ pkcco.getHints.asScala should equal(List(""))
+ }
+
it("allows setting the timeout to empty.") {
val pkcco = relyingParty(userId = userId).startRegistration(
StartRegistrationOptions
diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
index e1a32f6e6..10c721415 100644
--- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
+++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala
@@ -1071,24 +1071,37 @@ object Generators {
arbitrary[java.util.List[PublicKeyCredentialParameters]]
rp <- arbitrary[RelyingPartyIdentity]
timeout <- arbitrary[Optional[java.lang.Long]]
+ hints <- arbitrary[Option[Either[List[String], List[PublicKeyCredentialHint]]]]
user <- arbitrary[UserIdentity]
- } yield PublicKeyCredentialCreationOptions
- .builder()
- .rp(rp)
- .user(user)
- .challenge(challenge)
- .pubKeyCredParams(pubKeyCredParams)
- .attestation(attestation)
- .authenticatorSelection(authenticatorSelection)
- .excludeCredentials(excludeCredentials)
- .extensions(extensions)
- .timeout(timeout)
- .build()
+ } yield {
+ val b = PublicKeyCredentialCreationOptions
+ .builder()
+ .rp(rp)
+ .user(user)
+ .challenge(challenge)
+ .pubKeyCredParams(pubKeyCredParams)
+ .attestation(attestation)
+ .authenticatorSelection(authenticatorSelection)
+ .excludeCredentials(excludeCredentials)
+ .extensions(extensions)
+ .timeout(timeout)
+
+ hints.foreach {
+ case Left(h) => {
+ b.hints(h.asJava)
+ }
+ case Right(h) => {
+ b.hints(h: _*)
+ }
+ }
+
+ b.build()
+ }
)
)
implicit val arbitraryPublicKeyCredentialDescriptor
- : Arbitrary[PublicKeyCredentialDescriptor] = Arbitrary(
+ : Arbitrary[PublicKeyCredentialDescriptor] = Arbitrary(
halfsized(
for {
id <- arbitrary[ByteArray]
@@ -1103,6 +1116,13 @@ object Generators {
)
)
+ implicit val arbitraryPublicKeyCredentialHint
+ : Arbitrary[PublicKeyCredentialHint] = Arbitrary(
+ Gen.oneOf(
+ Gen.oneOf(PublicKeyCredentialHint.values()),
+ Gen.alphaNumStr.map(PublicKeyCredentialHint.of),
+ ))
+
implicit val arbitraryPublicKeyCredentialParameters
: Arbitrary[PublicKeyCredentialParameters] = Arbitrary(
halfsized(
@@ -1127,6 +1147,7 @@ object Generators {
extensions <- arbitrary[AssertionExtensionInputs]
rpId <- arbitrary[Optional[String]]
timeout <- arbitrary[Optional[java.lang.Long]]
+ hints <- arbitrary[Option[List[String]]]
userVerification <- arbitrary[UserVerificationRequirement]
} yield PublicKeyCredentialRequestOptions
.builder()
@@ -1135,7 +1156,8 @@ object Generators {
.extensions(extensions)
.rpId(rpId)
.timeout(timeout)
- .userVerification(userVerification)
+ .hints(hints.map(_.asJava).orNull)
+ .userVerification(userVerification)
.build()
)
)