Skip to content

Commit 5ab0e4d

Browse files
committed
Add experimental option FinishRegistrationOptions.isConditionalCreate
1 parent be81553 commit 5ab0e4d

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed

NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
* (Experimental) Added property `RegisteredCredential.transports`.
3333
** NOTE: Experimental features may receive breaking changes without a major
3434
version increase.
35+
* (Experimental) Added option `FinishRegistrationOptions.isConditionalCreate` to
36+
allow UP=0 in registration response for registration ceremonies with
37+
`mediation: "conditional"`.
38+
** NOTE: Experimental features may receive breaking changes without a major
39+
version increase.
3540

3641

3742
== Version 2.6.0 ==

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ public class FinishRegistrationOptions {
6161
*/
6262
private final ByteArray callerTokenBindingId;
6363

64+
/**
65+
* If <code>true</code>, then user presence (UP) will not be required during registration
66+
* verification. This is needed for <a
67+
* href="https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#dom-clientcapability-conditionalcreate">conditional
68+
* create</a> operations.
69+
*
70+
* <p>The default is <code>false</code> (UP is required).
71+
*
72+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
73+
* the standard matures.
74+
* @since 2.7.0
75+
* @see <a
76+
* href="https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#dom-clientcapability-conditionalcreate">
77+
* <code>conditionalCreate</code> client capability</a>.
78+
* @see <a href="https://www.w3.org/TR/2025/WD-webauthn-3-20250127/#authdata-flags-up"><code>UP
79+
* </code> flag in authenticator data</a>.
80+
*/
81+
@Deprecated @Builder.Default private final boolean isConditionalCreate = false;
82+
6483
/**
6584
* The <a href="https://tools.ietf.org/html/rfc8471#section-3.2">token binding ID</a> of the
6685
* connection to the client, if any.

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ final class FinishRegistrationSteps {
8989
private final Clock clock;
9090
private final boolean allowOriginPort;
9191
private final boolean allowOriginSubdomain;
92+
private final boolean isConditionalCreate;
9293

9394
static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions options) {
9495
return new FinishRegistrationSteps(
@@ -102,7 +103,8 @@ static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions
102103
new CredentialRepositoryV1ToV2Adapter(rp.getCredentialRepository()),
103104
rp.getClock(),
104105
rp.isAllowOriginPort(),
105-
rp.isAllowOriginSubdomain());
106+
rp.isAllowOriginSubdomain(),
107+
options.isConditionalCreate());
106108
}
107109

108110
FinishRegistrationSteps(RelyingPartyV2<?> rp, FinishRegistrationOptions options) {
@@ -117,7 +119,8 @@ static FinishRegistrationSteps fromV1(RelyingParty rp, FinishRegistrationOptions
117119
rp.getCredentialRepository(),
118120
rp.getClock(),
119121
rp.isAllowOriginPort(),
120-
rp.isAllowOriginSubdomain());
122+
rp.isAllowOriginSubdomain(),
123+
options.isConditionalCreate());
121124
}
122125

123126
public Step6 begin() {
@@ -303,8 +306,8 @@ class Step14 implements Step<Step15> {
303306
@Override
304307
public void validate() {
305308
assertTrue(
306-
response.getResponse().getParsedAuthenticatorData().getFlags().UP,
307-
"User Presence is required.");
309+
isConditionalCreate || response.getResponse().getParsedAuthenticatorData().getFlags().UP,
310+
"User Presence is required unless isConditionalCreate is true.");
308311
}
309312

310313
@Override

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

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class RelyingPartyRegistrationSpec
164164
pubkeyCredParams: Option[List[PublicKeyCredentialParameters]] = None,
165165
testData: RegistrationTestData,
166166
clock: Clock = Clock.systemUTC(),
167+
isConditionalCreate: Boolean = false,
167168
): FinishRegistrationSteps = {
168169
var builder = RelyingParty
169170
.builder()
@@ -191,6 +192,7 @@ class RelyingPartyRegistrationSpec
191192
)
192193
.response(testData.response)
193194
.callerTokenBindingId(callerTokenBindingId.toJava)
195+
.isConditionalCreate(isConditionalCreate)
194196
.build()
195197

196198
builder
@@ -873,6 +875,7 @@ class RelyingPartyRegistrationSpec
873875
)(chk: Step => B)(
874876
uvr: UserVerificationRequirement,
875877
authDataEdit: ByteArray => ByteArray,
878+
isConditionalCreate: Boolean,
876879
): B = {
877880
val steps = finishRegistration(
878881
testData = testData
@@ -884,14 +887,19 @@ class RelyingPartyRegistrationSpec
884887
.build()
885888
)
886889
)
887-
.editAuthenticatorData(authDataEdit)
890+
.editAuthenticatorData(authDataEdit),
891+
isConditionalCreate = isConditionalCreate,
888892
)
889893
chk(stepsToStep(steps))
890894
}
891895

892896
def checkFailsWith(
893897
stepsToStep: FinishRegistrationSteps => Step
894-
): (UserVerificationRequirement, ByteArray => ByteArray) => Unit =
898+
): (
899+
UserVerificationRequirement,
900+
ByteArray => ByteArray,
901+
Boolean,
902+
) => Unit =
895903
check(stepsToStep) { step =>
896904
step.validations shouldBe a[Failure[_]]
897905
step.validations.failed.get shouldBe an[
@@ -902,7 +910,11 @@ class RelyingPartyRegistrationSpec
902910

903911
def checkSucceedsWith(
904912
stepsToStep: FinishRegistrationSteps => Step
905-
): (UserVerificationRequirement, ByteArray => ByteArray) => Unit =
913+
): (
914+
UserVerificationRequirement,
915+
ByteArray => ByteArray,
916+
Boolean,
917+
) => Unit =
906918
check(stepsToStep) { step =>
907919
step.validations shouldBe a[Success[_]]
908920
step.tryNext shouldBe a[Success[_]]
@@ -912,15 +924,41 @@ class RelyingPartyRegistrationSpec
912924
}
913925

914926
describe("14. Verify that the User Present bit of the flags in authData is set.") {
915-
val (checkFails, checkSucceeds) = checks[
927+
val (chkf, chks) = checks[
916928
FinishRegistrationSteps#Step15,
917929
FinishRegistrationSteps#Step14,
918930
](_.begin.next.next.next.next.next.next.next.next)
931+
def checkFails(
932+
uvr: UserVerificationRequirement,
933+
authDataEdit: ByteArray => ByteArray,
934+
isConditionalCreate: Boolean = false,
935+
): Unit = chkf(uvr, authDataEdit, isConditionalCreate)
936+
def checkSucceeds(
937+
uvr: UserVerificationRequirement,
938+
authDataEdit: ByteArray => ByteArray,
939+
isConditionalCreate: Boolean = false,
940+
): Unit = chks(uvr, authDataEdit, isConditionalCreate)
919941

920942
it("Fails if UV is discouraged and flag is not set.") {
921943
checkFails(UserVerificationRequirement.DISCOURAGED, upOff)
922944
}
923945

946+
it("Fails if UV is discouraged, isConditionalCreate is false and flag is not set.") {
947+
checkFails(
948+
UserVerificationRequirement.DISCOURAGED,
949+
upOff,
950+
isConditionalCreate = false,
951+
)
952+
}
953+
954+
it("Succeeds if UV is discouraged, isConditionalCreate is true and flag is not set.") {
955+
checkSucceeds(
956+
UserVerificationRequirement.DISCOURAGED,
957+
upOff,
958+
isConditionalCreate = true,
959+
)
960+
}
961+
924962
it("Succeeds if UV is discouraged and flag is set.") {
925963
checkSucceeds(UserVerificationRequirement.DISCOURAGED, upOn)
926964
}
@@ -949,10 +987,20 @@ class RelyingPartyRegistrationSpec
949987
}
950988

951989
describe("15. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.") {
952-
val (checkFails, checkSucceeds) = checks[
990+
val (chkf, chks) = checks[
953991
FinishRegistrationSteps#Step16,
954992
FinishRegistrationSteps#Step15,
955993
](_.begin.next.next.next.next.next.next.next.next.next)
994+
def checkFails(
995+
uvr: UserVerificationRequirement,
996+
authDataEdit: ByteArray => ByteArray,
997+
isConditionalCreate: Boolean = false,
998+
): Unit = chkf(uvr, authDataEdit, isConditionalCreate)
999+
def checkSucceeds(
1000+
uvr: UserVerificationRequirement,
1001+
authDataEdit: ByteArray => ByteArray,
1002+
isConditionalCreate: Boolean = false,
1003+
): Unit = chks(uvr, authDataEdit, isConditionalCreate)
9561004

9571005
it("Succeeds if UV is discouraged and flag is not set.") {
9581006
checkSucceeds(UserVerificationRequirement.DISCOURAGED, uvOff)

0 commit comments

Comments
 (0)