Skip to content

Commit b62ee43

Browse files
committed
Implement experimental support for SPC response type
1 parent 4a794e5 commit b62ee43

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed

NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
== Version 2.6.0 (unreleased) ==
22

3+
New features:
4+
35
* Added method `getParsedPublicKey(): java.security.PublicKey` to
46
`RegistrationResult` and `RegisteredCredential`.
57
** Thanks to Jakob Heher (A-SIT) for the contribution, see
68
https://github.com/Yubico/java-webauthn-server/pull/299
9+
* (Experimental) Added option `isSecurePaymentConfirmation(boolean)` to
10+
`FinishAssertionOptions`. When set, `RelyingParty.finishAssertion()` will
11+
adapt the validation logic for a Secure Payment Confirmation (SPC) response
12+
instead of an ordinary WebAuthn response. See the JavaDoc for details.
713

814

915
== Version 2.5.0 ==

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
2828
import com.yubico.webauthn.data.ByteArray;
2929
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
30+
import com.yubico.webauthn.data.CollectedClientData;
3031
import com.yubico.webauthn.data.PublicKeyCredential;
3132
import java.util.Optional;
33+
import java.util.Set;
3234
import lombok.Builder;
3335
import lombok.NonNull;
3436
import lombok.Value;
@@ -59,6 +61,41 @@ public class FinishAssertionOptions {
5961
*/
6062
private final ByteArray callerTokenBindingId;
6163

64+
/**
65+
* EXPERIMENTAL FEATURE:
66+
*
67+
* <p>If set to <code>false</code> (the default), the <code>"type"</code> property in the <a
68+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-client-data">collected
69+
* client data</a> of the assertion will be verified to equal <code>"webauthn.get"</code>.
70+
*
71+
* <p>If set to <code>true</code>, it will instead be verified to equal <code>"payment.get"</code>
72+
* .
73+
*
74+
* <p>NOTE: If you're using <a
75+
* href="https://www.w3.org/TR/2023/CR-secure-payment-confirmation-20230615/">Secure Payment
76+
* Confirmation</a> (SPC), you likely also need to relax the origin validation logic. Right now
77+
* this library only supports matching against a finite {@link Set} of acceptable origins. If
78+
* necessary, your application may validate the origin externally (see {@link
79+
* PublicKeyCredential#getResponse()}, {@link AuthenticatorAssertionResponse#getClientData()} and
80+
* {@link CollectedClientData#getOrigin()}) and construct a new {@link RelyingParty} instance for
81+
* each SPC response, setting the {@link RelyingParty.RelyingPartyBuilder#origins(Set) origins}
82+
* setting on that instance to contain the pre-validated origin value.
83+
*
84+
* <p>Better support for relaxing origin validation may be added as the feature matures.
85+
*
86+
* @deprecated EXPERIMENTAL: This is an experimental feature. It is likely to change or be deleted
87+
* before reaching a mature release.
88+
* @see <a href="https://www.w3.org/TR/2023/CR-secure-payment-confirmation-20230615/">Secure
89+
* Payment Confirmation</a>
90+
* @see <a
91+
* href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-client-data">5.8.1.
92+
* Client Data Used in WebAuthn Signatures (dictionary CollectedClientData)</a>
93+
* @see RelyingParty.RelyingPartyBuilder#origins(Set)
94+
* @see CollectedClientData
95+
* @see CollectedClientData#getOrigin()
96+
*/
97+
@Deprecated @Builder.Default private final boolean isSecurePaymentConfirmation = false;
98+
6299
/**
63100
* The <a href="https://tools.ietf.org/html/rfc8471#section-3.2">token binding ID</a> of the
64101
* connection to the client, if any.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
final class FinishAssertionSteps {
5151

5252
private static final String CLIENT_DATA_TYPE = "webauthn.get";
53+
private static final String SPC_CLIENT_DATA_TYPE = "payment.get";
5354

5455
private final AssertionRequest request;
5556
private final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
@@ -61,6 +62,7 @@ final class FinishAssertionSteps {
6162
private final boolean allowOriginPort;
6263
private final boolean allowOriginSubdomain;
6364
private final boolean validateSignatureCounter;
65+
private final boolean isSecurePaymentConfirmation;
6466

6567
FinishAssertionSteps(RelyingParty rp, FinishAssertionOptions options) {
6668
this.request = options.getRequest();
@@ -72,6 +74,7 @@ final class FinishAssertionSteps {
7274
this.allowOriginPort = rp.isAllowOriginPort();
7375
this.allowOriginSubdomain = rp.isAllowOriginSubdomain();
7476
this.validateSignatureCounter = rp.isValidateSignatureCounter();
77+
this.isSecurePaymentConfirmation = options.isSecurePaymentConfirmation();
7578
}
7679

7780
public Step5 begin() {
@@ -288,10 +291,12 @@ class Step11 implements Step<Step12> {
288291

289292
@Override
290293
public void validate() {
294+
final String expectedType =
295+
isSecurePaymentConfirmation ? SPC_CLIENT_DATA_TYPE : CLIENT_DATA_TYPE;
291296
assertTrue(
292-
CLIENT_DATA_TYPE.equals(clientData.getType()),
297+
expectedType.equals(clientData.getType()),
293298
"The \"type\" in the client data must be exactly \"%s\", was: %s",
294-
CLIENT_DATA_TYPE,
299+
expectedType,
295300
clientData.getType());
296301
}
297302

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

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ package com.yubico.webauthn
2727
import com.fasterxml.jackson.core.`type`.TypeReference
2828
import com.fasterxml.jackson.databind.node.JsonNodeFactory
2929
import com.fasterxml.jackson.databind.node.ObjectNode
30+
import com.fasterxml.jackson.databind.node.TextNode
3031
import com.upokecenter.cbor.CBORObject
3132
import com.yubico.internal.util.JacksonCodecs
3233
import com.yubico.webauthn.data.AssertionExtensionInputs
@@ -179,6 +180,7 @@ class RelyingPartyAssertionSpec
179180
credentialId: ByteArray = Defaults.credentialId,
180181
credentialKey: KeyPair = Defaults.credentialKey,
181182
credentialRepository: Option[CredentialRepository] = None,
183+
isSecurePaymentConfirmation: Option[Boolean] = None,
182184
origins: Option[Set[String]] = None,
183185
requestedExtensions: AssertionExtensionInputs =
184186
Defaults.requestedExtensions,
@@ -283,6 +285,10 @@ class RelyingPartyAssertionSpec
283285
.response(response)
284286
.callerTokenBindingId(callerTokenBindingId.toJava)
285287

288+
isSecurePaymentConfirmation foreach { isSpc =>
289+
fao.isSecurePaymentConfirmation(isSpc)
290+
}
291+
286292
builder
287293
.build()
288294
._finishAssertion(fao.build())
@@ -941,14 +947,18 @@ class RelyingPartyAssertionSpec
941947
step.validations shouldBe a[Success[_]]
942948
}
943949

944-
def assertFails(typeString: String): Unit = {
950+
def assertFails(
951+
typeString: String,
952+
isSecurePaymentConfirmation: Option[Boolean] = None,
953+
): Unit = {
945954
val steps = finishAssertion(
946955
clientDataJson = JacksonCodecs.json.writeValueAsString(
947956
JacksonCodecs.json
948957
.readTree(Defaults.clientDataJson)
949958
.asInstanceOf[ObjectNode]
950959
.set("type", jsonFactory.textNode(typeString))
951-
)
960+
),
961+
isSecurePaymentConfirmation = isSecurePaymentConfirmation,
952962
)
953963
val step: FinishAssertionSteps#Step11 =
954964
steps.begin.next.next.next.next.next
@@ -973,6 +983,72 @@ class RelyingPartyAssertionSpec
973983
it("""The string "webauthn.create" fails.""") {
974984
assertFails("webauthn.create")
975985
}
986+
987+
it("""The string "payment.get" fails.""") {
988+
assertFails("payment.get")
989+
}
990+
991+
describe("If the isSecurePaymentConfirmation option is set,") {
992+
it("the default test case fails.") {
993+
val steps =
994+
finishAssertion(isSecurePaymentConfirmation = Some(true))
995+
val step: FinishAssertionSteps#Step11 =
996+
steps.begin.next.next.next.next.next
997+
998+
step.validations shouldBe a[Failure[_]]
999+
step.validations.failed.get shouldBe an[IllegalArgumentException]
1000+
}
1001+
1002+
it("""the default test case succeeds if type is overwritten with the value "payment.get".""") {
1003+
val json = JacksonCodecs.json()
1004+
val steps = finishAssertion(
1005+
isSecurePaymentConfirmation = Some(true),
1006+
clientDataJson = json.writeValueAsString(
1007+
json
1008+
.readTree(Defaults.clientDataJson)
1009+
.asInstanceOf[ObjectNode]
1010+
.set[ObjectNode]("type", new TextNode("payment.get"))
1011+
),
1012+
)
1013+
val step: FinishAssertionSteps#Step11 =
1014+
steps.begin.next.next.next.next.next
1015+
1016+
step.validations shouldBe a[Success[_]]
1017+
}
1018+
1019+
it("""any value other than "payment.get" fails.""") {
1020+
forAll { (typeString: String) =>
1021+
whenever(typeString != "payment.get") {
1022+
assertFails(
1023+
typeString,
1024+
isSecurePaymentConfirmation = Some(true),
1025+
)
1026+
}
1027+
}
1028+
forAll(Gen.alphaNumStr) { (typeString: String) =>
1029+
whenever(typeString != "payment.get") {
1030+
assertFails(
1031+
typeString,
1032+
isSecurePaymentConfirmation = Some(true),
1033+
)
1034+
}
1035+
}
1036+
}
1037+
1038+
it("""the string "webauthn.create" fails.""") {
1039+
assertFails(
1040+
"webauthn.create",
1041+
isSecurePaymentConfirmation = Some(true),
1042+
)
1043+
}
1044+
1045+
it("""the string "webauthn.get" fails.""") {
1046+
assertFails(
1047+
"webauthn.get",
1048+
isSecurePaymentConfirmation = Some(true),
1049+
)
1050+
}
1051+
}
9761052
}
9771053

9781054
it("12. Verify that the value of C.challenge equals the base64url encoding of options.challenge.") {

0 commit comments

Comments
 (0)