Skip to content

Commit

Permalink
Support user handle as alternative to username in StartAssertionOptions
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Sep 29, 2021
1 parent d8e2d60 commit c5b35e6
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 63 deletions.
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ New features:
`.fromJson(String)` suitable for encoding to and decoding from JSON.
* Added methods `AssertionRequest.toJson()` and `.fromJson(String)` suitable for
encoding to and decoding from JSON.
* Added methods `StartAssertionOptions.builder().userHandle(ByteArray)` and
`.userHandle(Optional<ByteArray>)` as alternatives to `.username(String)` and
`.username(Optional<String>)`. The `userHandle` methods fill the same function
as, and are mutually exclusive with, the `username` methods.

Fixes:

Expand Down
2 changes: 1 addition & 1 deletion README
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ First, generate authentication parameters and send them to the client:
[source,java]
----------
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username("alice")
.username("alice") // Or .userHandle(ByteArray) if preferred
.build());
String credentialGetJson = request.toCredentialsGetJson();
return credentialGetJson; // Send to client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package com.yubico.webauthn;

import com.yubico.internal.util.CollectionUtil;
import com.yubico.internal.util.OptionalUtil;
import com.yubico.webauthn.attestation.MetadataService;
import com.yubico.webauthn.data.AssertionExtensionInputs;
import com.yubico.webauthn.data.AttestationConveyancePreference;
Expand Down Expand Up @@ -449,8 +450,12 @@ public AssertionRequest startAssertion(StartAssertionOptions startAssertionOptio
.challenge(generateChallenge())
.rpId(identity.getId())
.allowCredentials(
startAssertionOptions
.getUsername()
OptionalUtil.orElseOptional(
startAssertionOptions.getUsername(),
() ->
startAssertionOptions
.getUserHandle()
.flatMap(credentialRepository::getUsernameForUserHandle))
.map(
un ->
new ArrayList<>(credentialRepository.getCredentialIdsForUsername(un))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package com.yubico.webauthn;

import com.yubico.webauthn.data.AssertionExtensionInputs;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions;
import com.yubico.webauthn.data.UserVerificationRequirement;
import java.util.Optional;
Expand All @@ -37,20 +38,10 @@
@Builder(toBuilder = true)
public class StartAssertionOptions {

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

private final ByteArray userHandle;

/**
* Extension inputs for this authentication operation.
*
Expand Down Expand Up @@ -91,8 +82,16 @@ public class StartAssertionOptions {
/**
* The username of the user to authenticate, if the user has already been identified.
*
* <p>If this is absent, that implies a first-factor authentication operation - meaning
* identification of the user is deferred until after receiving the response from the client.
* <p>Mutually exclusive with {@link #getUserHandle()}.
*
* <p>If this or {@link #getUserHandle()} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after receiving
* the response from the client.
*
* <p>The default is empty (absent).
*
Expand All @@ -104,6 +103,32 @@ public Optional<String> getUsername() {
return Optional.ofNullable(username);
}

/**
* The user handle of the user to authenticate, if the user has already been identified.
*
* <p>Mutually exclusive with {@link #getUsername()}.
*
* <p>If this or {@link #getUsername()} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after receiving
* the response from the client.
*
* <p>The default is empty (absent).
*
* @see #getUsername()
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle">User Handle</a>
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
* credential</a>
*/
public Optional<ByteArray> getUserHandle() {
return Optional.ofNullable(userHandle);
}

/**
* The value for {@link PublicKeyCredentialRequestOptions#getUserVerification()} for this
* authentication operation.
Expand Down Expand Up @@ -135,40 +160,130 @@ public Optional<Long> getTimeout() {

public static class StartAssertionOptionsBuilder {
private String username = null;
private ByteArray userHandle = null;
private UserVerificationRequirement userVerification = null;
private Long timeout = null;

/**
* The username of the user to authenticate, if the user has already been identified.
*
* <p>If this is absent, that implies a first-factor authentication operation - meaning
* identification of the user is deferred until after receiving the response from the client.
* <p>Mutually exclusive with {@link #userHandle(Optional)}. Setting this to a present value
* will set {@link #userHandle(Optional)} to empty.
*
* <p>If this or {@link #userHandle(Optional)} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after
* receiving the response from the client.
*
* <p>The default is empty (absent).
*
* @see #username(String)
* @see #userHandle(Optional)
* @see #userHandle(ByteArray)
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
* credential</a>
*/
public StartAssertionOptionsBuilder username(@NonNull Optional<String> username) {
this.username = username.orElse(null);
if (username.isPresent()) {
this.userHandle = null;
}
return this;
}

/**
* The username of the user to authenticate, if the user has already been identified.
*
* <p>If this is absent, that implies a first-factor authentication operation - meaning
* identification of the user is deferred until after receiving the response from the client.
* <p>Mutually exclusive with {@link #userHandle(Optional)}. Setting this to a non-null value
* will set {@link #userHandle(Optional)} to empty.
*
* <p>If this or {@link #userHandle(Optional)} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUserHandle()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after
* receiving the response from the client.
*
* <p>The default is empty (absent).
*
* @see #username(Optional)
* @see #userHandle(Optional)
* @see #userHandle(ByteArray)
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
* credential</a>
*/
public StartAssertionOptionsBuilder username(@NonNull String username) {
return this.username(Optional.of(username));
public StartAssertionOptionsBuilder username(String username) {
return this.username(Optional.ofNullable(username));
}

/**
* The user handle of the user to authenticate, if the user has already been identified.
*
* <p>Mutually exclusive with {@link #username(Optional)}. Setting this to a present value will
* set {@link #username(Optional)} to empty.
*
* <p>If this or {@link #username(Optional)} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after
* receiving the response from the client.
*
* <p>The default is empty (absent).
*
* @see #username(String)
* @see #username(Optional)
* @see #userHandle(ByteArray)
* @see <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#user-handle">User
* Handle</a>
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
* credential</a>
*/
public StartAssertionOptionsBuilder userHandle(@NonNull Optional<ByteArray> userHandle) {
this.userHandle = userHandle.orElse(null);
if (userHandle.isPresent()) {
this.username = null;
}
return this;
}

/**
* The user handle of the user to authenticate, if the user has already been identified.
*
* <p>Mutually exclusive with {@link #username(Optional)}. Setting this to a non-null value will
* set {@link #username(Optional)} to empty.
*
* <p>If this or {@link #username(Optional)} is present, then {@link
* RelyingParty#startAssertion(StartAssertionOptions)} will set {@link
* PublicKeyCredentialRequestOptions#getAllowCredentials()} to the list of that user's
* credentials.
*
* <p>If this and {@link #getUsername()} are both absent, that implies a first-factor
* authentication operation - meaning identification of the user is deferred until after
* receiving the response from the client.
*
* <p>The default is empty (absent).
*
* @see #username(String)
* @see #username(Optional)
* @see #userHandle(Optional)
* @see <a
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#client-side-discoverable-public-key-credential-source">Client-side-resident
* credential</a>
*/
public StartAssertionOptionsBuilder userHandle(ByteArray userHandle) {
return this.userHandle(Optional.ofNullable(userHandle));
}

/**
Expand Down Expand Up @@ -200,8 +315,8 @@ public StartAssertionOptionsBuilder userVerification(
* <p>The default is {@link UserVerificationRequirement#PREFERRED}.
*/
public StartAssertionOptionsBuilder userVerification(
@NonNull UserVerificationRequirement userVerification) {
return this.userVerification(Optional.of(userVerification));
UserVerificationRequirement userVerification) {
return this.userVerification(Optional.ofNullable(userVerification));
}

/**
Expand Down Expand Up @@ -235,5 +350,13 @@ public StartAssertionOptionsBuilder timeout(@NonNull Optional<Long> timeout) {
public StartAssertionOptionsBuilder timeout(long timeout) {
return this.timeout(Optional.of(timeout));
}

/*
* Workaround, see: https://github.com/rzwitserloot/lombok/issues/2623#issuecomment-714816001
* Consider reverting this workaround if Lombok fixes that issue.
*/
private StartAssertionOptionsBuilder timeout(Long timeout) {
return this.timeout(Optional.ofNullable(timeout));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.yubico.webauthn
import com.yubico.scalacheck.gen.JavaGenerators._
import com.yubico.webauthn.attestation.Attestation
import com.yubico.webauthn.attestation.Generators._
import com.yubico.webauthn.data.AssertionExtensionInputs
import com.yubico.webauthn.data.AttestationType
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs
import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs
Expand All @@ -11,8 +12,10 @@ import com.yubico.webauthn.data.ClientAssertionExtensionOutputs
import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs
import com.yubico.webauthn.data.Generators._
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor
import com.yubico.webauthn.data.UserVerificationRequirement
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen

import java.util.Optional

Expand Down Expand Up @@ -87,4 +90,24 @@ object Generators {
.build()
)

implicit val arbitraryStartAssertionOptions
: Arbitrary[StartAssertionOptions] = Arbitrary(
for {
extensions <- arbitrary[Option[AssertionExtensionInputs]]
timeout <- Gen.option(Gen.posNum[Long])
usernameOrUserHandle <- arbitrary[Option[Either[String, ByteArray]]]
userVerification <- arbitrary[Option[UserVerificationRequirement]]
} yield {
val b = StartAssertionOptions.builder()
extensions.foreach(b.extensions)
timeout.foreach(b.timeout)
usernameOrUserHandle.foreach {
case Left(username) => b.username(username)
case Right(userHandle) => b.userHandle(userHandle)
}
userVerification.foreach(b.userVerification)
b.build()
}
)

}
Loading

0 comments on commit c5b35e6

Please sign in to comment.