Skip to content

Commit c5b35e6

Browse files
committed
Support user handle as alternative to username in StartAssertionOptions
1 parent d8e2d60 commit c5b35e6

File tree

7 files changed

+344
-63
lines changed

7 files changed

+344
-63
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ New features:
4343
`.fromJson(String)` suitable for encoding to and decoding from JSON.
4444
* Added methods `AssertionRequest.toJson()` and `.fromJson(String)` suitable for
4545
encoding to and decoding from JSON.
46+
* Added methods `StartAssertionOptions.builder().userHandle(ByteArray)` and
47+
`.userHandle(Optional<ByteArray>)` as alternatives to `.username(String)` and
48+
`.username(Optional<String>)`. The `userHandle` methods fill the same function
49+
as, and are mutually exclusive with, the `username` methods.
4650

4751
Fixes:
4852

README

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ First, generate authentication parameters and send them to the client:
302302
[source,java]
303303
----------
304304
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
305-
.username("alice")
305+
.username("alice") // Or .userHandle(ByteArray) if preferred
306306
.build());
307307
String credentialGetJson = request.toCredentialsGetJson();
308308
return credentialGetJson; // Send to client

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package com.yubico.webauthn;
2626

2727
import com.yubico.internal.util.CollectionUtil;
28+
import com.yubico.internal.util.OptionalUtil;
2829
import com.yubico.webauthn.attestation.MetadataService;
2930
import com.yubico.webauthn.data.AssertionExtensionInputs;
3031
import com.yubico.webauthn.data.AttestationConveyancePreference;
@@ -449,8 +450,12 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio
449450
.challenge(generateChallenge())
450451
.rpId(identity.getId())
451452
.allowCredentials(
452-
startAssertionOptions
453-
.getUsername()
453+
OptionalUtil.orElseOptional(
454+
startAssertionOptions.getUsername(),
455+
() ->
456+
startAssertionOptions
457+
.getUserHandle()
458+
.flatMap(credentialRepository::getUsernameForUserHandle))
454459
.map(
455460
un ->
456461
new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un))))

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

Lines changed: 145 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
package com.yubico.webauthn;
2626

2727
import com.yubico.webauthn.data.AssertionExtensionInputs;
28+
import com.yubico.webauthn.data.ByteArray;
2829
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
2930
import com.yubico.webauthn.data.UserVerificationRequirement;
3031
import java.util.Optional;
@@ -37,20 +38,10 @@
3738
@Builder(toBuilder = true)
3839
public class StartAssertionOptions {
3940

40-
/**
41-
* The username of the user to authenticate, if the user has already been identified.
42-
*
43-
* <p>If this is absent, that implies a first-factor authentication operation - meaning
44-
* identification of the user is deferred until after receiving the response from the client.
45-
*
46-
* <p>The default is empty (absent).
47-
*
48-
* @see <a
49-
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
50-
* credential</a>
51-
*/
5241
private final String username;
5342

43+
private final ByteArray userHandle;
44+
5445
/**
5546
* Extension inputs for this authentication operation.
5647
*
@@ -91,8 +82,16 @@ public class StartAssertionOptions {
9182
/**
9283
* The username of the user to authenticate, if the user has already been identified.
9384
*
94-
* <p>If this is absent, that implies a first-factor authentication operation - meaning
95-
* identification of the user is deferred until after receiving the response from the client.
85+
* <p>Mutually exclusive with {@link #getUserHandle()}.
86+
*
87+
* <p>If this or {@link #getUserHandle()} is present, then {@link
88+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
89+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
90+
* credentials.
91+
*
92+
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
93+
* authentication operation - meaning identification of the user is deferred until after receiving
94+
* the response from the client.
9695
*
9796
* <p>The default is empty (absent).
9897
*
@@ -104,6 +103,32 @@ public Optional<String> getUsername() {
104103
return Optional.ofNullable(username);
105104
}
106105

106+
/**
107+
* The user handle of the user to authenticate, if the user has already been identified.
108+
*
109+
* <p>Mutually exclusive with {@link #getUsername()}.
110+
*
111+
* <p>If this or {@link #getUsername()} is present, then {@link
112+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
113+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
114+
* credentials.
115+
*
116+
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
117+
* authentication operation - meaning identification of the user is deferred until after receiving
118+
* the response from the client.
119+
*
120+
* <p>The default is empty (absent).
121+
*
122+
* @see #getUsername()
123+
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle">User Handle</a>
124+
* @see <a
125+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
126+
* credential</a>
127+
*/
128+
public Optional<ByteArray> getUserHandle() {
129+
return Optional.ofNullable(userHandle);
130+
}
131+
107132
/**
108133
* The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this
109134
* authentication operation.
@@ -135,40 +160,130 @@ public Optional<Long> getTimeout() {
135160

136161
public static class StartAssertionOptionsBuilder {
137162
private String username = null;
163+
private ByteArray userHandle = null;
138164
private UserVerificationRequirement userVerification = null;
139165
private Long timeout = null;
140166

141167
/**
142168
* The username of the user to authenticate, if the user has already been identified.
143169
*
144-
* <p>If this is absent, that implies a first-factor authentication operation - meaning
145-
* identification of the user is deferred until after receiving the response from the client.
170+
* <p>Mutually exclusive with {@link #userHandle(Optional)}. Setting this to a present value
171+
* will set {@link #userHandle(Optional)} to empty.
172+
*
173+
* <p>If this or {@link #userHandle(Optional)} is present, then {@link
174+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
175+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
176+
* credentials.
177+
*
178+
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
179+
* authentication operation - meaning identification of the user is deferred until after
180+
* receiving the response from the client.
146181
*
147182
* <p>The default is empty (absent).
148183
*
184+
* @see #username(String)
185+
* @see #userHandle(Optional)
186+
* @see #userHandle(ByteArray)
149187
* @see <a
150188
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
151189
* credential</a>
152190
*/
153191
public StartAssertionOptionsBuilder username(@NonNull Optional<String> username) {
154192
this.username = username.orElse(null);
193+
if (username.isPresent()) {
194+
this.userHandle = null;
195+
}
155196
return this;
156197
}
157198

158199
/**
159200
* The username of the user to authenticate, if the user has already been identified.
160201
*
161-
* <p>If this is absent, that implies a first-factor authentication operation - meaning
162-
* identification of the user is deferred until after receiving the response from the client.
202+
* <p>Mutually exclusive with {@link #userHandle(Optional)}. Setting this to a non-null value
203+
* will set {@link #userHandle(Optional)} to empty.
204+
*
205+
* <p>If this or {@link #userHandle(Optional)} is present, then {@link
206+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
207+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
208+
* credentials.
209+
*
210+
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
211+
* authentication operation - meaning identification of the user is deferred until after
212+
* receiving the response from the client.
163213
*
164214
* <p>The default is empty (absent).
165215
*
216+
* @see #username(Optional)
217+
* @see #userHandle(Optional)
218+
* @see #userHandle(ByteArray)
166219
* @see <a
167220
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
168221
* credential</a>
169222
*/
170-
public StartAssertionOptionsBuilder username(@NonNull String username) {
171-
return this.username(Optional.of(username));
223+
public StartAssertionOptionsBuilder username(String username) {
224+
return this.username(Optional.ofNullable(username));
225+
}
226+
227+
/**
228+
* The user handle of the user to authenticate, if the user has already been identified.
229+
*
230+
* <p>Mutually exclusive with {@link #username(Optional)}. Setting this to a present value will
231+
* set {@link #username(Optional)} to empty.
232+
*
233+
* <p>If this or {@link #username(Optional)} is present, then {@link
234+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
235+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
236+
* credentials.
237+
*
238+
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
239+
* authentication operation - meaning identification of the user is deferred until after
240+
* receiving the response from the client.
241+
*
242+
* <p>The default is empty (absent).
243+
*
244+
* @see #username(String)
245+
* @see #username(Optional)
246+
* @see #userHandle(ByteArray)
247+
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle">User
248+
* Handle</a>
249+
* @see <a
250+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
251+
* credential</a>
252+
*/
253+
public StartAssertionOptionsBuilder userHandle(@NonNull Optional<ByteArray> userHandle) {
254+
this.userHandle = userHandle.orElse(null);
255+
if (userHandle.isPresent()) {
256+
this.username = null;
257+
}
258+
return this;
259+
}
260+
261+
/**
262+
* The user handle of the user to authenticate, if the user has already been identified.
263+
*
264+
* <p>Mutually exclusive with {@link #username(Optional)}. Setting this to a non-null value will
265+
* set {@link #username(Optional)} to empty.
266+
*
267+
* <p>If this or {@link #username(Optional)} is present, then {@link
268+
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
269+
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
270+
* credentials.
271+
*
272+
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
273+
* authentication operation - meaning identification of the user is deferred until after
274+
* receiving the response from the client.
275+
*
276+
* <p>The default is empty (absent).
277+
*
278+
* @see #username(String)
279+
* @see #username(Optional)
280+
* @see #userHandle(Optional)
281+
* @see <a
282+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
283+
* credential</a>
284+
*/
285+
public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) {
286+
return this.userHandle(Optional.ofNullable(userHandle));
172287
}
173288

174289
/**
@@ -200,8 +315,8 @@ public StartAssertionOptionsBuilder userVerification(
200315
* <p>The default is {@link UserVerificationRequirement#PREFERRED}.
201316
*/
202317
public StartAssertionOptionsBuilder userVerification(
203-
@NonNull UserVerificationRequirement userVerification) {
204-
return this.userVerification(Optional.of(userVerification));
318+
UserVerificationRequirement userVerification) {
319+
return this.userVerification(Optional.ofNullable(userVerification));
205320
}
206321

207322
/**
@@ -235,5 +350,13 @@ public StartAssertionOptionsBuilder timeout(@NonNull Optional<Long> timeout) {
235350
public StartAssertionOptionsBuilder timeout(long timeout) {
236351
return this.timeout(Optional.of(timeout));
237352
}
353+
354+
/*
355+
* Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001
356+
* Consider reverting this workaround if Lombok fixes that issue.
357+
*/
358+
private StartAssertionOptionsBuilder timeout(Long timeout) {
359+
return this.timeout(Optional.ofNullable(timeout));
360+
}
238361
}
239362
}

webauthn-server-core/src/test/scala/com/yubico/webauthn/Generators.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.yubico.webauthn
33
import com.yubico.scalacheck.gen.JavaGenerators._
44
import com.yubico.webauthn.attestation.Attestation
55
import com.yubico.webauthn.attestation.Generators._
6+
import com.yubico.webauthn.data.AssertionExtensionInputs
67
import com.yubico.webauthn.data.AttestationType
78
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs
89
import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs
@@ -11,8 +12,10 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
1112
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs
1213
import com.yubico.webauthn.data.Generators._
1314
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
15+
import com.yubico.webauthn.data.UserVerificationRequirement
1416
import org.scalacheck.Arbitrary
1517
import org.scalacheck.Arbitrary.arbitrary
18+
import org.scalacheck.Gen
1619

1720
import java.util.Optional
1821

@@ -87,4 +90,24 @@ object Generators {
8790
.build()
8891
)
8992

93+
implicit val arbitraryStartAssertionOptions
94+
: Arbitrary[StartAssertionOptions] = Arbitrary(
95+
for {
96+
extensions <- arbitrary[Option[AssertionExtensionInputs]]
97+
timeout <- Gen.option(Gen.posNum[Long])
98+
usernameOrUserHandle <- arbitrary[Option[Either[String, ByteArray]]]
99+
userVerification <- arbitrary[Option[UserVerificationRequirement]]
100+
} yield {
101+
val b = StartAssertionOptions.builder()
102+
extensions.foreach(b.extensions)
103+
timeout.foreach(b.timeout)
104+
usernameOrUserHandle.foreach {
105+
case Left(username) => b.username(username)
106+
case Right(userHandle) => b.userHandle(userHandle)
107+
}
108+
userVerification.foreach(b.userVerification)
109+
b.build()
110+
}
111+
)
112+
90113
}

0 commit comments

Comments
 (0)