[java-idp-plugin-webauthn] 01/11: JWEBAUTHN-27 - Add basic authenticator policy

Phil Smart philip.smart at jisc.ac.uk
Fri Oct 18 17:13:26 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=12956a325cf0fda0e2e8054360cabb2e435e714a

commit 12956a325cf0fda0e2e8054360cabb2e435e714a
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Oct 2 11:03:28 2024 +0100

    JWEBAUTHN-27 - Add basic authenticator policy
    
     - Add basic authenticator policy.
    
    https://shibboleth.atlassian.net/browse/JWEBAUTHN-27
---
 .../webauthn/admin/policy/AuthenticatorPolicy.java |  68 ++++++++++
 .../authn/webauthn/authn/AuthenticatorSupport.java |  64 +++++++++
 .../admin/impl/CheckAuthenticatorPolicy.java       | 145 ++++++++++++++++++++
 .../impl/AbstractAuthenticatorPolicyRule.java      | 123 +++++++++++++++++
 .../policy/impl/AllowlistAuthenticatorPolicy.java  |  77 +++++++++++
 .../impl/AuthenticatorCapabilitiesPolicyRule.java  |  88 +++++++++++++
 .../policy/impl/AuthenticatorGetInfoUVCapable.java |  62 +++++++++
 .../impl/ChainingAuthenticatorPolicyRule.java      | 102 ++++++++++++++
 .../metadata/impl/FidoMetadataServiceFactory.java  |  33 ++++-
 .../webauthn-registration-beans.xml                |  29 ++++
 .../webauthn-registration-flow.xml                 |   1 +
 .../admin/impl/CheckAuthenticatorPolicyTest.java   | 135 +++++++++++++++++++
 .../impl/AllowlistAuthenticatorPolicyTest.java     |  69 ++++++++++
 .../AuthenticatorCapabilitiesPolicyRuleTest.java   |  60 +++++++++
 .../impl/ChainingAuthenticatorPolicyRuleTest.java  | 146 +++++++++++++++++++++
 .../authn/webauthn/impl/AbstractWebAuthnTest.java  |  16 ++-
 ...{logback-webauthn-test.xml => logback-test.xml} |   0
 17 files changed, 1216 insertions(+), 2 deletions(-)

diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorPolicy.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorPolicy.java
new file mode 100644
index 0000000..0aa05de
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorPolicy.java
@@ -0,0 +1,68 @@
+/*
+ * 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.admin.policy;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.yubico.fido.metadata.AAGUID;
+
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An API for applying policy checks to an authenticator. The authenticator is identified by its AAGUID.
+ */
+public interface AuthenticatorPolicy extends IdentifiedComponent{
+    
+    /**
+     * Representation of the three outcomes of an AuthenticatorPolicy.
+     */
+    public enum AuthenticatorPolicyOutcome {
+        /** Allow the authenticator. */
+        ALLOW,
+        /** Reject the authenticator. */
+        REJECT,
+        /** The policy was not active and should be ignored. */
+        IGNORE;
+        
+        /**
+         * Helper method to create an {@link AuthenticatorPolicyOutcome} from a boolean flag.
+         * 
+         * @param outcome the outcome created from the boolean
+         * @return the outcome associated with the boolean
+         */
+        public static AuthenticatorPolicyOutcome of(final boolean outcome) {
+            if (outcome) {
+                return AuthenticatorPolicyOutcome.ALLOW;
+            } else {
+                return AuthenticatorPolicyOutcome.REJECT;
+            }
+        }
+    }
+    
+    /**
+     * Execute the policy. Return true if allowed, false otherwise.
+     * 
+     * @param aaguid the authenticator attestation GUID.
+     * @param prc the profile request context
+     * 
+     * @return true if the authenticator was accepted, false if the authenticator was rejected, ignore if the
+     * policy is to be ignored (e.g. the policy is not active)
+     */
+    AuthenticatorPolicyOutcome accept(@Nonnull final AAGUID aaguid, @Nullable final ProfileRequestContext prc);
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AuthenticatorSupport.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AuthenticatorSupport.java
new file mode 100644
index 0000000..971315b
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/authn/AuthenticatorSupport.java
@@ -0,0 +1,64 @@
+/*
+ * 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.authn;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.exception.HexException;
+
+/**
+ * A helper class for various FIDO2 Authenticator functions.
+ */
+public final class AuthenticatorSupport {
+    
+    /** The acceptable AAGUID pattern, taken from @link {@link AAGUID}.*/
+    private static final Pattern AAGUID_PATTERN =
+            Pattern.compile(
+                    "^([0-9a-fA-F]{8})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{4})-?([0-9a-fA-F]{12})$");
+    
+    /** Private constructor.*/
+    private AuthenticatorSupport() {
+        
+    }
+    
+    /**
+     * Parse an AAGUID from a string value. 
+     * 
+     * @param value the value to parse
+     * @return the byte representation of the AAGUID, or null if not parsable.
+     */
+    @Nullable public static ByteArray parse(final String value) {
+        final Matcher matcher = AAGUID_PATTERN.matcher(value);
+        if (matcher.find()) {
+          try {
+            return ByteArray.fromHex(matcher.group(1))
+                .concat(ByteArray.fromHex(matcher.group(2)))
+                .concat(ByteArray.fromHex(matcher.group(3)))
+                .concat(ByteArray.fromHex(matcher.group(4)))
+                .concat(ByteArray.fromHex(matcher.group(5)));
+          } catch (final HexException e) {
+            return null;
+          }
+        } else {
+          return null;
+        }
+      }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicy.java
new file mode 100644
index 0000000..47f17e7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicy.java
@@ -0,0 +1,145 @@
+/*
+ * 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.admin.impl;
+
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.AttestedCredentialData;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy.AuthenticatorPolicyOutcome;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationErrorContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A policy engine action that checks with the configured policy if the authenticator can be used to register 
+ * credentials with the IdP. 
+ * 
+ * @event {WebAuthnRegistrationEventIds#INVALID_REGISTRATION_CTX}
+ * @pre <pre>ProfileRequestContext.getSubcontext(WebAuthnRegistrationContext.class) != null</pre>
+ * @post the authenticator is allowed to register a credential, or an error event is triggered
+ */
+public class CheckAuthenticatorPolicy extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(CheckAuthenticatorPolicy.class);
+
+    /** The stashed attestation response.*/
+    @NonnullBeforeExec 
+    private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation;
+    
+    /** The authenticator policy to check.*/
+    @Nullable private AuthenticatorPolicy authenticatorPolicy;
+
+    /**
+     * Constructor.
+     */
+    protected CheckAuthenticatorPolicy() {
+        super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
+    }
+    
+    /**
+     * Set the policy to verify that the authenticator is authorized before the credential is stored.
+     * 
+     * @param policy The authenticator policy to set.
+     */
+    public void setAuthenticatorPolicy(@Nullable final AuthenticatorPolicy policy) {
+        checkSetterPreconditions();
+        authenticatorPolicy = policy;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+        
+        if (!super.doPreExecute(profileRequestContext, context)) {
+            return false;
+        }
+       
+        attestation = context.getPublicKeyCredentialAttestationResponse();
+        if (attestation == null) {
+            log.error("{} Attestaion not available in registration context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final WebAuthnRegistrationContext context) {
+        
+        final AuthenticatorPolicy localPolicy = authenticatorPolicy;
+        if (localPolicy == null) {
+            // If no policy, nothing can be applied
+            log.trace("{} No authenticator policy to apply", getLogPrefix());
+            return;
+        }
+        
+        final Optional<AttestedCredentialData> attestedCredData = 
+                attestation.getResponse().getParsedAuthenticatorData().getAttestedCredentialData();
+        
+        if (attestedCredData.isEmpty()) {
+            log.warn("{} Public key registration failed for '{}', AAGUID not found", getLogPrefix(), 
+                    context.getUsername());
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            context.ensureSubcontext(WebAuthnRegistrationErrorContext.class)
+                .addClassifiedError(WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            return;
+            
+        }
+        
+        final ByteArray aaguid = attestedCredData.get().getAaguid();        
+        final AAGUID authenticatorAttestationGUID = new AAGUID(aaguid);
+        
+        if (localPolicy.accept(authenticatorAttestationGUID, profileRequestContext) 
+                == AuthenticatorPolicyOutcome.REJECT) {
+            if (log.isWarnEnabled()) {
+                log.warn("{} Public key registration failed for '{}', authenticator '{}' not allowed", getLogPrefix(), 
+                        context.getUsername(), authenticatorAttestationGUID.asGuidString());
+            }
+            ActionSupport.buildEvent(profileRequestContext, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            context.ensureSubcontext(WebAuthnRegistrationErrorContext.class)
+                .addClassifiedError(WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+            return;
+        } else {
+            if (log.isDebugEnabled()) {
+                log.debug("{} Authenticator '{}' allowed", getLogPrefix(), authenticatorAttestationGUID.asGuidString());
+            }
+        }
+             
+    } 
+    
+
+    
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorPolicyRule.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorPolicyRule.java
new file mode 100644
index 0000000..b95817b
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorPolicyRule.java
@@ -0,0 +1,123 @@
+/*
+ * 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.admin.policy.impl;
+
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.fido.metadata.FidoMetadataService;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A base class for {@link AuthenticatorPolicy authenticator policies}. Ensures the AAGUID is not null before it is
+ * passed to the policy rule implementation. Can be enabled and disabled by the activiation condition. 
+ * 
+ * <p>Returns {@link AuthenticatorPolicyOutcome#ALLOW} if the authenticator is accepted, returns
+ * {@link AuthenticatorPolicyOutcome#REJECT} if the authenticator is rejected, returns 
+ * {@link AuthenticatorPolicyOutcome#IGNORE} if the rule is to be ignored.</p>
+ */
+ at ThreadSafeAfterInit
+public abstract class AbstractAuthenticatorPolicyRule extends AbstractIdentifiableInitializableComponent 
+        implements AuthenticatorPolicy {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthenticatorPolicyRule.class);
+    
+    /** FIDO metadata service resolver.*/ 
+    @Nullable private FidoMetadataService fidoMetadataService;  
+    
+    /** Does this policy rule apply? Default is true. */
+    @Nonnull private BiPredicate<AAGUID, ProfileRequestContext> activationCondition;
+    
+    protected AbstractAuthenticatorPolicyRule() {
+        //default is always true
+        activationCondition = (prc,claims) -> true;
+    }
+    
+    /**
+     * Set an activation condition for this policy rule.
+     * 
+     * @param condition condition to set
+     */
+    public void setActivationConditionStrategy(@Nonnull final BiPredicate<AAGUID, ProfileRequestContext> condition) {
+        checkSetterPreconditions();        
+        activationCondition = Constraint.isNotNull(condition, "Activation condition cannot be null");
+    }
+    
+    /**
+     * Set an activation condition for this policy rule.
+     * 
+     * @param flag the flag to set
+     */
+    public void setActivationCondition(final boolean flag) {
+        checkSetterPreconditions();        
+        activationCondition = flag ? (prc,claims) -> true : (prc,claims) -> false;
+    }
+    
+    /**
+     * Set the attestation trust source.
+     * 
+     * @param service the attestation trust source.
+     */
+    public void setFidoMetadataService(@Nullable final FidoMetadataService trustSource) {
+        checkSetterPreconditions();
+        fidoMetadataService = trustSource;
+    }
+    
+    /**
+     * Get the metadata service to use. 
+     * 
+     * @return the metadata service.
+     */
+    @Nullable protected FidoMetadataService getFidoMetadataService() {
+        return fidoMetadataService;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public AuthenticatorPolicyOutcome accept(@Nullable final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {
+        if (!activationCondition.test(aaguid, prc)) {
+            //not active for this request
+            log.trace("AuthenticatorPolicy rule '{}' not active for this request", getId());
+            return AuthenticatorPolicyOutcome.IGNORE;
+        }
+        if (aaguid == null) {
+            return AuthenticatorPolicyOutcome.REJECT;
+        }
+        return doAccept(aaguid, prc);
+    }
+
+    /**
+     * Execute the policy. Return true if allowed, false otherwise. Implementations should override this method.
+     * 
+     * @param aaguid the authenticator attestation GUID.
+     * @param prc the profile request context
+     * 
+     * @return true if the policy allows the authenticator, false otherwise.
+     */
+    protected abstract AuthenticatorPolicyOutcome doAccept(@Nonnull AAGUID aaguid, @Nullable ProfileRequestContext prc);
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicy.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicy.java
new file mode 100644
index 0000000..0f556d1
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicy.java
@@ -0,0 +1,77 @@
+/*
+ * 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.admin.policy.impl;
+
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.fido.metadata.AAGUID;
+
+import net.shibboleth.idp.plugin.authn.webauthn.authn.AuthenticatorSupport;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A basic authenticator policy that rejects all authenticators not in the allowed list.
+ */
+public class AllowlistAuthenticatorPolicy extends AbstractAuthenticatorPolicyRule {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AllowlistAuthenticatorPolicy.class);
+    
+    /** A set of allowed authenticators, based on their AAGUID.*/
+    @Nonnull @NotLive @Unmodifiable private Set<AAGUID> allowedAuthenticators;
+    
+    /** Constructor.*/
+    public AllowlistAuthenticatorPolicy() {
+        allowedAuthenticators = CollectionSupport.emptySet();
+    }
+    
+    /**
+     * Set the allowable authenticators based on their AAGUID. 
+     * 
+     * @param allowed The allowed authenticators to set.
+     */
+    public void setAllowedAuthenticators(final Set<String> allowed) {
+        checkSetterPreconditions();
+        if (allowed != null) {
+            allowedAuthenticators = allowed.stream().map(strAAGUID -> {
+                final var aaguidBytes = AuthenticatorSupport.parse(strAAGUID);
+                if (aaguidBytes != null) {
+                    return new AAGUID(aaguidBytes);
+                } else {
+                    log.trace("AAGUID '{}' is not valid", strAAGUID);
+                    return null;
+                }                  
+            }).filter(Objects::nonNull).collect(CollectionSupport.nonnullCollector(Collectors.toSet())).get();
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public AuthenticatorPolicyOutcome doAccept(@Nonnull final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {        
+        return AuthenticatorPolicyOutcome.of(allowedAuthenticators.contains(aaguid));
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRule.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRule.java
new file mode 100644
index 0000000..b2907fc
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRule.java
@@ -0,0 +1,88 @@
+/*
+ * 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.admin.policy.impl;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.fido.metadata.FidoMetadataService;
+import com.yubico.fido.metadata.MetadataBLOBPayloadEntry;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An authenticator policy that inspects metadata to determine if the authenticator should be allowed
+ * or rejected.
+ */
+public class AuthenticatorCapabilitiesPolicyRule extends AbstractAuthenticatorPolicyRule {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AuthenticatorCapabilitiesPolicyRule.class);
+    
+    /** 
+     * A customizable predicate that determines if this authenticators capabilities should be accepted or rejected.
+     * Defaults to ALLOW.
+     */
+    @Nonnull private Predicate<Set<MetadataBLOBPayloadEntry>> authenticatorCapabilityAcceptor;
+    
+    /** Constructor.*/
+    public AuthenticatorCapabilitiesPolicyRule() {
+        authenticatorCapabilityAcceptor = PredicateSupport.alwaysTrue();
+    }
+    
+    /**
+     * Set the predicate that determines if this authenticators capabilities should be accepted or rejected
+     * 
+     * @param predicate the predicate which determines if this authenticator, based on its metadata, should be 
+     *      accepted.
+     */
+    public void setAuthenticatorCapabilityAcceptor(
+            @Nonnull final Predicate<Set<MetadataBLOBPayloadEntry>> predicate) {
+        checkSetterPreconditions();
+        authenticatorCapabilityAcceptor = Constraint.isNotNull(predicate,
+                "AuthenticatorCapabilityAcceptor can not be null");
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public AuthenticatorPolicyOutcome doAccept(
+            @Nonnull final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {
+        checkComponentActive();
+        final FidoMetadataService metadata = getFidoMetadataService();
+        if (metadata == null) {
+            log.warn("{} AuthenticatorCapabilities Policy Rule can not access attestation trust source, is metadata suported enabled?"
+                    + " rejecting", 
+                    getId());
+            return AuthenticatorPolicyOutcome.REJECT;
+        }
+        
+        final Set<MetadataBLOBPayloadEntry> entries = metadata.findEntries(aaguid);
+        if (entries == null) {
+            log.warn("{} No metadata to assess authenticator capabilities, rejecting", getId());
+            return AuthenticatorPolicyOutcome.REJECT;
+        }
+        return AuthenticatorPolicyOutcome.of(authenticatorCapabilityAcceptor.test(entries));
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorGetInfoUVCapable.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorGetInfoUVCapable.java
new file mode 100644
index 0000000..16cd1b1
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorGetInfoUVCapable.java
@@ -0,0 +1,62 @@
+/*
+ * 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.admin.policy.impl;
+
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import com.yubico.fido.metadata.AuthenticatorGetInfo;
+import com.yubico.fido.metadata.MetadataBLOBPayloadEntry;
+import com.yubico.fido.metadata.MetadataStatement;
+import com.yubico.fido.metadata.SupportedCtapOptions;
+
+/**
+ * An authenticator capability acceptor that only allows authenticators that support user verification. Either flag
+ * 'uv', signaling whether the authenticator supports internal UV (e.g., using a built-in fingerprint reader or 
+ * built-in keypad), or 'clientPin, signaling whether the authenticator supports external UV using PIN, are tested.
+ * 
+ * <p> From the CTAP specification: If present and set to true, it indicates that the device is capable of user 
+ * verification within itself and has been configured.If present and set to false, it indicates that the device is 
+ * capable of user verification within itself and has not been yet configured. If absent, it indicates that the device 
+ * is not capable of user verification within itself.</p>
+ */
+//TODO fix me, see https://github.com/Yubico/java-webauthn-server/issues/382
+public class AuthenticatorGetInfoUVCapable implements Predicate<Set<MetadataBLOBPayloadEntry>>{
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(final Set<MetadataBLOBPayloadEntry> metadata) {
+        if (metadata.size() != 1) {
+            return false;
+        }
+        final MetadataBLOBPayloadEntry entry = metadata.iterator().next();
+        final Optional<MetadataStatement> metadataStmt = entry.getMetadataStatement();
+        if (metadataStmt.isEmpty()) {
+            return false;
+        }
+        final Optional<AuthenticatorGetInfo> authenticatorGetInfo = metadataStmt.get().getAuthenticatorGetInfo();
+        if (authenticatorGetInfo.isEmpty()) {
+            return false;
+        }
+        final Optional<SupportedCtapOptions> options = authenticatorGetInfo.get().getOptions();
+        if (options.isEmpty()) {
+            return false;
+        }
+        return options.get().isUv()|| options.get().isClientPin();
+
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRule.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRule.java
new file mode 100644
index 0000000..d013c3b
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRule.java
@@ -0,0 +1,102 @@
+/*
+ * 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.admin.policy.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.yubico.fido.metadata.AAGUID;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link AuthenticatorPolicy} implementation that verifies an authenticator is acceptable based on a chain of 
+ * configured rules.
+ * 
+ * <p>Verification ends if any of the chained rules signals the authenticator should be rejected.</p>
+ */
+public class ChainingAuthenticatorPolicyRule extends AbstractAuthenticatorPolicyRule {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingAuthenticatorPolicyRule.class);
+    
+    /** An ordered chain of authenticator policies.*/
+    private List<AuthenticatorPolicy> authenticatorPolicyChain;
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (authenticatorPolicyChain == null) {
+            throw new ComponentInitializationException("List of claims validators can not be null");
+        } 
+    }
+    
+    /**
+     * Set the chain of policies that should be applied to the authenticator. Every policy must allow the 
+     * authenticator to be accepted. 
+     * 
+     * @param chain The authenticator policy chain to set.
+     */
+    public void setAuthenticatorPolicyChain(@Nullable final List<AuthenticatorPolicy> chain) {
+        checkSetterPreconditions();
+        if (chain != null) {
+            authenticatorPolicyChain = chain;
+        } else {
+            authenticatorPolicyChain = CollectionSupport.emptyList();
+        }
+    }    
+
+    /** {@inheritDoc} */
+    @Override
+    protected AuthenticatorPolicyOutcome doAccept(@Nonnull final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {
+        
+        for (final AuthenticatorPolicy policy : authenticatorPolicyChain) {
+            if (log.isTraceEnabled()) {
+                log.trace("Trying AuthenticatoryPolicy rule '{}' for authenticator '{}'", policy.getId(),
+                        aaguid.asGuidString());
+            }
+            final AuthenticatorPolicyOutcome outcome = policy.accept(aaguid, prc);
+            if (outcome  == AuthenticatorPolicyOutcome.REJECT) {
+                if (log.isDebugEnabled()) {
+                    log.debug("AuthenticatorPolicy rule '{}' rejected authenticator '{}'", policy.getId(), 
+                            aaguid.asGuidString());
+                }
+                return AuthenticatorPolicyOutcome.REJECT;
+            } else if (outcome == AuthenticatorPolicyOutcome.IGNORE){
+                if (log.isDebugEnabled()) {
+                    log.debug("AuthenticatorPolicy rule '{}' was ignored for authenticator '{}'", policy.getId(), 
+                            aaguid.asGuidString());
+                }
+            } else {
+                if (log.isTraceEnabled()) {
+                    log.trace("AuthenticatoryPolicy rule '{}' accepted authenticator '{}'", policy.getId(),  
+                            aaguid.asGuidString());
+                }
+            }
+        }
+        return AuthenticatorPolicyOutcome.ALLOW;
+    }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
index 71050bd..879543a 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/metadata/impl/FidoMetadataServiceFactory.java
@@ -74,6 +74,11 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      */
     @GuardedBy("this") @Nullable private Resource metadataBlobFile;
     
+    /** 
+     * Only verify the signature in the blob file from a fresh download.
+     */
+    @GuardedBy("this") private boolean verifyDownloadOnly;
+    
     /** The expected set of legal headers on the FIDO metadata blob.*/
     @GuardedBy("this") @NonnullAfterInit private String[] expectedLegalHeaders;
     
@@ -83,6 +88,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
     /** Constructor. */
     public FidoMetadataServiceFactory() {
         crls = CollectionSupport.emptyList();
+        verifyDownloadOnly = false;
     }
 
     /** {@inheritDoc} */
@@ -102,6 +108,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
                 .useTrustRoot(X509Support.decodeCertificate(getTrustRootFile().getFile()))
                 .useBlob(loadMetadataJwt(localMetadataBlobFile))
                 .useCrls(loadCrls())
+                .verifyDownloadsOnly(verifyDownloadOnly)
                 .build();
         } else if (localMetadataBlobUrl != null && localMetadataCacheFile != null){
             log.debug("{}: Loading FIDO metadata blob from '{}'", getId(), metadataBlobUrl);
@@ -111,7 +118,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
                 .downloadBlob(localMetadataBlobUrl.getURL())
                 .useBlobCacheFile(localMetadataCacheFile.getFile())
                 .useCrls(loadCrls())
-                .verifyDownloadsOnly(true)
+                .verifyDownloadsOnly(verifyDownloadOnly)
                 .build();
         } else {
             throw new FatalBeanException("Local FIDO metadata blob file not specified or the metadata blob URL and "
@@ -261,6 +268,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      * @return the metadata blob file.
      */
     @Nullable private synchronized Resource getMetadataBlobFile() {
+        checkComponentActive();
         return metadataBlobFile;
     }
     
@@ -280,6 +288,7 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      * @return the expected legal headers.
      */
     @NonnullAfterInit private synchronized String[] getExpectedLegalHeaders() {
+        checkComponentActive();
         return expectedLegalHeaders;
     }
     
@@ -304,8 +313,30 @@ public class FidoMetadataServiceFactory extends AbstractIdentifiableInitializabl
      * @return the crls.
      */
     @Nonnull @Live private synchronized List<Resource> getCrls() {
+        checkComponentActive();
         return crls;
     }
+    
+    /**
+     * Set if signatures are only verified in the blob file from a fresh download.
+     * 
+     * @param verifyDownloadOnly The verifyDownloadOnly to set.
+     */
+    public synchronized void setVerifyDownloadOnly(final boolean downloadOnly) {
+        checkSetterPreconditions();
+        verifyDownloadOnly = downloadOnly;
+    }
+    
+    /**
+     * Only verify the signature in the blob file from a fresh download?
+     * 
+     * @return true if signatures are only verified in a new blob file download (i.e. ignored from a cache file), 
+     * false if they are always verified, no matter where the blob is loaded from. 
+     */
+    public synchronized boolean isVerifyDownloadOnly() {
+        checkComponentActive();
+        return verifyDownloadOnly;
+    }
 
     /** {@inheritDoc} */
     @Override
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 1872f15..4eb73c8 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
@@ -34,6 +34,8 @@
         p:credentialRepository="#{getObject('shibboleth.authn.webauthn.CredentialRepositoryy') ?: getObject('shibboleth.authn.webauthn.DefaultCredentialRepository')}"
         p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
   
+    <bean id="AbstractAuthenticatorPolicyRule" scope="prototype" abstract="true"
+        p:fidoMetadataService="#{'false'.equals('%{idp.authn.webauthn.metadata.enabled:false}') ? null : getObject('shibboleth.authn.webauthn.DefaultWebAuthnFidoMetadataServiceFactory')}"/>
     
     <!-- Flow beans -->
     
@@ -171,6 +173,33 @@
         p:writeAuditLogAction="#{%{idp.authn.webauthn.registration.audit.enabled:false} ? getObject('WriteAdminAuditLog') : null}"
         p:auditContextCreationStrategy-ref="AdminAuditContextLookup" />        
 
+    <bean id="CheckAuthenticatorPolicy" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.CheckAuthenticatorPolicy"
+        p:authenticatorPolicy="#{getObject('%{idp.authn.webauthn.registration.authenticator.policy:shibboleth.authn.webauthn.registration.ChainedAuthenticatorPolicy}')}"
+        p:activationCondition="%{idp.authn.webauthn.registration.authenticator.policy.enabled:false}"/>
+        
+    <bean id="shibboleth.authn.webauthn.registration.ChainedAuthenticatorPolicy" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.ChainingAuthenticatorPolicyRule"
+        p:authenticatorPolicyChain="#{getObject('%{idp.authn.webauthn.registration.authenticator.policy.chainedlist:shibboleth.authn.webauthn.registration.ChainedAuthenticatorPolicies}')}"/>
+
+    <util:list id="shibboleth.authn.webauthn.registration.ChainedAuthenticatorPolicies">  
+      
+        <bean id="AllowlistAuthenticatorPolicy" parent="AbstractAuthenticatorPolicyRule" 
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.AllowlistAuthenticatorPolicy"
+            p:allowedAuthenticators="%{idp.authn.webauthn.registration.authenticator.policy.allowedAuthenticators:null}"
+            p:activationCondition="%{idp.authn.webauthn.registration.authenticator.policy.allowedAuthenticators.enabled:true}"/>
+             
+        <bean id="AuthenticatorCapabilitiesPolicyRuleUV" parent="AbstractAuthenticatorPolicyRule"
+            class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.AuthenticatorCapabilitiesPolicyRule"
+            p:activationCondition="%{idp.authn.webauthn.registration.authenticator.policy.authenticatorCapabilities.enabled:true}"
+            p:authenticatorCapabilityAcceptor="#{getObject('%{idp.authn.webauthn.registration.authenticator.policy.authenticatorCapabilities:shibboleth.authn.webauthn.registration.authenticator.policy.AuthenticatorGetInfoUVCapable}')}">
+        </bean>
+         
+    </util:list>
+    
+    <bean id="shibboleth.authn.webauthn.registration.authenticator.policy.AuthenticatorGetInfoUVCapable"
+                    class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.AuthenticatorGetInfoUVCapable" scope="prototype"/>  
+        
     <bean id="ValidateAuthenticatorAttestationResponse" parent="AbstractWebAuthnRegistrationAction"
         class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ValidateAuthenticatorAttestationResponse" />
 
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
index 5b1ec99..4f80a81 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/webauthn-registration/webauthn-registration-flow.xml
@@ -134,6 +134,7 @@
     
    <action-state id="AddKey">
         <evaluate expression="ExtractPublicKeyCredentialAttestationFromFormRequest"/>
+        <evaluate expression="CheckAuthenticatorPolicy"/>
         <evaluate expression="ValidateAuthenticatorAttestationResponse"/>
         <evaluate expression="StorePublicKeyCredential"/> 
         <evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicyTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicyTest.java
new file mode 100644
index 0000000..eb6c6d6
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/CheckAuthenticatorPolicyTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.admin.impl;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
+import com.yubico.webauthn.data.ByteArray;
+import com.yubico.webauthn.data.ClientRegistrationExtensionOutputs;
+import com.yubico.webauthn.data.PublicKeyCredential;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.WebAuthnRegistrationEventIds;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Test for {@link CheckAuthenticatorPolicy}.
+ */
+public class CheckAuthenticatorPolicyTest extends AbstractWebAuthnTest {
+    
+    private CheckAuthenticatorPolicy checkAction;
+    
+    private ByteArray aaguid;
+
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        checkAction = new CheckAuthenticatorPolicy();
+        final var context = addWebAuthnRegistrationContext();
+        context.setUsername(USERNAME);
+        checkAction.setWebAuthnClient(client);
+        checkAction.setCredentialRepository(credentialRepo);        
+        
+        final PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs>
+        attestationResponse = createAttestationReponse();
+        aaguid = attestationResponse.getResponse().getParsedAuthenticatorData().getAttestedCredentialData().get().getAaguid();
+        webAuthnRegContext.setPublicKeyCredentialAttestationResponse(attestationResponse);
+    }
+    
+    @Test
+    public void testAllowed() throws ComponentInitializationException {
+        
+        checkAction.initialize();
+        
+        final Event result = checkAction.execute(src);
+        assertNull(result);
+        
+    }
+    
+    @Test
+    public void testAllowedRule() throws ComponentInitializationException {
+        checkAction.setAuthenticatorPolicy(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Allowed policy";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.ALLOW;
+            }
+        });
+        checkAction.initialize();
+        
+        final Event result = checkAction.execute(src);
+        assertNull(result);
+        
+    }
+    
+    @Test
+    public void testIgnoredIsAllowedRule() throws ComponentInitializationException {
+        checkAction.setAuthenticatorPolicy(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Ignored policy";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.IGNORE;
+            }
+        });
+        checkAction.initialize();
+        
+        final Event result = checkAction.execute(src);
+        assertNull(result);
+        
+    }
+    
+    @Test
+    public void testNotAllowed() throws ComponentInitializationException {
+        checkAction.setAuthenticatorPolicy(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Not allowed policy";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.REJECT;
+            }
+        });
+        checkAction.initialize();
+        
+        final Event result = checkAction.execute(src);
+        assertNotNull(result);
+        assertFailure(result, WebAuthnRegistrationEventIds.INVALID_REGISTRATION);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicyTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicyTest.java
new file mode 100644
index 0000000..d30aedc
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AllowlistAuthenticatorPolicyTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.admin.policy.impl;
+
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.ByteArray;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy.AuthenticatorPolicyOutcome;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.AuthenticatorSupport;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Test for {@link AllowlistAuthenticatorPolicy}.
+ */
+public class AllowlistAuthenticatorPolicyTest extends AbstractWebAuthnTest {
+    
+    private AllowlistAuthenticatorPolicy policy;
+    
+    private ByteArray aaguid;
+
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        policy = new AllowlistAuthenticatorPolicy();
+        aaguid = AuthenticatorSupport.parse("adce0002-35bc-c60a-648b-0b25f1f05503");
+        policy.setId("AllowListAuthenticatorPolicy");
+        
+    }
+    
+    @Test
+    public void testAllowed() throws ComponentInitializationException {
+        policy.setAllowedAuthenticators(CollectionSupport.setOf(new AAGUID(aaguid).asGuidString()));
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.ALLOW);
+        
+    }
+    
+    @Test
+    public void testNotAllowed() throws ComponentInitializationException {
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.REJECT);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRuleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRuleTest.java
new file mode 100644
index 0000000..e3d9348
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AuthenticatorCapabilitiesPolicyRuleTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.admin.policy.impl;
+
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.ByteArray;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy.AuthenticatorPolicyOutcome;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.AuthenticatorSupport;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link AuthenticatorCapabilitiesPolicyRule}
+ */
+public class AuthenticatorCapabilitiesPolicyRuleTest extends AbstractWebAuthnTest {
+    
+    private AuthenticatorCapabilitiesPolicyRule policy;
+    
+    private ByteArray aaguid;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        policy = new AuthenticatorCapabilitiesPolicyRule();
+        aaguid = AuthenticatorSupport.parse("adce0002-35bc-c60a-648b-0b25f1f05503");
+        policy.setId("ChainingAuthenticatorPolicy");
+        fidoMetadataFactory.initialize();
+        policy.setFidoMetadataService(fidoMetadataFactory.getObject());
+    }
+    
+    @Test
+    public void testAllowed_AlwaysTrueAcceptor() throws ComponentInitializationException {
+        policy.setAuthenticatorCapabilityAcceptor(entries -> true);
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.ALLOW);
+        
+    }
+
+}
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRuleTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRuleTest.java
new file mode 100644
index 0000000..a2c8b9d
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorPolicyRuleTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.admin.policy.impl;
+
+import static org.testng.Assert.assertTrue;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.yubico.fido.metadata.AAGUID;
+import com.yubico.webauthn.data.ByteArray;
+
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy;
+import net.shibboleth.idp.plugin.authn.webauthn.admin.policy.AuthenticatorPolicy.AuthenticatorPolicyOutcome;
+import net.shibboleth.idp.plugin.authn.webauthn.authn.AuthenticatorSupport;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link ChainingAuthenticatorPolicyRule}.
+ */
+public class ChainingAuthenticatorPolicyRuleTest extends AbstractWebAuthnTest {
+    
+    private ChainingAuthenticatorPolicyRule policy;
+    
+    private ByteArray aaguid;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        policy = new ChainingAuthenticatorPolicyRule();
+        aaguid = AuthenticatorSupport.parse("adce0002-35bc-c60a-648b-0b25f1f05503");
+        policy.setId("ChainingAuthenticatorPolicy");
+    }
+    
+    @Test
+    public void testAllowed() throws ComponentInitializationException {
+        policy.setAuthenticatorPolicyChain(CollectionSupport.listOf(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Allowed Rule";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.ALLOW;
+            }
+        }));
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.ALLOW);
+        
+    }
+    
+    @Test
+    public void testDisallowed() throws ComponentInitializationException {
+        policy.setAuthenticatorPolicyChain(CollectionSupport.listOf(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Disallowed Rule";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.REJECT;
+            }
+        }));
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.REJECT);
+        
+    }
+    
+    @Test
+    public void testDisallowedSecondInChain() throws ComponentInitializationException {
+        policy.setAuthenticatorPolicyChain(CollectionSupport.listOf(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Allow Rule";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.ALLOW;
+            }
+        }, new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Disallowed Rule";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.REJECT;
+            }
+        }));
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.REJECT);
+        
+    }
+    
+    @Test
+    public void testIgnored() throws ComponentInitializationException {
+        policy.setAuthenticatorPolicyChain(CollectionSupport.listOf(new AuthenticatorPolicy() {
+            
+            @Override
+            public String getId() {
+                return "Ignored Rule";
+            }
+            
+            @Override
+            public AuthenticatorPolicyOutcome accept(final AAGUID aaguid, final ProfileRequestContext prc) {
+                return AuthenticatorPolicyOutcome.IGNORE;
+            }
+        }));
+        policy.initialize();
+        
+        final AuthenticatorPolicyOutcome accepted = policy.accept(new AAGUID(aaguid), prc);
+        assertTrue(accepted == AuthenticatorPolicyOutcome.ALLOW);
+        
+    }
+
+}
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 56f3d88..adb2a09 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
@@ -30,6 +30,7 @@ import java.util.TreeSet;
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.core.io.ClassPathResource;
 import org.springframework.webflow.execution.Event;
 import org.springframework.webflow.execution.RequestContext;
 
@@ -62,6 +63,7 @@ import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnManagementContext;
 import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.metadata.FidoMetadataServiceFactory;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.CredentialRegistration;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.WebAuthnCredentialRepository;
 import net.shibboleth.idp.plugin.authn.webauthn.storage.impl.InMemoryRegistrationStorage;
@@ -71,6 +73,7 @@ import net.shibboleth.idp.profile.testing.RequestContextBuilder;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.codec.Base64Support;
 import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.spring.resource.ResourceHelper;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import okhttp3.tls.HandshakeCertificates;
@@ -119,6 +122,9 @@ public abstract class AbstractWebAuthnTest {
     /** The relying party.*/
     protected RelyingParty rp;
     
+    /** A FIDO Metadata factory to test with.*/
+    protected FidoMetadataServiceFactory fidoMetadataFactory;
+    
     /** List of acceptable public key algorithms.*/
     @Nonnull protected final List<PublicKeyCredentialParameters> preferredPublickeyParams =
             CollectionSupport.listOf(
@@ -196,7 +202,15 @@ public abstract class AbstractWebAuthnTest {
         // The im-memory repo is for testing only
         credentialRepo = new InMemoryRegistrationStorage();
         
-        mockAuthenticator = new MockAuthenticator(RPID);
+        mockAuthenticator = new MockAuthenticator(RPID);  
+        
+        fidoMetadataFactory = new FidoMetadataServiceFactory();        
+        fidoMetadataFactory.setTrustRootFile(ResourceHelper.of(new ClassPathResource("root-r3.crt")));
+        fidoMetadataFactory.setExpectedLegalHeaders(new String[]{"headers"});
+        fidoMetadataFactory.setVerifyDownloadOnly(true);
+        fidoMetadataFactory.setMetadataBlobFile(ResourceHelper.of(new ClassPathResource("fido-metadata.bin")));
+        fidoMetadataFactory.setId("Test metadata factory");
+
          
     }  
     
diff --git a/webauthn-impl/src/test/resources/logback-webauthn-test.xml b/webauthn-impl/src/test/resources/logback-test.xml
similarity index 100%
rename from webauthn-impl/src/test/resources/logback-webauthn-test.xml
rename to webauthn-impl/src/test/resources/logback-test.xml

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list