Skip to content

Commit 28bcbb0

Browse files
committed
Add support for PublicKeyCredentialHint
1 parent 49f0aeb commit 28bcbb0

File tree

10 files changed

+312
-17
lines changed

10 files changed

+312
-17
lines changed

webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingParty.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,8 @@ public PublicKeyCredentialCreationOptions startRegistration(
493493
.appidExclude(appId)
494494
.credProps()
495495
.build()))
496-
.timeout(startRegistrationOptions.getTimeout());
496+
.timeout(startRegistrationOptions.getTimeout())
497+
.hints(startRegistrationOptions.getHints());
497498
attestationConveyancePreference.ifPresent(builder::attestation);
498499
return builder.build();
499500
}

webauthn-server-core/src/main/java/com/yubico/webauthn/RelyingPartyV2.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,8 @@ public PublicKeyCredentialCreationOptions startRegistration(
452452
.appidExclude(appId)
453453
.credProps()
454454
.build()))
455-
.timeout(startRegistrationOptions.getTimeout());
455+
.timeout(startRegistrationOptions.getTimeout())
456+
.hints(startRegistrationOptions.getHints());
456457
attestationConveyancePreference.ifPresent(builder::attestation);
457458
return builder.build();
458459
}
@@ -509,7 +510,8 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio
509510
startAssertionOptions
510511
.getExtensions()
511512
.merge(startAssertionOptions.getExtensions().toBuilder().appid(appId).build()))
512-
.timeout(startAssertionOptions.getTimeout());
513+
.timeout(startAssertionOptions.getTimeout())
514+
.hints(startAssertionOptions.getHints());
513515

514516
startAssertionOptions.getUserVerification().ifPresent(pkcro::userVerification);
515517

webauthn-server-core/src/main/java/com/yubico/webauthn/StartAssertionOptions.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626

2727
import com.yubico.webauthn.data.AssertionExtensionInputs;
2828
import com.yubico.webauthn.data.ByteArray;
29+
import com.yubico.webauthn.data.PublicKeyCredentialHint;
2930
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
3031
import com.yubico.webauthn.data.UserVerificationRequirement;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.List;
3135
import java.util.Optional;
3236
import lombok.Builder;
3337
import lombok.NonNull;
@@ -36,7 +40,7 @@
3640
/** Parameters for {@link RelyingParty#startAssertion(StartAssertionOptions)}. */
3741
@Value
3842
@Builder(toBuilder = true)
39-
public class StartAssertionOptions {
43+
public final class StartAssertionOptions {
4044

4145
private final String username;
4246

@@ -79,6 +83,23 @@ public class StartAssertionOptions {
7983
*/
8084
private final Long timeout;
8185

86+
private final List<String> hints;
87+
88+
private StartAssertionOptions(
89+
String username,
90+
ByteArray userHandle,
91+
@NonNull AssertionExtensionInputs extensions,
92+
UserVerificationRequirement userVerification,
93+
Long timeout,
94+
List<String> hints) {
95+
this.username = username;
96+
this.userHandle = userHandle;
97+
this.extensions = extensions;
98+
this.userVerification = userVerification;
99+
this.timeout = timeout;
100+
this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
101+
}
102+
82103
/**
83104
* The username of the user to authenticate, if the user has already been identified.
84105
*
@@ -370,5 +391,20 @@ public StartAssertionOptionsBuilder timeout(long timeout) {
370391
private StartAssertionOptionsBuilder timeout(Long timeout) {
371392
return this.timeout(Optional.ofNullable(timeout));
372393
}
394+
395+
public StartAssertionOptionsBuilder hints(@NonNull String... hints) {
396+
this.hints = Arrays.asList(hints);
397+
return this;
398+
}
399+
400+
public StartAssertionOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
401+
return this.hints(
402+
Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
403+
}
404+
405+
public StartAssertionOptionsBuilder hints(@NonNull List<String> hints) {
406+
this.hints = hints;
407+
return this;
408+
}
373409
}
374410
}

webauthn-server-core/src/main/java/com/yubico/webauthn/StartRegistrationOptions.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626

2727
import com.yubico.webauthn.data.AuthenticatorSelectionCriteria;
2828
import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
29+
import com.yubico.webauthn.data.PublicKeyCredentialHint;
2930
import com.yubico.webauthn.data.RegistrationExtensionInputs;
3031
import com.yubico.webauthn.data.UserIdentity;
32+
import java.util.Arrays;
33+
import java.util.Collections;
34+
import java.util.List;
3135
import java.util.Optional;
3236
import lombok.Builder;
3337
import lombok.NonNull;
@@ -36,7 +40,7 @@
3640
/** Parameters for {@link RelyingParty#startRegistration(StartRegistrationOptions)}. */
3741
@Value
3842
@Builder(toBuilder = true)
39-
public class StartRegistrationOptions {
43+
public final class StartRegistrationOptions {
4044

4145
/** Identifiers for the user creating a credential. */
4246
@NonNull private final UserIdentity user;
@@ -64,6 +68,21 @@ public class StartRegistrationOptions {
6468
*/
6569
private final Long timeout;
6670

71+
private final List<String> hints;
72+
73+
private StartRegistrationOptions(
74+
@NonNull UserIdentity user,
75+
AuthenticatorSelectionCriteria authenticatorSelection,
76+
@NonNull RegistrationExtensionInputs extensions,
77+
Long timeout,
78+
List<String> hints) {
79+
this.user = user;
80+
this.authenticatorSelection = authenticatorSelection;
81+
this.extensions = extensions;
82+
this.timeout = timeout;
83+
this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
84+
}
85+
6786
/**
6887
* Constraints on what kind of authenticator the user is allowed to use to create the credential,
6988
* and on features that authenticator must or should support.
@@ -157,5 +176,20 @@ public StartRegistrationOptionsBuilder timeout(@NonNull Optional<Long> timeout)
157176
public StartRegistrationOptionsBuilder timeout(long timeout) {
158177
return this.timeout(Optional.of(timeout));
159178
}
179+
180+
public StartRegistrationOptionsBuilder hints(@NonNull String... hints) {
181+
this.hints = Arrays.asList(hints);
182+
return this;
183+
}
184+
185+
public StartRegistrationOptionsBuilder hints(@NonNull PublicKeyCredentialHint... hints) {
186+
return this.hints(
187+
Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
188+
}
189+
190+
public StartRegistrationOptionsBuilder hints(@NonNull List<String> hints) {
191+
this.hints = hints;
192+
return this;
193+
}
160194
}
161195
}

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialCreationOptions.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.security.KeyFactory;
3737
import java.security.NoSuchAlgorithmException;
3838
import java.security.Signature;
39+
import java.util.Arrays;
3940
import java.util.Collections;
4041
import java.util.List;
4142
import java.util.Optional;
@@ -94,6 +95,8 @@ public class PublicKeyCredentialCreationOptions {
9495
*/
9596
private final Long timeout;
9697

98+
private final List<String> hints;
99+
97100
/**
98101
* Intended for use by Relying Parties that wish to limit the creation of multiple credentials for
99102
* the same account on a single authenticator. The client is requested to return an error if the
@@ -136,6 +139,7 @@ private PublicKeyCredentialCreationOptions(
136139
@NonNull @JsonProperty("pubKeyCredParams")
137140
List<PublicKeyCredentialParameters> pubKeyCredParams,
138141
@JsonProperty("timeout") Long timeout,
142+
@JsonProperty("hints") List<String> hints,
139143
@JsonProperty("excludeCredentials") Set<PublicKeyCredentialDescriptor> excludeCredentials,
140144
@JsonProperty("authenticatorSelection") AuthenticatorSelectionCriteria authenticatorSelection,
141145
@JsonProperty("attestation") AttestationConveyancePreference attestation,
@@ -145,6 +149,7 @@ private PublicKeyCredentialCreationOptions(
145149
this.challenge = challenge;
146150
this.pubKeyCredParams = filterAvailableAlgorithms(pubKeyCredParams);
147151
this.timeout = timeout;
152+
this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
148153
this.excludeCredentials =
149154
excludeCredentials == null
150155
? null
@@ -317,6 +322,22 @@ public PublicKeyCredentialCreationOptionsBuilder timeout(long timeout) {
317322
return this.timeout(Optional.of(timeout));
318323
}
319324

325+
public PublicKeyCredentialCreationOptionsBuilder hints(@NonNull String... hints) {
326+
this.hints = Arrays.asList(hints);
327+
return this;
328+
}
329+
330+
public PublicKeyCredentialCreationOptionsBuilder hints(
331+
@NonNull PublicKeyCredentialHint... hints) {
332+
return this.hints(
333+
Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
334+
}
335+
336+
public PublicKeyCredentialCreationOptionsBuilder hints(List<String> hints) {
337+
this.hints = hints;
338+
return this;
339+
}
340+
320341
/**
321342
* Intended for use by Relying Parties that wish to limit the creation of multiple credentials
322343
* for the same account on a single authenticator. The client is requested to return an error if
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2018, Yubico AB
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
//
7+
// 1. Redistributions of source code must retain the above copyright notice, this
8+
// list of conditions and the following disclaimer.
9+
//
10+
// 2. Redistributions in binary form must reproduce the above copyright notice,
11+
// this list of conditions and the following disclaimer in the documentation
12+
// and/or other materials provided with the distribution.
13+
//
14+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15+
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16+
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17+
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18+
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19+
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20+
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21+
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22+
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23+
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24+
25+
package com.yubico.webauthn.data;
26+
27+
import com.fasterxml.jackson.annotation.JsonCreator;
28+
import com.fasterxml.jackson.annotation.JsonValue;
29+
import java.util.stream.Stream;
30+
import lombok.AccessLevel;
31+
import lombok.AllArgsConstructor;
32+
import lombok.NonNull;
33+
import lombok.Value;
34+
35+
/**
36+
* Authenticators may communicate with Clients using a variety of transports. This enumeration
37+
* defines a hint as to how Clients might communicate with a particular Authenticator in order to
38+
* obtain an assertion for a specific credential. Note that these hints represent the Relying
39+
* Party's best belief as to how an Authenticator may be reached. A Relying Party may obtain a list
40+
* of transports hints from some attestation statement formats or via some out-of-band mechanism; it
41+
* is outside the scope of this specification to define that mechanism.
42+
*
43+
* <p>Authenticators may implement various transports for communicating with clients. This
44+
* enumeration defines hints as to how clients might communicate with a particular authenticator in
45+
* order to obtain an assertion for a specific credential. Note that these hints represent the
46+
* WebAuthn Relying Party's best belief as to how an authenticator may be reached. A Relying Party
47+
* may obtain a list of transports hints from some attestation statement formats or via some
48+
* out-of-band mechanism; it is outside the scope of the Web Authentication specification to define
49+
* that mechanism.
50+
*
51+
* @see <a
52+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#enumdef-authenticatortransport">§5.10.4.
53+
* Authenticator Transport Enumeration (enum AuthenticatorTransport)</a>
54+
*/
55+
@Value
56+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
57+
public class PublicKeyCredentialHint {
58+
59+
@JsonValue @NonNull private final String value;
60+
61+
public static final PublicKeyCredentialHint SECURITY_KEY =
62+
new PublicKeyCredentialHint("security-key");
63+
64+
public static final PublicKeyCredentialHint CLIENT_DEVICE =
65+
new PublicKeyCredentialHint("client-device");
66+
67+
public static final PublicKeyCredentialHint HYBRID = new PublicKeyCredentialHint("hybrid");
68+
69+
/**
70+
* @return An array containing all predefined values of {@link PublicKeyCredentialHint} known by
71+
* this implementation.
72+
*/
73+
public static PublicKeyCredentialHint[] values() {
74+
return new PublicKeyCredentialHint[] {SECURITY_KEY, CLIENT_DEVICE, HYBRID};
75+
}
76+
77+
/**
78+
* @return If <code>value</code> is the same as that of any of {@link #SECURITY_KEY}, {@link
79+
* #CLIENT_DEVICE} or {@link #HYBRID}, returns that constant instance. Otherwise returns a new
80+
* instance containing <code>value</code>.
81+
* @see #valueOf(String)
82+
*/
83+
@JsonCreator
84+
public static PublicKeyCredentialHint of(@NonNull String value) {
85+
return Stream.of(values())
86+
.filter(v -> v.getValue().equals(value))
87+
.findAny()
88+
.orElseGet(() -> new PublicKeyCredentialHint(value));
89+
}
90+
91+
/**
92+
* @return If <code>name</code> equals <code>"SECURITY_KEY"</code>, <code>"CLIENT_DEVICE"</code>
93+
* or <code>"HYBRID"</code>, returns the constant by that name.
94+
* @throws IllegalArgumentException if <code>name</code> is anything else.
95+
* @see #of(String)
96+
*/
97+
public static PublicKeyCredentialHint valueOf(String name) {
98+
switch (name) {
99+
case "SECURITY_KEY":
100+
return SECURITY_KEY;
101+
case "CLIENT_DEVICE":
102+
return CLIENT_DEVICE;
103+
case "HYBRID":
104+
return HYBRID;
105+
default:
106+
throw new IllegalArgumentException(
107+
"No constant com.yubico.webauthn.data.PublicKeyCredentialHint." + name);
108+
}
109+
}
110+
}

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredentialRequestOptions.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import com.fasterxml.jackson.databind.node.ObjectNode;
3232
import com.yubico.internal.util.CollectionUtil;
3333
import com.yubico.internal.util.JacksonCodecs;
34+
import java.util.Arrays;
35+
import java.util.Collections;
3436
import java.util.List;
3537
import java.util.Optional;
3638
import lombok.Builder;
@@ -66,6 +68,8 @@ public class PublicKeyCredentialRequestOptions {
6668
*/
6769
private final Long timeout;
6870

71+
private final List<String> hints;
72+
6973
/**
7074
* Specifies the relying party identifier claimed by the caller.
7175
*
@@ -112,12 +116,14 @@ public class PublicKeyCredentialRequestOptions {
112116
private PublicKeyCredentialRequestOptions(
113117
@NonNull @JsonProperty("challenge") ByteArray challenge,
114118
@JsonProperty("timeout") Long timeout,
119+
@JsonProperty("hints") List<String> hints,
115120
@JsonProperty("rpId") String rpId,
116121
@JsonProperty("allowCredentials") List<PublicKeyCredentialDescriptor> allowCredentials,
117122
@JsonProperty("userVerification") UserVerificationRequirement userVerification,
118123
@NonNull @JsonProperty("extensions") AssertionExtensionInputs extensions) {
119124
this.challenge = challenge;
120125
this.timeout = timeout;
126+
this.hints = hints == null ? Collections.emptyList() : Collections.unmodifiableList(hints);
121127
this.rpId = rpId;
122128
this.allowCredentials =
123129
allowCredentials == null ? null : CollectionUtil.immutableList(allowCredentials);
@@ -213,6 +219,22 @@ public PublicKeyCredentialRequestOptionsBuilder timeout(long timeout) {
213219
return this.timeout(Optional.of(timeout));
214220
}
215221

222+
public PublicKeyCredentialRequestOptionsBuilder hints(@NonNull String... hints) {
223+
this.hints = Arrays.asList(hints);
224+
return this;
225+
}
226+
227+
public PublicKeyCredentialRequestOptionsBuilder hints(
228+
@NonNull PublicKeyCredentialHint... hints) {
229+
return this.hints(
230+
Arrays.stream(hints).map(PublicKeyCredentialHint::getValue).toArray(String[]::new));
231+
}
232+
233+
public PublicKeyCredentialRequestOptionsBuilder hints(List<String> hints) {
234+
this.hints = hints;
235+
return this;
236+
}
237+
216238
/**
217239
* Specifies the relying party identifier claimed by the caller.
218240
*

0 commit comments

Comments
 (0)