diff --git a/NEWS b/NEWS index 39f984f47..677142541 100644 --- a/NEWS +++ b/NEWS @@ -51,6 +51,10 @@ New features: * (Experimental) Added property `RegisteredCredential.transports`. ** NOTE: Experimental features may receive breaking changes without a major version increase. +* (Experimental) Added property `credProps.authenticatorDisplayName`. + ** NOTE: Experimental features may receive breaking changes without a major + version increase. +* (Experimental) Added `credProps` extension to assertion extension outputs. == Version 2.5.2 == diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java index 5763af7af..50ec81975 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResult.java @@ -35,6 +35,7 @@ import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; +import com.yubico.webauthn.data.Extensions; import com.yubico.webauthn.data.PublicKeyCredential; import com.yubico.webauthn.data.PublicKeyCredentialRequestOptions; import com.yubico.webauthn.data.UserIdentity; @@ -281,4 +282,33 @@ public Optional getAuthenticatorExtensio return AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData( credentialResponse.getResponse().getParsedAuthenticatorData()); } + + /** + * Retrieve a suitable nickname for this credential, if one is available. This MAY differ from + * {@link RegistrationResult#getAuthenticatorDisplayName() the value returned during + * registration}, if any. In that case the application may want to offer the user to update the + * previously stored value, if any. + * + *

This returns the authenticatorDisplayName output from the + * credProps extension. + * + * @return A user-chosen or vendor-default display name for the credential, if available. + * Otherwise empty. + * @see + * authenticatorDisplayName in §10.1.3. Credential Properties Extension + * (credProps) + * @see RegistrationResult#getAuthenticatorDisplayName() + * @see Extensions.CredentialProperties.CredentialPropertiesOutput#getAuthenticatorDisplayName() + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + */ + @JsonIgnore + @Deprecated + public Optional getAuthenticatorDisplayName() { + return getClientExtensionOutputs() + .flatMap(outputs -> outputs.getCredProps()) + .flatMap(credProps -> credProps.getAuthenticatorDisplayName()); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java index 5b027ffbc..27df1a515 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/AssertionResultV2.java @@ -35,6 +35,7 @@ import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientAssertionExtensionOutputs; +import com.yubico.webauthn.data.Extensions; import com.yubico.webauthn.data.PublicKeyCredential; import java.util.Optional; import lombok.AccessLevel; @@ -243,4 +244,33 @@ public Optional getAuthenticatorExtensio return AuthenticatorAssertionExtensionOutputs.fromAuthenticatorData( credentialResponse.getResponse().getParsedAuthenticatorData()); } + + /** + * Retrieve a suitable nickname for this credential, if one is available. This MAY differ from + * {@link RegistrationResult#getAuthenticatorDisplayName() the value returned during + * registration}, if any. In that case the application may want to offer the user to update the + * previously stored value, if any. + * + *

This returns the authenticatorDisplayName output from the + * credProps extension. + * + * @return A user-chosen or vendor-default display name for the credential, if available. + * Otherwise empty. + * @see + * authenticatorDisplayName in §10.1.3. Credential Properties Extension + * (credProps) + * @see RegistrationResult#getAuthenticatorDisplayName() + * @see Extensions.CredentialProperties.CredentialPropertiesOutput#getAuthenticatorDisplayName() + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + */ + @JsonIgnore + @Deprecated + public Optional getAuthenticatorDisplayName() { + return getClientExtensionOutputs() + .flatMap(outputs -> outputs.getCredProps()) + .flatMap(credProps -> credProps.getAuthenticatorDisplayName()); + } } diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java index 499003730..c1b8b9c66 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/RegistrationResult.java @@ -39,6 +39,7 @@ import com.yubico.webauthn.data.AuthenticatorResponse; import com.yubico.webauthn.data.ByteArray; import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs; +import com.yubico.webauthn.data.Extensions; import com.yubico.webauthn.data.PublicKeyCredential; import com.yubico.webauthn.data.PublicKeyCredentialDescriptor; import java.io.IOException; @@ -367,6 +368,33 @@ public Optional isDiscoverable() { .flatMap(credProps -> credProps.getRk()); } + /** + * Retrieve a suitable nickname for this credential, if one is available. + * + *

This returns the authenticatorDisplayName output from the + * credProps extension. + * + * @return A user-chosen or vendor-default display name for the credential, if available. + * Otherwise empty. + * @see + * authenticatorDisplayName in §10.1.3. Credential Properties Extension + * (credProps) + * @see AssertionResult#getAuthenticatorDisplayName() + * @see AssertionResultV2#getAuthenticatorDisplayName() + * @see Extensions.CredentialProperties.CredentialPropertiesOutput#getAuthenticatorDisplayName() + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + */ + @JsonIgnore + @Deprecated + public Optional getAuthenticatorDisplayName() { + return getClientExtensionOutputs() + .flatMap(outputs -> outputs.getCredProps()) + .flatMap(credProps -> credProps.getAuthenticatorDisplayName()); + } + /** * The attestation diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java index 3c6579d66..81c9af07d 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/ClientAssertionExtensionOutputs.java @@ -64,13 +64,18 @@ public class ClientAssertionExtensionOutputs implements ClientExtensionOutputs { */ private final Boolean appid; + private final Extensions.CredentialProperties.CredentialPropertiesOutput credProps; + private final Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob; @JsonCreator private ClientAssertionExtensionOutputs( @JsonProperty("appid") Boolean appid, + @JsonProperty("credProps") + Extensions.CredentialProperties.CredentialPropertiesOutput credProps, @JsonProperty("largeBlob") Extensions.LargeBlob.LargeBlobAuthenticationOutput largeBlob) { this.appid = appid; + this.credProps = credProps; this.largeBlob = largeBlob; } @@ -81,6 +86,9 @@ public Set getExtensionIds() { if (appid != null) { ids.add(Extensions.Appid.EXTENSION_ID); } + if (credProps != null) { + ids.add(Extensions.CredentialProperties.EXTENSION_ID); + } if (largeBlob != null) { ids.add(Extensions.LargeBlob.EXTENSION_ID); } @@ -100,6 +108,24 @@ public Optional getAppid() { return Optional.ofNullable(appid); } + /** + * The extension output for the Credential Properties Extension (credProps), if any. + * + *

This value MAY be present but have all members empty if the extension was successfully + * processed but no credential properties could be determined. + * + * @see com.yubico.webauthn.data.Extensions.CredentialProperties.CredentialPropertiesOutput + * @see §10.4. + * Credential Properties Extension (credProps) + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change as + * the standard matures. + */ + @Deprecated + public Optional getCredProps() { + return Optional.ofNullable(credProps); + } + /** * The extension output for the Large blob diff --git a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java index d25d0f901..0f762fb1f 100644 --- a/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java +++ b/webauthn-server-core/src/main/java/com/yubico/webauthn/data/Extensions.java @@ -6,6 +6,9 @@ import com.fasterxml.jackson.annotation.JsonValue; import com.upokecenter.cbor.CBORObject; import com.upokecenter.cbor.CBORType; +import com.yubico.webauthn.AssertionResult; +import com.yubico.webauthn.AssertionResultV2; +import com.yubico.webauthn.RegistrationResult; import com.yubico.webauthn.StartRegistrationOptions; import com.yubico.webauthn.extension.uvm.KeyProtectionType; import com.yubico.webauthn.extension.uvm.MatcherProtectionType; @@ -71,9 +74,15 @@ public static class CredentialPropertiesOutput { @JsonProperty("rk") private final Boolean rk; + @JsonProperty("authenticatorDisplayName") + private final String authenticatorDisplayName; + @JsonCreator - private CredentialPropertiesOutput(@JsonProperty("rk") Boolean rk) { + private CredentialPropertiesOutput( + @JsonProperty("rk") Boolean rk, + @JsonProperty("authenticatorDisplayName") String authenticatorDisplayName) { this.rk = rk; + this.authenticatorDisplayName = authenticatorDisplayName; } /** @@ -105,6 +114,34 @@ private CredentialPropertiesOutput(@JsonProperty("rk") Boolean rk) { public Optional getRk() { return Optional.ofNullable(rk); } + + /** + * This OPTIONAL property is a human-palatable description of the credential's managing + * authenticator, chosen by the user. + * + *

If the application supports setting "nicknames" for registered credentials, then this + * value may be a suitable default value for such a nickname. + * + *

In an authentication ceremony, if this value is different from the stored nickname, then + * the application may want to offer the user to update the stored nickname to match this + * value. + * + * @return A user-chosen or vendor-default display name for the credential, if available. + * Otherwise empty. + * @see + * authenticatorDisplayName in §10.1.3. Credential Properties Extension + * (credProps) + * @see RegistrationResult#getAuthenticatorDisplayName() + * @see AssertionResult#getAuthenticatorDisplayName() + * @see AssertionResultV2#getAuthenticatorDisplayName() + * @deprecated EXPERIMENTAL: This feature is from a not yet mature standard; it could change + * as the standard matures. + */ + @Deprecated + public Optional getAuthenticatorDisplayName() { + return Optional.ofNullable(authenticatorDisplayName); + } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala index 6d4c711f2..ef809d116 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyAssertionSpec.scala @@ -38,6 +38,7 @@ import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.CollectedClientData +import com.yubico.webauthn.data.Extensions.CredentialProperties.CredentialPropertiesOutput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry @@ -2845,6 +2846,55 @@ class RelyingPartyAssertionSpec ) } } + + describe("exposes the credProps.authenticatorDisplayName extension output as getAuthenticatorDisplayName()") { + val pkcTemplate = + TestAuthenticator.createAssertion( + challenge = + request.getPublicKeyCredentialRequestOptions.getChallenge, + credentialKey = credentialKeypair, + credentialId = credential.getId, + ) + + it("""when set to "hej".""") { + val pkc = pkcTemplate.toBuilder + .clientExtensionResults( + pkcTemplate.getClientExtensionResults.toBuilder + .credProps( + CredentialPropertiesOutput + .builder() + .authenticatorDisplayName("hej") + .build() + ) + .build() + ) + .build() + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkc) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal( + Some("hej") + ) + } + + it("when not available.") { + val pkc = pkcTemplate + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkc) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal(None) + } + } } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala index 4f16eb1fd..49ee87a4b 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyRegistrationSpec.scala @@ -4289,6 +4289,51 @@ class RelyingPartyRegistrationSpec } } + describe("expose the credProps.authenticatorDisplayName extension output as RegistrationResult.getAuthenticatorDisplayName()") { + val testDataBase = RegistrationTestData.Packed.BasicAttestation + val testData = testDataBase.copy(requestedExtensions = + testDataBase.request.getExtensions.toBuilder.credProps().build() + ) + + it("""when set to "hej".""") { + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response( + testData.response.toBuilder + .clientExtensionResults( + ClientRegistrationExtensionOutputs + .builder() + .credProps( + CredentialPropertiesOutput + .builder() + .authenticatorDisplayName("hej") + .build() + ) + .build() + ) + .build() + ) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal(Some("hej")) + } + + it("when not available.") { + val result = rp.finishRegistration( + FinishRegistrationOptions + .builder() + .request(testData.request) + .response(testData.response) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal(None) + } + } + describe("support the largeBlob extension") { it("being enabled at registration time.") { val testData = RegistrationTestData.Packed.BasicAttestation diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala index 402f2f1d7..794db38fb 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/RelyingPartyV2AssertionSpec.scala @@ -38,6 +38,7 @@ import com.yubico.webauthn.data.AuthenticatorTransport import com.yubico.webauthn.data.ByteArray import com.yubico.webauthn.data.ClientAssertionExtensionOutputs import com.yubico.webauthn.data.CollectedClientData +import com.yubico.webauthn.data.Extensions.CredentialProperties.CredentialPropertiesOutput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationInput import com.yubico.webauthn.data.Extensions.LargeBlob.LargeBlobAuthenticationOutput import com.yubico.webauthn.data.Extensions.Uvm.UvmEntry @@ -2920,6 +2921,55 @@ class RelyingPartyV2AssertionSpec ) } } + + describe("exposes the credProps.authenticatorDisplayName extension output as getAuthenticatorDisplayName()") { + val pkcTemplate = + TestAuthenticator.createAssertion( + challenge = + request.getPublicKeyCredentialRequestOptions.getChallenge, + credentialKey = credentialKeypair, + credentialId = credential.getId, + ) + + it("""when set to "hej".""") { + val pkc = pkcTemplate.toBuilder + .clientExtensionResults( + pkcTemplate.getClientExtensionResults.toBuilder + .credProps( + CredentialPropertiesOutput + .builder() + .authenticatorDisplayName("hej") + .build() + ) + .build() + ) + .build() + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkc) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal( + Some("hej") + ) + } + + it("when not available.") { + val pkc = pkcTemplate + val result = rp.finishAssertion( + FinishAssertionOptions + .builder() + .request(request) + .response(pkc) + .build() + ) + + result.getAuthenticatorDisplayName.toScala should equal(None) + } + } } } } diff --git a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala index 2a7ce9df3..e1a32f6e6 100644 --- a/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala +++ b/webauthn-server-core/src/test/scala/com/yubico/webauthn/data/Generators.scala @@ -867,8 +867,14 @@ object Generators { object CredProps { def credentialPropertiesOutput: Gen[CredentialPropertiesOutput] = for { - rk <- arbitrary[Boolean] - } yield CredentialPropertiesOutput.builder().rk(rk).build() + rk <- arbitrary[Option[Boolean]] + authenticatorDisplayName <- arbitrary[Option[String]] + } yield { + val b = CredentialPropertiesOutput.builder() + rk.foreach(b.rk(_)) + authenticatorDisplayName.foreach(b.authenticatorDisplayName) + b.build() + } } object LargeBlob {