[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-5 - Add support for FIDO Alliance Metadata
Phil Smart
philip.smart at jisc.ac.uk
Fri Mar 15 10:37:46 UTC 2024
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-webauthn.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-webauthn.git;a=commit;h=9b3b2d7b9f507b31fc73c7ef5bebf79a64aebc75
The following commit(s) were added to refs/heads/main by this push:
new 9b3b2d7 JWEBAUTHN-5 - Add support for FIDO Alliance Metadata
9b3b2d7 is described below
commit 9b3b2d7b9f507b31fc73c7ef5bebf79a64aebc75
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Mar 15 10:37:41 2024 +0000
JWEBAUTHN-5 - Add support for FIDO Alliance Metadata
- Added support for the Yubico metadata service if enabled
- Allows verification of authenticator attestations during registration
if the attestation statment is requested and supplied by the
authenticator
- Added to credential storage record when stored
- Enhances the registration UI to show Authenticator information
- Add local metadata for testing
https://shibboleth.atlassian.net/browse/JWEBAUTHN-5
---
pom.xml | 25 ++-
webauthn-api/pom.xml | 5 +
.../webauthn/storage/CredentialRegistration.java | 184 +++++++++++++---
webauthn-impl/pom.xml | 15 ++
.../impl/AbstractWebAuthnRegistrationAction.java | 35 ++-
.../admin/impl/StorePublicKeyCredential.java | 43 +++-
.../client/impl/YubicoWebauthnClientFactory.java | 74 ++++++-
.../metadata/FidoMetadataServiceFactory.java | 241 +++++++++++++++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 16 +-
.../webauthn-registration-beans.xml | 14 +-
.../authn/WebAuthn/webauthn-abstract-beans.xml | 10 +-
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 6 +
.../idp/plugin/authn/webauthn/css/webauthn.css | 6 +
.../authn/webauthn/views/webauthn-register.vm | 20 +-
.../authn/webauthn/impl/AbstractWebAuthnTest.java | 42 ++++
.../FidoMetadataServiceResolverFactoryTest.java | 101 +++++++++
webauthn-impl/src/test/resources/fido-metadata.bin | 1 +
webauthn-impl/src/test/resources/root-r3.crt | Bin 0 -> 867 bytes
18 files changed, 770 insertions(+), 68 deletions(-)
diff --git a/pom.xml b/pom.xml
index b68127b..d446944 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,8 +19,10 @@
<description>WebAuthn plugin for the Shibboleth IdP.</description>
<properties>
- <maven-dist-enforcer-data.version>1.0.15-SNAPSHOT</maven-dist-enforcer-data.version>
+ <maven-dist-enforcer-data.version>1.0.15</maven-dist-enforcer-data.version>
<shibboleth.projectName>java-idp-plugin-webauthn</shibboleth.projectName>
+ <okhttp3.mockserver.version>4.9.3</okhttp3.mockserver.version>
+ <okhttp3.tls.version>4.9.3</okhttp3.tls.version>
<idp.groupId>net.shibboleth.idp</idp.groupId>
<idp.version>5.0.0</idp.version>
<opensaml.groupId>org.opensaml</opensaml.groupId>
@@ -39,8 +41,7 @@
<com-upokecenter.version>4.5.2</com-upokecenter.version>
<numbers.groupId>com.github.peteroupc</numbers.groupId>
<numbers.version>1.8.2</numbers.version>
- <checkstyle.configLocation>
- ${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
+ <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
</properties>
<modules>
@@ -86,6 +87,11 @@
<artifactId>idp-plugin-webauthn-impl</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${yubico.groupId}</groupId>
+ <artifactId>webauthn-server-attestation</artifactId>
+ <version>${yubico-webauthn.version}</version>
+ </dependency>
<dependency>
<groupId>${yubico.groupId}</groupId>
<artifactId>webauthn-server-core</artifactId>
@@ -161,6 +167,19 @@
<type>pom</type>
<scope>import</scope>
</dependency>
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${okhttp3.mockserver.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp-tls</artifactId>
+ <version>${okhttp3.tls.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/webauthn-api/pom.xml b/webauthn-api/pom.xml
index 018f0aa..7603c4a 100644
--- a/webauthn-api/pom.xml
+++ b/webauthn-api/pom.xml
@@ -55,6 +55,11 @@
<artifactId>webauthn-server-core</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${yubico.groupId}</groupId>
+ <artifactId>webauthn-server-attestation</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
index 126a0b9..e65b8d7 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
@@ -21,6 +21,7 @@ import java.time.Instant;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
+import java.util.Set;
import java.util.SortedSet;
import javax.annotation.Nonnull;
@@ -33,11 +34,16 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.yubico.fido.metadata.MetadataBLOBPayloadEntry;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.AuthenticatorTransport;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
/**
* Registration record used to hold registered credentials.
*
@@ -58,19 +64,19 @@ public class CredentialRegistration {
* The set of {@link AuthenticatorTransport transports} the authenticator can
* use to communicate with the client.
*/
- @Nonnull private final SortedSet<AuthenticatorTransport> transports;
+ @Nonnull @Unmodifiable @NonnullElements private final SortedSet<AuthenticatorTransport> transports;
/** The time the registration took place. */
@Nonnull private final Instant registrationTime;
- /** Is the credential a discovery type (passkey). Empty if not known. */
+ /** Is the credential a discoverable type (passkey). Empty if not known. */
@Nonnull private final Optional<Boolean> discoverable;
/** The credential to register. */
- @Nonnull private final RegisteredCredential credential;
+ @Nonnull @Unmodifiable @NonnullElements private final RegisteredCredential credential;
- /** Optional attestation metadata about the authenticator. */
- @Nullable private final Object attestationMetadata;
+ /** Optional attestation metadata about the authenticator. Will be an empty set if not used. */
+ @Nonnull @Unmodifiable @NonnullElements private final Set<MetadataBLOBPayloadEntry> attestationMetadata;
/** Was the user verified during registration. */
private final boolean userVerified;
@@ -81,6 +87,7 @@ public class CredentialRegistration {
*
* @param builder the builder
*/
+ //TODO look at nullable here.
private CredentialRegistration(final Builder builder) {
this.userIdentity = builder.userIdentity;
this.transports = builder.transports;
@@ -90,53 +97,158 @@ public class CredentialRegistration {
this.discoverable = builder.discoverable;
this.attestationMetadata = builder.attestationMetadata;
this.userVerified = builder.userVerified;
+
}
+ /**
+ * Was user verification performed during registration?
+ *
+ * @return true if user registration was performed, false otherwise
+ */
@JsonGetter("userVerified")
public boolean isUserVerified() {
return userVerified;
}
+ /**
+ * Is the created credential discoverable?
+ *
+ * @return true if the credential is discoverable, false if it isn't, empty if unknown.
+ */
@JsonGetter("discoverable")
@Nonnull public Optional<Boolean> isDiscoverable() {
return discoverable;
}
+ /**
+ * Get the nickname of this credential.
+ *
+ * @return the nickname
+ */
@JsonGetter("nickname")
- public String getNickname() {
+ @Nullable public String getNickname() {
return credentialNickname;
}
+ /**
+ * Get the time the registration took place.
+ *
+ * @return the time of registration
+ */
@JsonGetter("registrationTime")
- public Instant getRegistrationTime() {
+ @Nonnull public Instant getRegistrationTime() {
return registrationTime;
}
+ /**
+ * Get the name of the user identity registered.
+ *
+ * @return the name of the user identity
+ */
@JsonIgnore
- public String getUsername() {
+ @Nonnull public String getUsername() {
return userIdentity.getName();
}
+ /**
+ * Get the identity of the user this credential has been registered for.
+ *
+ * @return the users identity
+ */
@JsonGetter("userIdentity")
- public UserIdentity getUserIdentity() {
+ @Nonnull public UserIdentity getUserIdentity() {
return userIdentity;
}
+ /**
+ * Get the registered credential
+ *
+ * @return the registered credential
+ */
@JsonGetter("credential")
- public RegisteredCredential getCredential() {
+ @Nonnull public RegisteredCredential getCredential() {
return credential;
}
+ /**
+ * Get the set of {@link AuthenticatorTransport transports} the authenticator can
+ * use to communicate with the client.
+ *
+ * @return the set of transports the authenticator can use to communicate with the client
+ */
@JsonGetter("transports")
- public SortedSet<AuthenticatorTransport> getTransports() {
+ @Nonnull @Unmodifiable @NonnullElements public SortedSet<AuthenticatorTransport> getTransports() {
return transports;
}
- // TODO do we need these here. They should be transformed where needed
+ /**
+ * Get metadata about an authenticators attestation.
+ *
+ * @return the metadata about an authenticators attestation
+ */
+ @JsonGetter("attestationMetadata")
+ @Nonnull @Unmodifiable @NonnullElements public Set<MetadataBLOBPayloadEntry> getAttestationMetadata(){
+ return attestationMetadata;
+
+ }
+
+ /**
+ * Get the credential ID as a base64URL encoded string.
+ *
+ * @return the credential ID base64URL encoded
+ */
@JsonIgnore
public String getCredentialIdBase64Url() {
return credential.getCredentialId().getBase64Url();
}
+
+ /**
+ * Get the human-readable, short description of the authenticator (in English) iff the attestation metadata exist.
+ *
+ * <p>If there is more than one metadata entry, it picks the first it can find with a description.</p>
+ *
+ * @return the human-readable, short description of the authenticator (in English), or <code>null</code>.
+ */
+ @JsonIgnore
+ @Nullable public String getAuthenticatorDescription() {
+ if (attestationMetadata != null && !attestationMetadata.isEmpty()) {
+ final Optional<Optional<String>> descriptionFound = attestationMetadata.stream()
+ .filter(mtd -> mtd.getMetadataStatement().isPresent())
+ .map(mtd -> mtd.getMetadataStatement().get().getDescription()).findFirst();
+
+ if (descriptionFound.isEmpty() || descriptionFound.get().isEmpty() ||
+ descriptionFound.get().get().isEmpty()) {
+ return null;
+ }
+ return descriptionFound.get().get();
+ }
+ return null;
+ }
+
+ /**
+ * Get a <code>data:</code> URL encoded PNG icon for the authenticator.
+ *
+ * <p>If there is more than one metadata entry, it picks the first it can find with an icon.</p>
+ *
+ * @return the icon encoded as a PNG <code>data:</code> URL
+ */
+ @JsonIgnore
+ @Nullable public String getIcon() {
+ if (attestationMetadata != null && !attestationMetadata.isEmpty()) {
+ final Optional<Optional<String>> iconFound = attestationMetadata.stream()
+ .filter(mtd -> mtd.getMetadataStatement().isPresent())
+ .map(mtd -> mtd.getMetadataStatement().get().getIcon()).findFirst();
+
+ if (iconFound.isEmpty() || iconFound.get().isEmpty() ||
+ iconFound.get().get().isEmpty()) {
+ return null;
+ }
+ return iconFound.get().get();
+ }
+ return null;
+
+ }
+
/**
* Convert the credential registration into a {@link PublicKeyCredentialDescriptor}.
*
@@ -181,7 +293,7 @@ public class CredentialRegistration {
* @return a new {@link CredentialRegistration} instance
*/
@JsonIgnore
- public CredentialRegistration withCredential(final RegisteredCredential newRegisteredCred) {
+ public CredentialRegistration withCredential(@Nonnull final RegisteredCredential newRegisteredCred) {
return CredentialRegistration.builder()
.withUserIdentity(userIdentity)
.withTransports(transports)
@@ -201,33 +313,34 @@ public class CredentialRegistration {
/** Builder stage.*/
public interface IUserIdentityStage {
- public ITransportsStage withUserIdentity(UserIdentity userIdentity);
+ @Nonnull public ITransportsStage withUserIdentity(@Nonnull final UserIdentity userIdentity);
}
/** Builder stage.*/
public interface ITransportsStage {
- public IRegistrationTimeStage withTransports(SortedSet<AuthenticatorTransport> transports);
+ @Nonnull public IRegistrationTimeStage withTransports(@Nonnull SortedSet<AuthenticatorTransport> transports);
}
/** Builder stage.*/
public interface IRegistrationTimeStage {
- public ICredentialStage withRegistrationTime(Instant registrationTime);
+ @Nonnull public ICredentialStage withRegistrationTime(@Nonnull final Instant registrationTime);
}
/** Builder stage.*/
public interface ICredentialStage {
- public IBuildStage withCredential(RegisteredCredential credential);
+ @Nonnull public IBuildStage withCredential(@Nonnull final RegisteredCredential credential);
}
/** Builder stage.*/
public interface IBuildStage {
- public IBuildStage withCredentialNickname(String credentialNickname);
+ @Nonnull public IBuildStage withCredentialNickname(@Nullable final String credentialNickname);
- public IBuildStage withDiscoverable(Optional<Boolean> discoverable);
+ @Nonnull public IBuildStage withDiscoverable(@Nonnull final Optional<Boolean> discoverable);
- public IBuildStage withAttestationMetadata(Object attestationMetadata);
+ @Nonnull public IBuildStage withAttestationMetadata(
+ @Nonnull final Set<MetadataBLOBPayloadEntry> attestationMetadata);
- public IBuildStage withUserVerified(boolean userVerified);
+ @Nonnull public IBuildStage withUserVerified(boolean userVerified);
@Nonnull public CredentialRegistration build();
}
@@ -240,78 +353,79 @@ public class CredentialRegistration {
private SortedSet<AuthenticatorTransport> transports;
private Instant registrationTime;
private RegisteredCredential credential;
- private String credentialNickname;
- private Optional<Boolean> discoverable;
- private Object attestationMetadata;
+ @Nullable private String credentialNickname;
+ @Nonnull private Optional<Boolean> discoverable;
+ @Nonnull private Set<MetadataBLOBPayloadEntry> attestationMetadata;
private boolean userVerified;
/** Constructor.*/
private Builder() {
// Create empty, corresponds to 'unknown'
- discoverable = Optional.empty();
-
+ discoverable = Optional.empty();
userVerified = false;
transports = Collections.emptySortedSet();
+ attestationMetadata = CollectionSupport.emptySet();
}
@Override
@JsonProperty("userIdentity")
- public ITransportsStage withUserIdentity(final UserIdentity user) {
+ @Nonnull public ITransportsStage withUserIdentity(@Nonnull final UserIdentity user) {
userIdentity = user;
return this;
}
@Override
@JsonProperty("transports")
- public IRegistrationTimeStage withTransports(final SortedSet<AuthenticatorTransport> authenticatorTransports) {
+ @Nonnull public IRegistrationTimeStage withTransports(@Nonnull
+ final SortedSet<AuthenticatorTransport> authenticatorTransports) {
transports = authenticatorTransports;
return this;
}
@Override
@JsonProperty("registrationTime")
- public ICredentialStage withRegistrationTime(final Instant time) {
+ @Nonnull public ICredentialStage withRegistrationTime(@Nonnull final Instant time) {
registrationTime = time;
return this;
}
@Override
@JsonProperty("credential")
- public IBuildStage withCredential(final RegisteredCredential cred) {
+ @Nonnull public IBuildStage withCredential(@Nonnull final RegisteredCredential cred) {
credential = cred;
return this;
}
@Override
@JsonProperty("nickname")
- public IBuildStage withCredentialNickname(final String credNickname) {
+ @Nonnull public IBuildStage withCredentialNickname(@Nullable final String credNickname) {
credentialNickname = credNickname;
return this;
}
@Override
@JsonProperty("discoverable")
- public IBuildStage withDiscoverable(final Optional<Boolean> isDiscoverable) {
+ @Nonnull public IBuildStage withDiscoverable(@Nonnull final Optional<Boolean> isDiscoverable) {
discoverable = isDiscoverable;
return this;
}
@Override
@JsonProperty("attestationMetadata")
- public IBuildStage withAttestationMetadata(final Object attestationMtd) {
+ @Nonnull public IBuildStage withAttestationMetadata(@Nonnull final Set<MetadataBLOBPayloadEntry> attestationMtd) {
attestationMetadata = attestationMtd;
return this;
}
@Override
@JsonProperty("userVerified")
- public IBuildStage withUserVerified(final boolean isUserVerified) {
+ @Nonnull public IBuildStage withUserVerified(final boolean isUserVerified) {
userVerified = isUserVerified;
return this;
}
@Override
- public CredentialRegistration build() {
+ @Nonnull public CredentialRegistration build() {
return new CredentialRegistration(this);
}
}
diff --git a/webauthn-impl/pom.xml b/webauthn-impl/pom.xml
index e8296ca..c3c4bae 100644
--- a/webauthn-impl/pom.xml
+++ b/webauthn-impl/pom.xml
@@ -31,6 +31,11 @@
<artifactId>webauthn-server-core</artifactId>
<scope>compile</scope>
</dependency>
+ <dependency>
+ <groupId>${yubico.groupId}</groupId>
+ <artifactId>webauthn-server-attestation</artifactId>
+ <scope>compile</scope>
+ </dependency>
<dependency><!-- TODO check the IdP provides this -->
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
@@ -172,6 +177,16 @@
<scope>provided</scope>
</dependency>
<!-- Test dependencies -->
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp3</groupId>
+ <artifactId>okhttp-tls</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>${idp.groupId}</groupId>
<artifactId>idp-testing</artifactId>
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
index 946b65e..7edcac6 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AbstractWebAuthnRegistrationAction.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.util.function.Function;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
@@ -27,6 +28,8 @@ import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
+import com.yubico.fido.metadata.FidoMetadataService;
+
import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationClient;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
@@ -69,6 +72,14 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
/** The credential respository to store valid credentials in.*/
@NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
+ /** Optional FIDO metadata service resolver.*/
+ @Nullable private FidoMetadataService fidoMetadataService;
+
+ /** Constructor.*/
+ protected AbstractWebAuthnRegistrationAction() {
+ //prc -> WebAuthnContext
+ webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+ }
/**
* Set the WebAuthn client used to handle registration and authentication ceremonies.
@@ -85,18 +96,30 @@ public abstract class AbstractWebAuthnRegistrationAction extends AbstractProfile
*
* @return the webAuthnClient.
*/
- @NonnullBeforeExec public WebAuthnAuthenticationClient getWebAuthnClient() {
+ @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
checkComponentActive();
return webAuthnClient;
}
-
- /** Constructor.*/
- protected AbstractWebAuthnRegistrationAction() {
- //prc -> WebAuthnContext
- webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+ /**
+ * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @param service The FIDO metadata service to set.
+ */
+ public void setFidoMetadataService(@Nullable final FidoMetadataService service) {
+ checkSetterPreconditions();
+ fidoMetadataService = service;
}
+ /**
+ * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @return the fido metadata service.
+ */
+ @Nullable protected FidoMetadataService getFidoMetadataService() {
+ checkComponentActive();
+ return fidoMetadataService;
+ }
/**
* Set WebAuthn registration context lookup strategy to use.
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
index ce0bd73..90e40bd 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -18,6 +18,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.time.Instant;
+import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
@@ -30,6 +31,9 @@ import org.opensaml.storage.StorageSerializer;
import org.opensaml.storage.StorageService;
import org.slf4j.Logger;
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.fido.metadata.FidoMetadataService;
+import com.yubico.fido.metadata.MetadataBLOBPayloadEntry;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.data.ByteArray;
@@ -40,6 +44,9 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationCont
import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistrationSerializer;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
@@ -114,21 +121,28 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
.publicKeyCose(registrationResult.getPublicKeyCose())
.build();
+ //TODO user identity should come from somewhere else?
final UserIdentity user = UserIdentity.builder()
.name(username)
.displayName(username)
.id(new ByteArray(context.getUserId()))
.build();
+ assert user != null;
+ assert credential != null;
+ final Instant now = Instant.now();
+ assert now != null;
+ final Optional<Boolean> isDiscoverable = registrationResult.isDiscoverable();
+ assert isDiscoverable != null;
+
final CredentialRegistration registration = CredentialRegistration.builder()
.withUserIdentity(user)
.withTransports(registrationResult.getKeyId().getTransports().orElse(new TreeSet<>()))
- .withRegistrationTime(Instant.now())
+ .withRegistrationTime(now)
.withCredential(credential)
- //TODO this should not be null, we should support attestation even if not initially used
- .withAttestationMetadata(null)
+ .withAttestationMetadata(getAttestationMetadata(registrationResult.getAaguid()))
.withCredentialNickname(context.getCredentialNickname())
- .withDiscoverable(registrationResult.isDiscoverable())
+ .withDiscoverable(isDiscoverable)
.withUserVerified(registrationResult.isUserVerified())
.build();
@@ -150,4 +164,25 @@ public class StorePublicKeyCredential extends AbstractWebAuthnRegistrationAction
}
+ /**
+ * Find attestation metadata for the authenticator.
+ *
+ * @param authenticatorId the authenticator attestation GUID
+ *
+ * @return the attestation metadata relating to the authenticator attestation GUID
+ */
+ @Nonnull @NotLive @NonnullElements private Set<MetadataBLOBPayloadEntry> getAttestationMetadata(
+ final ByteArray authenticatorId) {
+ final FidoMetadataService localMetadataService = getFidoMetadataService();
+ if (localMetadataService != null) {
+ final Set<MetadataBLOBPayloadEntry> found =
+ localMetadataService.findEntries(new AAGUID(authenticatorId));
+ if (found == null) {
+ return CollectionSupport.emptySet();
+ }
+ return CollectionSupport.copyToSet(found);
+ }
+ return CollectionSupport.emptySet();
+ }
+
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
index 5fa6d0c..85b6c31 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebauthnClientFactory.java
@@ -25,9 +25,11 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
+import org.slf4j.Logger;
import org.springframework.beans.factory.FactoryBean;
import com.google.common.base.Predicates;
+import com.yubico.fido.metadata.FidoMetadataService;
import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.COSEAlgorithmIdentifier;
@@ -42,6 +44,7 @@ import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.AbstractInitializableComponent;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
/**
@@ -50,6 +53,9 @@ import net.shibboleth.shared.primitive.StringSupport;
@ThreadSafe
public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
implements FactoryBean<WebAuthnAuthenticationClient> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(YubicoWebauthnClientFactory.class);
/** The relying party identifier.*/
@GuardedBy("this") @NonnullAfterInit private String relyingPartyId;
@@ -75,6 +81,12 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
/** List of acceptable public key algorithms.*/
@GuardedBy("this") @Nonnull @NonnullElements private List<PublicKeyCredentialParameters> preferredPublickeyParams;
+ /** Should we allow an untrusted attestation? */
+ @GuardedBy("this") private boolean allowUntrustedAttestation;
+
+ /** If configured, a FIDO metadata service resolver.*/
+ @GuardedBy("this") @Nullable private FidoMetadataService fidoMetadataService;
+
/** Constructor.*/
public YubicoWebauthnClientFactory() {
allowOriginPort = false;
@@ -117,17 +129,32 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
.name(getRelyingPartyName())
.build()).credentialRepository(getCredentialRepository())
.allowOriginPort(isAllowOriginPort())
- .allowOriginSubdomain(isAllowOriginSubdomain());
+ .allowOriginSubdomain(isAllowOriginSubdomain())
+ .allowUntrustedAttestation(isAllowUntrustedAttestation())
+ .validateSignatureCounter(allowOriginPort);
+
+ // Add the fido metadata service if one has been configured
+ final FidoMetadataService localMetadataService = getFidoMetadataService();
+ if (localMetadataService != null) {
+ builder.attestationTrustSource(localMetadataService);
+ }
+
+ log.info("Built Yubico WebAuthn Client for RelyingParty '{}', using FIDO metadata '{}', allowOriginPort '{}', "
+ + "allowOriginSubdomain '{}', allowUntrustedMetadata '{}'", getRelyingPartyId(),
+ localMetadataService != null ? "yes" : "no", isAllowOriginPort(), isAllowOriginSubdomain(),
+ isAllowUntrustedAttestation());
if (!getOrigins().isEmpty()) {
final RelyingParty rp = builder.origins(getOrigins()).build();
- assert rp != null;
+ assert rp != null;
return new YubicoWebAuthnAuthenticationClient(rp, getPreferredPublickeyParams());
} else {
final RelyingParty rp = builder.build();
assert rp != null;
return new YubicoWebAuthnAuthenticationClient(rp, getPreferredPublickeyParams());
- }
+ }
+
+
}
@@ -331,6 +358,47 @@ public class YubicoWebauthnClientFactory extends AbstractInitializableComponent
checkSetterPreconditions();
allowOriginSubdomain = allow;
}
+
+ /**
+ * Set if untrusted attestations (registrations) are allowed.
+ *
+ * @param allow are untrusted attestations allowed?
+ */
+ public synchronized void setAllowUntrustedAttestation(final boolean allow) {
+ checkSetterPreconditions();
+ allowUntrustedAttestation = allow;
+
+ }
+
+ /**
+ * Allow untrusted attestations?
+ *
+ * @return if untrusted attestations are allowed.
+ */
+ private synchronized boolean isAllowUntrustedAttestation() {
+ checkComponentActive();
+ return allowUntrustedAttestation;
+ }
+
+ /**
+ * Set the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @param service The FIDO metadata service to set.
+ */
+ public synchronized void setFidoMetadataService(@Nullable final FidoMetadataService service) {
+ checkSetterPreconditions();
+ fidoMetadataService = service;
+ }
+
+ /**
+ * Get the FIDO Alliance metadata service resolver to use as the attestation trust source.
+ *
+ * @return the fido metadata service.
+ */
+ @Nullable public synchronized FidoMetadataService getFidoMetadataService() {
+ checkComponentActive();
+ return fidoMetadataService;
+ }
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceFactory.java
new file mode 100644
index 0000000..0126fb2
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceFactory.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.metadata;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+
+import org.opensaml.security.x509.X509Support;
+import org.slf4j.Logger;
+import org.springframework.beans.FatalBeanException;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.core.io.Resource;
+
+import com.yubico.fido.metadata.FidoMetadataDownloader;
+import com.yubico.fido.metadata.FidoMetadataService;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+
+/**
+ * Spring factory bean for creating a {@link FidoMetadataService}.
+ */
+public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializableComponent
+ implements FactoryBean<FidoMetadataService> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(FidoMetadataServiceFactory.class);
+
+ /** The file location of the trust root that verifies the downloaded metadata's signature.*/
+ @GuardedBy("this") @NonnullAfterInit private Resource trustRootFile;
+
+ /** Where to cache the metadata blob.*/
+ @GuardedBy("this") @Nullable private Resource cacheFile;
+
+ /** The HTTPS location of the metadata blob to download.*/
+ @GuardedBy("this") @Nullable private Resource metadataBlobUrl;
+
+ /** The File location of the metadata blob. If this file is specified, metadata will never be downloaded over HTTP.*/
+ @GuardedBy("this") @Nullable private Resource metadataBlobFile;
+
+ /** The expected set of legal headers on the FIDO metadata blob.*/
+ @GuardedBy("this") @NonnullAfterInit private String[] expectedLegalHeaders;
+
+ /** {@inheritDoc} */
+ @Override
+ public FidoMetadataService getObject() throws Exception {
+
+ FidoMetadataDownloader downloader = null;
+ final Resource localMetadataBlobUrl = getMetadataBlobUrl();
+ final Resource localMetadataBlobFile = getMetadataBlobFile();
+ final Resource localMetadataCacheFile = getCacheFile();
+
+ if (localMetadataBlobFile != null) {
+ log.debug("{}: Loading FIDO metadata blob from local file '{}'",getId(), metadataBlobFile);
+ downloader = FidoMetadataDownloader.builder()
+ .expectLegalHeader(getExpectedLegalHeaders())
+ .useTrustRoot(X509Support.decodeCertificate(getTrustRootFile().getFile()))
+ .useBlob(loadMetadataJwt(localMetadataBlobFile))
+ .build();
+ } else if (localMetadataBlobUrl != null && localMetadataCacheFile != null){
+ log.debug("{}: Loading FIDO metadata blob from '{}'", getId(), metadataBlobUrl);
+ downloader = FidoMetadataDownloader.builder()
+ .expectLegalHeader(getExpectedLegalHeaders())
+ .useTrustRoot(X509Support.decodeCertificate(getTrustRootFile().getFile()))
+ .downloadBlob(localMetadataBlobUrl.getURL())
+ .useBlobCacheFile(localMetadataCacheFile.getFile())
+ .verifyDownloadsOnly(true)
+ .build();
+ } else {
+ throw new FatalBeanException("Local FIDO metadata blob file not specified or the metadata blob URL and "
+ + "local cache file not specified. Please use either a local file or a known URL");
+ }
+ assert downloader != null;
+ try {
+ final FidoMetadataService mds = FidoMetadataService.builder()
+ .useBlob(downloader.loadCachedBlob())
+ .build();
+ log.debug("{}: loaded FIDO metadata blob", getId());
+ return mds;
+ } catch (final Exception e) {
+ throw new FatalBeanException("Can not construct FIDO Metadata service", e);
+ }
+ }
+
+
+ /**
+ * Load the given metadata blob file.
+ *
+ * @param file the metadata blob file to load
+ *
+ * @return the metadata blob as a string
+ *
+ * @throws FileNotFoundException if the file does not exist
+ */
+ @Nonnull private String loadMetadataJwt(@Nonnull final Resource file) throws IOException {
+ if (!file.exists()) {
+ throw new FileNotFoundException("Metadata blob file does not exist");
+ }
+ return file.getContentAsString(StandardCharsets.UTF_8);
+ }
+
+
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (trustRootFile == null) {
+ throw new ComponentInitializationException("trustRootFile cannot be null");
+ }
+ if (metadataBlobUrl == null && metadataBlobFile == null) {
+ throw new ComponentInitializationException("Metadata blob URL or file must be set");
+ }
+
+ }
+
+ /**
+ * Set where to cache the metadata blob.
+ *
+ * @param file The cacheFile to set.
+ */
+ public synchronized void setCacheFile(@Nullable final Resource file) {
+ checkSetterPreconditions();
+ cacheFile = file;
+ }
+
+ /**
+ * Get where to cache the metadata blob.
+ *
+ * @return where to cache the metadata blob.
+ */
+ @Nullable public synchronized Resource getCacheFile() {
+ checkComponentActive();
+ return cacheFile;
+ }
+
+ /**
+ * Set the trust root file.
+ *
+ * @param file the trust root file
+ */
+ public synchronized void setTrustRootFile(@Nonnull final Resource file) {
+ checkSetterPreconditions();
+ trustRootFile = Constraint.isNotNull(file, "trustRootCacheFile can not be null");
+ }
+
+ /**
+ * Get the trust root file.
+ *
+ * @return the trust root file
+ */
+ @NonnullAfterInit public synchronized Resource getTrustRootFile() {
+ checkComponentActive();
+ return trustRootFile;
+ }
+
+ /**
+ * Get the URL of the metadata blob file to fetch.
+ *
+ * @return the metadata blob file URL.
+ */
+ @Nullable public synchronized Resource getMetadataBlobUrl() {
+ checkComponentActive();
+ return metadataBlobUrl;
+ }
+
+ /**
+ * Set the URL of the metadata blob file to fetch.
+ *
+ * @param url the metadata blob file
+ */
+ public synchronized void setMetadataBlobUrl(@Nullable final Resource url) {
+ checkSetterPreconditions();
+ metadataBlobUrl = url;
+ }
+
+ /**
+ * Set the location of the local metadata blob file. If this file is specified, metadata will never be
+ * downloaded over HTTP.
+ *
+ * @param metadataBlobFile The metadataBlobFile to set.
+ */
+ public synchronized void setMetadataBlobFile(@Nullable final Resource file) {
+ checkSetterPreconditions();
+ metadataBlobFile = file;
+ }
+
+ /**
+ * Get the metadata blob file.
+ *
+ * @return the metadata blob file.
+ */
+ @Nullable public synchronized Resource getMetadataBlobFile() {
+ return metadataBlobFile;
+ }
+
+ /**
+ * Set the expected legal headers on the FIDO metadata blob.
+ *
+ * @param headers The expected legal headers to set.
+ */
+ public synchronized void setExpectedLegalHeaders(@Nonnull final String[] headers) {
+ checkSetterPreconditions();
+ expectedLegalHeaders = Constraint.isNotNull(headers, "expectedLegalHeaders can not be null");
+ }
+
+ /**
+ * Get the expected legal headers on the FIDO metadata blob.
+ *
+ * @return the expected legal headers.
+ */
+ @NonnullAfterInit public synchronized String[] getExpectedLegalHeaders() {
+ return expectedLegalHeaders;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Class<?> getObjectType() {
+ return FidoMetadataService.class;
+ }
+
+}
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 9047d51..269b7d6 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -72,9 +72,11 @@
p:relyingPartyName="%{idp.authn.webauthn.relyingPartyName}"
p:allowOriginPort="%{idp.authn.webauthn.allowOriginPort:false}"
p:allowOriginSubdomain="%{idp.authn.webauthn.allowOriginSubdomain:false}"
+ p:allowUntrustedAttestation="%{idp.authn.webauthn.allowUntrustedAttestation:false}"
p:origins="%{idp.authn.webauthn.origins:}"
p:preferredPublickeyParams="%{idp.authn.webauthn.preferredPublicKeyParams:EdDSA,ES256,ES384,ES512,RS1,RS256,RS384,RS512}"
- p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository"/>
+ p:credentialRepository-ref="shibboleth.authn.webauthn.DefaultCredentialRepository"
+ p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
<bean id="shibboleth.authn.webauthn.DefaultCredentialRepository" scope="singleton"
@@ -83,7 +85,17 @@
p:serializer-ref="shibboleth.authn.webauthn.DefaultCredentialRepositoryStorageSerializer"/>
<bean id="shibboleth.authn.webauthn.DefaultCredentialRepositoryStorageSerializer"
- class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistrationSerializer"/>
+ class="net.shibboleth.idp.plugin.authn.webauthn.storage.impl.CredentialRegistrationSerializer"/>
+
+ <!-- The optional FIDO Metadata service. Lazy-init, only constructed if used. -->
+ <bean id="shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory"
+ class="net.shibboleth.idp.plugin.authn.webauthn.metadata.FidoMetadataServiceFactory" scope="singleton"
+ lazy-init="true"
+ p:trustRootFile="%{idp.authn.webauthn.metadata.trustRootFile:}"
+ p:cacheFile="%{idp.authn.webauthn.metadata.cacheFile:}"
+ p:metadataBlobUrl="%{idp.authn.webauthn.metadata.metadataBlobUrl:https://mds.fidoalliance.org}"
+ p:metadataBlobFile="%{idp.authn.webauthn.metadata.metadataBlobFile:}"
+ p:expectedLegalHeaders="%{idp.authn.webauthn.metadata.expectedLegalHeaders:Retrieval and use of this BLOB indicates acceptance of the appropriate agreement located at https://fidoalliance.org/metadata/metadata-legal-terms/}"/>
<!--
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
index eb55081..ce418d5 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-beans.xml
@@ -17,7 +17,17 @@
<bean id="shibboleth.ChildLookup.WebAuthnRegistrationContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext) }" />
-
+
+ <!-- Abstract parent beans -->
+
+ <bean id="AbstractWebAuthnRegistrationAction" scope="prototype" abstract="true"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"
+ p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"
+ p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
+
+
+ <!-- Flow beans -->
+
<bean id="PopulateInitialWebAuthnRegistrationContext" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
<property name="usernameLookupStrategy">
@@ -53,7 +63,7 @@
p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
p:userVerificationRequirement="%{idp.authn.webauthn.registration.userVerification:discouraged}" />
- <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnRegistrationAction"
+ <bean id="LookupRegisteredCredentials" parent="AbstractWebAuthnBaseAction"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.LookupRegisteredCredentials"
p:webAuthnBaseContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext" />
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
index c2c9107..745baf1 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-abstract-beans.xml
@@ -9,14 +9,8 @@
default-init-method="initialize" default-destroy-method="destroy">
- <!-- Parent beans -->
- <bean id="AbstractWebAuthnAuthenticationAction" scope="prototype" abstract="true"
- p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"/>
-
- <bean id="AbstractWebAuthnRegistrationAction" scope="prototype" abstract="true"
- p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"
- p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"/>
-
+ <!-- Abstract parent beans -->
+
<bean id="AbstractWebAuthnBaseAction" scope="prototype" abstract="true"
p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"
p:credentialRepository="#{getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"/>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 87ec29c..d476edd 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -14,7 +14,13 @@
parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.ChildLookup.AuthenticationContext"
c:g-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContext" />
+ <!-- Abstract parent beans -->
+
+ <bean id="AbstractWebAuthnAuthenticationAction" scope="prototype" abstract="true"
+ p:webAuthnClient="#{getObject('shibboleth.authn.webauthn.DefaultWebAuthnAuthenticationClientFactory')}"/>
+ <!-- Flow beans -->
+
<bean id="PopulateWebAuthnAuthenticationContextPasswordless" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.PopulateWebAuthnAuthenticationContext">
<property name="usernameLookupStrategy">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
index 0f98e1e..e9e2433 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/css/webauthn.css
@@ -85,4 +85,10 @@ tr:hover {
text-decoration: none;
display: inline-block;
font-size: 16px;
+}
+
+.authenticator-logo {
+ display: inline;
+ margin-right: auto;
+ margin-left: auto;
}
\ No newline at end of file
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
index f4b7319..751808a 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register.vm
@@ -121,15 +121,25 @@
#if ($webauthnRegContext.existingCredentials)
<table>
<tr>
- <th>#springMessageText("idp.webauthn.register.table.keyName", "Key Name")</th>
- <th>#springMessageText("idp.webauthn.register.table.transports", "Transports")</th>
- <th>#springMessageText("idp.webauthn.register.table.passkey", "Passkey?")</th>
- <th>#springMessageText("idp.webauthn.register.table.registrationTime", "Registration Time")</th>
- <th>#springMessageText("idp.webauthn.register.table.action", "Action")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.keyName", "Key Name")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.authenticatorDescription", "Authenticator")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.transports", "Transports")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.passkey", "Passkey?")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.registrationTime", "Registration Time")</th>
+ <th>#springMessageText("idp.webauthn.register.table.header.action", "Action")</th>
</tr>
#foreach($cred in $webauthnRegContext.existingCredentials)
<tr>
<td>$encoder.encodeForHTML($cred.nickname)</td>
+ #if ($cred.authenticatorDescription)
+ <td>
+ #if ($cred.icon)
+ <img class="authenticator-logo" src="$encoder.encodeForHTML($cred.icon) alt="authenticator-icon"/>
+ #end
+ $encoder.encodeForHTML($cred.authenticatorDescription)</td>
+ #else
+ <td>#springMessageText("idp.webauthn.register.table.unknownCredential", "unknown")</td>
+ #end
<td>$encoder.encodeForHTML($webAuthnEncoder.formatTransports($cred.transports))</td>
<td>$encoder.encodeForHTML($webAuthnEncoder.formatDiscoverable($cred.isDiscoverable()))</td>
<td>$encoder.encodeForHTML($webAuthnEncoder.formatInstant($cred.registrationTime))</td>
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
index 53186ee..098391a 100644
--- a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnTest.java
@@ -14,6 +14,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.impl;
+import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,6 +40,10 @@ import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileR
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.collection.CollectionSupport;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.tls.HandshakeCertificates;
+import okhttp3.tls.HeldCertificate;
/** Abstract class for tests that require context setup.*/
public abstract class AbstractWebAuthnTest {
@@ -116,6 +121,43 @@ public abstract class AbstractWebAuthnTest {
prc.addSubcontext(webAuthnRegContext);
}
+ /**
+ * Create a running server that mimics responses from a WebAuthn Metadata Service.
+ * Creates a new self-signed certificate.
+ *
+ * @return the simple server.
+ *
+ * @throws UnknownHostException on error.
+ */
+ protected MockWebServer createSimpleServer() throws UnknownHostException {
+ //start mock server
+ final MockWebServer mockServer = new MockWebServer();
+ final HeldCertificate localhostCertificate = new HeldCertificate.Builder()
+ .addSubjectAlternativeName("localhost")
+ .build();
+ final HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder()
+ .heldCertificate(localhostCertificate)
+ .build();
+ mockServer.useHttps(serverCertificates.sslSocketFactory(), false);
+
+ return mockServer;
+ }
+
+ /**
+ * Queue a mock response. Simulating a response from the OP.
+ *
+ * @param mockOPServer the mock server
+ * @param code the response HTTP code
+ * @param body the response body
+ * @param contentType the content type header
+ */
+ protected void queueMockServerResponse(final MockWebServer mockOPServer, final int code,
+ final String body, final String contentType) {
+ mockOPServer.enqueue(new MockResponse().setResponseCode(code)
+ .setHeader("content-type", contentType)
+ .setBody(body));
+ }
+
/**
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceResolverFactoryTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceResolverFactoryTest.java
new file mode 100644
index 0000000..a54c035
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/FidoMetadataServiceResolverFactoryTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.webauthn.metadata;
+
+import static org.testng.Assert.assertNotNull;
+
+import java.net.MalformedURLException;
+import java.nio.charset.StandardCharsets;
+
+import org.springframework.beans.FatalBeanException;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.fido.metadata.FidoMetadataService;
+
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import okhttp3.mockwebserver.MockWebServer;
+
+/**
+ * Tests for the {@link FidoMetadataServiceFactory}.
+ */
+public class FidoMetadataServiceResolverFactoryTest extends AbstractWebAuthnTest{
+
+
+ /** The factory to test.*/
+ private FidoMetadataServiceFactory factory;
+
+ /** The metadata blob resource to use as if provided over HTTP.*/
+ private Resource httpMetadataBlobResource;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws MalformedURLException {
+ // Turn on CRL DP checking, otherwise the Yubico downloader will fail to find revocation status
+ System.setProperty("com.sun.security.enableCRLDP", "true");
+
+ factory = new FidoMetadataServiceFactory();
+ factory.setCacheFile(new FileSystemResource("cache-file.bin"));
+ factory.setTrustRootFile(new ClassPathResource("root-r3.crt"));
+ factory.setExpectedLegalHeaders(new String[]{"headers"});
+ factory.setId("Test metadata factory");
+ httpMetadataBlobResource = new ClassPathResource("fido-metadata.bin");
+ }
+
+
+ @Test
+ public void testSuccessfullMetadataLoadFromFile() throws Exception {
+ // Setting the local metadata blob file will force the factory to load from the local file
+ factory.setMetadataBlobFile(new ClassPathResource("fido-metadata.bin"));
+ factory.initialize();
+ final FidoMetadataService resolver = factory.getObject();
+ assertNotNull(resolver);
+ assert resolver != null;
+ }
+
+ @Test(expectedExceptions = FatalBeanException.class)
+ public void testUnsuccessfull_RequiresCacheFileWhenURLUsed() throws Exception {
+ // Setting the local metadata blob file will force the factory to load from the local file
+ factory.setMetadataBlobUrl(new UrlResource("https://localhost:9918/"));
+ factory.setCacheFile(null);
+ factory.initialize();
+ final FidoMetadataService resolver = factory.getObject();
+ assertNotNull(resolver);
+ assert resolver != null;
+ }
+
+
+ //TODO add this in, TLS trust is harder to fake here.
+ //@Test
+ public void testSuccessfullMetadataLoadFromURL() throws Exception {
+
+ final MockWebServer mockOPServer = createSimpleServer();
+ queueMockServerResponse(mockOPServer, 200, httpMetadataBlobResource.getContentAsString(StandardCharsets.UTF_8),
+ "application/octet-stream");
+
+ mockOPServer.start(9918);
+
+ factory.setMetadataBlobUrl(new UrlResource("https://localhost:9918/"));
+ factory.initialize();
+ final FidoMetadataService resolver = factory.getObject();
+ assertNotNull(resolver);
+ assert resolver != null;
+ }
+
+}
diff --git a/webauthn-impl/src/test/resources/fido-metadata.bin b/webauthn-impl/src/test/resources/fido-metadata.bin
new file mode 100644
index 0000000..27007ee
--- /dev/null
+++ b/webauthn-impl/src/test/resources/fido-metadata.bin
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlITURDQ0JoaWdBd0lCQWdJTU11QlUrMkZ3dGl0N1RrUEtNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1HSXhDekFKQmdOVkJBWVRBa0pGTVJrd0Z3WURWUVFLRXhCSGJHOWlZV3hUYVdkdUlHNTJMWE5oTVRnd05nWURWUVFERXk5SGJHOWlZV3hUYVdkdUlFVjRkR1Z1WkdWa0lGWmhiR2xrWVhScGIyNGdRMEVnTFNCVFNFRXlOVFlnTFNCSE16QWVGdzB5TXpBMk1EZ3lNREF3TVRGYUZ3MHlOREEzTURreU1EQXdNVEJhTUlIdE1SMHdHd1lEVlFRUERCUlFjbWwyWVhSbElFOXlaMkZ1YVhwaGRHbHZiakVRTUE0R0ExVUVCUk1ITXpRMU5ESTROREVUTUJFR0N5c0dBUVFCZ2pjOEFnRURFd0pWVXpFYk1C [...]
\ No newline at end of file
diff --git a/webauthn-impl/src/test/resources/root-r3.crt b/webauthn-impl/src/test/resources/root-r3.crt
new file mode 100644
index 0000000..232c4b6
Binary files /dev/null and b/webauthn-impl/src/test/resources/root-r3.crt differ
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list