[java-idp-plugin-webauthn] 03/11: JWEBAUTHN-27 - Add basic authenticator policy
Phil Smart
philip.smart at jisc.ac.uk
Fri Oct 18 17:13:28 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=f6b04893d1b2ef13f4b74b2d00dc6890e4c14175
commit f6b04893d1b2ef13f4b74b2d00dc6890e4c14175
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Oct 3 11:43:41 2024 +0100
JWEBAUTHN-27 - Add basic authenticator policy
- Add engine to allow 'assessors' to derive qualities, capabilities, or
properties of authenticators which are stored inside the credential
registration.
- These properties are not yet used. Still a WIP.
https://shibboleth.atlassian.net/browse/JWEBAUTHN-27
---
.../policy/AuthenticatorCapabilitiesAssessor.java | 47 ++++++++
.../context/WebAuthnRegistrationContext.java | 34 ++++++
.../webauthn/storage/CredentialRegistration.java | 46 +++++++-
.../impl/AttachAuthenticatorCapabilities.java | 124 +++++++++++++++++++++
.../admin/impl/StorePublicKeyCredential.java | 51 +++++----
.../AbstractAuthenticatorCapabilitiesAssessor.java | 121 ++++++++++++++++++++
.../ChainingAuthenticatorCapabilitiesAssessor.java | 88 +++++++++++++++
.../impl/ChainingAuthenticatorPolicyRule.java | 2 +-
.../SecondFactorOnlyAuthenticatorAssessor.java | 86 ++++++++++++++
.../webauthn-registration-beans.xml | 24 +++-
.../webauthn-registration-flow.xml | 1 +
.../authn/webauthn/conf/authn/webauthn.properties | 12 ++
12 files changed, 612 insertions(+), 24 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorCapabilitiesAssessor.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorCapabilitiesAssessor.java
new file mode 100644
index 0000000..170c80e
--- /dev/null
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/AuthenticatorCapabilitiesAssessor.java
@@ -0,0 +1,47 @@
+/*
+ * 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 java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.yubico.fido.metadata.AAGUID;
+
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.component.IdentifiedComponent;
+
+/**
+ * An API for assessing the capabilities of an authenticator and describing that in a Map. The authenticator is
+ * identified by its AAGUID.
+ */
+public interface AuthenticatorCapabilitiesAssessor extends IdentifiedComponent {
+
+ /**
+ * Assess the capabilities of this authenticator and return any capabilities/properties/labels/tags appropriate.
+ *
+ * @param aaguid the authenticator attestation GUID.
+ * @param prc the profile request context
+ *
+ * @return a map of capabilities/properties/labels/tags relating to the authenticator, could be empty.
+ */
+ @Nonnull @NotLive @Unmodifiable
+ Map<String, String> assess(@Nullable final AAGUID aaguid, @Nullable final ProfileRequestContext prc);
+
+}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
index ec4d421..a2e9765 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/context/WebAuthnRegistrationContext.java
@@ -14,6 +14,8 @@
package net.shibboleth.idp.plugin.authn.webauthn.context;
+import java.util.Map;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
@@ -27,6 +29,9 @@ import com.yubico.webauthn.data.PublicKeyCredentialCreationOptions;
import com.yubico.webauthn.data.ResidentKeyRequirement;
import net.shibboleth.idp.plugin.authn.webauthn.admin.RegistrationResult;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
/**
@@ -79,6 +84,12 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
*/
@Nullable private String displayName;
+ /**
+ * A Map of authenticator capabilities. For example, should this authenticator only be used for second-factor
+ * authentication.
+ */
+ @Nullable private Map<String, String> authenticatorCapabilities;
+
/**
@@ -305,5 +316,28 @@ public final class WebAuthnRegistrationContext extends BaseWebAuthnContext {
@Nullable public String getDisplayName() {
return displayName;
}
+
+ /**
+ * Set the capabilities map of the authenticator.
+ *
+ * @param capabilities the map of capabilities to set
+ */
+ @Nonnull public BaseWebAuthnContext setAuthenticatorCapabilities(@Nullable final Map<String, String> capabilities) {
+ authenticatorCapabilities = capabilities;
+ return this;
+ }
+
+ /**
+ * Get the capabilities map of the authenticator.
+ *
+ * @return Returns the authenticatorCapabilities.
+ */
+ @Nonnull @NotLive @Unmodifiable public Map<String, String> getAuthenticatorCapabilities() {
+ if (authenticatorCapabilities != null) {
+ return CollectionSupport.copyToMap(authenticatorCapabilities);
+ } else {
+ return CollectionSupport.emptyMap();
+ }
+ }
}
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
index 8134a71..829bf16 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/storage/CredentialRegistration.java
@@ -16,6 +16,7 @@ package net.shibboleth.idp.plugin.authn.webauthn.storage;
import java.time.Instant;
import java.util.Collections;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedSet;
@@ -36,7 +37,9 @@ import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import com.yubico.webauthn.data.UserIdentity;
import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotLive;
import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.logic.Constraint;
/**
@@ -73,11 +76,17 @@ public final class CredentialRegistration {
/** The credential to register. */
@Nonnull @Unmodifiable @NonnullElements private final RegisteredCredential credential;
- /** The AAGUID of the authenticator.*/
+ /** The AAGUID of the authenticator that created this credential.*/
@Nullable private final byte[] aaguid;
/** Was the user verified during registration. */
private final boolean userVerified;
+
+ /**
+ * Capabilities of the authenticator as determined by the registration process and any configured authenticator
+ * assessors (not necessarily just from FIDO metadata).
+ */
+ @Nonnull private final Map<String,String> authenticatorCapabilities;
/**
*
@@ -95,6 +104,7 @@ public final class CredentialRegistration {
this.discoverable = builder.discoverable;
this.userVerified = builder.userVerified;
this.aaguid = builder.aaguid;
+ this.authenticatorCapabilities = builder.authenticatorCapabilities;
}
@@ -214,6 +224,16 @@ public final class CredentialRegistration {
return credential.getCredentialId().getBase64Url();
}
+ /**
+ * Get the capabilities of the authenticator that created this credential.
+ *
+ * @return the authenticatorCapabilities.
+ */
+ @JsonGetter("authenticatorCapabilities")
+ @Nonnull @NotLive @Unmodifiable public Map<String, String> getAuthenticatorCapabilities() {
+ return CollectionSupport.copyToMap(authenticatorCapabilities);
+ }
+
/**
* Convert the credential registration into a {@link PublicKeyCredentialDescriptor}.
*
@@ -371,6 +391,15 @@ public final class CredentialRegistration {
* @return the next builder stage
*/
@Nonnull public IBuildStage withAaguid(byte[] aaguid);
+
+ /**
+ * Set the capabilities of the authenticator as determined by the registration process
+ * (not FIDO necessarily FIDO metadata).
+ *
+ * @param capabilities the capabilities of the authenticator
+ * @return the next builder stage
+ */
+ @Nonnull public IBuildStage withAuthenticatorCapabilities(@Nullable final Map<String,String> capabilities);
/**
* Build this credential registration.
@@ -402,6 +431,11 @@ public final class CredentialRegistration {
private boolean userVerified;
/** The AAGUID of the authenticator.*/
@Nullable private byte[] aaguid;
+ /**
+ * Capabilities of the authenticator as determined by the registration process
+ * (not FIDO neccisarily FIDO metadata).
+ */
+ @Nonnull private Map<String,String> authenticatorCapabilities;
/** Constructor.*/
@SuppressWarnings("null")
@@ -410,6 +444,7 @@ public final class CredentialRegistration {
discoverable = Optional.empty();
userVerified = false;
transports = Collections.emptySortedSet();
+ authenticatorCapabilities = Collections.emptyMap();
}
@Override
@@ -484,6 +519,15 @@ public final class CredentialRegistration {
aaguid = authenticatorGuid;
return this;
}
+
+ @Override
+ @JsonProperty("authenticatorCapabilities")
+ public IBuildStage withAuthenticatorCapabilities(@Nullable final Map<String, String> capabilities) {
+ if (capabilities != null) {
+ authenticatorCapabilities = CollectionSupport.copyToMap(capabilities);
+ }
+ return this;
+ }
}
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AttachAuthenticatorCapabilities.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AttachAuthenticatorCapabilities.java
new file mode 100644
index 0000000..c2816c7
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AttachAuthenticatorCapabilities.java
@@ -0,0 +1,124 @@
+/*
+ * 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.Map;
+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.AuthenticatorCapabilitiesAssessor;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnAction;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Attach capabilities to the authenticator that has created the credential registration. For example, this
+ * authenticator should only be used for 2FA.
+ */
+public class AttachAuthenticatorCapabilities extends AbstractWebAuthnAction<WebAuthnRegistrationContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AttachAuthenticatorCapabilities.class);
+
+ /** The stashed attestation response.*/
+ @NonnullBeforeExec
+ private PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> attestation;
+
+ /** The assessor that checks the capabilities of the authenticator.*/
+ @Nullable private AuthenticatorCapabilitiesAssessor authenticatorAssessor;
+
+ /**
+ * Constructor.
+ */
+ protected AttachAuthenticatorCapabilities() {
+ super(new ChildContextLookup<>(WebAuthnRegistrationContext.class));
+ }
+
+ /**
+ * Set the assessor that checks the capabilities of the authenticator.
+ *
+ * @param assessor the authenticator assessor to set.
+ */
+ public void setAuthenticatorAssessor(@Nullable final AuthenticatorCapabilitiesAssessor assessor) {
+ checkSetterPreconditions();
+ authenticatorAssessor = assessor;
+ }
+
+ /** {@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) {
+
+ log.trace("{} Attaching authenticator capabilities", getLogPrefix());
+
+ final AuthenticatorCapabilitiesAssessor localAssessor = authenticatorAssessor;
+ if (localAssessor == null) {
+ // If no assessor, there is nothing to do
+ log.trace("{} No authenticator assessor to apply", getLogPrefix());
+ return;
+ }
+
+ final Optional<AttestedCredentialData> attestedCredData =
+ attestation.getResponse().getParsedAuthenticatorData().getAttestedCredentialData();
+
+ if (attestedCredData.isEmpty()) {
+ log.warn("{} Attested Credential Data can not assess authenticator capabilities", getLogPrefix());
+ return;
+
+ }
+
+ final ByteArray aaguid = attestedCredData.get().getAaguid();
+ final AAGUID authenticatorAttestationGUID = new AAGUID(aaguid);
+ final Map<String, String> capabilities =
+ localAssessor.assess(authenticatorAttestationGUID, profileRequestContext);
+ context.setAuthenticatorCapabilities(capabilities);
+
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
index f7a7177..22a5447 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/StorePublicKeyCredential.java
@@ -19,6 +19,7 @@ import java.util.Optional;
import java.util.TreeSet;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
@@ -111,8 +112,6 @@ public class StorePublicKeyCredential extends AbstractWebAuthnAuditingAction<Web
return;
}
- final byte[] userId = context.getUserId();
-
try {
final RegisteredCredential credential = RegisteredCredential.builder()
.credentialId(registrationResult.getKeyId().getId())
@@ -143,6 +142,7 @@ public class StorePublicKeyCredential extends AbstractWebAuthnAuditingAction<Web
.withCredentialNickname(context.getCredentialNickname())
.withDiscoverable(isDiscoverable)
.withUserVerified(registrationResult.isUserVerified())
+ .withAuthenticatorCapabilities(context.getAuthenticatorCapabilities())
.build();
final boolean added = repository.addRegistrationByUsername(username, registration);
@@ -154,23 +154,8 @@ public class StorePublicKeyCredential extends AbstractWebAuthnAuditingAction<Web
auditFailure(profileRequestContext, "credential-added");
return;
}
-
- if (log.isInfoEnabled()) {
-
- String userIdBase64;
- try {
- userIdBase64 = context.getUserId()!=null ? Base64Support.encodeURLSafe(userId) : null;
- } catch (final EncodingException e) {
- // Do nothing, just set a 'null' userId;
- userIdBase64 = "null";
- }
-
- log.info("{} Added public key credential registration for user '{}' with user.id '{}' and key '{}'. "
- + "Using a discoverable credential '{}' and user verification '{}'",
- getLogPrefix(), username, userIdBase64, registrationResult.getKeyId().getId().getBase64Url(),
- registrationResult.isDiscoverable().isPresent() ? registrationResult.isDiscoverable() :
- "unknown", registrationResult.isUserVerified());
- }
+ // Else valid, so log and audit
+ logRegistration(context, username, registrationResult);
auditSuccess(profileRequestContext, "credential-added");
} catch (final Exception e) {
@@ -180,10 +165,36 @@ public class StorePublicKeyCredential extends AbstractWebAuthnAuditingAction<Web
auditFailure(profileRequestContext, "credential-added");
return;
}
-
}
+ /**
+ * Log a successful registration event.
+ *
+ * @param context the registration context
+ * @param username the username of the user that registered a credential
+ * @param registrationResult the result of registration
+ */
+ private void logRegistration(@Nonnull final WebAuthnRegistrationContext context, @Nullable final String username,
+ @Nonnull final RegistrationResult registrationResult) {
+ if (log.isInfoEnabled()) {
+ String userIdBase64;
+ try {
+ final byte[] userId = context.getUserId();
+ userIdBase64 = userId !=null ? Base64Support.encodeURLSafe(userId) : null;
+ } catch (final EncodingException e) {
+ // Do nothing, just set a 'null' userId;
+ userIdBase64 = "null";
+ }
+
+ log.info("{} Added public key credential registration for user '{}' with user.id '{}' and key '{}'. "
+ + "Using a discoverable credential '{}' and user verification '{}'",
+ getLogPrefix(), username, userIdBase64, registrationResult.getKeyId().getId().getBase64Url(),
+ registrationResult.isDiscoverable().isPresent() ? registrationResult.isDiscoverable() :
+ "unknown", registrationResult.isUserVerified());
+ }
+ }
+
// Checkstyle: MethodLength|CyclomaticComplexity ON
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorCapabilitiesAssessor.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorCapabilitiesAssessor.java
new file mode 100644
index 0000000..d6e9bce
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/AbstractAuthenticatorCapabilitiesAssessor.java
@@ -0,0 +1,121 @@
+/*
+ * 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.Map;
+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.AuthenticatorCapabilitiesAssessor;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A base class for {@link AuthenticatorCapabilitiesAssessor authenticator assessors}. Can be enabled and disabled
+ * by the activiation condition.
+ *
+ * <p>Returns a map of authenticator capabilities or properties.</p>
+ */
+public abstract class AbstractAuthenticatorCapabilitiesAssessor extends AbstractIdentifiableInitializableComponent
+ implements AuthenticatorCapabilitiesAssessor {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthenticatorCapabilitiesAssessor.class);
+
+ /** FIDO metadata service resolver.*/
+ @Nullable private FidoMetadataService fidoMetadataService;
+
+ /** Does this assessor apply? Default is true. */
+ @Nonnull private BiPredicate<AAGUID, ProfileRequestContext> activationCondition;
+
+ /** Constructor.*/
+ protected AbstractAuthenticatorCapabilitiesAssessor() {
+ //default is always true
+ activationCondition = (prc,claims) -> true;
+ }
+
+ /**
+ * Set an activation condition for this assessor.
+ *
+ * @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 assessor.
+ *
+ * @param flag the flag to set
+ */
+ public void setActivationCondition(final boolean flag) {
+ checkSetterPreconditions();
+ activationCondition = flag ? (prc,claims) -> true : (prc,claims) -> false;
+ }
+
+ /**
+ * Set the FIDO Metadata service.
+ *
+ * @param service the FIDO Metadata service.
+ */
+ 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 Map<String, String> assess(@Nullable final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {
+ if (!activationCondition.test(aaguid, prc)) {
+ //not active for this request
+ log.trace("AuthenticatorCapabilitiesAssessor '{}' not active for this request", getId());
+ return CollectionSupport.emptyMap();
+ }
+ return doAssess(aaguid, prc);
+ }
+
+ /**
+ * Execute the assessor. Implementations should override this method.
+ *
+ * @param aaguid the authenticator attestation GUID.
+ * @param prc the profile request context
+ *
+ * @return a map of capabilities.
+ */
+ @Nonnull
+ protected abstract Map<String, String> doAssess(@Nullable AAGUID aaguid, @Nullable ProfileRequestContext prc);
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorCapabilitiesAssessor.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorCapabilitiesAssessor.java
new file mode 100644
index 0000000..b7734ec
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/ChainingAuthenticatorCapabilitiesAssessor.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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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.AuthenticatorCapabilitiesAssessor;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A {@link AbstractAuthenticatorCapabilitiesAssessor} implementation that is based on a chain of
+ * configured assessors. The outcomes from each are aggregated together (last entry for a given key will win).
+ */
+public class ChainingAuthenticatorCapabilitiesAssessor extends AbstractAuthenticatorCapabilitiesAssessor {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ChainingAuthenticatorCapabilitiesAssessor.class);
+
+ /** An ordered chain of authenticator assessors.*/
+ private List<AuthenticatorCapabilitiesAssessor> authenticatorAssessorChain;
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (authenticatorAssessorChain == null) {
+ throw new ComponentInitializationException("List of authenticator assessors can not be null");
+ }
+ }
+
+ /**
+ * Set the chain of assessors that should be used to evaluate the authenticator.
+ *
+ * @param chain The authenticator policy chain to set.
+ */
+ public void setAuthenticatorAssessorChain(@Nullable final List<AuthenticatorCapabilitiesAssessor> chain) {
+ checkSetterPreconditions();
+ if (chain != null) {
+ authenticatorAssessorChain = chain;
+ } else {
+ authenticatorAssessorChain = CollectionSupport.emptyList();
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Map<String,String> doAssess(@Nullable final AAGUID aaguid, @Nullable final ProfileRequestContext prc) {
+
+ final Map<String, String> allOutcomes = new HashMap<>();
+ for (final AuthenticatorCapabilitiesAssessor assessor : authenticatorAssessorChain) {
+ if (log.isTraceEnabled()) {
+ log.trace("Trying AuthenticatorCapabilitiesAssessor '{}' for authenticator '{}'", assessor.getId(),
+ aaguid != null ? aaguid.asGuidString() : "unknown");
+ }
+ final Map<String,String> outcome = assessor.assess(aaguid, prc);
+ log.trace("AuthenticatorCapabilitiesAssessor '{}' produced result '{}'", assessor.getId(), outcome);
+ allOutcomes.putAll(outcome);
+ }
+ log.trace("All AuthenticatorCapabilitiesAssessors produced result '{}'", allOutcomes);
+ return CollectionSupport.copyToMap(allOutcomes);
+ }
+
+}
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
index d013c3b..da43205 100644
--- 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
@@ -49,7 +49,7 @@ public class ChainingAuthenticatorPolicyRule extends AbstractAuthenticatorPolicy
super.doInitialize();
if (authenticatorPolicyChain == null) {
- throw new ComponentInitializationException("List of claims validators can not be null");
+ throw new ComponentInitializationException("List of authenticator policies can not be null");
}
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/SecondFactorOnlyAuthenticatorAssessor.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/SecondFactorOnlyAuthenticatorAssessor.java
new file mode 100644
index 0000000..ae9733d
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/policy/impl/SecondFactorOnlyAuthenticatorAssessor.java
@@ -0,0 +1,86 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+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.NotEmpty;
+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;
+
+/**
+ * Tag credentials created by authenticators that should only be used for second-factor authentication.
+ */
+public class SecondFactorOnlyAuthenticatorAssessor extends AbstractAuthenticatorCapabilitiesAssessor {
+
+ /** The name of the capability.*/
+ @Nonnull @NotEmpty public static final String CAPABILITY_NAME = "2faOnly";
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SecondFactorOnlyAuthenticatorAssessor.class);
+
+ /** A set of authenticators that should only be used for second factor authentication, based on their AAGUID.*/
+ @Nonnull @NotLive @Unmodifiable private Set<AAGUID> secondFactorOnlyAuthenticators;
+
+ /** Constructor.*/
+ public SecondFactorOnlyAuthenticatorAssessor() {
+ secondFactorOnlyAuthenticators = CollectionSupport.emptySet();
+ }
+
+ /**
+ * Set the authenticators that should only be used for second factor authentication based on their AAGUID.
+ *
+ * @param allowed The second factor authenticators to set.
+ */
+ public void setSecondFactorOnlyAuthenticators(final Set<String> allowed) {
+ checkSetterPreconditions();
+ if (allowed != null) {
+ secondFactorOnlyAuthenticators = 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 Map<String, String> doAssess(final AAGUID aaguid, final ProfileRequestContext prc) {
+ final boolean secondFactorOnly = secondFactorOnlyAuthenticators.contains(aaguid);
+ final HashMap<String,String> capabilities = new HashMap<>();
+ capabilities.put(CAPABILITY_NAME, secondFactorOnly ? "true" : "false");
+ return CollectionSupport.copyToMap(capabilities);
+ }
+
+
+}
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 4eb73c8..a1558b1 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
@@ -178,10 +178,10 @@
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"
+ <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"
@@ -197,6 +197,26 @@
</util:list>
+ <bean id="AttachAuthenticatorCapabilities" parent="AbstractWebAuthnRegistrationAction" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.AttachAuthenticatorCapabilities"
+ p:authenticatorAssessor="#{getObject('%{idp.authn.webauthn.registration.authenticator.assessor:shibboleth.authn.webauthn.registration.ChainedAuthenticatorCapabilitiesAssessor}')}"
+ p:activationCondition="%{idp.authn.webauthn.registration.authenticator.assessor.enabled:false}"/>
+
+ <bean id="shibboleth.authn.webauthn.registration.ChainedAuthenticatorCapabilitiesAssessor" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.ChainingAuthenticatorCapabilitiesAssessor"
+ p:authenticatorAssessorChain="#{getObject('%{idp.authn.webauthn.registration.authenticator.assessor.chainedlist:shibboleth.authn.webauthn.registration.ChainedAuthenticatorAssessors}')}"/>
+
+
+ <util:list id="shibboleth.authn.webauthn.registration.ChainedAuthenticatorAssessors">
+
+ <bean id="shibboleth.authn.webauthn.registration.SecondFactorOnlyAssessor" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.SecondFactorOnlyAuthenticatorAssessor"
+ p:secondFactorOnlyAuthenticators="%{idp.authn.webauthn.registration.authenticator.assessor.secondFactorOnlyAuthenticators:null}"
+ p:activationCondition="%{idp.authn.webauthn.registration.authenticator.assessor.secondFactorOnlyAssessor.enabled:true}"/>
+
+ </util:list>
+
+
<bean id="shibboleth.authn.webauthn.registration.authenticator.policy.AuthenticatorGetInfoUVCapable"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.policy.impl.AuthenticatorGetInfoUVCapable" scope="prototype"/>
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 4f80a81..c729ece 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
@@ -135,6 +135,7 @@
<action-state id="AddKey">
<evaluate expression="ExtractPublicKeyCredentialAttestationFromFormRequest"/>
<evaluate expression="CheckAuthenticatorPolicy"/>
+ <evaluate expression="AttachAuthenticatorCapabilities"/>
<evaluate expression="ValidateAuthenticatorAttestationResponse"/>
<evaluate expression="StorePublicKeyCredential"/>
<evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
index 7e42482..a2b515e 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/conf/authn/webauthn.properties
@@ -97,6 +97,18 @@ idp.authn.webauthn.supportedPrincipals = \
# When the 'authenticator capabilities' policy is enabled, which policy bean should be applied
#idp.authn.webauthn.registration.authenticator.policy.authenticatorCapabilities = shibboleth.authn.webauthn.registration.authenticator.policy.AuthenticatorGetInfoUVCapable
+# Enable the authenticator assessor engine?
+#idp.authn.webauthn.registration.authenticator.assessor.enabled = false
+# Set the authenticator assessors to use, defaults to a chained set of assessors
+#idp.authn.webauthn.registration.authenticator.assessor = shibboleth.authn.webauthn.registration.ChainedAuthenticatorCapabilitiesAssessor
+# When using the chained assessors, give the name of the list of assessors
+#idp.authn.webauthn.registration.authenticator.assessor.chainedlist = shibboleth.authn.webauthn.registration.ChainedAuthenticatorAssessors
+# When using the default chained set of assessors, should we enable the 'second factor only' assessor
+#idp.authn.webauthn.registration.authenticator.assessor.secondFactorOnlyAssessor.enabled = true
+# When using the default chained set of assessors, give a comma seperated list of authenticators (by attestation GUIDs (AAGUID)) to tag as only allowed for second factor authentication
+#idp.authn.webauthn.registration.authenticator.assessor.secondFactorOnlyAuthenticators =
+
+
# Allow inline self-enrolment
#idp.authnwebauthn.registration.allowInline = true
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list