Skip to content

Support authenticatorAttachment in PublicKeyCredential #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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