From 28bcbb018aeca4830a9fa975d6bd4cf5d0c7b881 Mon Sep 17 00:00:00 2001 From: Dennis Fokin Date: Fri, 6 Sep 2024 08:53:38 +0200 Subject: [PATCH 01/14] Add support for PublicKeyCredentialHint --- .../com/yubico/webauthn/RelyingParty.java | 3 +- .../com/yubico/webauthn/RelyingPartyV2.java | 6 +- .../webauthn/StartAssertionOptions.java | 38 +++++- .../webauthn/StartRegistrationOptions.java | 36 +++++- .../PublicKeyCredentialCreationOptions.java | 21 ++++ .../data/PublicKeyCredentialHint.java | 110 ++++++++++++++++++ .../PublicKeyCredentialRequestOptions.java | 22 ++++ .../com/yubico/webauthn/Generators.scala | 11 ++ .../RelyingPartyStartOperationSpec.scala | 34 ++++++ .../com/yubico/webauthn/data/Generators.scala | 48 ++++++-- 10 files changed, 312 insertions(+), 17 deletions(-) create mode 100644 webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java 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..be6a2dc22 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..9ce2883c3 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..df119e0fa 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,8 +26,12 @@ 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.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import lombok.Builder; import lombok.NonNull; @@ -36,7 +40,7 @@ /** Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. */ @Value @Builder(toBuilder = true) -public class StartAssertionOptions { +public final class StartAssertionOptions { private final String username; @@ -79,6 +83,23 @@ 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. * @@ -370,5 +391,20 @@ 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..a8d51d766 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,8 +26,12 @@ 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.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import lombok.Builder; import lombok.NonNull; @@ -36,7 +40,7 @@ /** 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; @@ -64,6 +68,21 @@ 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. @@ -157,5 +176,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..105586a41 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,6 +36,7 @@ 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; @@ -94,6 +95,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 +139,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 +149,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 +322,22 @@ 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..86cfe359a --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java @@ -0,0 +1,110 @@ +// 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; + +/** + * 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; + + public static final PublicKeyCredentialHint SECURITY_KEY = + new PublicKeyCredentialHint("security-key"); + + public static final PublicKeyCredentialHint CLIENT_DEVICE = + new PublicKeyCredentialHint("client-device"); + + public static final PublicKeyCredentialHint HYBRID = new PublicKeyCredentialHint("hybrid"); + + /** + * @return An array containing all predefined values of {@link PublicKeyCredentialHint} known by + * this implementation. + */ + public static PublicKeyCredentialHint[] values() { + return new PublicKeyCredentialHint[] {SECURITY_KEY, CLIENT_DEVICE, HYBRID}; + } + + /** + * @return If value is the same as that of any of {@link #SECURITY_KEY}, {@link + * #CLIENT_DEVICE} or {@link #HYBRID}, returns that constant instance. Otherwise returns a new + * instance containing value. + * @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 "SECURITY_KEY", "CLIENT_DEVICE" + * or "HYBRID", 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..61c7c46d9 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,8 @@ 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 +68,8 @@ public class PublicKeyCredentialRequestOptions { */ private final Long timeout; + private final List hints; + /** * Specifies the relying party identifier claimed by the caller. * @@ -112,12 +116,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 +219,22 @@ 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..c9655c7db 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 @@ -10,6 +10,7 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredential +import com.yubico.webauthn.data.PublicKeyCredentialHint import com.yubico.webauthn.data.UserVerificationRequirement import org.bouncycastle.asn1.x500.X500Name import org.scalacheck.Arbitrary @@ -97,12 +98,22 @@ 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..52aa8d7b7 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 @@ -36,6 +36,7 @@ import com.yubico.webauthn.data.Generators.Extensions.registrationExtensionInput import com.yubico.webauthn.data.Generators._ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions import com.yubico.webauthn.data.PublicKeyCredentialDescriptor +import com.yubico.webauthn.data.PublicKeyCredentialHint import com.yubico.webauthn.data.PublicKeyCredentialParameters import com.yubico.webauthn.data.RegistrationExtensionInputs import com.yubico.webauthn.data.RelyingPartyIdentity @@ -981,6 +982,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..0baea9816 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,19 +1071,33 @@ 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() + } ) ) @@ -1103,6 +1117,14 @@ 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 +1149,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,6 +1158,7 @@ object Generators { .extensions(extensions) .rpId(rpId) .timeout(timeout) + .hints(hints.map(_.asJava).orNull) .userVerification(userVerification) .build() ) From d2f301eae99256fde5c7f5d61a6184a6bb51cc9c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:01:56 +0200 Subject: [PATCH 02/14] Remove spurious final class modifiers These are already implied by the `@Value` Lombok annotation - the explicit modifier probably appeared when we used delombok to generate the new constructors. (Most) other classes in the repo don't have the explicit `final` modifier, so let's leave it out here too so it doesn't seem like these classes are different. --- .../main/java/com/yubico/webauthn/StartAssertionOptions.java | 2 +- .../main/java/com/yubico/webauthn/StartRegistrationOptions.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 df119e0fa..4cf49eebb 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 @@ -40,7 +40,7 @@ /** Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. */ @Value @Builder(toBuilder = true) -public final class StartAssertionOptions { +public class StartAssertionOptions { private final String username; 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 a8d51d766..a9c21e3b5 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 @@ -40,7 +40,7 @@ /** Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}. */ @Value @Builder(toBuilder = true) -public final class StartRegistrationOptions { +public class StartRegistrationOptions { /** Identifiers for the user creating a credential. */ @NonNull private final UserIdentity user; From 7a82286221459fc8954202dbb2af041f0260da52 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:22:02 +0200 Subject: [PATCH 03/14] Remove redundant braces --- .../test/scala/com/yubico/webauthn/data/Generators.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 0baea9816..5da241018 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 @@ -1088,12 +1088,8 @@ object Generators { .timeout(timeout) hints.foreach { - case Left(h) => { - b.hints(h.asJava) - } - case Right(h) => { - b.hints(h: _*) - } + case Left(h) => b.hints(h.asJava) + case Right(h) => b.hints(h: _*) } b.build() From a5c0d1c429f228028eff77a9d8e8be1af8379faf Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:22:23 +0200 Subject: [PATCH 04/14] Use arbitrary strings in PublicKeyCredentialHint generator --- .../src/test/scala/com/yubico/webauthn/data/Generators.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5da241018..aa6354eee 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 @@ -1117,7 +1117,7 @@ object Generators { : Arbitrary[PublicKeyCredentialHint] = Arbitrary( Gen.oneOf( Gen.oneOf(PublicKeyCredentialHint.values()), - Gen.alphaNumStr.map(PublicKeyCredentialHint.of), + arbitrary[String].map(PublicKeyCredentialHint.of), ) ) From 89e78ffa5c902467d9f46de2c2320e19366d3011 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:26:44 +0200 Subject: [PATCH 05/14] Generate PublicKeyCredentialRequestOptions using both List and PublicKeyCredentialHint... --- .../com/yubico/webauthn/data/Generators.scala | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) 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 aa6354eee..e6a49fab4 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 @@ -1145,18 +1145,26 @@ object Generators { extensions <- arbitrary[AssertionExtensionInputs] rpId <- arbitrary[Optional[String]] timeout <- arbitrary[Optional[java.lang.Long]] - hints <- arbitrary[Option[List[String]]] + hints <- + arbitrary[Option[Either[List[String], List[PublicKeyCredentialHint]]]] userVerification <- arbitrary[UserVerificationRequirement] - } yield PublicKeyCredentialRequestOptions - .builder() - .challenge(challenge) - .allowCredentials(allowCredentials) - .extensions(extensions) - .rpId(rpId) - .timeout(timeout) - .hints(hints.map(_.asJava).orNull) - .userVerification(userVerification) - .build() + } yield { + val b = PublicKeyCredentialRequestOptions + .builder() + .challenge(challenge) + .allowCredentials(allowCredentials) + .extensions(extensions) + .rpId(rpId) + .timeout(timeout) + .userVerification(userVerification) + + hints.foreach { + case Left(h) => b.hints(h.asJava) + case Right(h) => b.hints(h: _*) + } + + b.build() + } ) ) From 8834c470edd92c143984dc184b452655e489fb8a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:29:23 +0200 Subject: [PATCH 06/14] Generate PublicKeyCredentialHints using all three setters --- .../com/yubico/webauthn/data/Generators.scala | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 e6a49fab4..3d0c1d7c0 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 @@ -1072,7 +1072,9 @@ object Generators { rp <- arbitrary[RelyingPartyIdentity] timeout <- arbitrary[Optional[java.lang.Long]] hints <- - arbitrary[Option[Either[List[String], List[PublicKeyCredentialHint]]]] + arbitrary[Option[Either[Either[List[String], Array[String]], List[ + PublicKeyCredentialHint + ]]]] user <- arbitrary[UserIdentity] } yield { val b = PublicKeyCredentialCreationOptions @@ -1088,8 +1090,9 @@ object Generators { .timeout(timeout) hints.foreach { - case Left(h) => b.hints(h.asJava) - case Right(h) => b.hints(h: _*) + case Left(Left(h: List[String])) => b.hints(h.asJava) + case Left(Right(h: Array[String])) => b.hints(h: _*) + case Right(h: List[PublicKeyCredentialHint]) => b.hints(h: _*) } b.build() @@ -1146,7 +1149,9 @@ object Generators { rpId <- arbitrary[Optional[String]] timeout <- arbitrary[Optional[java.lang.Long]] hints <- - arbitrary[Option[Either[List[String], List[PublicKeyCredentialHint]]]] + arbitrary[Option[Either[Either[List[String], Array[String]], List[ + PublicKeyCredentialHint + ]]]] userVerification <- arbitrary[UserVerificationRequirement] } yield { val b = PublicKeyCredentialRequestOptions @@ -1159,8 +1164,9 @@ object Generators { .userVerification(userVerification) hints.foreach { - case Left(h) => b.hints(h.asJava) - case Right(h) => b.hints(h: _*) + case Left(Left(h: List[String])) => b.hints(h.asJava) + case Left(Right(h: Array[String])) => b.hints(h: _*) + case Right(h: List[PublicKeyCredentialHint]) => b.hints(h: _*) } b.build() From a97acb62def8514df6559e6393a33901b9f59016 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:35:08 +0200 Subject: [PATCH 07/14] Make hints @NonNull The reason we couldn't do this previously is that we were setting `hints` to `null` in the `PublicKeyCredentialRequestOptions` generator. We no longer do since commit 89e78ffa5c902467d9f46de2c2320e19366d3011, so we can now require this to be non-null. --- .../webauthn/data/PublicKeyCredentialCreationOptions.java | 2 +- .../yubico/webauthn/data/PublicKeyCredentialRequestOptions.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 105586a41..cdfd3a7df 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 @@ -333,7 +333,7 @@ public PublicKeyCredentialCreationOptionsBuilder hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } - public PublicKeyCredentialCreationOptionsBuilder hints(List hints) { + public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull List hints) { this.hints = hints; return this; } 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 61c7c46d9..8ffe1d0ce 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 @@ -230,7 +230,7 @@ public PublicKeyCredentialRequestOptionsBuilder hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } - public PublicKeyCredentialRequestOptionsBuilder hints(List hints) { + public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull List hints) { this.hints = hints; return this; } From bed7091a5db4264d716c423086ebcbc7040d965d Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 12:58:45 +0200 Subject: [PATCH 08/14] Extract describe parent from hints tests in RelyingPartyStartOperationSpec --- .../RelyingPartyStartOperationSpec.scala | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) 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 52aa8d7b7..27c34d58a 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 @@ -982,37 +982,41 @@ 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")) - } + describe("allows setting the hints") { + val rp = relyingParty(userId = userId) - 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("to a value not in the spec.") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .hints("hej") + .build() + ) + pkcco.getHints.asScala should equal(List("hej")) + } - 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("to a value in the spec.") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .hints(PublicKeyCredentialHint.SECURITY_KEY) + .build() + ) + pkcco.getHints.asScala should equal(List("security-key")) + } + + it("to empty") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .hints("") + .build() + ) + pkcco.getHints.asScala should equal(List("")) + } } it("allows setting the timeout to empty.") { From dd31b535aebb8f72f062d581ef17b3ebe080428c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 13:12:57 +0200 Subject: [PATCH 09/14] Test setting hints to empty list instead of empty string --- .../webauthn/RelyingPartyStartOperationSpec.scala | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 27c34d58a..3645626ef 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 @@ -1007,15 +1007,11 @@ class RelyingPartyStartOperationSpec pkcco.getHints.asScala should equal(List("security-key")) } - it("to empty") { + it("or not, defaulting to the empty list.") { val pkcco = rp.startRegistration( - StartRegistrationOptions - .builder() - .user(userId) - .hints("") - .build() + StartRegistrationOptions.builder().user(userId).build() ) - pkcco.getHints.asScala should equal(List("")) + pkcco.getHints.asScala should equal(List()) } } From 8a7ce197730f77e5577c2e8cf49391d98da8631f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 13:18:11 +0200 Subject: [PATCH 10/14] Expand test cases of setting hints --- .../RelyingPartyStartOperationSpec.scala | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) 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 3645626ef..e03ea6142 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 @@ -985,26 +985,48 @@ class RelyingPartyStartOperationSpec describe("allows setting the hints") { val rp = relyingParty(userId = userId) - it("to a value not in the spec.") { + it("to string values in the spec or not.") { val pkcco = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) - .hints("hej") + .hints("hej", "security-key", "hoj", "client-device", "hybrid") .build() ) - pkcco.getHints.asScala should equal(List("hej")) + pkcco.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + PublicKeyCredentialHint.HYBRID.getValue, + ) + ) } - it("to a value in the spec.") { + it("to PublicKeyCredentialHint values in the spec or not.") { val pkcco = rp.startRegistration( StartRegistrationOptions .builder() .user(userId) - .hints(PublicKeyCredentialHint.SECURITY_KEY) + .hints( + PublicKeyCredentialHint.of("hej"), + PublicKeyCredentialHint.HYBRID, + PublicKeyCredentialHint.SECURITY_KEY, + PublicKeyCredentialHint.of("hoj"), + PublicKeyCredentialHint.CLIENT_DEVICE, + ) .build() ) - pkcco.getHints.asScala should equal(List("security-key")) + pkcco.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.HYBRID.getValue, + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + ) + ) } it("or not, defaulting to the empty list.") { From a67ac4ab9af76488f19776d07d34af565c6a7abc Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 13:25:10 +0200 Subject: [PATCH 11/14] Add tests of hints in RelyingPartyV2.startAssertion --- .../RelyingPartyStartOperationSpec.scala | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) 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 e03ea6142..8967f59eb 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 @@ -1596,6 +1596,59 @@ class RelyingPartyStartOperationSpec } } + describe("allows setting the hints") { + val rp = relyingParty(userId = userId) + + it("to string values in the spec or not.") { + val pkcro = rp.startAssertion( + StartAssertionOptions + .builder() + .hints("hej", "security-key", "hoj", "client-device", "hybrid") + .build() + ) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + PublicKeyCredentialHint.HYBRID.getValue, + ) + ) + } + + it("to PublicKeyCredentialHint values in the spec or not.") { + val pkcro = rp.startAssertion( + StartAssertionOptions + .builder() + .hints( + PublicKeyCredentialHint.of("hej"), + PublicKeyCredentialHint.HYBRID, + PublicKeyCredentialHint.SECURITY_KEY, + PublicKeyCredentialHint.of("hoj"), + PublicKeyCredentialHint.CLIENT_DEVICE, + ) + .build() + ) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.HYBRID.getValue, + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + ) + ) + } + + it("or not, defaulting to the empty list.") { + val pkcro = rp.startAssertion(StartAssertionOptions.builder().build()) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List() + ) + } + } + it("allows setting the timeout to empty.") { val req = relyingParty(userId = userId).startAssertion( StartAssertionOptions From 4c3bde44a005134e00bb8cdc5eb58c8d4ed565bd Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 13:54:52 +0200 Subject: [PATCH 12/14] Copy tests of startRegistration hints from RelyingPartyV2 to RelyingParty --- .../RelyingPartyStartOperationSpec.scala | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 8967f59eb..34ef3693b 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 @@ -219,6 +219,64 @@ class RelyingPartyStartOperationSpec } } + describe("allows setting the hints") { + val rp = relyingParty(userId = userId) + + it("to string values in the spec or not.") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .hints("hej", "security-key", "hoj", "client-device", "hybrid") + .build() + ) + pkcco.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + PublicKeyCredentialHint.HYBRID.getValue, + ) + ) + } + + it("to PublicKeyCredentialHint values in the spec or not.") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .hints( + PublicKeyCredentialHint.of("hej"), + PublicKeyCredentialHint.HYBRID, + PublicKeyCredentialHint.SECURITY_KEY, + PublicKeyCredentialHint.of("hoj"), + PublicKeyCredentialHint.CLIENT_DEVICE, + ) + .build() + ) + pkcco.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.HYBRID.getValue, + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + ) + ) + } + + it("or not, defaulting to the empty list.") { + val pkcco = rp.startRegistration( + StartRegistrationOptions + .builder() + .user(userId) + .build() + ) + pkcco.getHints.asScala should equal(List()) + } + } + it("allows setting the timeout to empty.") { val pkcco = relyingParty(userId = userId).startRegistration( StartRegistrationOptions From d220ef20b5fce1a2e2b36526c7a0cfcafc5c7870 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 6 Sep 2024 13:55:12 +0200 Subject: [PATCH 13/14] Pass hints through in RelyingParty.startAssertion --- .../com/yubico/webauthn/RelyingParty.java | 3 +- .../RelyingPartyStartOperationSpec.scala | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) 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 be6a2dc22..b8f0bafcb 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 @@ -538,7 +538,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/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyStartOperationSpec.scala index 34ef3693b..4afd9a8f6 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 @@ -818,6 +818,59 @@ class RelyingPartyStartOperationSpec } } + describe("allows setting the hints") { + val rp = relyingParty(userId = userId) + + it("to string values in the spec or not.") { + val pkcro = rp.startAssertion( + StartAssertionOptions + .builder() + .hints("hej", "security-key", "hoj", "client-device", "hybrid") + .build() + ) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + PublicKeyCredentialHint.HYBRID.getValue, + ) + ) + } + + it("to PublicKeyCredentialHint values in the spec or not.") { + val pkcro = rp.startAssertion( + StartAssertionOptions + .builder() + .hints( + PublicKeyCredentialHint.of("hej"), + PublicKeyCredentialHint.HYBRID, + PublicKeyCredentialHint.SECURITY_KEY, + PublicKeyCredentialHint.of("hoj"), + PublicKeyCredentialHint.CLIENT_DEVICE, + ) + .build() + ) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List( + "hej", + PublicKeyCredentialHint.HYBRID.getValue, + PublicKeyCredentialHint.SECURITY_KEY.getValue, + "hoj", + PublicKeyCredentialHint.CLIENT_DEVICE.getValue, + ) + ) + } + + it("or not, defaulting to the empty list.") { + val pkcro = rp.startAssertion(StartAssertionOptions.builder().build()) + pkcro.getPublicKeyCredentialRequestOptions.getHints.asScala should equal( + List() + ) + } + } + it("allows setting the timeout to empty.") { val req = relyingParty(userId = userId).startAssertion( StartAssertionOptions From 76948865b1b3815cf1fb2c0dcf1ba6629a6e6cc0 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 13 Sep 2024 13:19:21 +0200 Subject: [PATCH 14/14] Add JavaDoc for hints and PublicKeyCredentialHint --- .../webauthn/StartAssertionOptions.java | 135 ++++++++++++++++++ .../webauthn/StartRegistrationOptions.java | 130 +++++++++++++++++ .../PublicKeyCredentialCreationOptions.java | 129 +++++++++++++++++ .../data/PublicKeyCredentialHint.java | 105 ++++++++++++-- .../PublicKeyCredentialRequestOptions.java | 135 ++++++++++++++++++ 5 files changed, 619 insertions(+), 15 deletions(-) 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 4cf49eebb..02f2cdba9 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,6 +26,7 @@ import com.yubico.webauthn.data.AssertionExtensionInputs; import com.yubico.webauthn.data.ByteArray; +import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import com.yubico.webauthn.data.PublicKeyCredentialHint; import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; import com.yubico.webauthn.data.UserVerificationRequirement; @@ -83,6 +84,38 @@ public class StartAssertionOptions { */ private final Long timeout; + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialRequestOptions} so they can be used in the argument to + * navigator.credentials.get() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see PublicKeyCredentialRequestOptions#getHints() + * @see StartAssertionOptionsBuilder#hints(List) + * @see StartAssertionOptionsBuilder#hints(String...) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ private final List hints; private StartAssertionOptions( @@ -392,16 +425,118 @@ private StartAssertionOptionsBuilder timeout(Long timeout) { return this.timeout(Optional.ofNullable(timeout)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialRequestOptions} so they can be used in the argument + * to navigator.credentials.get() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see PublicKeyCredentialRequestOptions#getHints() + * @see StartAssertionOptions#getHints() + * @see StartAssertionOptionsBuilder#hints(List) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public StartAssertionOptionsBuilder hints(@NonNull String... hints) { this.hints = Arrays.asList(hints); return this; } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialRequestOptions} so they can be used in the argument + * to navigator.credentials.get() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see PublicKeyCredentialRequestOptions#getHints() + * @see StartAssertionOptions#getHints() + * @see StartAssertionOptionsBuilder#hints(List) + * @see StartAssertionOptionsBuilder#hints(String...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public StartAssertionOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) { return this.hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialRequestOptions} so they can be used in the argument + * to navigator.credentials.get() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see PublicKeyCredentialRequestOptions#getHints() + * @see StartAssertionOptions#getHints() + * @see StartAssertionOptionsBuilder#hints(String...) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ 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 a9c21e3b5..7d9d18ab9 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 @@ -68,6 +68,37 @@ public class StartRegistrationOptions { */ private final Long timeout; + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this occurs, + * the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialCreationOptions} so they can be used in the argument + * to navigator.credentials.create() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptionsBuilder#hints(List) + * @see StartRegistrationOptionsBuilder#hints(String...) + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ private final List hints; private StartRegistrationOptions( @@ -177,16 +208,115 @@ public StartRegistrationOptionsBuilder timeout(long timeout) { return this.timeout(Optional.of(timeout)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialCreationOptions} so they can be used in the argument + * to navigator.credentials.create() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see StartRegistrationOptionsBuilder#hints(List) + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public StartRegistrationOptionsBuilder hints(@NonNull String... hints) { this.hints = Arrays.asList(hints); return this; } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialCreationOptions} so they can be used in the argument + * to navigator.credentials.create() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see StartRegistrationOptionsBuilder#hints(List) + * @see StartRegistrationOptionsBuilder#hints(String...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public StartRegistrationOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) { return this.hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through to the {@link PublicKeyCredentialCreationOptions} so they can be used in the argument + * to navigator.credentials.create() on the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see StartRegistrationOptionsBuilder#hints(String...) + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ 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 cdfd3a7df..2ed1c22de 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 @@ -33,6 +33,7 @@ import com.yubico.internal.util.JacksonCodecs; import com.yubico.webauthn.FinishRegistrationOptions; import com.yubico.webauthn.RelyingParty; +import com.yubico.webauthn.StartRegistrationOptions; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.Signature; @@ -95,6 +96,38 @@ public class PublicKeyCredentialCreationOptions { */ private final Long timeout; + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this occurs, + * the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.create() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see PublicKeyCredentialCreationOptionsBuilder#hints(List) + * @see PublicKeyCredentialCreationOptionsBuilder#hints(String...) + * @see PublicKeyCredentialCreationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ private final List hints; /** @@ -322,17 +355,113 @@ public PublicKeyCredentialCreationOptionsBuilder timeout(long timeout) { return this.timeout(Optional.of(timeout)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.create() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see PublicKeyCredentialCreationOptions#getHints() + * @see PublicKeyCredentialCreationOptionsBuilder#hints(List) + * @see PublicKeyCredentialCreationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull String... hints) { this.hints = Arrays.asList(hints); return this; } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.create() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see PublicKeyCredentialCreationOptions#getHints() + * @see PublicKeyCredentialCreationOptionsBuilder#hints(List) + * @see PublicKeyCredentialCreationOptionsBuilder#hints(String...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialCreationOptionsBuilder hints( @NonNull PublicKeyCredentialHint... hints) { return this.hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this registration operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of registering with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of registering a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict preferences in {@link #getAuthenticatorSelection()}. When this + * occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.create() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartRegistrationOptions#getHints() + * @see PublicKeyCredentialCreationOptions#getHints() + * @see PublicKeyCredentialCreationOptionsBuilder#hints(String...) + * @see PublicKeyCredentialCreationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialCreationOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull List hints) { this.hints = hints; return this; 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 index 86cfe359a..c063bc0d3 100644 --- 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 @@ -26,6 +26,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder; +import com.yubico.webauthn.StartAssertionOptions; +import com.yubico.webauthn.StartAssertionOptions.StartAssertionOptionsBuilder; +import com.yubico.webauthn.StartRegistrationOptions; +import com.yubico.webauthn.StartRegistrationOptions.StartRegistrationOptionsBuilder; +import com.yubico.webauthn.attestation.AttestationTrustSource; import java.util.stream.Stream; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -33,24 +39,25 @@ import lombok.Value; /** - * 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. + * Hints to guide the user agent in interacting with the user. * - *

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. + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of using an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the option + * of using a built-in passkey provider. * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + * @see StartRegistrationOptions#getHints() + * @see StartAssertionOptions#getHints() + * @see PublicKeyCredentialCreationOptions.hints + * @see PublicKeyCredentialRequestOptions.hints * @see §5.10.4. - * Authenticator Transport Enumeration (enum AuthenticatorTransport) + * href="https://www.w3.org/TR/2023/WD-webauthn-3-20230927/#enumdef-publickeycredentialhints">§5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -58,12 +65,80 @@ public class PublicKeyCredentialHint { @JsonValue @NonNull private final String value; + /** + * Indicates that the application believes that users will satisfy this request with a physical + * security key. + * + *

For example, an enterprise application may set this hint if they have issued security keys + * to their employees and will only accept those authenticators for registration and + * authentication. In that case, the application should probably also set {@link + * RelyingPartyBuilder#attestationTrustSource(AttestationTrustSource) attestationTrustSource} and + * set {@link RelyingPartyBuilder#allowUntrustedAttestation(boolean) allowUntrustedAttestation} to + * false. See also the + * webauthn-server-attestation module. + * + *

For compatibility with older user agents, when this hint is used in {@link + * StartRegistrationOptions}, the + * {@link StartRegistrationOptionsBuilder#authenticatorSelection(AuthenticatorSelectionCriteria) authenticatorSelection}.{@link AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder#authenticatorAttachment(AuthenticatorAttachment) authenticatorAttachment} + * parameter SHOULD be set to {@link AuthenticatorAttachment#CROSS_PLATFORM}. + * + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see + * security-key in §5.8.7. User-agent Hints Enumeration (enum + * PublicKeyCredentialHints) + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public static final PublicKeyCredentialHint SECURITY_KEY = new PublicKeyCredentialHint("security-key"); + /** + * Indicates that the application believes that users will satisfy this request with an + * authenticator built into the client device. + * + *

For compatibility with older user agents, when this hint is used in {@link + * StartRegistrationOptions}, the + * {@link StartRegistrationOptionsBuilder#authenticatorSelection(AuthenticatorSelectionCriteria) authenticatorSelection}.{@link AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder#authenticatorAttachment(AuthenticatorAttachment) authenticatorAttachment} + * parameter SHOULD be set to {@link AuthenticatorAttachment#PLATFORM}. + * + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see + * client-device in §5.8.7. User-agent Hints Enumeration (enum + * PublicKeyCredentialHints) + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public static final PublicKeyCredentialHint CLIENT_DEVICE = new PublicKeyCredentialHint("client-device"); + /** + * Indicates that the application believes that users will satisfy this request with + * general-purpose authenticators such as smartphones. For example, a consumer application may + * believe that only a small fraction of their customers possesses dedicated security keys. This + * option also implies that the local platform authenticator should not be promoted in the UI. + * + *

For compatibility with older user agents, when this hint is used in {@link + * StartRegistrationOptions}, the + * {@link StartRegistrationOptionsBuilder#authenticatorSelection(AuthenticatorSelectionCriteria) authenticatorSelection}.{@link AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteriaBuilder#authenticatorAttachment(AuthenticatorAttachment) authenticatorAttachment} + * parameter SHOULD be set to {@link AuthenticatorAttachment#CROSS_PLATFORM}. + * + * @see StartRegistrationOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see StartAssertionOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see + * hybrid in §5.8.7. User-agent Hints Enumeration (enum PublicKeyCredentialHints) + * + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public static final PublicKeyCredentialHint HYBRID = new PublicKeyCredentialHint("hybrid"); /** 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 8ffe1d0ce..da37870cc 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,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.yubico.internal.util.CollectionUtil; import com.yubico.internal.util.JacksonCodecs; +import com.yubico.webauthn.StartAssertionOptions; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -68,6 +69,38 @@ public class PublicKeyCredentialRequestOptions { */ private final Long timeout; + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.get() on the + * client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartAssertionOptions#getHints() + * @see PublicKeyCredentialRequestOptionsBuilder#hints(List) + * @see PublicKeyCredentialRequestOptionsBuilder#hints(String...) + * @see PublicKeyCredentialRequestOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ private final List hints; /** @@ -219,17 +252,119 @@ public PublicKeyCredentialRequestOptionsBuilder timeout(long timeout) { return this.timeout(Optional.of(timeout)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.get() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartAssertionOptions#getHints() + * @see PublicKeyCredentialRequestOptions#getHints() + * @see PublicKeyCredentialRequestOptionsBuilder#hints(List) + * @see PublicKeyCredentialRequestOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull String... hints) { this.hints = Arrays.asList(hints); return this; } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.get() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartAssertionOptions#getHints() + * @see PublicKeyCredentialRequestOptions#getHints() + * @see PublicKeyCredentialRequestOptionsBuilder#hints(List) + * @see PublicKeyCredentialRequestOptionsBuilder#hints(String...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialRequestOptionsBuilder hints( @NonNull PublicKeyCredentialHint... hints) { return this.hints( Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new)); } + /** + * Zero or more hints, in descending order of preference, to guide the user agent in interacting + * with the user during this authentication operation. + * + *

Setting this property multiple times overwrites any value set previously. + * + *

For example, the {@link PublicKeyCredentialHint#SECURITY_KEY} hint may be used to ask the + * client to emphasize the option of authenticating with an external security key, or the {@link + * PublicKeyCredentialHint#CLIENT_DEVICE} hint may be used to ask the client to emphasize the + * option of authenticating a built-in passkey provider. + * + *

These hints are not requirements, and do not bind the user-agent, but may guide it in + * providing the best experience by using contextual information about the request. + * + *

Hints MAY contradict information contained in {@link + * PublicKeyCredentialDescriptor#getTransports()}. When this occurs, the hints take precedence. + * + *

This library does not take these hints into account in any way, other than passing them + * through so they can be used in the argument to navigator.credentials.get() on + * the client side. + * + *

The default is empty. + * + * @see PublicKeyCredentialHint + * @see StartAssertionOptions#getHints() + * @see PublicKeyCredentialRequestOptions#getHints() + * @see PublicKeyCredentialRequestOptionsBuilder#hints(String...) + * @see PublicKeyCredentialRequestOptionsBuilder#hints(PublicKeyCredentialHint...) + * @see PublicKeyCredentialRequestOptions.hints + * @see §5.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull List hints) { this.hints = hints; return this;