[java-idp-plugin-webauthn] branch main updated: JWEBAUTHN-12 - Add an access control predicate for guarding against username changes
Phil Smart
philip.smart at jisc.ac.uk
Fri May 10 11:27:17 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=fb8227f3bee9bfddb8e41ceaeed1b1521ca3d283
The following commit(s) were added to refs/heads/main by this push:
new fb8227f JWEBAUTHN-12 - Add an access control predicate for guarding against username changes
fb8227f is described below
commit fb8227f3bee9bfddb8e41ceaeed1b1521ca3d283
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri May 10 12:27:14 2024 +0100
JWEBAUTHN-12 - Add an access control predicate for guarding against
username changes
- Added a plugable access control guard that checks the principal name
in the subject context is the same as the username in the registration
context.
- exposed options to transform the username when entered into the
registration page or the passwordless page.
https://shibboleth.atlassian.net/browse/JWEBAUTHN-12
---
.../client/WebAuthnAuthenticationClient.java | 2 +
.../impl/AllowCurrentUserAccessPredicate.java | 171 +++++++++++++++++++++
.../impl/ExtractUsernameFromRegistrationForm.java} | 87 ++++-------
.../impl/PopulateWebAuthnRegistrationContext.java | 45 +++++-
.../impl/YubicoWebAuthnAuthenticationClient.java | 2 +-
.../impl/AbstractWebAuthnExtractionAction.java | 166 ++++++++++++++++++++
.../authn/webauthn/impl/BaseWebAuthnAction.java | 5 +-
.../webauthn/impl/ExtractUsernameFromForm.java | 21 ++-
.../LookupRegisteredCredentialsFromUserHandle.java | 2 +-
.../META-INF/net.shibboleth.idp/postconfig.xml | 2 +-
.../webauthn-registration-beans.xml | 22 ++-
.../webauthn-registration-flow.xml | 1 +
.../idp/flows/authn/WebAuthn/webauthn-beans.xml | 7 +-
.../idp/flows/authn/WebAuthn/webauthn-flow.xml | 5 +-
.../authn/webauthn/conf/authn/webauthn.properties | 12 +-
.../plugin/authn/webauthn/views/webauthn-authn.vm | 4 +
.../webauthn/views/webauthn-register-username.vm | 3 -
.../impl/AllowCurrentUserAccessPredicateTest.java | 98 ++++++++++++
18 files changed, 554 insertions(+), 101 deletions(-)
diff --git a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
index 35adacb..54cd9c4 100644
--- a/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
+++ b/webauthn-api/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/WebAuthnAuthenticationClient.java
@@ -26,6 +26,8 @@ import net.shibboleth.idp.plugin.authn.webauthn.exception.WebAuthnAuthentication
* <p>Tied to the Yubico library data model, other clients will need to be adapted to support this model, and provide
* translations between their internal representation and the types required.</p>
*
+ * TODO: should be go all in for Yubico data model here, otherwise we need our own variant of PublicKeyCredential etc.
+ *
* <p>Implementations must be thread-safe</p>
*/
@ThreadSafe
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java
new file mode 100644
index 0000000..accffa0
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicate.java
@@ -0,0 +1,171 @@
+/*
+ * 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.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An access control predicate that should implement comparison logic to allow access only to authenticated users who
+ * initiate the WebAuthn registration process. If the authenticated user is not the same as the user who started the
+ * registration process (as determined by the initial username collection step), access should be denied.
+ *
+ * <p>It is important to prevent users from changing their username between the registration and authentication
+ * flows. If this is allowed, it would create a loophole in the WebAuthn authentication process, which could
+ * allow a user to bypass it. For instance, a user who starts the registration of a new WebAuthn credential, without any
+ * previous WebAuthn credentials, could then login with an inferior authentication mechanism as another user who
+ * should only be using WebAuthn.</p>
+ *
+ * <p>More complicated comparison predicates might be needed if either the principal in the subject context and or the
+ * username in the registration context have been transformed into different forms but are still logically
+ * equivalent.</p>
+ */
+public class AllowCurrentUserAccessPredicate extends AbstractIdentifiableInitializableComponent
+ implements Predicate<ProfileRequestContext> {
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AllowCurrentUserAccessPredicate.class);
+
+ /** Lookup strategy to locate the webauthn registration context. */
+ @Nonnull
+ private Function<ProfileRequestContext,WebAuthnRegistrationContext> webauthnRegistrationContextLookupStrategy;
+
+ /** Strategy function to lookup SubjectContext. */
+ @Nonnull private Function<ProfileRequestContext,SubjectContext> subjectContextLookupStrategy;
+
+ /** The predicate that performs the comparison. */
+ @Nonnull private BiPredicate<ProfileRequestContext, Pair<String,String>> comparisonPredicate;
+
+ /** Constructor.*/
+ public AllowCurrentUserAccessPredicate() {
+ webauthnRegistrationContextLookupStrategy = new ChildContextLookup<>(WebAuthnRegistrationContext.class);
+ subjectContextLookupStrategy = new ChildContextLookup<>(SubjectContext.class);
+ comparisonPredicate = new DefaultCurrentUserComparisonPredicate();
+ }
+
+ /**
+ * Set the lookup strategy to use to locate the {@link SubjectContext}.
+ *
+ * @param strategy lookup function to use
+ */
+ public void setSubjectContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,SubjectContext> strategy) {
+ checkSetterPreconditions();
+ subjectContextLookupStrategy = Constraint.isNotNull(strategy, "SubjectContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the predicate that performs the comparison between the principal name from the subject context (as the first
+ * item in the pair) with the username in the registration context (as the second item in the pair). The profile
+ * request context adds additional information if required.
+ *
+ * @param predicate The comparison predicate to set.
+ */
+ public void setComparisonPredicate(
+ @Nonnull final BiPredicate<ProfileRequestContext, Pair<String,String>> predicate) {
+ checkSetterPreconditions();
+ comparisonPredicate = Constraint.isNotNull(predicate, "ComparisonPredicate can not be null");
+ }
+
+ /**
+ * Set the WebAuthn registration context lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setWebauthnRegistrationContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,WebAuthnRegistrationContext> strategy) {
+ checkSetterPreconditions();
+
+ webauthnRegistrationContextLookupStrategy =
+ Constraint.isNotNull(strategy, "WebauthnContextLookuplookup strategy cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean test(final ProfileRequestContext profileRequestContext) {
+
+ final WebAuthnRegistrationContext regContext =
+ webauthnRegistrationContextLookupStrategy.apply(profileRequestContext);
+ final String usernameFromRegistrationContext = regContext.getUsername();
+
+ final SubjectContext subjectContext = subjectContextLookupStrategy.apply(profileRequestContext);
+ if (subjectContext == null) {
+ log.debug("{}: No subject context found, access requires authentication.", getId());
+ return false;
+ }
+ final String usernameFromSubjectContext = subjectContext.getPrincipalName();
+
+ return comparisonPredicate.test(profileRequestContext,
+ new Pair<>(usernameFromSubjectContext, usernameFromRegistrationContext));
+
+ }
+
+ /** Default comparison predicate which just compares the equality of the input strings.*/
+ public static class DefaultCurrentUserComparisonPredicate
+ implements BiPredicate<ProfileRequestContext, Pair<String,String>> {
+
+ /** Class logger. */
+ @Nonnull @NotEmpty
+ private final Logger log = LoggerFactory.getLogger(DefaultCurrentUserComparisonPredicate.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext profileRequestContext,
+ @Nullable final Pair<String,String> usernamePair) {
+
+ if (profileRequestContext == null || usernamePair == null) {
+ log.debug("Required context and username information not found, denying access");
+ return false;
+ }
+
+ final String usernameFromSubjectContext = usernamePair.getFirst();
+ final String usernameFromRegistrationContext = usernamePair.getSecond();
+
+ if (usernameFromRegistrationContext == null) {
+ log.debug("No username in registration context, granting access");
+ return true;
+ }
+
+ if (usernameFromSubjectContext == null) {
+ log.debug("No username in subject context, access requires authentication");
+ return false;
+ }
+ final boolean match = usernameFromSubjectContext.equals(usernameFromRegistrationContext);
+ log.debug("Username in registration context '{}' {} with the authenticated principal '{}'",
+ usernameFromRegistrationContext, match ? "matched" : "did not match",
+ usernameFromSubjectContext);
+ return match;
+ }
+
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
similarity index 53%
copy from webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
copy to webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
index 9f49786..b3ac5e1 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/ExtractUsernameFromRegistrationForm.java
@@ -12,7 +12,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.authn.webauthn.impl;
+package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.util.function.Function;
@@ -25,10 +25,9 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import jakarta.servlet.http.HttpServletRequest;
-import net.shibboleth.idp.authn.AbstractExtractionAction;
import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnExtractionAction;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.logic.Constraint;
@@ -36,56 +35,50 @@ import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action to populate a username into the {@link WebAuthnAuthenticationContext}.
- *
- * TODO FINISH. Really maybe should be similar to CheckPasswordlessEnrollment from Duo
+ * An action to populate a username into the {@link BaseWebAuthnContext}.
*
* @event {@link AuthnEventIds#UNKNOWN_USERNAME}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>WebAuthnAuthenticationContext.getUsername() == null</pre>
+ * @pre <pre>ProfileRequestContext.getSubcontext(BaseWebAuthnContext.class) != null</pre>
+ * @post <pre>BaseWebAuthnContext.getUsername() == null</pre>
*/
-public class ExtractUsernameFromForm extends AbstractExtractionAction {
+public class ExtractUsernameFromRegistrationForm extends AbstractWebAuthnExtractionAction {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromForm.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromRegistrationForm.class);
- /** Strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on. */
- @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webAuthnContextLookupStrategy;
+ /** Strategy used to locate the {@link BaseWebAuthnContext} to operate on. */
+ @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnContextLookupStrategy;
/** Form parameter name to carry username. */
@Nonnull @NotEmpty private String usernameFieldName;
-
- /** Parameter name for SSO bypass. */
- @Nonnull @NotEmpty private String ssoBypassFieldName;
/** Context to operate on. */
- @NonnullBeforeExec private WebAuthnAuthenticationContext webAuthnContext;
+ @NonnullBeforeExec private BaseWebAuthnContext webAuthnContext;
/** Constructor.*/
- public ExtractUsernameFromForm() {
+ public ExtractUsernameFromRegistrationForm() {
usernameFieldName = "j_username";
- ssoBypassFieldName = "donotcache";
webAuthnContextLookupStrategy =
- new ChildContextLookup<>(WebAuthnAuthenticationContext.class).compose(
- new ChildContextLookup<>(AuthenticationContext.class));
+ new ChildContextLookup<>(BaseWebAuthnContext.class).compose(
+ new ChildContextLookup<>(ProfileRequestContext.class));
}
/**
- * Set the strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on.
+ * Set the strategy used to locate the {@link BaseWebAuthnContext} to operate on.
*
* @param strategy lookup strategy
*/
public void setWebAuthnContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+ @Nonnull final Function<ProfileRequestContext,BaseWebAuthnContext> strategy) {
checkSetterPreconditions();
webAuthnContextLookupStrategy =
- Constraint.isNotNull(strategy, "WebAuthnAuthenticationContext lookup strategy cannot be null");
+ Constraint.isNotNull(strategy, "WebAuthnContext lookup strategy cannot be null");
}
/**
- * Sets the name of the form field to carry the username.
+ * Sets the name of the form field that carries the username.
*
* @param name field name
*/
@@ -96,25 +89,11 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
"Username form field name cannot be null or empty");
}
- /**
- * Set the SSO bypass parameter name.
- *
- * @param fieldName the SSO bypass parameter name
- */
- public void setSSOBypassFieldName(@Nonnull @NotEmpty final String fieldName) {
- checkSetterPreconditions();
-
- ssoBypassFieldName = Constraint.isNotNull(
- StringSupport.trimOrNull(fieldName), "SSO Bypass field name cannot be null or empty.");
- }
-
-
/** {@inheritDoc} */
@Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext) {
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ if (!super.doPreExecute(profileRequestContext)) {
return false;
}
@@ -128,10 +107,9 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
/** {@inheritDoc} */
@Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext) {
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final String username = getUsernameFromForm(profileRequestContext, authenticationContext);
+ final String username = getUsernameFromForm(profileRequestContext);
if (username == null) {
log.warn("{} Unable to find username in HTTP request", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
@@ -145,29 +123,18 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
/**
* Gets the username from a form submission.
*
- * <p>Also processes do-not-cache instruction.</p>
- *
* @param profileRequestContext profile request context
- * @param authenticationContext authentication context
*
* @return submitted username, after applying any configured transforms
*/
- @Nullable private String getUsernameFromForm(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nonnull final AuthenticationContext authenticationContext) {
+ @Nullable private String getUsernameFromForm(@Nonnull final ProfileRequestContext profileRequestContext) {
final HttpServletRequest request = getHttpServletRequest();
- if (request != null) {
- // FIXME this will not work atm?
- final String donotcache = request.getParameter(ssoBypassFieldName);
- if (donotcache != null && "1".equals(donotcache)) {
- log.debug("{} Recording do-not-cache instruction in authentication context", getLogPrefix());
- authenticationContext.setResultCacheable(false);
- } else {
- authenticationContext.setResultCacheable(true);
- }
+ if (request != null) {
return applyTransforms(request.getParameter(usernameFieldName));
}
return null;
}
-
-}
\ No newline at end of file
+
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
index 911d065..a465a14 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/PopulateWebAuthnRegistrationContext.java
@@ -16,6 +16,7 @@
package net.shibboleth.idp.plugin.authn.webauthn.admin.impl;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
@@ -27,8 +28,9 @@ import org.slf4j.Logger;
import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
import net.shibboleth.idp.profile.AbstractProfileAction;
-import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.logic.PredicateSupport;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
@@ -39,6 +41,7 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
* @post a {@link WebAuthnRegistrationContext} is created, and the username has been populated.
*/
+//TODO The population actions are similar, can they be generalised?
public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
/** Class logger. */
@@ -51,16 +54,40 @@ public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
/** Lookup strategy to determine the username to extract and register WebAuthn credentials for. */
@Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
+ /** Is the username required? */
+ @Nonnull private Predicate<ProfileRequestContext> usernameRequiredPredicate;
+
/** Constructor.*/
public PopulateWebAuthnRegistrationContext() {
// Default creates a WebAuthn registration context under the profile request context.
+ //TODO does this makes sense directly under the PRC?
webAuthnRegistrationContextCreationStrategy =
new ChildContextLookup<>(WebAuthnRegistrationContext.class, true);
-
- usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+ usernameRequiredPredicate = PredicateSupport.alwaysFalse();
+ usernameLookupStrategy = FunctionSupport.constant(null);
}
+ /**
+ * Set a flag to determine if the username is required or not.
+ *
+ * @param flag is the username required?
+ */
+ public void setUsernameRequired(final boolean flag) {
+ checkSetterPreconditions();
+ usernameRequiredPredicate = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+ }
+
+ /**
+ * Set a strategy to determine if the username is required or not.
+ *
+ * @param predicate the predicate to set.
+ */
+ public void setUsernameRequiredPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate){
+ checkSetterPreconditions();
+ usernameRequiredPredicate = Constraint.isNotNull(predicate, "Username required predicate can not be null");
+ }
+
/**
* Set the strategy used to lookup or create the WebAuthn registration context.
*
@@ -97,16 +124,20 @@ public class PopulateWebAuthnRegistrationContext extends AbstractProfileAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
-
final String username = usernameLookupStrategy.apply(profileRequestContext);
- if (username == null) {
+ if (usernameRequiredPredicate.test(profileRequestContext) && username == null) {
log.error("{} Error creating WebAuthnRegistrationContext, no username found", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
- context.setUsername(username);
+ if (username != null) {
+ context.setUsername(username);
+ log.debug("{} Created WebAuthn registration context for user '{}'",getLogPrefix(), context.getUsername());
+ } else {
+ log.debug("{} Created WebAuthn registration context",getLogPrefix());
+ }
- log.debug("Created WebAuthn registration context for user '{}'", context.getUsername());
+
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
index 2337097..ab92652 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/client/impl/YubicoWebAuthnAuthenticationClient.java
@@ -171,7 +171,7 @@ public class YubicoWebAuthnAuthenticationClient implements WebAuthnAuthenticatio
log.debug("Attempting validation of assumed discoverable credential with userHandle from response '{}'",
authenticatorAssertionResponse.getResponse().getUserHandle());
} else {
- log.debug("Attempting validation of credential with known username '{}' and userHandle '{}'",
+ log.debug("Attempting validation of credential with username '{}' and userHandle '{}'",
username, userId);
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java
new file mode 100644
index 0000000..4f34910
--- /dev/null
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/AbstractWebAuthnExtractionAction.java
@@ -0,0 +1,166 @@
+/*
+ * 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.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AbstractExtractionAction;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * A base class for WebAuthn actions that extract usernames for subsequent use.
+ *
+ * <p>The base class adds a common mechanism for applying regular expression transforms to
+ * the username prior to being added to the context tree.</p>
+ *
+ * Similar to {@link AbstractExtractionAction}, but without the requirement of it running inside an authentication flow.
+ */
+public class AbstractWebAuthnExtractionAction extends AbstractProfileAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractWebAuthnExtractionAction.class);
+
+ /** Match patterns and replacement strings to apply. */
+ @Nonnull private List<Pair<Pattern,String>> transforms;
+
+ /** Convert to uppercase prior to transforms? */
+ private boolean uppercase;
+
+ /** Convert to lowercase prior to transforms? */
+ private boolean lowercase;
+
+ /** Trim prior to transforms? */
+ private boolean trim;
+
+ /** Constructor. */
+ public AbstractWebAuthnExtractionAction() {
+ transforms = CollectionSupport.emptyList();
+
+ uppercase = false;
+ lowercase = false;
+ trim = false;
+ }
+
+ /**
+ * A collection of regular expression and replacement pairs.
+ *
+ * @param newTransforms collection of replacement transforms
+ */
+ public void setTransforms(@Nullable final Collection<Pair<String, String>> newTransforms) {
+ checkSetterPreconditions();
+ if (newTransforms != null) {
+ transforms = new ArrayList<>();
+ for (final Pair<String,String> p : newTransforms) {
+ final Pattern pattern = Pattern.compile(StringSupport.trimOrNull(p.getFirst()));
+ transforms.add(new Pair<>(pattern, Constraint.isNotNull(
+ StringSupport.trimOrNull(p.getSecond()), "Replacement expression cannot be null")));
+ }
+ } else {
+ transforms = CollectionSupport.emptyList();
+ }
+ }
+
+ /**
+ * Controls conversion to uppercase prior to applying any transforms.
+ *
+ * @param flag uppercase flag
+ */
+ public void setUppercase(final boolean flag) {
+ checkSetterPreconditions();
+ uppercase = flag;
+ }
+
+ /**
+ * Controls conversion to lowercase prior to applying any transforms.
+ *
+ * @param flag lowercase flag
+ */
+ public void setLowercase(final boolean flag) {
+ checkSetterPreconditions();
+ lowercase = flag;
+ }
+
+ /**
+ * Controls whitespace trimming prior to applying any transforms.
+ *
+ * @param flag trim flag
+ */
+ public void setTrim(final boolean flag) {
+ checkSetterPreconditions();
+ trim = flag;
+ }
+
+ /**
+ * Apply any configured regular expression replacements to an input value and return the result.
+ *
+ * @param input the input string
+ *
+ * @return the result of applying the expressions
+ */
+ @Nullable @NotEmpty protected String applyTransforms(@Nullable final String input) {
+
+ if (input == null) {
+ return null;
+ }
+
+ String s = input;
+
+ if (trim) {
+ log.debug("{} Trimming whitespace of input string '{}'", getLogPrefix(), s);
+ s = s.trim();
+ }
+
+ if (lowercase) {
+ log.debug("{} Converting input string '{}' to lowercase", getLogPrefix(), s);
+ s = s.toLowerCase();
+ } else if (uppercase) {
+ log.debug("{} Converting input string '{}' to uppercase", getLogPrefix(), s);
+ s = s.toUpperCase();
+ }
+
+ if (transforms.isEmpty()) {
+ return s;
+ }
+
+ for (final Pair<Pattern,String> p : transforms) {
+ final Pattern pattern = p.getFirst();
+ if (pattern != null) {
+ final Matcher m = pattern.matcher(s);
+ log.debug("{} Applying replacement expression '{}' against input '{}'", getLogPrefix(),
+ pattern.pattern(), s);
+ s = m.replaceAll(p.getSecond());
+ log.debug("{} Result of replacement is '{}'", getLogPrefix(), s);
+ }
+ }
+
+ return s;
+ }
+
+}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
index c4be7c8..754df41 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/BaseWebAuthnAction.java
@@ -23,7 +23,6 @@ import net.shibboleth.idp.plugin.authn.webauthn.client.WebAuthnAuthenticationCli
import net.shibboleth.idp.plugin.authn.webauthn.storage.StorageServiceCredentialRepository;
import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
@@ -33,7 +32,7 @@ import net.shibboleth.shared.logic.Constraint;
public abstract class BaseWebAuthnAction extends AbstractProfileAction {
/** The WebAuthn client to use.*/
- @NonnullBeforeExec private WebAuthnAuthenticationClient webAuthnClient;
+ @NonnullAfterInit private WebAuthnAuthenticationClient webAuthnClient;
/** The credential respository to store valid credentials in.*/
@NonnullAfterInit private StorageServiceCredentialRepository credentialRepository;
@@ -69,7 +68,7 @@ public abstract class BaseWebAuthnAction extends AbstractProfileAction {
*
* @return the webAuthnClient.
*/
- @NonnullBeforeExec protected WebAuthnAuthenticationClient getWebAuthnClient() {
+ @NonnullAfterInit protected WebAuthnAuthenticationClient getWebAuthnClient() {
checkComponentActive();
return webAuthnClient;
}
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
index 9f49786..36120f7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/ExtractUsernameFromForm.java
@@ -28,7 +28,7 @@ import jakarta.servlet.http.HttpServletRequest;
import net.shibboleth.idp.authn.AbstractExtractionAction;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.BaseWebAuthnContext;
import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.logic.Constraint;
@@ -36,21 +36,21 @@ import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
/**
- * An action to populate a username into the {@link WebAuthnAuthenticationContext}.
+ * An action to populate a username into the {@link BaseWebAuthnContext}.
*
* TODO FINISH. Really maybe should be similar to CheckPasswordlessEnrollment from Duo
*
* @event {@link AuthnEventIds#UNKNOWN_USERNAME}
* @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- * @post <pre>WebAuthnAuthenticationContext.getUsername() == null</pre>
+ * @post <pre>BaseWebAuthnContext.getUsername() == null</pre>
*/
public class ExtractUsernameFromForm extends AbstractExtractionAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ExtractUsernameFromForm.class);
- /** Strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on. */
- @Nonnull private Function<ProfileRequestContext,WebAuthnAuthenticationContext> webAuthnContextLookupStrategy;
+ /** Strategy used to locate the {@link BaseWebAuthnContext} to operate on. */
+ @Nonnull private Function<ProfileRequestContext,BaseWebAuthnContext> webAuthnContextLookupStrategy;
/** Form parameter name to carry username. */
@Nonnull @NotEmpty private String usernameFieldName;
@@ -59,7 +59,7 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
@Nonnull @NotEmpty private String ssoBypassFieldName;
/** Context to operate on. */
- @NonnullBeforeExec private WebAuthnAuthenticationContext webAuthnContext;
+ @NonnullBeforeExec private BaseWebAuthnContext webAuthnContext;
/** Constructor.*/
public ExtractUsernameFromForm() {
@@ -67,17 +67,17 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
ssoBypassFieldName = "donotcache";
webAuthnContextLookupStrategy =
- new ChildContextLookup<>(WebAuthnAuthenticationContext.class).compose(
+ new ChildContextLookup<>(BaseWebAuthnContext.class).compose(
new ChildContextLookup<>(AuthenticationContext.class));
}
/**
- * Set the strategy used to locate the {@link WebAuthnAuthenticationContext} to operate on.
+ * Set the strategy used to locate the {@link BaseWebAuthnContext} to operate on.
*
* @param strategy lookup strategy
*/
public void setWebAuthnContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext,WebAuthnAuthenticationContext> strategy) {
+ @Nonnull final Function<ProfileRequestContext,BaseWebAuthnContext> strategy) {
checkSetterPreconditions();
webAuthnContextLookupStrategy =
@@ -85,7 +85,7 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
}
/**
- * Sets the name of the form field to carry the username.
+ * Sets the name of the form field that carries the username.
*
* @param name field name
*/
@@ -157,7 +157,6 @@ public class ExtractUsernameFromForm extends AbstractExtractionAction {
final HttpServletRequest request = getHttpServletRequest();
if (request != null) {
- // FIXME this will not work atm?
final String donotcache = request.getParameter(ssoBypassFieldName);
if (donotcache != null && "1".equals(donotcache)) {
log.debug("{} Recording do-not-cache instruction in authentication context", getLogPrefix());
diff --git a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
index 039c619..3b608c7 100644
--- a/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
+++ b/webauthn-impl/src/main/java/net/shibboleth/idp/plugin/authn/webauthn/impl/LookupRegisteredCredentialsFromUserHandle.java
@@ -152,7 +152,7 @@ public class LookupRegisteredCredentialsFromUserHandle extends AbstractWebAuthnA
}
}
- if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && credentialsFound == false) {
+ if (triggerEventOnNoCredentialsPredicate.test(profileRequestContext) && !credentialsFound) {
log.debug("{} Triggering event '{}' ", getLogPrefix(), noCredentialsEventId);
ActionSupport.buildEvent(profileRequestContext, noCredentialsEventId);
return;
diff --git a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 21d5d4c..1c37da8 100644
--- a/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -47,7 +47,7 @@
<bean parent="shibboleth.AdminFlow"
c:id="http://shibboleth.net/ns/profiles/admin/webauthn/register-credential"
p:loggingId="%{idp.authn.webauthn.admin.registration.logging:WebAuthnCredentialRegistration}"
- p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByAnyone}"
+ p:policyName="%{idp.authn.webauthn.admin.registration.accessPolicy:AccessByCurrentUser}"
p:nonBrowserSupported="false"
p:authenticated="%{idp.authn.webauthn.admin.registration.authenticated:true}"
p:resolveAttributes="%{idp.authn.webauthn.admin.registration.resolveAttributes:true}">
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 93e9170..e6badd1 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
@@ -29,21 +29,27 @@
<!-- Flow beans -->
<bean id="PopulateInitialWebAuthnRegistrationContext" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext">
- <property name="usernameLookupStrategy">
- <bean id="usernameFromHttpRequest" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromHttpRequest"
- p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
- </property>
- </bean>
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext"
+ p:usernameRequired="false">
+ </bean>
+
+ <bean id="ExtractUsernameFromForm" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.ExtractUsernameFromRegistrationForm"
+ p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnRegistrationContext"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:uppercase="%{idp.authn.webauthn.registration.username.uppercase:false}"
+ p:lowercase="%{idp.authn.webauthn.registration.username.lowercase:false}"
+ p:trim="%{idp.authn.webauthn.registration.username.trime:false}"
+ p:transforms="#{getObject('shibboleth.authn.webauthn.registration.UsernameTransformations')}"/>
<!-- Important that this gets the username from the subject context, not the initial context that is created -->
<bean id="PopulateWebAuthnRegistrationContext" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.admin.impl.PopulateWebAuthnRegistrationContext"
+ p:usernameRequired="true"
p:usernameLookupStrategy="#{getObject('%{idp.authn.webauthn.registration.usernameLookupStrategy:shibboleth.authn.webauthn.RegistrationUsernameLookupStrategy}')}">
</bean>
- <!-- Default username comes from previous c14n or session. -->
+ <!-- Default username comes from the subject context. -->
<bean id="shibboleth.authn.webauthn.RegistrationUsernameLookupStrategy"
class="net.shibboleth.idp.plugin.authn.webauthn.context.navigate.UsernameLookupFromSubjectContext" />
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 d193b7f..1afa84e 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
@@ -32,6 +32,7 @@
<!-- TODO if we want to do this, we would want this to populate a different context -->
<action-state id="ExtractUsernameAndPopulateContext">
<evaluate expression="PopulateInitialWebAuthnRegistrationContext"/>
+ <evaluate expression="ExtractUsernameFromForm"/>
<evaluate expression="LookupRegisteredCredentials"/>
<evaluate expression="'proceed'" />
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
index 7079a9d..5e3c719 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-beans.xml
@@ -75,7 +75,12 @@
<bean id="ExtractUsernameFromForm" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.ExtractUsernameFromForm"
- p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:webAuthnContextLookupStrategy-ref="shibboleth.ChildLookup.WebAuthnAuthenticationContextFromAuthenticationContext"
+ p:uppercase="%{idp.authn.webauthn.passwordless.username.uppercase:false}"
+ p:lowercase="%{idp.authn.webauthn.passwordless.username.lowercase:false}"
+ p:trim="%{idp.authn.webauthn.passwordless.username.trime:false}"
+ p:transforms="#{getObject('shibboleth.authn.webauthn.passwordless.UsernameTransformations')}"/>
<bean id="EnsureAllowedCredentialsIsEmpty" parent="AbstractWebAuthnAuthenticationAction" scope="prototype"
class="net.shibboleth.idp.plugin.authn.webauthn.impl.EnsureAllowedCredentialsIsEmpty"/>
diff --git a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
index 0c69d56..510d089 100644
--- a/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
+++ b/webauthn-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/WebAuthn/webauthn-flow.xml
@@ -31,10 +31,7 @@
then="CollectUsernameView"
else="PasswordlessLoginProceed" />
</decision-state>
-
-
- <!-- need decision state, we might have already collected username, so might not need it again e.g. when part of a registration flow -->
-
+
<view-state id="CollectUsernameView" view="webauthn/webauthn-authn-username">
<on-render>
<evaluate expression="environment" result="viewScope.environment" />
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 406450c..ad7bbed 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
@@ -39,7 +39,7 @@ idp.authn.webauthn.supportedPrincipals = \
# Does the registration page require authentication. Should always be true.
#idp.authn.webauthn.admin.registration.authenticate = true
# Access policy for the registration flow
-#idp.authn.webauthn.admin.registration.accessPolicy = AccessByAnyone
+#idp.authn.webauthn.admin.registration.accessPolicy = AccessByCurrentUser
# Restrict the usable authentication flows
#idp.authn.webauthn.admin.registration.authenticationFlows =
# Should user.id and display name be resolve from the attribute resolver?
@@ -88,6 +88,11 @@ idp.authn.webauthn.supportedPrincipals = \
# State the preference of the IdP during registration to receive an authenticator attestation. One-of 'none', 'indirect', 'direct', or 'enterprise'.
#idp.authn.webauthn.registration.attestationConveyancePreference = none
+# Basic transformations that should be applied to the username that is initially collected in the registration flow
+#idp.authn.webauthn.registration.username.uppercase = false
+#idp.authn.webauthn.registration.username.lowercase = false
+#idp.authn.webauthn.registration.username.trime = false
+
#### Administrator properties
@@ -124,3 +129,8 @@ idp.authn.webauthn.supportedPrincipals = \
# to any registered credentials?
#idp.authn.webauthn.signalEventOnNoCredentialsRegisteredForUserHandle = false
#idp.authn.webauthn.userHandleNoRegisteredCredentialsEventId = NoCredentialsRegisteredForUserHandle
+
+# Basic transformations that should be applied to the username that is collected as part of the usernameless flow
+#idp.authn.webauthn.passwordless.username.uppercase = false
+#idp.authn.webauthn.passwordless.username.lowercase = false
+#idp.authn.webauthn.passwordless.username.trime = false
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
index efa7284..7d6ad5b 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-authn.vm
@@ -145,6 +145,10 @@
<div id="unsupportedDiv" class="hidden">
#springMessageText("idp.webauthn.authn.unsupported", "Your browser is not WebAuthn compatible")
</div>
+ <ul>
+ <li><a href="#springMessageText('idp.webauthn.enrollment.url', '/idp/profile/admin/webauthn-registration')">#springMessageText("idp.webauthn.enrollment", "Enroll New Devices")</a></li>
+ <li><a href="#springMessageText('idp.url.helpdesk', '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+ </ul>
</section>
<footer>
<div class="container container-footer">
diff --git a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
index cd1e9cf..d3310e4 100644
--- a/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
+++ b/webauthn-impl/src/main/resources/net/shibboleth/idp/plugin/authn/webauthn/views/webauthn-register-username.vm
@@ -50,9 +50,6 @@ $response.addHeader("Content-Security-Policy", "script-src-elem 'nonce-$nonce'")
<label for="username">#springMessageText("idp.login.username", "Username")</label>
<input id="j_username" name="j_username" type="text" required
value="#if($username)$encoder.encodeForHTML($username)#end" />
-
- <input type="checkbox" name="donotcache" value="1" id="donotcache" />
- <label for="donotcache">#springMessageText("idp.login.donotcache", "Don't Remember Login")</label>
<div class="grid">
<div class="grid-item">
diff --git a/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java
new file mode 100644
index 0000000..8d5023b
--- /dev/null
+++ b/webauthn-impl/src/test/java/net/shibboleth/idp/plugin/authn/webauthn/admin/impl/AllowCurrentUserAccessPredicateTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.authn.webauthn.context.WebAuthnRegistrationContext;
+import net.shibboleth.idp.plugin.authn.webauthn.impl.AbstractWebAuthnTest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link AllowCurrentUserAccessPredicate}
+ */
+public class AllowCurrentUserAccessPredicateTest extends AbstractWebAuthnTest {
+
+ private AllowCurrentUserAccessPredicate predicate;
+
+ private WebAuthnRegistrationContext context;
+
+ @Override
+ @BeforeMethod
+ public void setup() throws Exception {
+ super.setup();
+ context = addWebAuthnRegistrationContext();
+
+ predicate = new AllowCurrentUserAccessPredicate();
+ predicate.setId("CurrentUserAccessPredicate");
+ }
+
+ @Test
+ public void testAccessGranted() throws ComponentInitializationException {
+ prc.addSubcontext(new SubjectContext().setPrincipalName(USERNAME));
+ predicate.initialize();
+ context.setUsername(USERNAME);
+
+ final boolean access = predicate.test(prc);
+ assertTrue(access);
+ }
+
+ @Test
+ public void testAccessDenied_DifferentUsername() throws ComponentInitializationException {
+ prc.addSubcontext(new SubjectContext().setPrincipalName("differet"));
+ predicate.initialize();
+ context.setUsername(USERNAME);
+
+ final boolean access = predicate.test(prc);
+ assertFalse(access);
+ }
+
+ @Test
+ public void testAccessDenied_NoPrincipalName() throws ComponentInitializationException {
+ prc.addSubcontext(new SubjectContext());
+ predicate.initialize();
+ context.setUsername(USERNAME);
+
+ final boolean access = predicate.test(prc);
+ assertFalse(access);
+ }
+
+ @Test
+ public void testAccessDenied_NoSubjectContext() throws ComponentInitializationException {
+ predicate.initialize();
+ context.setUsername(USERNAME);
+
+ final boolean access = predicate.test(prc);
+ assertFalse(access);
+ }
+
+ @Test
+ public void testAccessGranted_NoUsernameInRegistrationContext() throws ComponentInitializationException {
+ prc.addSubcontext(new SubjectContext().setPrincipalName(USERNAME));
+ //context.setUsername(USERNAME);
+
+ predicate.initialize();
+
+ final boolean access = predicate.test(prc);
+ assertTrue(access);
+ }
+
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list