[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