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..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 @@ -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(); } @@ -537,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/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..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,8 +26,13 @@ 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; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Optional; import lombok.Builder; import lombok.NonNull; @@ -79,6 +84,55 @@ 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( + 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 +424,122 @@ public StartAssertionOptionsBuilder timeout(long timeout) { 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 e78184fb5..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 @@ -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; @@ -64,6 +68,52 @@ 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( + @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 +207,119 @@ public StartRegistrationOptionsBuilder timeout(@NonNull Optional timeout) 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 3d2b6033f..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,9 +33,11 @@ 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; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -94,6 +96,40 @@ 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; + /** * 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 +172,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 +182,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 +355,118 @@ 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; + } + /** * 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..c063bc0d3 --- /dev/null +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialHint.java @@ -0,0 +1,185 @@ +// 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 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; +import lombok.NonNull; +import lombok.Value; + +/** + * Hints to guide the user agent in interacting with the user. + * + *

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.8.7. + * User-agent Hints Enumeration (enum PublicKeyCredentialHints) + */ +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +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"); + + /** + * @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..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,9 @@ 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; import java.util.Optional; import lombok.Builder; @@ -66,6 +69,40 @@ 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; + /** * Specifies the relying party identifier claimed by the caller. * @@ -112,12 +149,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 +252,124 @@ 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; + } + /** * 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..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 @@ -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 @@ -218,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 @@ -759,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 @@ -981,6 +1093,61 @@ 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 @@ -1540,6 +1707,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 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..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 @@ -1071,19 +1071,32 @@ object Generators { arbitrary[java.util.List[PublicKeyCredentialParameters]] rp <- arbitrary[RelyingPartyIdentity] timeout <- arbitrary[Optional[java.lang.Long]] + hints <- + arbitrary[Option[Either[Either[List[String], Array[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(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() + } ) ) @@ -1103,6 +1116,14 @@ object Generators { ) ) + implicit val arbitraryPublicKeyCredentialHint + : Arbitrary[PublicKeyCredentialHint] = Arbitrary( + Gen.oneOf( + Gen.oneOf(PublicKeyCredentialHint.values()), + arbitrary[String].map(PublicKeyCredentialHint.of), + ) + ) + implicit val arbitraryPublicKeyCredentialParameters : Arbitrary[PublicKeyCredentialParameters] = Arbitrary( halfsized( @@ -1127,16 +1148,29 @@ object Generators { extensions <- arbitrary[AssertionExtensionInputs] rpId <- arbitrary[Optional[String]] timeout <- arbitrary[Optional[java.lang.Long]] + hints <- + arbitrary[Option[Either[Either[List[String], Array[String]], List[ + PublicKeyCredentialHint + ]]]] userVerification <- arbitrary[UserVerificationRequirement] - } yield PublicKeyCredentialRequestOptions - .builder() - .challenge(challenge) - .allowCredentials(allowCredentials) - .extensions(extensions) - .rpId(rpId) - .timeout(timeout) - .userVerification(userVerification) - .build() + } yield { + val b = PublicKeyCredentialRequestOptions + .builder() + .challenge(challenge) + .allowCredentials(allowCredentials) + .extensions(extensions) + .rpId(rpId) + .timeout(timeout) + .userVerification(userVerification) + + hints.foreach { + 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() + } ) )