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() ) )