Skip to content

Commit

Permalink
Merge pull request #250 from luisgoncalves/feature/credential-authent…
Browse files Browse the repository at this point in the history
…icator-attachment

Support authenticatorAttachment in PublicKeyCredential

See: #250
  • Loading branch information
emlun committed Dec 16, 2022
2 parents 4f4332b + 0c817c2 commit 35c983d
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 25 deletions.
17 changes: 17 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
== Version 2.3.0 (unreleased) ==

New features:

* (Experimental) Added `authenticatorAttachment` property to response objects:
** NOTE: Experimental features may receive breaking changes without a major
version increase.
** Added method `getAuthenticatorAttachment()` to `PublicKeyCredential` and
corresponding builder method
`authenticatorAttachment(AuthenticatorAttachment)`.
** Added method `getAuthenticatorAttachment()` to `RegistrationResult` and
`AssertionResult`, which echo `getAuthenticatorAttachment()` from the
corresponding `PublicKeyCredential`.
** Thanks to GitHub user luisgoncalves for the contribution, see
https://github.com/Yubico/java-webauthn-server/pull/250


== Version 2.2.0 ==

`webauthn-server-core`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.yubico.webauthn.data.AuthenticatorAssertionExtensionOutputs;
import com.yubico.webauthn.data.AuthenticatorAssertionResponse;
import com.yubico.webauthn.data.AuthenticatorAttachment;
import com.yubico.webauthn.data.AuthenticatorData;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.ClientAssertionExtensionOutputs;
Expand Down Expand Up @@ -195,6 +196,20 @@ public boolean isBackedUp() {
return credentialResponse.getResponse().getParsedAuthenticatorData().getFlags().BS;
}

/**
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
* attachment modality</a> in effect at the time the asserted credential was used.
*
* @see PublicKeyCredential#getAuthenticatorAttachment()
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
* the standard matures.
*/
@Deprecated
@JsonIgnore
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
return credentialResponse.getAuthenticatorAttachment();
}

/**
* The new <a href="https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#signcount">signature
* count</a> of the credential used for the assertion.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.yubico.webauthn.RelyingParty.RelyingPartyBuilder;
import com.yubico.webauthn.attestation.AttestationTrustSource;
import com.yubico.webauthn.data.AttestationType;
import com.yubico.webauthn.data.AuthenticatorAttachment;
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
import com.yubico.webauthn.data.AuthenticatorRegistrationExtensionOutputs;
import com.yubico.webauthn.data.ByteArray;
Expand Down Expand Up @@ -175,6 +176,20 @@ public boolean isBackedUp() {
return credential.getResponse().getParsedAuthenticatorData().getFlags().BS;
}

/**
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
* attachment modality</a> in effect at the time the credential was created.
*
* @see PublicKeyCredential#getAuthenticatorAttachment()
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
* the standard matures.
*/
@Deprecated
@JsonIgnore
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
return credential.getAuthenticatorAttachment();
}

/**
* The signature count returned with the created credential.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Optional;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down Expand Up @@ -73,18 +72,8 @@ public enum AuthenticatorAttachment {

@JsonValue @Getter @NonNull private final String value;

private static Optional<AuthenticatorAttachment> fromString(@NonNull String value) {
return Stream.of(values()).filter(v -> v.value.equals(value)).findAny();
}

@JsonCreator
private static AuthenticatorAttachment fromJsonString(@NonNull String value) {
return fromString(value)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format(
"Unknown %s value: %s",
AuthenticatorAttachment.class.getSimpleName(), value)));
return Stream.of(values()).filter(v -> v.value.equals(value)).findAny().orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@
package com.yubico.webauthn.data;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.yubico.internal.util.JacksonCodecs;
import com.yubico.webauthn.AssertionResult;
import com.yubico.webauthn.FinishAssertionOptions;
import com.yubico.webauthn.FinishRegistrationOptions;
import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.RelyingParty;
import java.io.IOException;
import java.util.Optional;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NonNull;
Expand All @@ -46,7 +51,6 @@
*/
@Value
@Builder(toBuilder = true)
@JsonIgnoreProperties({"authenticatorAttachment"})
public class PublicKeyCredential<
A extends AuthenticatorResponse, B extends ClientExtensionOutputs> {

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

private final AuthenticatorAttachment authenticatorAttachment;

/**
* A map containing extension identifier → client extension output entries produced by the
* extension’s client extension processing.
Expand All @@ -83,6 +89,7 @@ private PublicKeyCredential(
@JsonProperty("id") ByteArray id,
@JsonProperty("rawId") ByteArray rawId,
@NonNull @JsonProperty("response") A response,
@JsonProperty("authenticatorAttachment") AuthenticatorAttachment authenticatorAttachment,
@NonNull @JsonProperty("clientExtensionResults") B clientExtensionResults,
@NonNull @JsonProperty("type") PublicKeyCredentialType type) {
if (id == null && rawId == null) {
Expand All @@ -95,16 +102,41 @@ private PublicKeyCredential(

this.id = id == null ? rawId : id;
this.response = response;
this.authenticatorAttachment = authenticatorAttachment;
this.clientExtensionResults = clientExtensionResults;
this.type = type;
}

private PublicKeyCredential(
ByteArray id,
@NonNull A response,
AuthenticatorAttachment authenticatorAttachment,
@NonNull B clientExtensionResults,
@NonNull PublicKeyCredentialType type) {
this(id, null, response, clientExtensionResults, type);
this(id, null, response, authenticatorAttachment, clientExtensionResults, type);
}

/**
* The <a href="https://w3c.github.io/webauthn/#authenticator-attachment-modality">authenticator
* attachment modality</a> in effect at the time the credential was created or used.
*
* <p>If parsed from JSON, this will be present if and only if the input was a valid value of
* {@link AuthenticatorAttachment}.
*
* <p>The same value will also be available via {@link
* RegistrationResult#getAuthenticatorAttachment()} or {@link
* AssertionResult#getAuthenticatorAttachment()} on the result from {@link
* RelyingParty#finishRegistration(FinishRegistrationOptions)} or {@link
* RelyingParty#finishAssertion(FinishAssertionOptions)}.
*
* @see RegistrationResult#getAuthenticatorAttachment()
* @see AssertionResult#getAuthenticatorAttachment()
* @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as
* the standard matures.
*/
@Deprecated
public Optional<AuthenticatorAttachment> getAuthenticatorAttachment() {
return Optional.ofNullable(authenticatorAttachment);
}

public static <A extends AuthenticatorResponse, B extends ClientExtensionOutputs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.upokecenter.cbor.CBORObject
import com.yubico.internal.util.JacksonCodecs
import com.yubico.webauthn.data.AssertionExtensionInputs
import com.yubico.webauthn.data.AuthenticatorAssertionResponse
import com.yubico.webauthn.data.AuthenticatorAttachment
import com.yubico.webauthn.data.AuthenticatorDataFlags
import com.yubico.webauthn.data.AuthenticatorTransport
import com.yubico.webauthn.data.ByteArray
Expand Down Expand Up @@ -2592,6 +2593,36 @@ class RelyingPartyAssertionSpec
resultWithBeOnly.isBackedUp should be(false)
resultWithBackup.isBackedUp should be(true)
}

it(
"exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
) {
val pkcTemplate =
TestAuthenticator.createAssertion(
challenge =
request.getPublicKeyCredentialRequestOptions.getChallenge,
credentialKey = credentialKeypair,
credentialId = credential.getId,
)

forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
val pkc = pkcTemplate.toBuilder
.authenticatorAttachment(authenticatorAttachment.orNull)
.build()

val result = rp.finishAssertion(
FinishAssertionOptions
.builder()
.request(request)
.response(pkc)
.build()
)

result.getAuthenticatorAttachment should equal(
pkc.getAuthenticatorAttachment
)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import com.yubico.webauthn.attestation.AttestationTrustSource
import com.yubico.webauthn.attestation.AttestationTrustSource.TrustRootsResult
import com.yubico.webauthn.data.AttestationObject
import com.yubico.webauthn.data.AttestationType
import com.yubico.webauthn.data.AuthenticatorAttachment
import com.yubico.webauthn.data.AuthenticatorAttestationResponse
import com.yubico.webauthn.data.AuthenticatorData
import com.yubico.webauthn.data.AuthenticatorDataFlags
Expand Down Expand Up @@ -4619,6 +4620,33 @@ class RelyingPartyRegistrationSpec
resultWithBeOnly.isBackedUp should be(false)
resultWithBackup.isBackedUp should be(true)
}

it(
"exposes getAuthenticatorAttachment() with the authenticatorAttachment value from the PublicKeyCredential."
) {
val (pkcTemplate, _, _) =
TestAuthenticator.createUnattestedCredential(challenge =
request.getChallenge
)

forAll { authenticatorAttachment: Option[AuthenticatorAttachment] =>
val pkc = pkcTemplate.toBuilder
.authenticatorAttachment(authenticatorAttachment.orNull)
.build()

val result = rp.finishRegistration(
FinishRegistrationOptions
.builder()
.request(request)
.response(pkc)
.build()
)

result.getAuthenticatorAttachment should equal(
pkc.getAuthenticatorAttachment
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ class EnumsSpec

describe("AuthenticatorAttachment") {
describe("can be parsed from JSON") {
it("but throws IllegalArgumentException for unknown values.") {
val result = Try(
it("and ignores for unknown values.") {
val result =
json.readValue("\"foo\"", classOf[AuthenticatorAttachment])
)
result.failed.get.getCause shouldBe an[IllegalArgumentException]
result should be(null)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ import com.yubico.webauthn.extension.appid.Generators._
import org.junit.runner.RunWith
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.junit.JUnitRunner
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

import scala.jdk.OptionConverters.RichOptional

@RunWith(classOf[JUnitRunner])
class JsonIoSpec
extends AnyFunSpec
Expand Down Expand Up @@ -351,15 +352,16 @@ class JsonIoSpec
)
}

it("allows and ignores an authenticatorAttachment attribute.") {
it(
"allows an authenticatorAttachment attribute, but ignores unknown values."
) {
def test[P <: PublicKeyCredential[_, _]](tpe: TypeReference[P])(implicit
a: Arbitrary[P]
): Unit = {
forAll(
a.arbitrary,
Gen.oneOf(
arbitrary[AuthenticatorAttachment].map(_.getValue),
arbitrary[String],
arbitrary[String].suchThat(s =>
!AuthenticatorAttachment.values.map(_.getValue).contains(s)
),
) { (value: P, authenticatorAttachment: String) =>
val tree: ObjectNode = json.valueToTree(value)
Expand All @@ -370,8 +372,37 @@ class JsonIoSpec
val encoded = json.writeValueAsString(tree)
println(authenticatorAttachment)
val decoded = json.readValue(encoded, tpe)
decoded.getAuthenticatorAttachment.asScala should be(None)
}

forAll(
a.arbitrary,
arbitrary[AuthenticatorAttachment],
) { (value: P, authenticatorAttachment: AuthenticatorAttachment) =>
val tree: ObjectNode = json.valueToTree(value)
tree.set(
"authenticatorAttachment",
new TextNode(authenticatorAttachment.getValue),
)
val encoded = json.writeValueAsString(tree)
println(authenticatorAttachment)
val decoded = json.readValue(encoded, tpe)

decoded.getAuthenticatorAttachment.asScala should equal(
Some(authenticatorAttachment)
)
}

forAll(
a.arbitrary
) { (value: P) =>
val tree: ObjectNode = json.valueToTree(
value.toBuilder.authenticatorAttachment(null).build()
)
val encoded = json.writeValueAsString(tree)
val decoded = json.readValue(encoded, tpe)

decoded should equal(value)
decoded.getAuthenticatorAttachment.asScala should be(None)
}
}

Expand Down

1 comment on commit 35c983d

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutation test results

Package Coverage Stats Prev Prev
Overall 80 % 🔹 1220 🔻 / 1514 🔹 80 % 1222 / 1514
com.yubico.fido.metadata 68 % 🔹 215 🔻 / 316 🔹 68 % 216 / 316
com.yubico.internal.util 37 % 🔹 36 🔹 / 97 🔹 37 % 36 / 97
com.yubico.webauthn 87 % 🔹 542 🔺 / 622 🔺 87 % 540 / 620
com.yubico.webauthn.attestation 92 % 🔹 13 🔹 / 14 🔹 92 % 13 / 14
com.yubico.webauthn.data 93 % 🔹 389 🔻 / 418 🔻 93 % 392 / 420
com.yubico.webauthn.extension.appid 100 % 🏆 13 🔹 / 13 🔹 100 % 13 / 13
com.yubico.webauthn.extension.uvm 50 % 🔹 12 🔹 / 24 🔹 50 % 12 / 24
com.yubico.webauthn.meta 0 % 🔹 0 🔹 / 10 🔹 0 % 0 / 10

Previous run: 747bb78

Detailed reports: workflow run #191

Please sign in to comment.