diff --git a/NEWS b/NEWS index 7a8410a81..ac05bf270 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,15 @@ Fixes: * Fixed bug in `RelyingParty.finishAssertion` that would throw a nondescript `NoSuchElementException` if username and user handle are both absent, instead of an `IllegalArgumentException` with a better error message. +* Fixed bug in `RelyingParty.finishAssertion` where if + `StartAssertionOptions.userHandle` was set, it did not propagate to + `RelyingParty.finishAssertion` and caused an error saying username and user + handle are both absent unless a user handle was returned by the authenticator. + +New features: + +* Added `userHandle` field to `AssertionRequest` as part of above bug fix. + `userHandle` is mutually exclusive with `username`. == Version 1.12.2 == diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java index 13f29b89b..c531b8131 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionRequest.java @@ -37,7 +37,7 @@ /** * A combination of a {@link PublicKeyCredentialRequestOptions} and, optionally, a {@link - * #getUsername() username}. + * #getUsername() username} or {@link #getUserHandle() user handle}. */ @Value @Builder(toBuilder = true) @@ -52,26 +52,58 @@ public class AssertionRequest { /** * The username of the user to authenticate, if the user has already been identified. * - *
If this is absent, this indicates that this is a request for an assertion by a This is mutually exclusive with {@link #getUserHandle() userHandle}; setting this will unset
+ * {@link #getUserHandle() userHandle}. When parsing from JSON, {@link #getUserHandle()
+ * userHandle} takes precedence over this.
+ *
+ * If both this and {@link #getUserHandle() userHandle} are empty, this indicates that this is
+ * a request for an assertion by a client-side-resident
* credential, and identification of the user has been deferred until the response is
* received.
*/
private final String username;
+ /**
+ * The user handle of the user to authenticate, if the user has already been identified.
+ *
+ * This is mutually exclusive with {@link #getUsername() username}; setting this will unset
+ * {@link #getUsername() username}. When parsing from JSON, this takes precedence over {@link
+ * #getUsername() username}.
+ *
+ * If both this and {@link #getUsername() username} are empty, this indicates that this is a
+ * request for an assertion by a client-side-resident
+ * credential, and identification of the user has been deferred until the response is
+ * received.
+ */
+ private final ByteArray userHandle;
+
@JsonCreator
private AssertionRequest(
@NonNull @JsonProperty("publicKeyCredentialRequestOptions")
PublicKeyCredentialRequestOptions publicKeyCredentialRequestOptions,
- @JsonProperty("username") String username) {
+ @JsonProperty("username") String username,
+ @JsonProperty("userHandle") ByteArray userHandle) {
this.publicKeyCredentialRequestOptions = publicKeyCredentialRequestOptions;
- this.username = username;
+
+ if (userHandle != null) {
+ this.username = null;
+ this.userHandle = userHandle;
+ } else {
+ this.username = username;
+ this.userHandle = null;
+ }
}
/**
* The username of the user to authenticate, if the user has already been identified.
*
- * If this is absent, this indicates that this is a request for an assertion by a This is mutually exclusive with {@link #getUserHandle()}; if this is present, then {@link
+ * #getUserHandle()} will be empty.
+ *
+ * If both this and {@link #getUserHandle()} are empty, this indicates that this is a request
+ * for an assertion by a client-side-resident
* credential, and identification of the user has been deferred until the response is
* received.
@@ -80,6 +112,22 @@ public Optional This is mutually exclusive with {@link #getUsername()}; if this is present, then {@link
+ * #getUsername()} will be empty.
+ *
+ * If both this and {@link #getUsername()} are empty, this indicates that this is a request for
+ * an assertion by a client-side-resident
+ * credential, and identification of the user has been deferred until the response is
+ * received.
+ */
+ public Optional If this is absent, this indicates that this is a request for an assertion by a This is mutually exclusive with {@link #userHandle(ByteArray)}; setting this to non-empty
+ * will unset {@link #userHandle(ByteArray)}.
+ *
+ * If this is empty, this indicates that this is a request for an assertion by a client-side-resident
* credential, and identification of the user has been deferred until the response is
* received.
@@ -173,13 +225,55 @@ public AssertionRequestBuilder username(@NonNull Optional If this is absent, this indicates that this is a request for an assertion by a This is mutually exclusive with {@link #userHandle(ByteArray)}; setting this to non- If this is empty, this indicates that this is a request for an assertion by a client-side-resident
* credential, and identification of the user has been deferred until the response is
* received.
*/
public AssertionRequestBuilder username(String username) {
this.username = username;
+ if (username != null) {
+ this.userHandle = null;
+ }
+ return this;
+ }
+
+ /**
+ * The user handle of the user to authenticate, if the user has already been identified.
+ *
+ * This is mutually exclusive with {@link #username(String)}; setting this to non-empty will
+ * unset {@link #username(String)}.
+ *
+ * If both this and {@link #username(String)} are empty, this indicates that this is a
+ * request for an assertion by a client-side-resident
+ * credential, and identification of the user has been deferred until the response is
+ * received.
+ */
+ public AssertionRequestBuilder userHandle(@NonNull Optional This is mutually exclusive with {@link #username(String)}; setting this to non- If both this and {@link #username(String)} are empty, this indicates that this is a
+ * request for an assertion by a client-side-resident
+ * credential, and identification of the user has been deferred until the response is
+ * received.
+ */
+ public AssertionRequestBuilder userHandle(ByteArray userHandle) {
+ if (userHandle != null) {
+ this.username = null;
+ }
+ this.userHandle = userHandle;
return this;
}
}
diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
index 04024cad7..49e8ccec5 100644
--- a/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
+++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/FinishAssertionSteps.java
@@ -119,10 +119,11 @@ default AssertionResult run() throws InvalidSignatureCountException {
class Step0 implements Step
+ * null
will unset {@link #userHandle(ByteArray)}.
+ *
+ * null
+ *
will unset {@link #username(String)}.
+ *
+ *