Skip to content

Commit 35c983d

Browse files
committed
Merge pull request #250 from luisgoncalves/feature/credential-authenticator-attachment
Support authenticatorAttachment in PublicKeyCredential See: #250
2 parents 4f4332b + 0c817c2 commit 35c983d

File tree

9 files changed

+182
-25
lines changed

9 files changed

+182
-25
lines changed

NEWS

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
== Version 2.3.0 (unreleased) ==
2+
3+
New features:
4+
5+
* (Experimental) Added `authenticatorAttachment` property to response objects:
6+
** NOTE: Experimental features may receive breaking changes without a major
7+
version increase.
8+
** Added method `getAuthenticatorAttachment()` to `PublicKeyCredential` and
9+
corresponding builder method
10+
`authenticatorAttachment(AuthenticatorAttachment)`.
11+
** Added method `getAuthenticatorAttachment()` to `RegistrationResult` and
12+
`AssertionResult`, which echo `getAuthenticatorAttachment()` from the
13+
corresponding `PublicKeyCredential`.
14+
** Thanks to GitHub user luisgoncalves for the contribution, see
15+
https://github.com/Yubico/java-webauthn-server/pull/250
16+
17+
118
== Version 2.2.0 ==
219

320
`webauthn-server-core`:

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.fasterxml.jackson.annotation.JsonProperty;
3030
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs;
3131
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
32+
import com.yubico.webauthn.data.AuthenticatorAttachment;
3233
import com.yubico.webauthn.data.AuthenticatorData;
3334
import com.yubico.webauthn.data.ByteArray;
3435
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
@@ -195,6 +196,20 @@ public boolean isBackedUp() {
195196
return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BS;
196197
}
197198

199+
/**
200+
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
201+
* attachment modality</a> in effect at the time the asserted credential was used.
202+
*
203+
* @see PublicKeyCredential#getAuthenticatorAttachment()
204+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
205+
* the standard matures.
206+
*/
207+
@Deprecated
208+
@JsonIgnore
209+
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
210+
return credentialResponse.getAuthenticatorAttachment();
211+
}
212+
198213
/**
199214
* The new <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
200215
* count</a> of the credential used for the assertion.

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder;
3232
import com.yubico.webauthn.attestation.AttestationTrustSource;
3333
import com.yubico.webauthn.data.AttestationType;
34+
import com.yubico.webauthn.data.AuthenticatorAttachment;
3435
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
3536
import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs;
3637
import com.yubico.webauthn.data.ByteArray;
@@ -175,6 +176,20 @@ public boolean isBackedUp() {
175176
return credential.getResponse().getParsedAuthenticatorData().getFlags().BS;
176177
}
177178

179+
/**
180+
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
181+
* attachment modality</a> in effect at the time the credential was created.
182+
*
183+
* @see PublicKeyCredential#getAuthenticatorAttachment()
184+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
185+
* the standard matures.
186+
*/
187+
@Deprecated
188+
@JsonIgnore
189+
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
190+
return credential.getAuthenticatorAttachment();
191+
}
192+
178193
/**
179194
* The signature count returned with the created credential.
180195
*

webauthn-server-core/src/main/java/com/yubico/webauthn/data/AuthenticatorAttachment.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
import com.fasterxml.jackson.annotation.JsonCreator;
2828
import com.fasterxml.jackson.annotation.JsonValue;
29-
import java.util.Optional;
3029
import java.util.stream.Stream;
3130
import lombok.AllArgsConstructor;
3231
import lombok.Getter;
@@ -73,18 +72,8 @@ public enum AuthenticatorAttachment {
7372

7473
@JsonValue @Getter @NonNull private final String value;
7574

76-
private static Optional<AuthenticatorAttachment> fromString(@NonNull String value) {
77-
return Stream.of(values()).filter(v -> v.value.equals(value)).findAny();
78-
}
79-
8075
@JsonCreator
8176
private static AuthenticatorAttachment fromJsonString(@NonNull String value) {
82-
return fromString(value)
83-
.orElseThrow(
84-
() ->
85-
new IllegalArgumentException(
86-
String.format(
87-
"Unknown %s value: %s",
88-
AuthenticatorAttachment.class.getSimpleName(), value)));
77+
return Stream.of(values()).filter(v -> v.value.equals(value)).findAny().orElse(null);
8978
}
9079
}

webauthn-server-core/src/main/java/com/yubico/webauthn/data/PublicKeyCredential.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,16 @@
2525
package com.yubico.webauthn.data;
2626

2727
import com.fasterxml.jackson.annotation.JsonCreator;
28-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
2928
import com.fasterxml.jackson.annotation.JsonProperty;
3029
import com.fasterxml.jackson.core.type.TypeReference;
3130
import com.yubico.internal.util.JacksonCodecs;
31+
import com.yubico.webauthn.AssertionResult;
32+
import com.yubico.webauthn.FinishAssertionOptions;
33+
import com.yubico.webauthn.FinishRegistrationOptions;
34+
import com.yubico.webauthn.RegistrationResult;
35+
import com.yubico.webauthn.RelyingParty;
3236
import java.io.IOException;
37+
import java.util.Optional;
3338
import lombok.AllArgsConstructor;
3439
import lombok.Builder;
3540
import lombok.NonNull;
@@ -46,7 +51,6 @@
4651
*/
4752
@Value
4853
@Builder(toBuilder = true)
49-
@JsonIgnoreProperties({"authenticatorAttachment"})
5054
public class PublicKeyCredential<
5155
A extends AuthenticatorResponse, B extends ClientExtensionOutputs> {
5256

@@ -68,6 +72,8 @@ public class PublicKeyCredential<
6872
*/
6973
@NonNull private final A response;
7074

75+
private final AuthenticatorAttachment authenticatorAttachment;
76+
7177
/**
7278
* A map containing extension identifier → client extension output entries produced by the
7379
* extension’s client extension processing.
@@ -83,6 +89,7 @@ private PublicKeyCredential(
8389
@JsonProperty("id") ByteArray id,
8490
@JsonProperty("rawId") ByteArray rawId,
8591
@NonNull @JsonProperty("response") A response,
92+
@JsonProperty("authenticatorAttachment") AuthenticatorAttachment authenticatorAttachment,
8693
@NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults,
8794
@NonNull @JsonProperty("type") PublicKeyCredentialType type) {
8895
if (id == null && rawId == null) {
@@ -95,16 +102,41 @@ private PublicKeyCredential(
95102

96103
this.id = id == null ? rawId : id;
97104
this.response = response;
105+
this.authenticatorAttachment = authenticatorAttachment;
98106
this.clientExtensionResults = clientExtensionResults;
99107
this.type = type;
100108
}
101109

102110
private PublicKeyCredential(
103111
ByteArray id,
104112
@NonNull A response,
113+
AuthenticatorAttachment authenticatorAttachment,
105114
@NonNull B clientExtensionResults,
106115
@NonNull PublicKeyCredentialType type) {
107-
this(id, null, response, clientExtensionResults, type);
116+
this(id, null, response, authenticatorAttachment, clientExtensionResults, type);
117+
}
118+
119+
/**
120+
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
121+
* attachment modality</a> in effect at the time the credential was created or used.
122+
*
123+
* <p>If parsed from JSON, this will be present if and only if the input was a valid value of
124+
* {@link AuthenticatorAttachment}.
125+
*
126+
* <p>The same value will also be available via {@link
127+
* RegistrationResult#getAuthenticatorAttachment()} or {@link
128+
* AssertionResult#getAuthenticatorAttachment()} on the result from {@link
129+
* RelyingParty#finishRegistration(FinishRegistrationOptions)} or {@link
130+
* RelyingParty#finishAssertion(FinishAssertionOptions)}.
131+
*
132+
* @see RegistrationResult#getAuthenticatorAttachment()
133+
* @see AssertionResult#getAuthenticatorAttachment()
134+
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
135+
* the standard matures.
136+
*/
137+
@Deprecated
138+
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
139+
return Optional.ofNullable(authenticatorAttachment);
108140
}
109141

110142
public static <A extends AuthenticatorResponse, B extends ClientExtensionOutputs>

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.upokecenter.cbor.CBORObject
3131
import com.yubico.internal.util.JacksonCodecs
3232
import com.yubico.webauthn.data.AssertionExtensionInputs
3333
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
34+
import com.yubico.webauthn.data.AuthenticatorAttachment
3435
import com.yubico.webauthn.data.AuthenticatorDataFlags
3536
import com.yubico.webauthn.data.AuthenticatorTransport
3637
import com.yubico.webauthn.data.ByteArray
@@ -2592,6 +2593,36 @@ class RelyingPartyAssertionSpec
25922593
resultWithBeOnly.isBackedUp should be(false)
25932594
resultWithBackup.isBackedUp should be(true)
25942595
}
2596+
2597+
it(
2598+
"exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
2599+
) {
2600+
val pkcTemplate =
2601+
TestAuthenticator.createAssertion(
2602+
challenge =
2603+
request.getPublicKeyCredentialRequestOptions.getChallenge,
2604+
credentialKey = credentialKeypair,
2605+
credentialId = credential.getId,
2606+
)
2607+
2608+
forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
2609+
val pkc = pkcTemplate.toBuilder
2610+
.authenticatorAttachment(authenticatorAttachment.orNull)
2611+
.build()
2612+
2613+
val result = rp.finishAssertion(
2614+
FinishAssertionOptions
2615+
.builder()
2616+
.request(request)
2617+
.response(pkc)
2618+
.build()
2619+
)
2620+
2621+
result.getAuthenticatorAttachment should equal(
2622+
pkc.getAuthenticatorAttachment
2623+
)
2624+
}
2625+
}
25952626
}
25962627
}
25972628
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import com.yubico.webauthn.attestation.AttestationTrustSource
4242
import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult
4343
import com.yubico.webauthn.data.AttestationObject
4444
import com.yubico.webauthn.data.AttestationType
45+
import com.yubico.webauthn.data.AuthenticatorAttachment
4546
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
4647
import com.yubico.webauthn.data.AuthenticatorData
4748
import com.yubico.webauthn.data.AuthenticatorDataFlags
@@ -4619,6 +4620,33 @@ class RelyingPartyRegistrationSpec
46194620
resultWithBeOnly.isBackedUp should be(false)
46204621
resultWithBackup.isBackedUp should be(true)
46214622
}
4623+
4624+
it(
4625+
"exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
4626+
) {
4627+
val (pkcTemplate, _, _) =
4628+
TestAuthenticator.createUnattestedCredential(challenge =
4629+
request.getChallenge
4630+
)
4631+
4632+
forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
4633+
val pkc = pkcTemplate.toBuilder
4634+
.authenticatorAttachment(authenticatorAttachment.orNull)
4635+
.build()
4636+
4637+
val result = rp.finishRegistration(
4638+
FinishRegistrationOptions
4639+
.builder()
4640+
.request(request)
4641+
.response(pkc)
4642+
.build()
4643+
)
4644+
4645+
result.getAuthenticatorAttachment should equal(
4646+
pkc.getAuthenticatorAttachment
4647+
)
4648+
}
4649+
}
46224650
}
46234651
}
46244652

webauthn-server-core/src/test/scala/com/yubico/webauthn/data/EnumsSpec.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ class EnumsSpec
6161

6262
describe("AuthenticatorAttachment") {
6363
describe("can be parsed from JSON") {
64-
it("but throws IllegalArgumentException for unknown values.") {
65-
val result = Try(
64+
it("and ignores for unknown values.") {
65+
val result =
6666
json.readValue("\"foo\"", classOf[AuthenticatorAttachment])
67-
)
68-
result.failed.get.getCause shouldBe an[IllegalArgumentException]
67+
result should be(null)
6968
}
7069
}
7170
}

webauthn-server-core/src/test/scala/com/yubico/webauthn/data/JsonIoSpec.scala

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ import com.yubico.webauthn.extension.appid.Generators._
4141
import org.junit.runner.RunWith
4242
import org.scalacheck.Arbitrary
4343
import org.scalacheck.Arbitrary.arbitrary
44-
import org.scalacheck.Gen
4544
import org.scalatest.funspec.AnyFunSpec
4645
import org.scalatest.matchers.should.Matchers
4746
import org.scalatestplus.junit.JUnitRunner
4847
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
4948

49+
import scala.jdk.OptionConverters.RichOptional
50+
5051
@RunWith(classOf[JUnitRunner])
5152
class JsonIoSpec
5253
extends AnyFunSpec
@@ -351,15 +352,16 @@ class JsonIoSpec
351352
)
352353
}
353354

354-
it("allows and ignores an authenticatorAttachment attribute.") {
355+
it(
356+
"allows an authenticatorAttachment attribute, but ignores unknown values."
357+
) {
355358
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit
356359
a: Arbitrary[P]
357360
): Unit = {
358361
forAll(
359362
a.arbitrary,
360-
Gen.oneOf(
361-
arbitrary[AuthenticatorAttachment].map(_.getValue),
362-
arbitrary[String],
363+
arbitrary[String].suchThat(s =>
364+
!AuthenticatorAttachment.values.map(_.getValue).contains(s)
363365
),
364366
) { (value: P, authenticatorAttachment: String) =>
365367
val tree: ObjectNode = json.valueToTree(value)
@@ -370,8 +372,37 @@ class JsonIoSpec
370372
val encoded = json.writeValueAsString(tree)
371373
println(authenticatorAttachment)
372374
val decoded = json.readValue(encoded, tpe)
375+
decoded.getAuthenticatorAttachment.asScala should be(None)
376+
}
377+
378+
forAll(
379+
a.arbitrary,
380+
arbitrary[AuthenticatorAttachment],
381+
) { (value: P, authenticatorAttachment: AuthenticatorAttachment) =>
382+
val tree: ObjectNode = json.valueToTree(value)
383+
tree.set(
384+
"authenticatorAttachment",
385+
new TextNode(authenticatorAttachment.getValue),
386+
)
387+
val encoded = json.writeValueAsString(tree)
388+
println(authenticatorAttachment)
389+
val decoded = json.readValue(encoded, tpe)
390+
391+
decoded.getAuthenticatorAttachment.asScala should equal(
392+
Some(authenticatorAttachment)
393+
)
394+
}
395+
396+
forAll(
397+
a.arbitrary
398+
) { (value: P) =>
399+
val tree: ObjectNode = json.valueToTree(
400+
value.toBuilder.authenticatorAttachment(null).build()
401+
)
402+
val encoded = json.writeValueAsString(tree)
403+
val decoded = json.readValue(encoded, tpe)
373404

374-
decoded should equal(value)
405+
decoded.getAuthenticatorAttachment.asScala should be(None)
375406
}
376407
}
377408

0 commit comments

Comments
 (0)