[java-idp-plugin-duo] branch main updated: JDUO-80 - Use of Duo as a Passwordless solution
Scott Cantor
cantor.2 at osu.edu
Thu Jan 4 15:26:20 UTC 2024
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=88102f0660f661fac502b241371640bd4554f6dd
The following commit(s) were added to refs/heads/main by this push:
new 88102f06 JDUO-80 - Use of Duo as a Passwordless solution
88102f06 is described below
commit 88102f0660f661fac502b241371640bd4554f6dd
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Thu Jan 4 10:25:01 2024 -0500
JDUO-80 - Use of Duo as a Passwordless solution
https://shibboleth.atlassian.net/browse/JDUO-80
---
.../plugin/authn/duo/DefaultDuoCleanupHook.java | 156 +++++++++
.../authn/duo/DefaultDuoOIDCIntegration.java | 58 +++-
.../idp/plugin/authn/duo/DuoOIDCIntegration.java | 33 +-
.../authn/duo/DynamicDuoOIDCIntegration.java | 1 -
.../plugin/authn/duo/SimpleDuoOIDCIntegration.java | 63 ++--
.../authn/duo/context/DuoPasswordlessContext.java | 93 ++++++
.../duo/impl/CheckPasswordlessEnrollment.java | 362 +++++++++++++++++++++
.../duo/impl/PopulateDuoAuthenticationContext.java | 227 +++++++++----
.../impl/ValidateDuoTokenAuthenticationResult.java | 66 ++--
.../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml | 103 ++++--
.../flows/authn/DuoOIDC/duo-oidc-authn-flow.xml | 50 ++-
.../idp/plugin/authn/duo/messages.properties | 18 +
.../idp/plugin/authn/duo/views/passwordless.vm | 122 +++++++
.../authn/duo/impl/AbstractDuoActionTest.java | 35 +-
.../duo/impl/CheckPasswordlessEnrollmentTest.java | 321 ++++++++++++++++++
.../duo/impl/DefaultDuoOIDCClientRegistryTest.java | 1 +
.../DefaultRedirectURICreationStrategyTest.java | 1 +
.../impl/DuoAudienceClaimLookupStrategyTest.java | 1 +
.../duo/impl/DuoIssuerClaimLookupStrategyTest.java | 1 +
.../duo/impl/DuoNonceClaimLookupStrategyTest.java | 3 +-
.../idp/plugin/authn/duo/impl/DuoSupportTest.java | 1 +
.../impl/DuoUsernameClaimLookupStrategyTest.java | 5 +-
.../duo/impl/ExchangeCodeForDuoTokenTest.java | 1 +
.../impl/PopulateDuoAuthenticationContextTest.java | 129 +++++++-
.../duo/impl/ValidateDuoResponseStateTest.java | 1 +
.../ValidateDuoTokenAuthenticationResultTest.java | 67 +++-
.../authn/duo/impl/ValidateTokenClaimsTest.java | 1 +
.../authn/duo/impl/ValidateTokenSignatureTest.java | 1 +
.../plugin/authn/mock/MockDuoOIDCClient_FAIL.java | 7 +
.../plugin/authn/mock/MockDuoOIDCClient_OK.java | 7 +
.../mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java | 7 +
.../authn/mock/MockDuoOIDCClient_UNKNOWN.java | 7 +
.../authn/util/mock/IdPPropertyConfigurer.java | 12 +-
.../authn/util/mock/TestResourceConverter.java | 127 ++++++++
.../idp/plugin/authn/duo/impl/SealerKeyStore.jks | Bin 0 -> 984 bytes
.../idp/plugin/authn/duo/impl/SealerKeyStore.kver | 1 +
.../idp/plugin/authn/duo/nimbus/module.properties | 2 +
.../idp/plugin/authn/duo/sdk/module.properties | 2 +
pom.xml | 2 +-
39 files changed, 1909 insertions(+), 186 deletions(-)
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoCleanupHook.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoCleanupHook.java
new file mode 100644
index 00000000..0e12bbc3
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoCleanupHook.java
@@ -0,0 +1,156 @@
+/*
+ * 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.duo;
+
+import java.util.function.Consumer;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.google.common.net.UrlEscapers;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * A default cleanup hook for the DuoOIDC flow that handles both standard
+ * and passwordless scenarios, with configuration flexibility for the deployer.
+ *
+ * @since 2.1.0
+ */
+public class DefaultDuoCleanupHook extends AbstractInitializableComponent implements Consumer<ProfileRequestContext> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultDuoCleanupHook.class);
+
+ /** Username cookie name. */
+ @Nullable @NotEmpty private String cookieName;
+
+ /** Optional cookie manager to use. */
+ @Nullable private CookieManager cookieManager;
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /** Whether to remove a {@link DuoPasswordlessContext} if present. */
+ private boolean removePasswordlessContext;
+
+ /** Constructor. */
+ public DefaultDuoCleanupHook() {
+ removePasswordlessContext = true;
+ }
+
+ /**
+ * Set cookie name to use for cached username.
+ *
+ * @param name cookie name
+ */
+ public void setCookieName(@Nullable final String name) {
+ checkSetterPreconditions();
+
+ cookieName = StringSupport.trimOrNull(name);
+ }
+
+ /**
+ * Sets optional {@link CookieManager} to use.
+ *
+ * @param manager cookie manager
+ */
+ public void setCookieManager(@Nullable final CookieManager manager) {
+ checkSetterPreconditions();
+
+ cookieManager = manager;
+ }
+
+ /**
+ * Sets optional {@link DataSealer} to use.
+ *
+ * @param sealer data sealer
+ */
+ public void setDataSealer(@Nullable final DataSealer sealer) {
+ checkSetterPreconditions();
+
+ dataSealer = sealer;
+ }
+
+ /**
+ * Sets whether to remove a {@link DuoPasswordlessContext} if present.
+ *
+ * @param flag flag to set
+ */
+ public void setRemovePasswordlessContext(final boolean flag) {
+ checkSetterPreconditions();
+
+ removePasswordlessContext = flag;
+ }
+
+// Checkstyle: CyclomaticComplexity OFF
+ /** {@inheritDoc} */
+ public void accept(@Nullable final ProfileRequestContext input) {
+ checkComponentActive();
+
+ final AuthenticationContext authnCtx = input != null ? input.getSubcontext(AuthenticationContext.class) : null;
+ if (authnCtx == null) {
+ return;
+ }
+
+ final DuoOIDCAuthenticationContext duoCtx = authnCtx.getSubcontext(DuoOIDCAuthenticationContext.class);
+ if (duoCtx != null) {
+ duoCtx.removeFromParent();
+ }
+
+ final DuoPasswordlessContext passwordlessCtx = authnCtx.getSubcontext(DuoPasswordlessContext.class);
+ if (passwordlessCtx != null) {
+ if (removePasswordlessContext) {
+ passwordlessCtx.removeFromParent();
+ }
+
+ final String localCookieName = cookieName;
+ if (authnCtx.isResultCacheable()) {
+ if (cookieManager != null && dataSealer != null && localCookieName != null) {
+ String wrapped = passwordlessCtx.getUsername();
+ if (wrapped != null) {
+ try {
+ assert dataSealer != null;
+ wrapped = dataSealer.wrap(wrapped);
+ assert cookieManager != null;
+ cookieManager.addCookie(localCookieName,
+ UrlEscapers.urlFormParameterEscaper().escape(wrapped));
+ } catch (final DataSealerException e) {
+ wrapped = null;
+ log.warn("Error sealing username cookie", e);
+ }
+ }
+ }
+ } else if (cookieManager != null && localCookieName != null) {
+ cookieManager.unsetCookie(localCookieName);
+ }
+ }
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
index 41b7503d..936df113 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
@@ -54,7 +54,10 @@ public final class DefaultDuoOIDCIntegration
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(DefaultDuoOIDCIntegration.class);
-
+
+ /** Passwordless indicator. */
+ @GuardedBy("this") private boolean passwordless;
+
/** API host. */
@GuardedBy("this") @NonnullAfterInit @NotEmpty private String apiHost;
@@ -71,7 +74,10 @@ public final class DefaultDuoOIDCIntegration
@GuardedBy("this") @Nullable private String registeredRedirectURI;
/** A set of 'allowed' origins that can be used as the scheme, host, and port portion of the redirectURI.*/
- @GuardedBy("this") @Nonnull @NonnullElements @Unmodifiable private Set<String> allowedOrigins;
+ @GuardedBy("this") @Nonnull @NonnullElements private Set<String> allowedOrigins;
+
+ /** A set of 'allowed' factors. */
+ @GuardedBy("this") @Nullable @NonnullElements private Set<String> allowedFactors;
/** The URL path to the health endpoint.*/
@GuardedBy("this") @NonnullAfterInit @NotEmpty private String healthEndpoint;
@@ -84,12 +90,32 @@ public final class DefaultDuoOIDCIntegration
/** Container for supported principals. */
@GuardedBy("this") @Nonnull private final Subject supportedPrincipals;
-
+
/** Constructor. */
public DefaultDuoOIDCIntegration() {
supportedPrincipals = new Subject();
allowedOrigins = CollectionSupport.emptySet();
}
+
+ /**
+ * Sets whether this integration is suitable for use as a single factor.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ *
+ * @since 2.1.0
+ */
+ public synchronized void setPasswordless(final boolean flag) {
+ checkSetterPreconditions();
+ passwordless = flag;
+ }
+
+ /** {@inheritDoc} */
+ public synchronized boolean isPasswordless() {
+ checkComponentActive();
+ return passwordless;
+ }
/**
* Set the origins that are allowed to form the scheme, host, and port part of a computed redirect_uri.
@@ -97,15 +123,31 @@ public final class DefaultDuoOIDCIntegration
* @param hosts the hostnames to allow.
*/
public synchronized void setAllowedOrigins(@Nullable @NonnullElements final Collection<String> hosts) {
- checkSetterPreconditions();
- allowedOrigins = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(
- Constraint.isNotNull(hosts, "Types cannot be null")));
+ checkSetterPreconditions();
+ allowedOrigins = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(hosts));
}
/** {@inheritDoc} */
@Nonnull @NotLive @Unmodifiable public synchronized Set<String> getAllowedOrigins() {
- //set is unmodifiable and string is immutable - so not live.
- return CollectionSupport.copyToSet(allowedOrigins);
+ return allowedOrigins;
+ }
+
+ /**
+ * Set the allowable factors.
+ *
+ * @param factors the factors to allow
+ *
+ * @since 2.1.0
+ */
+ public synchronized void setAllowedFactors(@Nullable @NonnullElements final Collection<String> factors) {
+ checkSetterPreconditions();
+ allowedFactors = factors != null ?
+ CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(factors)) : null;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable @NotLive @Unmodifiable public synchronized Set<String> getAllowedFactors() {
+ return allowedFactors;
}
/** {@inheritDoc} */
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
index 77e4e3b9..4bf6ec95 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
@@ -14,12 +14,16 @@
package net.shibboleth.idp.plugin.authn.duo;
+import java.util.Set;
+
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.shibboleth.idp.authn.principal.PrincipalSupportingComponent;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
-
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
/**
* Interface to a particular Duo OIDC integration point. In part replaces
@@ -78,7 +82,28 @@ public interface DuoOIDCIntegration extends PrincipalSupportingComponent {
*/
@Nonnull @NotEmpty String getTokenEndpoint();
-
-
+ /**
+ * Gets whether the integration is suitable for use as a passwordless single factor.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @return true iff the integration limits methods to passwordless
+ *
+ * @since 2.1.0
+ */
+ default boolean isPasswordless() {
+ return false;
+ }
-}
+ /**
+ * Gets the set of allowable factors to enforce during validation.
+ *
+ * @return allowable factors, or null for any
+ *
+ * @since 2.1.0
+ */
+ default @Nullable @NonnullElements @Unmodifiable @NotLive Set<String> getAllowedFactors() {
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DynamicDuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DynamicDuoOIDCIntegration.java
index 3b013f5a..44d02758 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DynamicDuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DynamicDuoOIDCIntegration.java
@@ -53,7 +53,6 @@ public interface DynamicDuoOIDCIntegration extends DuoOIDCIntegration{
*/
boolean isRedirectURIPreregistered();
-
/**
* <p>Set the redirectURI from the one given in a thread-safe way.</p>
*
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/SimpleDuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/SimpleDuoOIDCIntegration.java
index 29067330..c6d80a8a 100644
--- a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/SimpleDuoOIDCIntegration.java
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/SimpleDuoOIDCIntegration.java
@@ -36,12 +36,19 @@ import net.shibboleth.shared.primitive.StringSupport;
/**
- * A data wrapper for use with Duo OIDC integrations which does not support redirectURI generation.
+ * A data wrapper for use with Duo OIDC integrations which does not support redirectURI generation.
+ *
+ * <p>This class was replaced by the {@link DefaultDuoOIDCIntegration} subclass.</p>
+ *
+ * @deprecated
*/
@ThreadSafe
-public final class SimpleDuoOIDCIntegration
- extends AbstractInitializableComponent implements DuoOIDCIntegration{
+ at Deprecated(forRemoval=true, since="2.1.0")
+public final class SimpleDuoOIDCIntegration extends AbstractInitializableComponent implements DuoOIDCIntegration {
+ /** Passwordless indicator. */
+ @GuardedBy("this") private boolean passwordless;
+
/** API host. */
@GuardedBy("this") @NonnullAfterInit @NotEmpty private String apiHost;
@@ -71,6 +78,26 @@ public final class SimpleDuoOIDCIntegration
supportedPrincipals = new Subject();
}
+ /**
+ * Sets whether this integration is suitable for use as a single factor.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ *
+ * @since 2.1.0
+ */
+ public synchronized void setPasswordless(final boolean flag) {
+ checkSetterPreconditions();
+ passwordless = flag;
+ }
+
+ /** {@inheritDoc} */
+ public synchronized boolean isPasswordless() {
+ checkComponentActive();
+ return passwordless;
+ }
+
/** {@inheritDoc} */
@Nonnull @NotEmpty public synchronized String getAPIHost() {
checkComponentActive();
@@ -84,8 +111,7 @@ public final class SimpleDuoOIDCIntegration
* @param host API host
*/
public synchronized void setAPIHost(@Nonnull @NotEmpty final String host) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
apiHost = Constraint.isNotNull(StringSupport.trimOrNull(host), "API host cannot be null or empty");
}
@@ -103,8 +129,7 @@ public final class SimpleDuoOIDCIntegration
* @param endpoint the endpoint.
*/
public synchronized void setHealthCheckEndpoint(@Nonnull @NotEmpty final String endpoint) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
healthEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint),
"Health check endpoint cannot be null or empty");
@@ -123,8 +148,7 @@ public final class SimpleDuoOIDCIntegration
* @param endpoint the endpoint.
*/
public synchronized void setAuthorizeEndpoint(@Nonnull @NotEmpty final String endpoint) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
authorizeEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint),
"Authorize endpoint cannot be null or empty");
@@ -143,8 +167,7 @@ public final class SimpleDuoOIDCIntegration
* @param endpoint the endpoint.
*/
public synchronized void setTokenEndpoint(@Nonnull @NotEmpty final String endpoint) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
tokenEndpoint = Constraint.isNotNull(StringSupport.trimOrNull(endpoint),
"Token endpoint cannot be null or empty");
@@ -161,8 +184,7 @@ public final class SimpleDuoOIDCIntegration
* @param url the url.
*/
public synchronized void setRedirectURI(@Nonnull final String url) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
redirectURI = Constraint.isNotNull(StringSupport.trimOrNull(url), "Redirect URI cannot be null or empty");;
}
@@ -174,8 +196,7 @@ public final class SimpleDuoOIDCIntegration
* @param id the client identifier.
*/
public synchronized void setClientId(@Nonnull @NotEmpty final String id) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
clientId = Constraint.isNotNull(StringSupport.trimOrNull(id), "ClientID cannot be null or empty");
}
@@ -193,8 +214,7 @@ public final class SimpleDuoOIDCIntegration
* @param key secret key
*/
public synchronized void setSecretKey(@Nonnull @NotEmpty final String key) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
secretKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Secret key cannot be null or empty");
}
@@ -225,8 +245,7 @@ public final class SimpleDuoOIDCIntegration
*/
public synchronized <T extends Principal> void setSupportedPrincipals(
@Nullable @NonnullElements final Collection<T> principals) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
supportedPrincipals.getPrincipals().clear();
@@ -238,9 +257,9 @@ public final class SimpleDuoOIDCIntegration
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
- if (getAPIHost() == null || getClientId() == null || getSecretKey() == null
- || getHealthCheckEndpoint() == null || getAuthorizeEndpoint() == null
- || getTokenEndpoint() == null || getRedirectURI() == null) {
+ if (apiHost == null || clientId == null || secretKey == null
+ || healthEndpoint == null || authorizeEndpoint == null
+ || tokenEndpoint == null || redirectURI == null) {
throw new ComponentInitializationException("API host, clientId, secret key,"
+ "token endpoint, health check endpoint, authorization endpoint, and "
+ "redirectURI must be set");
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoPasswordlessContext.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoPasswordlessContext.java
new file mode 100644
index 00000000..26627774
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/context/DuoPasswordlessContext.java
@@ -0,0 +1,93 @@
+/*
+ * 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.duo.context;
+
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * Context that tracks username and signals usage of Duo in a passwordless mode.
+ *
+ * <p>This is used for a more specialized use of the Duo service as a single factor.
+ * The presence of the context acts as a signal of this behavior, and the username
+ * is tracked here since it is typicall set by calling code or collected from a view.</p>
+ *
+ * @parent {@link AuthenticationContext}
+ * @added By configuration to signal username collection and enrollment checking
+ *
+ * @since 2.1.0
+ */
+public final class DuoPasswordlessContext extends BaseContext {
+
+ /** Username. */
+ @Nullable private String username;
+
+ /** Whether user has appropriate devices enrolled. */
+ boolean enrolled;
+
+ /**
+ * Get the username.
+ *
+ * @return username
+ */
+ @Nullable public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Set the username.
+ *
+ * <p>When changing the context's existing value, the context will clear the
+ * {@link #isEnrolled()} setting to false.</p>
+ *
+ * @param name username
+ *
+ * @return this context
+ */
+ @Nonnull public DuoPasswordlessContext setUsername(@Nullable final String name) {
+ if (!Objects.equals(name, username)) {
+ username = name;
+ enrolled = false;
+ }
+ return this;
+ }
+
+ /**
+ * Gets whether the user is determined to have appropriate devices enrolled.
+ *
+ * @return whether the user is determined to have appropriate devices enrolled
+ */
+ public boolean isEnrolled() {
+ return enrolled;
+ }
+
+ /**
+ * Sets whether the user is determined to have appropriate devices enrolled.
+ *
+ * @param flag flag to set
+ *
+ * @return this context
+ */
+ @Nonnull public DuoPasswordlessContext setEnrolled(final boolean flag) {
+ enrolled = flag;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollment.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollment.java
new file mode 100644
index 00000000..84ebb0b4
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollment.java
@@ -0,0 +1,362 @@
+/*
+ * 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.duo.impl;
+
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+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.duo.context.DuoPasswordlessContext;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.net.URISupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+
+/**
+ * An action to populate a username into a {@link DuoPasswordlessContext}, either from a form
+ * submission, a cookie, or an existing session, and perform a check for suitability for
+ * that user to conduct passwordless use of Duo, typically based on device enrollments.
+ *
+ * <p>If no username is found, then {@link AuthnEventIds#UNKNOWN_USERNAME} is signaled.</p>
+ *
+ * <p>If the username in the context is "changed" from its existing state, then the condition
+ * for passwordless usage is executed and the result stored into the context. A failed check
+ * results in the @event {@link AuthnEventIds#REQUEST_UNSUPPORTED} event.</p>
+ *
+ * <p>The action also processes a signal to avoid caching the result of authentication (and also
+ * avoid caching the username entered) for shared machines.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#UNKNOWN_USERNAME}
+ * @event {@link AuthnEventIds#REQUEST_UNSUPPORTED}
+ * @post {@link DuoPasswordlessContext#setUsername(String)} is called with an existing value if found
+ * and {@link DuoPasswordlessContext#setEnrolled(boolean)} is called with the relevant value
+ *
+ * @since 2.1.0
+ */
+public class CheckPasswordlessEnrollment extends AbstractExtractionAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(CheckPasswordlessEnrollment.class);
+
+ /** Strategy used to locate the {@link DuoPasswordlessContext} to operate on. */
+ @Nonnull private Function<ProfileRequestContext,DuoPasswordlessContext> duoPasswordlessContextLookupStrategy;
+
+ /** Condition indicating passwordless use is valid. */
+ @Nonnull private BiPredicate<ProfileRequestContext,String> passwordlessCondition;
+
+ /** Form parameter name to carry username. */
+ @Nonnull @NotEmpty private String usernameFieldName;
+
+ /** Parameter name for SSO bypass. */
+ @Nonnull @NotEmpty private String ssoBypassFieldName;
+
+ /** Username cookie name. */
+ @Nullable @NotEmpty private String cookieName;
+
+ /** Optional cookie manager to use. */
+ @Nullable private CookieManager cookieManager;
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /** Whether to pull username from existing session or not. */
+ private boolean checkSession;
+
+ /** Context to operate on. */
+ @NonnullBeforeExec private DuoPasswordlessContext passwordlessContext;
+
+ /** Constructor.*/
+ public CheckPasswordlessEnrollment() {
+ duoPasswordlessContextLookupStrategy =
+ new ChildContextLookup<>(DuoPasswordlessContext.class).compose(
+ new ChildContextLookup<>(AuthenticationContext.class));
+
+ // TODO: BiPredicateSupport.alwaysTrue once API is bumped.
+ passwordlessCondition = (a,b) -> {
+ return true;
+ };
+
+ usernameFieldName = "j_username";
+ ssoBypassFieldName = "donotcache";
+ }
+
+ /**
+ * Set the strategy used to locate the {@link DuoPasswordlessContext} to operate on.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDuoContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoPasswordlessContext> strategy) {
+ checkSetterPreconditions();
+
+ duoPasswordlessContextLookupStrategy =
+ Constraint.isNotNull(strategy, "DuoPasswordlessContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set a {@link BiPredicate} to run to determine whether to proceed with passwordless Duo.
+ *
+ * @param condition condition to set
+ */
+ public void setPasswordlessCondition(@Nonnull final BiPredicate<ProfileRequestContext,String> condition) {
+ checkSetterPreconditions();
+
+ passwordlessCondition = Constraint.isNotNull(condition, "Condition cannot be null");
+ }
+
+ /**
+ * Sets the name of the form field to carry the username.
+ *
+ * @param name field name
+ */
+ public void setUsernameFieldName(@Nonnull final String name) {
+ checkSetterPreconditions();
+
+ usernameFieldName = Constraint.isNotNull(StringSupport.trimOrNull(name) ,
+ "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.");
+ }
+
+ /**
+ * Set cookie name to use for cached username.
+ *
+ * @param name cookie name
+ */
+ public void setCookieName(@Nullable final String name) {
+ checkSetterPreconditions();
+
+ cookieName = StringSupport.trimOrNull(name);
+ }
+
+ /**
+ * Sets optional {@link CookieManager} to use.
+ *
+ * @param manager cookie manager
+ */
+ public void setCookieManager(@Nullable final CookieManager manager) {
+ checkSetterPreconditions();
+
+ cookieManager = manager;
+ }
+
+ /**
+ * Sets optional {@link DataSealer} to use.
+ *
+ * @param sealer data sealer
+ */
+ public void setDataSealer(@Nullable final DataSealer sealer) {
+ checkSetterPreconditions();
+
+ dataSealer = sealer;
+ }
+
+ /**
+ * Sets whether tp pull username from existing session as a fallback.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setCheckSession(final boolean flag) {
+ checkSetterPreconditions();
+
+ checkSession = flag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ return false;
+ }
+
+ passwordlessContext = duoPasswordlessContextLookupStrategy.apply(profileRequestContext);
+ if (passwordlessContext == null) {
+ log.debug("{} No DuoPasswordlessContext found, nothing to do", getLogPrefix());
+ return false;
+ }
+ return true;
+ }
+
+// Checkstyle: CyclomaticComplexity OFF
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ boolean usernameChanged = false;
+
+ String username = getUsernameFromForm(authenticationContext);
+ if (username != null) {
+ if (!username.equals(passwordlessContext.getUsername()) ) {
+ log.debug("{} Populating username '{}' from form submission into Duo passwordless context",
+ getLogPrefix(), username);
+ passwordlessContext.setUsername(username);
+ usernameChanged = true;
+ }
+ } else {
+ username = getUsernameFromCookie(profileRequestContext);
+ if (username != null) {
+ if (!username.equals(passwordlessContext.getUsername()) ) {
+ log.debug("{} Populating cached username '{}' from cookie into Duo passwordless context",
+ getLogPrefix(), username);
+ passwordlessContext.setUsername(username);
+ usernameChanged = true;
+ }
+ } else {
+ username = getUsernameFromSession(profileRequestContext, authenticationContext);
+ if (username != null && !username.equals(passwordlessContext.getUsername())) {
+ log.debug("{} Populating username '{}' from session into Duo passwordless context", getLogPrefix(),
+ username);
+ passwordlessContext.setUsername(username);
+ usernameChanged = true;
+ }
+ }
+ }
+
+ if (passwordlessContext.getUsername() == null) {
+ passwordlessContext.setEnrolled(false);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.UNKNOWN_USERNAME);
+ return;
+ }
+
+ if (usernameChanged) {
+ passwordlessContext.setEnrolled(
+ passwordlessCondition.test(profileRequestContext, passwordlessContext.getUsername()));
+ log.debug("{} Username '{}' found to be {} of passwordless attempt", getLogPrefix(),
+ passwordlessContext.getUsername(), passwordlessContext.isEnrolled() ? "capable" : "incapable");
+ } else {
+ log.debug("{} Username not available, leaving DuoPasswordlessContext unchanged", getLogPrefix());
+ }
+
+ if (!passwordlessContext.isEnrolled()) {
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.REQUEST_UNSUPPORTED);
+ }
+ }
+// Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Gets the username from a form submission.
+ *
+ * <p>Also processes do-not-cache instruction.</p>
+ *
+ * @param authenticationContext authentication context
+ *
+ * @return submitted username, after applying any configured transforms
+ */
+ @Nullable private String getUsernameFromForm(@Nonnull final AuthenticationContext authenticationContext) {
+
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request != null) {
+ 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);
+ }
+
+ return applyTransforms(request.getParameter(usernameFieldName));
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the username from an existing sealed cookie, if any.
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return username from existing sealed cookie, or null
+ */
+ @Nullable private String getUsernameFromCookie(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (cookieManager != null && dataSealer != null && cookieName != null) {
+ final String cookie = URISupport.doURLDecode(cookieManager.getCookieValue(cookieName, null));
+ if (cookie != null) {
+ try {
+ assert dataSealer != null;
+ return dataSealer.unwrap(cookie);
+ } catch (final DataSealerException e) {
+ log.warn("{} Unable to unwrap sealed username cookie", getLogPrefix(), e);
+ assert cookieName != null;
+ assert cookieManager != null;
+ cookieManager.unsetCookie(cookieName);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the username from an existing {@link IdPSession}, if any.
+ *
+ * @param profileRequestContext profile request context
+ * @param authenticationContext authentication context
+ *
+ * @return username from existing session, or null
+ */
+ @Nullable private String getUsernameFromSession(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (checkSession && !authenticationContext.getActiveResults().isEmpty()) {
+ final SessionContext sessionContext = profileRequestContext.getSubcontext(SessionContext.class);
+ if (sessionContext != null) {
+ final IdPSession idpSession = sessionContext.getIdPSession();
+ if (idpSession != null) {
+ return idpSession.getPrincipalName();
+ }
+ }
+ }
+
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
index 4447da1f..e7651698 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
@@ -36,6 +36,7 @@ import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.DynamicDuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext;
import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -46,7 +47,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
/**
* An action to create (or lookup) and populate the {@link DuoOIDCAuthenticationContext}
- * with the username, chosen {@link DuoOIDCIntegration}, and {@link DuoOIDCClient} appropriate for this request.
+ * with the username, chosen {@link DuoOIDCIntegration}, and {@link DuoOIDCClient} appropriate for this request.
+ *
+ * <p>Operates in 2 modes, one for passwordless (indicated by presence of a {@link DuoPasswordlessContext}, or
+ * a standard mode. The difference is in how the username and integration to use are derived.</p>
*
* <p>Determines the usable redirect_uri, either from one registered, or computed from the
* HTTP request. Is set once, before the client is constructed, for every client. If however, the client supports
@@ -54,10 +58,10 @@ import net.shibboleth.shared.primitive.LoggerFactory;
*
* <p>Adds the nonce part of the state parameter for matching on callback from the 2FA check.</p>
*
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
- * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
* @post See above.
*/
public class PopulateDuoAuthenticationContext extends AbstractAuthenticationAction {
@@ -68,28 +72,38 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
/** Strategy used to locate or create the {@link DuoOIDCAuthenticationContext} to populate. */
@Nonnull private Function<ProfileRequestContext,DuoOIDCAuthenticationContext> duoAuthContextCreationStrategy;
+ /** Strategy used to locate a {@link DuoPasswordlessContext} if present. */
+ @Nonnull private Function<ProfileRequestContext,DuoPasswordlessContext> passwordlessContextLookupStrategy;
+
/** Lookup strategy for username to match against Duo identity. */
@Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
/** Lookup strategy for Duo integration. */
- @Nonnull private Function<ProfileRequestContext, DuoOIDCIntegration> duoIntegrationLookupStrategy;
+ @Nonnull private Function<ProfileRequestContext, DuoOIDCIntegration> standardDuoIntegrationLookupStrategy;
+
+ /** Lookup strategy for Duo integration for passwordless use. */
+ @Nonnull private Function<ProfileRequestContext, DuoOIDCIntegration> passwordlessDuoIntegrationLookupStrategy;
/** Strategy used to compute the redirectURI from the given Duo integration if supported.*/
- @Nullable
- private BiFunction<HttpServletRequest, DynamicDuoOIDCIntegration, String> redirectURICreationStrategy;
+ @Nullable private BiFunction<HttpServletRequest, DynamicDuoOIDCIntegration, String> redirectURICreationStrategy;
/** The registry for locating the DuoClient for the established integration.*/
@NonnullAfterInit private DuoOIDCClientRegistry clientRegistry;
-
+
/** Constructor.*/
public PopulateDuoAuthenticationContext() {
//default creates duo authentication context under authentication context.
duoAuthContextCreationStrategy =
- new ChildContextLookup<>(DuoOIDCAuthenticationContext.class, true).
- compose(new ChildContextLookup<>(AuthenticationContext.class));
+ new ChildContextLookup<>(DuoOIDCAuthenticationContext.class, true).compose(
+ new ChildContextLookup<>(AuthenticationContext.class));
+
+ passwordlessContextLookupStrategy =
+ new ChildContextLookup<>(DuoPasswordlessContext.class).compose(
+ new ChildContextLookup<>(AuthenticationContext.class));
usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
- duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+ standardDuoIntegrationLookupStrategy = FunctionSupport.constant(null);
+ passwordlessDuoIntegrationLookupStrategy = FunctionSupport.constant(null);
}
/**
@@ -98,8 +112,7 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
* @param duoRegistry the registry
*/
public void setClientRegistry(@Nonnull final DuoOIDCClientRegistry duoRegistry) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
clientRegistry = Constraint.isNotNull(duoRegistry,"DuoClient registry can not be null");
}
@@ -111,8 +124,7 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
*/
public void setUsernameLookupStrategy(
@Nonnull final Function<ProfileRequestContext, String> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
}
@@ -125,40 +137,67 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
*/
public void setRedirectURICreationStrategy(
@Nonnull final BiFunction<HttpServletRequest, DynamicDuoOIDCIntegration, String> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
redirectURICreationStrategy = Constraint.isNotNull(strategy, "RedirectURI"
+ " creation strategy cannot be null");
}
/**
- * Set the strategy used to locate the {@link DuoOIDCAuthenticationContext} to operate on.
+ * Set the strategy used to create/locate the {@link DuoOIDCAuthenticationContext} to operate on.
*
- * @param strategy lookup strategy
+ * @param strategy creation strategy
*/
public void setDuoContextCreationStrategy(
@Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
duoAuthContextCreationStrategy = Constraint.isNotNull(strategy, "DuoAuthenticationContext"
+ " creation strategy cannot be null");
}
+
+ /**
+ * Set the strategy used to locate the {@link DuoPasswordlessContext} to operate on.
+ *
+ * @param strategy lookup strategy
+ *
+ * @since 2.1.0
+ */
+ public void setPasswordlessContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoPasswordlessContext> strategy) {
+ checkSetterPreconditions();
+ passwordlessContextLookupStrategy = Constraint.isNotNull(strategy, "DuoPasswordlessContext"
+ + " lookup strategy cannot be null");
+ }
+
/**
- * Set DuoIntegration lookup strategy to use.
+ * Set standard DuoIntegration lookup strategy to use.
*
* @param strategy lookup strategy
*/
- public void setDuoIntegrationLookupStrategy(
+ public void setStandardDuoIntegrationLookupStrategy(
@Nonnull final Function<ProfileRequestContext, DuoOIDCIntegration> strategy) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
- duoIntegrationLookupStrategy = Constraint.isNotNull(strategy, "DuoIntegration lookup strategy cannot be null");
+ standardDuoIntegrationLookupStrategy =
+ Constraint.isNotNull(strategy, "Standard DuoIntegration lookup strategy cannot be null");
}
-
+
+ /**
+ * Set passwordless DuoIntegration lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ *
+ * @since 2.1.0
+ */
+ public void setPasswordlessDuoIntegrationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, DuoOIDCIntegration> strategy) {
+ checkSetterPreconditions();
+ passwordlessDuoIntegrationLookupStrategy =
+ Constraint.isNotNull(strategy, "Passwordless DuoIntegration lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
@@ -173,62 +212,134 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
-
- final DuoOIDCAuthenticationContext context = duoAuthContextCreationStrategy.apply(profileRequestContext);
- if (context == null) {
- log.error("{} Error creating DuoAuthenticationContext", getLogPrefix());
+ final HttpServletRequest request = getHttpServletRequest();
+ if (request == null) {
+ log.warn("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
-
- final DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
- if (duoIntegration == null) {
- log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+
+ final DuoOIDCAuthenticationContext duoContext = duoAuthContextCreationStrategy.apply(profileRequestContext);
+ if (duoContext == null) {
+ log.error("{} Error creating DuoAuthenticationContext", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return;
}
- context.setIntegration(duoIntegration);
- final HttpServletRequest request = getHttpServletRequest();
- if (request == null) {
- log.warn("{} Profile action does not contain an HttpServletRequest", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return;
- }
-
- final String username = usernameLookupStrategy.apply(profileRequestContext);
- if (username == null) {
- log.warn("{} No principal name available to initiate a Duo 2FA request", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
- return;
+ final DuoPasswordlessContext passwordlessContext =
+ passwordlessContextLookupStrategy.apply(profileRequestContext);
+ if (passwordlessContext != null) {
+ if (!doPasswordless(profileRequestContext, duoContext, passwordlessContext)) {
+ duoContext.removeFromParent();
+ return;
+ }
+ } else {
+ if (!doStandard(profileRequestContext, duoContext)) {
+ duoContext.removeFromParent();
+ return;
+ }
}
- context.setUsername(username);
+ final DuoOIDCIntegration duoIntegration = duoContext.getIntegration();
+ assert duoIntegration != null;
+
// Generate state, stash in the context for checking on return.
final String nonce = DuoSupport.generateNonce(32);
// Store only the nonce component as the request state. The SWF key is added by the controller
// And included in the authorization request to Duo.
- context.setRequestState(nonce);
-
+ duoContext.setRequestState(nonce);
+
try {
- computeAndStoreRedirectURIIfSupported(duoIntegration, request, context);
+ computeAndStoreRedirectURIIfSupported(duoIntegration, request, duoContext);
//Configure the Duo client for the established integration
final DuoOIDCClient client = clientRegistry.getClientOrCreate(duoIntegration);
- context.setClient(client);
+ duoContext.setClient(client);
} catch (final DuoException e) {
log.warn("{} Unable to establish a Duo Client for the given integration", getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ duoContext.removeFromParent();
return;
}
- log.debug("Created Duo authentication context for '{}'",username);
+ log.debug("Created Duo authentication context for '{}'", duoContext.getUsername());
+ }
+
+ /**
+ * Perform standard context creation and lookups.
+ *
+ * @param profileRequestContext profile request context
+ * @param duoContext newly created Duo context
+ * @param passwordlessContext Duo passwordless context
+ *
+ * @return true iff processing should continue
+ */
+ private boolean doPasswordless(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final DuoOIDCAuthenticationContext duoContext,
+ @Nonnull final DuoPasswordlessContext passwordlessContext) {
+
+ if (passwordlessContext.getUsername() == null) {
+ log.warn("{} No principal name available to initiate a Duo 2FA request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return false;
+ }
+ duoContext.setUsername(passwordlessContext.getUsername());
+
+ if (!passwordlessContext.isEnrolled()) {
+ log.warn("{} Context indicates user '{}' is not eligible for passwordless, proceeding anyway",
+ getLogPrefix(), duoContext.getUsername());
+ }
+
+ final DuoOIDCIntegration duoIntegration = passwordlessDuoIntegrationLookupStrategy.apply(profileRequestContext);
+ if (duoIntegration == null) {
+ log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ } else if (!duoIntegration.isPasswordless()) {
+ log.warn("{} DuoIntegration returned by lookup strategy was not passwordless", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ duoContext.setIntegration(duoIntegration);
+
+ return true;
+ }
+
+ /**
+ * Perform standard context creation and lookups.
+ *
+ * @param profileRequestContext profile request context
+ * @param duoContext newly created Duo context
+ *
+ * @return true iff processing should continue
+ */
+ private boolean doStandard(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+ final DuoOIDCIntegration duoIntegration = standardDuoIntegrationLookupStrategy.apply(profileRequestContext);
+ if (duoIntegration == null) {
+ log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ duoContext.setIntegration(duoIntegration);
+
+ final String username = usernameLookupStrategy.apply(profileRequestContext);
+ if (username == null) {
+ log.warn("{} No principal name available to initiate a Duo 2FA request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return false;
+ }
+ duoContext.setUsername(username);
+
+ return true;
}
/**
- * <p>For {@link DynamicDuoOIDCIntegration DynamicDuoOIDCIntegrations}, apply the redirect_uri creation
- * strategy to compute a redirect_uri to use.</p>
+ * For {@link DynamicDuoOIDCIntegration DynamicDuoOIDCIntegrations}, apply the redirect_uri creation
+ * strategy to compute a redirect_uri to use.
*
* <p>The redirect_uri is computed for each request, but is only set once as the usable redirect_uri
* on the integration itself i.e. for the client to read using {@link DuoOIDCIntegration#getRedirectURI()}.
@@ -274,4 +385,4 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
}
}
-}
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
index 900c7414..e64d8826 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
@@ -18,7 +18,7 @@ import java.security.Principal;
import java.text.ParseException;
import java.util.Collection;
import java.util.Map;
-import java.util.function.Consumer;
+import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -40,6 +40,7 @@ import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
import net.shibboleth.idp.authn.duo.DuoFactorPrincipal;
import net.shibboleth.idp.authn.duo.DuoPrincipal;
import net.shibboleth.idp.authn.impl.AbstractAuditingValidationAction;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
import net.shibboleth.idp.plugin.authn.duo.DuoException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
@@ -86,6 +87,9 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
/** Attempted username. */
@NonnullBeforeExec @NotEmpty private String username;
+ /** Factor used. */
+ @Nullable private String factorUsed;
+
/** Hook to map context information (often Duo factors in the Duo token) to principal collections.*/
@Nullable private Function<ProfileRequestContext,Collection<Principal>> contextToPrincipalMappingStrategy;
@@ -113,8 +117,7 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
*/
public void setContextToPrincipalMappingStrategy(@Nullable final
Function<ProfileRequestContext,Collection<Principal>> hook) {
- ifInitializedThrowUnmodifiabledComponentException();
- ifDestroyedThrowDestroyedComponentException();
+ checkSetterPreconditions();
contextToPrincipalMappingStrategy = hook;
}
@@ -183,7 +186,6 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
/** {@inheritDoc} */
@Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@Nonnull final AuthenticationContext authenticationContext) {
-
Map<String, Object> authStatusObject = null;
try {
@@ -207,11 +209,21 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
statusMsgObj instanceof final String authResultStatusMsg) {
if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(authResultStatus)){
- if (log.isInfoEnabled()) {
- final String factorUsed = extractFactor();
- log.info("{} Duo 2FA authentication succeeded for '{}', using second-factor '{}'",
- getLogPrefix(),duoContext.getUsername(), factorUsed != null ? factorUsed : "unspecified");
+ factorUsed = extractFactor();
+
+ // Check if factor is allowed.
+ final Set<String> allowedFactors = duoIntegration.getAllowedFactors();
+ if (allowedFactors != null && !allowedFactors.contains(factorUsed)) {
+ log.error("{} Duo 2FA authentication failed for '{}', factor '{}' disallowed",getLogPrefix(),
+ username, factorUsed);
+ handleError(profileRequestContext, authenticationContext, AuthnEventIds.INVALID_CREDENTIALS,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ recordFailure(profileRequestContext);
+ return;
}
+
+ log.info("{} Duo 2FA authentication succeeded for '{}', using second-factor '{}'",
+ getLogPrefix(),duoContext.getUsername(), factorUsed != null ? factorUsed : "unspecified");
//must build authentication before recording success. recordSuccess runs
//the cleanup hook which removes the Duo context and prevents useful operation
//of the contextToPrincipalMappingStrategy.
@@ -273,13 +285,20 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
@Nonnull protected Subject populateSubject(@Nonnull final Subject subject) {
// Always add the DuoPrincipal and the DuoFactorPrincipal if present
- final String factor = extractFactor();
final String localUsername = username;
assert localUsername != null;
subject.getPrincipals().add(new DuoPrincipal(localUsername));
- if (factor != null) {
- subject.getPrincipals().add(new DuoFactorPrincipal(factor));
- log.trace("{} Added DuoFactorPrincipal '{}' to DuoPrincipal '{}'", getLogPrefix(), factor, localUsername);
+
+ // Add UsernamePrincipal if passwordless.
+ if (duoIntegration.isPasswordless()) {
+ subject.getPrincipals().add(new UsernamePrincipal(localUsername));
+ }
+
+ final String localFactor = factorUsed;
+ if (localFactor != null) {
+ subject.getPrincipals().add(new DuoFactorPrincipal(localFactor));
+ log.trace("{} Added DuoFactorPrincipal '{}' to DuoPrincipal '{}'", getLogPrefix(), localFactor,
+ localUsername);
}
// Always add any principals specified on the integration
@@ -310,26 +329,5 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
// Bypass c14n. We already operate on a canonical name, so just re-confirm it.
profileRequestContext.ensureSubcontext(SubjectCanonicalizationContext.class).setPrincipalName(username);
}
-
- /**
- * A default cleanup hook that removes the {@link DuoOIDCAuthenticationContext} from the tree.
- */
- public static class DuoOIDCCleanupHook implements Consumer<ProfileRequestContext> {
-
- /** {@inheritDoc} */
- @Override
- public void accept(@Nullable final ProfileRequestContext input) {
- if (input != null) {
- final AuthenticationContext authnCtx = input.getSubcontext(AuthenticationContext.class);
- if (authnCtx != null) {
- final DuoOIDCAuthenticationContext duoCtx =
- authnCtx.getSubcontext(DuoOIDCAuthenticationContext.class);
- if (duoCtx != null) {
- authnCtx.removeSubcontext(duoCtx);
- }
- }
- }
- }
- }
-}
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index e19fca86..c673bd5e 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -9,6 +9,35 @@
default-init-method="initialize" default-destroy-method="destroy">
+ <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"
+ p:cacheSeconds="%{idp.message.cacheSeconds:300}"
+ p:basenames="classpath:/net/shibboleth/idp/plugin/authn/duo/messages"
+ p:defaultEncoding="UTF-8" />
+
+ <!-- Used in views to calculate CSP hashes and nonces, remove and adjust beans in flow when compatibility bumped past 5.0 -->
+
+ <bean id="DuoCSPDigester" class="net.shibboleth.shared.codec.StringDigester"
+ c:algorithm="SHA256" c:format="BASE64" />
+
+ <bean id="DuoCSPNonce" destroy-method=""
+ class="net.shibboleth.shared.security.IdentifierGenerationStrategy" factory-method="getInstance">
+ <constructor-arg>
+ <util:constant
+ static-field="net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType.SECURE" />
+ </constructor-arg>
+ <constructor-arg>
+ <bean class="net.shibboleth.shared.security.RandomIdentifierParameterSpec"
+ c:identifierSize="16">
+ <constructor-arg name="source">
+ <null/>
+ </constructor-arg>
+ <constructor-arg name="identifierEncoder">
+ <null/>
+ </constructor-arg>
+ </bean>
+ </constructor-arg>
+ </bean>
+
<!-- location of the Duo OIDC IdP servlet -->
<bean id="shibboleth.authn.DuoOIDC.externalServletPath" class="java.lang.String"
c:_0="%{idp.duo.oidc.externalAuthnPath:/Authn/Duo/2FA}" />
@@ -17,14 +46,13 @@
<bean id="shibboleth.authn.DuoOIDC.externalAuthorizationPath" class="java.lang.String"
c:_0="servletRelative:#{getObject('shibboleth.authn.DuoOIDC.externalServletPath')}#{T(net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI).AUTHORIZE_PATH_SEGMENT}" />
-
<!-- Default Duo Integration -->
- <bean id="shibboleth.authn.DuoOIDC.DuoIntegration"
+ <bean id="shibboleth.authn.DuoOIDC.DuoIntegration" lazy-init="false"
class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration"
p:APIHost="%{idp.duo.oidc.apiHost:none}"
p:clientId="%{idp.duo.oidc.clientId:none}"
p:secretKey="%{idp.duo.oidc.secretKey:none}"
- p:registeredRedirectURI="%{idp.duo.oidc.redirectURL:}"
+ p:registeredRedirectURI="%{idp.duo.oidc.redirectURL:}"
p:healthCheckEndpoint="%{idp.duo.oidc.endpoint.health:/oauth/v1/health_check}"
p:tokenEndpoint="%{idp.duo.oidc.endpoint.token:/oauth/v1/token}"
p:authorizeEndpoint="%{idp.duo.oidc.endpoint.authorize:/oauth/v1/authorize}"
@@ -32,8 +60,24 @@
<bean id="shibboleth.authn.DuoOIDC.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
c:target-ref="shibboleth.authn.DuoOIDC.DuoIntegration" />
+ <!-- Default passwordless integration. -->
+ <bean id="shibboleth.authn.DuoOIDC.Passwordless.DuoIntegration" lazy-init="false"
+ class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration"
+ p:passwordless="true"
+ p:APIHost="%{idp.duo.oidc.passwordless.apiHost:none}"
+ p:clientId="%{idp.duo.oidc.passwordless.clientId:none}"
+ p:secretKey="%{idp.duo.oidc.passwordless.secretKey:none}"
+ p:registeredRedirectURI="%{idp.duo.oidc.passwordless.redirectURL:%{idp.duo.oidc.redirectURL:}}"
+ p:healthCheckEndpoint="%{idp.duo.oidc.passwordless.endpoint.health:%{idp.duo.oidc.endpoint.health:/oauth/v1/health_check}}"
+ p:tokenEndpoint="%{idp.duo.oidc.passwordless.endpoint.token:%{idp.duo.oidc.endpoint.token:/oauth/v1/token}}"
+ p:authorizeEndpoint="%{idp.duo.oidc.passwordless.endpoint.authorize:%{idp.duo.oidc.endpoint.authorize:/oauth/v1/authorize}}"
+ p:allowedOrigins="%{idp.duo.oidc.passwordless.redirecturl.allowedOrigins:%{idp.duo.oidc.redirecturl.allowedOrigins:}}"
+ p:allowedFactors="#{{ 'Platform authenticator (2fa)', 'Roaming authenticator (2fa)' }}" />
+ <bean id="shibboleth.authn.DuoOIDC.Passwordless.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
+ c:target="#{getObject('shibboleth.authn.DuoOIDC.Passwordless.DuoIntegration')}" />
+
<!-- Default "optional" non-browser integration. -->
- <bean id="shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegration"
+ <bean id="shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegration" lazy-init="false"
class="net.shibboleth.idp.authn.duo.BasicDuoIntegration"
p:APIHost="%{idp.duo.oidc.nonbrowser.apiHost:%{idp.duo.oidc.apiHost:none}}"
p:integrationKey="%{idp.duo.oidc.nonbrowser.integrationKey:none}"
@@ -46,9 +90,9 @@
class="net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy" />
<!-- Duo Client factory and bean registry -->
- <bean id="shibboleth.authn.DuoOIDC.clientRegistry" scope="singleton"
- class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
- p:clientFactory-ref="%{idp.duo.oidc.clientFactoryBean:shibboleth.authn.DuoOIDC.clientFactory}"/>
+ <bean id="shibboleth.authn.DuoOIDC.clientRegistry" scope="singleton"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
+ p:clientFactory-ref="%{idp.duo.oidc.clientFactoryBean:shibboleth.authn.DuoOIDC.clientFactory}"/>
<!--
Load all (or none) factory bean definitions from the classpath. The defaulted factory bean must be called
@@ -64,9 +108,7 @@
to change the location of the user config file. -->
<import resource="conditional:%{idp.home}/conf/authn/%{idp.duo.oidc.user.config:duo-oidc-authn-config.xml}" />
- <!--
- Non-Browser actions and beans. Code lives in idp-authn-impl for now, may migrate here later.
- -->
+ <!-- Non-Browser actions and beans -->
<bean id="ExtractDuoAuthenticationFromHeaders" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ExtractDuoAuthenticationFromHeaders"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
@@ -99,11 +141,29 @@
p:classifiedMessages="#{getObject('shibboleth.authn.DuoOIDC.ClassifiedMessageMap')}"
p:resultCachingPredicate="#{getObject('shibboleth.authn.DuoOIDC.resultCachingPredicate')}" />
- <!-- Duo OIDC AuthAPI beans -->
+ <!-- Passwordless beans -->
+ <!-- TODO: change to shibboleth.BiConditions.TRUE once API moves to 5.1. -->
+ <bean id="DefaultPasswordlessCondition" parent="shibboleth.BiConditions.Expression" c:_0="true" />
+ <bean id="CheckPasswordlessEnrollment"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.CheckPasswordlessEnrollment" scope="prototype"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:passwordlessCondition="#{getObject('shibboleth.authn.DuoOIDC.Passwordless.Condition') ?: getObject('DefaultPasswordlessCondition')}"
+ p:dataSealer="#{'%{idp.authn.usernameCookieName:}'.trim().isEmpty() ? null : getObject('shibboleth.DataSealer')}"
+ p:cookieManager="#{'%{idp.authn.usernameCookieName:}'.trim().isEmpty() ? null : getObject('shibboleth.PersistentCookieManager')}"
+ p:cookieName="%{idp.authn.usernameCookieName:}"
+ p:usernameFieldName="#{'%{idp.authn.DuoOIDC.usernameFieldName:j_username}'.trim()}"
+ p:checkSession="%{idp.authn.usernameFromSession:false}"
+ p:lowercase="%{idp.authn.DuoOIDC.lowercase:false}"
+ p:uppercase="%{idp.authn.DuoOIDC.uppercase:false}"
+ p:trim="%{idp.authn.Password.trim:true}"
+ p:transforms="#{getObject('shibboleth.authn.DuoOIDC.Transforms')}" />
+
+ <!-- Duo OIDC beans -->
<bean id="PopulateDuoAuthenticationContext" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.PopulateDuoAuthenticationContext"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
- p:duoIntegrationLookupStrategy-ref="shibboleth.authn.DuoOIDC.DuoIntegrationStrategy"
+ p:standardDuoIntegrationLookupStrategy-ref="shibboleth.authn.DuoOIDC.DuoIntegrationStrategy"
+ p:passwordlessDuoIntegrationLookupStrategy-ref="shibboleth.authn.DuoOIDC.Passwordless.DuoIntegrationStrategy"
p:redirectURICreationStrategy-ref="shibboleth.authn.DuoOIDC.RedirectURICreationStrategy"
p:usernameLookupStrategy-ref="shibboleth.authn.DuoOIDC.UsernameLookupStrategy"
p:clientRegistry-ref="shibboleth.authn.DuoOIDC.clientRegistry" />
@@ -124,6 +184,9 @@
<bean id="ValidateDuoResponseState" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoResponseState" />
+ <bean id="ExchangeCodeForDuoToken" scope="prototype"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
+
<bean id="ValidateTokenSignature" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateTokenSignature"
p:signatureAlgorithm="HS512"/>
@@ -220,11 +283,11 @@
<value>iat</value>
</util:set>
- <bean id="ExchangeCodeForDuoToken" scope="prototype"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken" />
-
<bean id="shibboleth.authn.DuoOIDC.DefaultCleanupHook"
- class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationResult.DuoOIDCCleanupHook" />
+ class="net.shibboleth.idp.plugin.authn.duo.DefaultDuoCleanupHook"
+ p:dataSealer="#{'%{idp.authn.usernameCookieName:}'.trim().isEmpty() ? null : getObject('shibboleth.DataSealer')}"
+ p:cookieManager="#{'%{idp.authn.usernameCookieName:}'.trim().isEmpty() ? null : getObject('shibboleth.PersistentCookieManager')}"
+ p:cookieName="%{idp.authn.usernameCookieName:}" />
<bean id="ValidateDuoTokenAuthenticationResult" scope="prototype"
class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationResult"
@@ -238,10 +301,10 @@
<!-- Audit logging beans -->
- <!--
- The first context logger clears any previous audit information from the first authentication factor. The
- others are accumulative.
- -->
+ <!--
+ The first context logger clears any previous audit information from the first authentication factor. The
+ others are accumulative.
+ -->
<bean id="PreDuoPopulateAuditContext" parent="shibboleth.authn.AbstractPopulateAuditContext"
p:fieldExtractors="#{getObject('shibboleth.authn.DuoOIDC.PreDuoPopulateAuditExtractors') ?: getObject('shibboleth.authn.DuoOIDC.DefaultPreDuoPopulateAuditExtractors')}"
p:clearAuditContext="true"/>
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
index 6956f4d1..e01c7ed2 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
@@ -13,7 +13,7 @@
<!-- Fall through to a different flow if header extract fails on a passive or non-browser request. -->
<transition on="#{ opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).isPassive() || !opensamlProfileRequestContext.isBrowserProfile() }" to="ReselectFlow" />
- <transition on="NoCredentials" to="CheckDuoOIDCAuthAPI" />
+ <transition on="NoCredentials" to="CheckForPasswordless" />
</action-state>
<action-state id="ValidateDuoAuthAPI">
@@ -23,12 +23,56 @@
<transition on="proceed" to="proceed" />
</action-state>
+ <decision-state id="CheckForPasswordless">
+ <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).containsSubcontext(T(net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext))"
+ then="CheckPasswordlessEnrollment1"
+ else="CheckDuoOIDCAuthAPI" />
+ </decision-state>
+
+ <action-state id="CheckPasswordlessEnrollment1">
+ <evaluate expression="CheckPasswordlessEnrollment" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="PasswordlessView" />
+ <transition on="UnknownUsername" to="PasswordlessView" />
+ <transition on="RequestUnsupported" to="PasswordlessView" />
+ </action-state>
+
+ <view-state id="PasswordlessView" view="passwordless">
+ <on-render>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext))" result="viewScope.passwordlessContext" />
+ <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
+ <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('DuoCSPDigester')" result="viewScope.cspDigester" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('DuoCSPNonce')" result="viewScope.cspNonce" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+ </on-render>
+
+ <transition on="proceed" to="CheckPasswordlessEnrollment2" />
+ <transition on="cancel" to="RequestUnsupported" />
+ </view-state>
+
+ <action-state id="CheckPasswordlessEnrollment2">
+ <evaluate expression="CheckPasswordlessEnrollment" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckDuoOIDCAuthAPI" />
+ <transition on="UnknownUsername" to="PasswordlessView" />
+ <transition on="RequestUnsupported" to="PasswordlessView" />
+ </action-state>
+
<action-state id="CheckDuoOIDCAuthAPI">
<evaluate expression="PopulateDuoAuthenticationContext" />
<evaluate expression="HealthCheckDuoOIDCAuthAPI" />
<evaluate expression="PreDuoPopulateAuditContext" />
<evaluate expression="WritePreDuoAuthnAuditLog"/>
<evaluate expression="'proceed'" />
+
<transition on="proceed" to="Duo2FAAuthorizationRequest" />
</action-state>
@@ -50,6 +94,7 @@
<evaluate expression="PreStateValidationPopulateAuditContext" />
<evaluate expression="ValidateDuoResponseState"/>
<evaluate expression="'proceed'" />
+
<transition on="proceed" to="ExchangeCodeForDuoToken" />
</action-state>
@@ -59,8 +104,9 @@
<evaluate expression="ValidateTokenSignature"/>
<evaluate expression="ValidateTokenClaims"/>
<!-- final validation of the response status to build an authn result -->
- <evaluate expression="ValidateDuoTokenAuthenticationResult"/>
+ <evaluate expression="ValidateDuoTokenAuthenticationResult"/>
<evaluate expression="'proceed'" />
+
<transition on="proceed" to="proceed" />
</action-state>
diff --git a/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties
new file mode 100644
index 00000000..ebb51830
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties
@@ -0,0 +1,18 @@
+# In addition to the Apache 2.0 license, this content is also licensed
+# under the Creative Commons Attribution-ShareAlike 3.0 Unported license
+# (see http://creativecommons.org/licenses/by-sa/3.0/).
+
+# This is the as-delivered set of messages used in various views
+# so that internationalized translations can be made available and
+# so administrators can locally override or supplement the values
+# in their own message files.
+
+
+idp.duo.passwordless.explain = If you've enrolled a passkey or device/token for passwordless login, please enter your \
+ username below and press the corresponding button. To bypass this option, just press the alternate button to perform a \
+ traditional login.
+
+idp.duo.passwordless.proceed = Login with Passkey or Device
+idp.duo.passwordless.cancel = Login with Password
+
+idp.duo.passwordless.unsupported = You have not enrolled a qualifying device for Passwordless use.
diff --git a/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/passwordless.vm b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/passwordless.vm
new file mode 100644
index 00000000..07ba6a28
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/passwordless.vm
@@ -0,0 +1,122 @@
+##
+## Velocity Template for collection of username for Duo Passwordless use
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## authenticationContext - context with authentication request information
+## passwordlessContext - context with Duo username and enrollment status
+## rpUIContext - the context with SP UI information from the metadata
+## encoder - HTMLEncoder class
+## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+## cspNonce - Calculates secure nonces (call generateIdentifier)
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.profile.context.RelyingPartyContext'))
+#set ($username = $passwordlessContext.getUsername())
+#set ($eventCtx = $profileRequestContext.getSubcontext('org.opensaml.profile.context.EventContext'))
+#if ($eventCtx)
+#set ($eventId = $eventCtx.getEvent())
+#end
+#set ($onClick = "document.password.j_username.value = document.passwordless.j_username.value")
+$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onClick)'")
+##
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+ </head>
+ <body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+
+ #set ($serviceName = $rpUIContext.serviceName)
+ #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
+ <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
+ #end
+ </header>
+
+ <section>
+ #*
+ //
+ // SP Description & Logo (optional)
+ // These idpui lines will display added information (if available
+ // in the metadata) about the Service Provider (SP) that requested
+ // authentication. These idpui lines are "active" in this example
+ // (not commented out) - this extra SP info will be displayed.
+ // Remove or comment out these lines to stop the display of the
+ // added SP information.
+ //
+ *#
+ #set ($logo = $rpUIContext.getLogo())
+ #if ($logo)
+ <img class="service-logo" src= "$encoder.encodeForHTMLAttribute($logo)" alt="$encoder.encodeForHTMLAttribute($serviceName)">
+ #end
+ #set ($desc = $rpUIContext.getServiceDescription())
+ #if ($desc)
+ <p>$encoder.encodeForHTML($desc)</p>
+ #end
+
+ <!-- Two forms are used below to allow the return key to trigger the Passwordless option. -->
+
+ <blockquote>#springMessageText("idp.duo.passwordless.explain", "If you've enrolled a passkey or device/token for passwordless login,
+ please enter your username below and press the corresponding button. To bypass this option, just press the alternate button
+ to perform a traditional login.")</blockquote>
+
+ <form name="password" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <input name="j_username" type="hidden" value="" />
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="_eventId_cancel" onClick="$onClick"
+ >#springMessageText("idp.duo.passwordless.cancel", "Login with Password")</button>
+ </div>
+ </div>
+ </form>
+
+ #if ($eventId == "RequestUnsupported")
+ <p class="output-message output--error">$encoder.encodeForHTML("#springMessageText('idp.duo.passwordless.unsupported', 'You have not enrolled a qualifying device for Passwordless use.')")</p>
+ #end
+
+ <form name="passwordless" action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+ <label for="username">#springMessageText("idp.login.username", "Username")</label>
+ <input name="j_username" type="text"
+ 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>
+
+ <input id="_shib_idp_revokeConsent" type="checkbox" name="_shib_idp_revokeConsent" value="true" />
+ <label for="_shib_idp_revokeConsent">#springMessageText("idp.attribute-release.revoke", "Clear prior granting of permission for release of your information to this service.")</label>
+
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="_eventId_proceed"
+ >#springMessageText("idp.duo.passwordless.proceed", "Login with Passkey or Device")</button>
+ </div>
+ </div>
+ </form>
+
+ <ul>
+ <li><a href="#springMessageText("idp.url.helpdesk", '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+ </ul>
+ </section>
+ </main>
+ <footer class="footer">
+ <div class="cc">
+ <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+ </body>
+</html>
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
index 40f82746..d3184a0a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
@@ -21,6 +21,7 @@ import static org.testng.Assert.fail;
import java.text.ParseException;
import java.time.Instant;
+import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -49,6 +50,7 @@ import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileR
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
@@ -57,7 +59,6 @@ import net.shibboleth.shared.component.ComponentInitializationException;
*/
public abstract class AbstractDuoActionTest {
-
protected static final String CLIENT_ID = "DIU6GEFWG5LIUBVV2M3P";
protected static final String REDIRECT_URI = "http://localhost/";
@@ -876,10 +877,33 @@ public abstract class AbstractDuoActionTest {
ac.addSubcontext(dc);
}
- /** Add fabricated duo integration to the duo context. */
- protected void addDuoIntegrationToContext() {
+ /**
+ * Add fabricated duo integration to the duo context.
+ *
+ * @throws ComponentInitializationException on error
+ */
+ protected void addDuoIntegrationToContext() throws ComponentInitializationException {
+ assertNotNull(dc,"try addDuoContext() before adding the duo integration");
+ final var integ = createDummyDuoIntegration();
+ integ.initialize();
+ dc.setIntegration(integ);
+ }
+
+ /**
+ * Add fabricated duo integration to the duo context.
+ *
+ * @param factors allowed factors
+ *
+ * @throws ComponentInitializationException on error
+ */
+ protected void addPasswordlessDuoIntegrationToContext(@Nullable final Set<String> factors)
+ throws ComponentInitializationException {
assertNotNull(dc,"try addDuoContext() before adding the duo integration");
- dc.setIntegration(createDummyDuoIntegration());
+ final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.setPasswordless(true);
+ integ.setAllowedFactors(factors);
+ integ.initialize();
+ dc.setIntegration(integ);
}
/**
@@ -896,9 +920,10 @@ public abstract class AbstractDuoActionTest {
integ.setAuthorizeEndpoint(AUTHORIZE_URI);
integ.setTokenEndpoint(TOKEN_URI);
integ.setHealthCheckEndpoint(HEALTH_URI);
+ integ.setAllowedOrigins(CollectionSupport.singletonList("https://example.com"));
//do not initialize so the tests can mutate and initialize if required
//integ.initialize();
return integ;
}
-}
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollmentTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollmentTest.java
new file mode 100644
index 00000000..d2fffac3
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckPasswordlessEnrollmentTest.java
@@ -0,0 +1,321 @@
+/*
+ * 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.duo.impl;
+
+
+import java.time.Instant;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.net.UrlEscapers;
+
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext;
+import net.shibboleth.idp.plugin.authn.util.mock.TestResourceConverter;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.SPSession;
+import net.shibboleth.idp.session.SessionException;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.net.CookieManager;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.shared.security.impl.BasicKeystoreKeyStrategy;
+import net.shibboleth.shared.servlet.impl.HttpServletRequestResponseContext;
+import net.shibboleth.shared.servlet.impl.ThreadLocalHttpServletRequestSupplier;
+import net.shibboleth.shared.servlet.impl.ThreadLocalHttpServletResponseSupplier;
+
+/** {@link CheckPasswordlessEnrollment} unit test. */
+ at SuppressWarnings("javadoc")
+public class CheckPasswordlessEnrollmentTest extends AbstractDuoActionTest {
+
+ @Nonnull @NotEmpty public static final String COOKIE_NAME = "_shib_idp_username";
+
+ private DataSealer dataSealer;
+
+ private CookieManager cookieManager;
+
+ private CheckPasswordlessEnrollment action;
+
+ @BeforeClass public void init() throws ComponentInitializationException {
+
+ final BasicKeystoreKeyStrategy strategy = new BasicKeystoreKeyStrategy();
+
+ strategy.setKeyAlias("secret");
+ strategy.setKeyPassword("kpassword");
+ strategy.setKeystorePassword("password");
+ strategy.setKeystoreResource(TestResourceConverter.of(
+ new ClassPathResource("net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.jks")));
+ strategy.setKeyVersionResource(TestResourceConverter.of(
+ new ClassPathResource("net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.kver")));
+ strategy.initialize();
+
+ dataSealer = new DataSealer();
+ dataSealer.setKeyStrategy(strategy);
+ dataSealer.initialize();
+
+ cookieManager = new CookieManager();
+ cookieManager.setHttpServletRequestSupplier(new ThreadLocalHttpServletRequestSupplier());
+ cookieManager.setHttpServletResponseSupplier(new ThreadLocalHttpServletResponseSupplier());
+ cookieManager.setCookiePath("/");
+ cookieManager.setMaxAge(300);
+ cookieManager.initialize();
+ }
+
+ @BeforeMethod public void setUp() throws ComponentInitializationException {
+ super.setup();
+
+ ac.ensureSubcontext(DuoPasswordlessContext.class);
+
+ HttpServletRequestResponseContext.loadCurrent(new MockHttpServletRequest(), new MockHttpServletResponse());
+
+ action = new CheckPasswordlessEnrollment();
+ action.setPasswordlessCondition((a,b) -> {return true;});
+ action.setDataSealer(dataSealer);
+ action.setCookieManager(cookieManager);
+ action.setCookieName(COOKIE_NAME);
+ action.setHttpServletRequestSupplier(new ThreadLocalHttpServletRequestSupplier());
+ action.setHttpServletResponseSupplier(new ThreadLocalHttpServletResponseSupplier());
+ action.setCheckSession(true);
+ action.initialize();
+ }
+
+ @AfterMethod public void tearDown() {
+ HttpServletRequestResponseContext.clearCurrent();
+ }
+
+ @Test public void testNoServlet() throws ComponentInitializationException {
+ action = new CheckPasswordlessEnrollment();
+ action.initialize();
+ final Event event = action.execute(src);
+
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.UNKNOWN_USERNAME);
+ }
+
+ @Test public void testMissingIdentity() {
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.UNKNOWN_USERNAME);
+ }
+
+ @Test public void testUnchangedIdentity() {
+ ac.ensureSubcontext(DuoPasswordlessContext.class).setUsername("bar");
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertEvent(event, AuthnEventIds.REQUEST_UNSUPPORTED);
+ }
+
+ @Test public void testUnchangedIdentityEnrolled() {
+ ac.ensureSubcontext(DuoPasswordlessContext.class).setUsername("bar").setEnrolled(true);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ }
+
+ @Test public void testFromForm() {
+ ensureMockRequest().addParameter("j_username", "foo");
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ final AuthenticationContext authCtx = prc.ensureSubcontext(AuthenticationContext.class);
+ Assert.assertTrue(authCtx.isResultCacheable());
+ final DuoPasswordlessContext duoCtx = authCtx.ensureSubcontext(DuoPasswordlessContext.class);
+ Assert.assertEquals(duoCtx.getUsername(), "foo");
+ Assert.assertTrue(duoCtx.isEnrolled());
+ }
+
+ @Test public void testSSOBypass() {
+ ensureMockRequest().addParameter("j_username", "foo");
+ ensureMockRequest().addParameter("donotcache", "1");
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ final AuthenticationContext authCtx = prc.ensureSubcontext(AuthenticationContext.class);
+ Assert.assertFalse(authCtx.isResultCacheable());
+ final DuoPasswordlessContext duoCtx = authCtx.ensureSubcontext(DuoPasswordlessContext.class);
+ Assert.assertEquals(duoCtx.getUsername(), "foo");
+ Assert.assertTrue(duoCtx.isEnrolled());
+ }
+
+ @Test public void testFromCookie() throws DataSealerException {
+ // Wrong field name.
+ ensureMockRequest().addParameter("username", "foo");
+
+ final String wrapped = dataSealer.wrap("foo");
+ final Cookie cookie = new Cookie(COOKIE_NAME, UrlEscapers.urlFormParameterEscaper().escape(wrapped));
+ ensureMockRequest().setCookies(cookie);
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ final AuthenticationContext authCtx = prc.ensureSubcontext(AuthenticationContext.class);
+ Assert.assertTrue(authCtx.isResultCacheable());
+ final DuoPasswordlessContext duoCtx = authCtx.ensureSubcontext(DuoPasswordlessContext.class);
+ Assert.assertEquals(duoCtx.getUsername(), "foo");
+ Assert.assertTrue(duoCtx.isEnrolled());
+ }
+
+ @Test public void testFromSession() throws DataSealerException {
+ // Wrong field name.
+ ensureMockRequest().addParameter("username", "foo");
+
+ // Wrong cookie name.
+ final String wrapped = dataSealer.wrap("foo");
+ final Cookie cookie = new Cookie(COOKIE_NAME + "1", UrlEscapers.urlFormParameterEscaper().escape(wrapped));
+ ensureMockRequest().setCookies(cookie);
+
+ prc.ensureSubcontext(SessionContext.class).setIdPSession(new MockIdPSession());
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+ final AuthenticationContext authCtx = prc.ensureSubcontext(AuthenticationContext.class);
+ Assert.assertTrue(authCtx.isResultCacheable());
+ final DuoPasswordlessContext duoCtx = authCtx.ensureSubcontext(DuoPasswordlessContext.class);
+ Assert.assertEquals(duoCtx.getUsername(), "foo");
+ Assert.assertTrue(duoCtx.isEnrolled());
+ }
+
+ @Nonnull private MockHttpServletRequest ensureMockRequest() {
+ final HttpServletRequest request = HttpServletRequestResponseContext.getRequest();
+ return MockHttpServletRequest.class.cast(request);
+ }
+
+ @Nonnull private MockHttpServletResponse ensureMockResponse() {
+ final HttpServletResponse request = HttpServletRequestResponseContext.getResponse();
+ return MockHttpServletResponse.class.cast(request);
+ }
+
+ private class MockIdPSession implements IdPSession {
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String getId() {
+ return "id";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public String getPrincipalName() {
+ return "foo";
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public Instant getCreationInstant() {
+ return Instant.now();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public Instant getLastActivityInstant() {
+ return Instant.now();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean checkAddress(@Nonnull String address) throws SessionException {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean checkTimeout() throws SessionException {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public Set<AuthenticationResult> getAuthenticationResults() {
+ return CollectionSupport.emptySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public AuthenticationResult getAuthenticationResult(@Nonnull String flowId) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public AuthenticationResult addAuthenticationResult(@Nonnull AuthenticationResult result)
+ throws SessionException {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void updateAuthenticationResultActivity(@Nonnull AuthenticationResult result) throws SessionException {
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean removeAuthenticationResult(@Nonnull AuthenticationResult result) throws SessionException {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull
+ public Set<SPSession> getSPSessions() {
+ return CollectionSupport.emptySet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public SPSession getSPSession(@Nonnull String serviceId) {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public SPSession addSPSession(@Nonnull SPSession spSession) throws SessionException {
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean removeSPSession(@Nonnull SPSession spSession) throws SessionException {
+ return false;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
index 3ce30685..91a8caa6 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
@@ -42,6 +42,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Tests for the {@link DefaultDuoOIDCClientRegistry}.
*/
+ at SuppressWarnings("javadoc")
public class DefaultDuoOIDCClientRegistryTest {
/** The registry to test. */
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultRedirectURICreationStrategyTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultRedirectURICreationStrategyTest.java
index c4473cc3..db988cb7 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultRedirectURICreationStrategyTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultRedirectURICreationStrategyTest.java
@@ -44,6 +44,7 @@ import net.shibboleth.idp.plugin.authn.duo.DynamicDuoOIDCIntegration;
/** Tests for the DefaultDuoOIDCIntegration class.*/
+ at SuppressWarnings("javadoc")
public class DefaultRedirectURICreationStrategyTest {
/** Static callback path from servlet request.*/
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAudienceClaimLookupStrategyTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAudienceClaimLookupStrategyTest.java
index 10c8138d..39c0ff62 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAudienceClaimLookupStrategyTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAudienceClaimLookupStrategyTest.java
@@ -14,6 +14,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Test for the {@link DuoAudienceClaimLookupStrategy}.
*/
+ at SuppressWarnings("javadoc")
public class DuoAudienceClaimLookupStrategyTest extends AbstractDuoActionTest{
/** The strategy.*/
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoIssuerClaimLookupStrategyTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoIssuerClaimLookupStrategyTest.java
index 97eaca12..7f89f7db 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoIssuerClaimLookupStrategyTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoIssuerClaimLookupStrategyTest.java
@@ -27,6 +27,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Test for the {@link DuoIssuerClaimLookupStrategy}.
*/
+ at SuppressWarnings("javadoc")
public class DuoIssuerClaimLookupStrategyTest extends AbstractDuoActionTest{
/** The strategy.*/
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategyTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategyTest.java
index 86766e6e..3fd592de 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategyTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategyTest.java
@@ -26,6 +26,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Test for the {@link DuoNonceClaimLookupStrategy}.
*/
+ at SuppressWarnings("javadoc")
public class DuoNonceClaimLookupStrategyTest extends AbstractDuoActionTest{
/** The strategy.*/
@@ -42,7 +43,7 @@ public class DuoNonceClaimLookupStrategyTest extends AbstractDuoActionTest{
}
@Test
- public void applySuccess() {
+ public void applySuccess() throws ComponentInitializationException {
addDuoContext();
addDuoIntegrationToContext();
dc.setNonce("testnonce");
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupportTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupportTest.java
index 0ab88a02..6424628a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupportTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupportTest.java
@@ -23,6 +23,7 @@ import net.shibboleth.idp.plugin.authn.duo.DuoException;
/**
* Tests for the DuoSupport class.
*/
+ at SuppressWarnings("javadoc")
public class DuoSupportTest {
@Test public void testCreateAndExtractKeyAndNonceFromState() throws DuoException {
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategyTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategyTest.java
index 76bba890..1f04204d 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategyTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategyTest.java
@@ -27,6 +27,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Test for the {@link DuoUsernameClaimLookupStrategy}.
*/
+ at SuppressWarnings("javadoc")
public class DuoUsernameClaimLookupStrategyTest extends AbstractDuoActionTest{
/** The strategy.*/
@@ -43,7 +44,7 @@ public class DuoUsernameClaimLookupStrategyTest extends AbstractDuoActionTest{
}
@Test
- public void applySuccess() {
+ public void applySuccess() throws ComponentInitializationException {
addDuoContext();
addDuoIntegrationToContext();
dc.setUsername("username");
@@ -52,7 +53,7 @@ public class DuoUsernameClaimLookupStrategyTest extends AbstractDuoActionTest{
}
@Test
- public void applyNoUsername() {
+ public void applyNoUsername() throws ComponentInitializationException {
addDuoContext();
addDuoIntegrationToContext();
dc.setUsername(null);
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java
index 90d1f03c..2edfb70a 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java
@@ -30,6 +30,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Tests for {@link ExchangeCodeForDuoToken}.
*/
+ at SuppressWarnings("javadoc")
public class ExchangeCodeForDuoTokenTest extends AbstractDuoActionTest {
/** The action to test. */
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java
index 7e8f9ca2..6b37df43 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java
@@ -27,6 +27,7 @@ import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.DuoClientException;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCClient;
@@ -35,13 +36,14 @@ import net.shibboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.DuoRegistryException;
import net.shibboleth.idp.plugin.authn.duo.SimpleDuoOIDCIntegration;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.plugin.authn.duo.context.DuoPasswordlessContext;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.testing.ConstantSupplier;
/**
* Tests for the {@link PopulateDuoAuthenticationContext} strategy.
*/
-public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
+public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest {
/** The action to test. */
private PopulateDuoAuthenticationContext action;
@@ -80,10 +82,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
integ.setAuthorizeEndpoint(AUTHORIZE_URI);
integ.setTokenEndpoint(TOKEN_URI);
integ.setHealthCheckEndpoint(HEALTH_URI);
-
+ integ.initialize();
//set the duo integration strategy to lookup
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
@@ -105,6 +107,53 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
assertEquals(duoContext.getRedirectURIOverride(), null);
}
+ /**
+ * Test successful passwordless execution assuming a client that does not support dynamic redirect URIs.
+ *
+ * @throws ComponentInitializationException on error.
+ * @throws DuoClientException on error.
+ * @throws DuoRegistryException one error.
+ */
+ @Test
+ public void testPasswordlessExecuteSuccessWithStaticClient()
+ throws ComponentInitializationException, DuoRegistryException, DuoClientException {
+
+ final SimpleDuoOIDCIntegration integ = new SimpleDuoOIDCIntegration();
+ integ.setPasswordless(true);
+ integ.setAPIHost(API_HOST);
+ integ.setClientId(CLIENT_ID);
+ integ.setRedirectURI("https://simple-static.redirect/");
+ integ.setSecretKey(SECRET);
+ integ.setAuthorizeEndpoint(AUTHORIZE_URI);
+ integ.setTokenEndpoint(TOKEN_URI);
+ integ.setHealthCheckEndpoint(HEALTH_URI);
+ integ.initialize();
+
+ //set the duo integration strategy to lookup
+ action.setPasswordlessDuoIntegrationLookupStrategy(prc -> integ);
+
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+ final DuoOIDCClient mockClient = Mockito.mock(DuoOIDCClient.class);
+ Mockito.when(mockClientRegistry.getClientOrCreate(any(DuoOIDCIntegration.class))).thenReturn(mockClient);
+
+ action.setClientRegistry(mockClientRegistry);
+ action.initialize();
+
+ final DuoPasswordlessContext passwordlessCtx =
+ prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(DuoPasswordlessContext.class);
+ passwordlessCtx.setUsername("jdoe");
+
+ final Event event = action.execute(src);
+ //success here is a null event
+ assertNull(event);
+ //simple client, so no override set
+ final var duoContext = ac.getSubcontext(DuoOIDCAuthenticationContext.class);
+ assertNotNull(duoContext);
+ assert duoContext != null;
+ assertEquals(duoContext.getRedirectURIOverride(), null);
+ }
+
/**
* Test that should return an authentication exception if the integration is dynamic
* but a redirectURI strategy has not been set.
@@ -117,7 +166,7 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
throws ComponentInitializationException, DuoRegistryException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
@@ -151,7 +200,7 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
//cleanout pre-registered redirect
integ.setRegisteredRedirectURI(null);
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
action.setRedirectURICreationStrategy((http,duoInteg)
@@ -181,9 +230,53 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
.getRedirectURIOverride(), "https://example.com/idp/profile/Authn/Duo/2FA/callback");
}
-
-
-
+ /**
+ * Test successful execution assuming a client that does not support dynamic redirect URIs.
+ *
+ * @throws ComponentInitializationException on error.
+ * @throws DuoClientException on error.
+ * @throws DuoRegistryException one error.
+ */
+ @Test
+ public void testPasswordlessExecuteSuccessWithDynamicClient()
+ throws ComponentInitializationException, DuoRegistryException, DuoClientException {
+ final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.setPasswordless(true);
+ //cleanout pre-registered redirect
+ integ.setRegisteredRedirectURI(null);
+ integ.initialize();
+
+ //set the duo integration strategy to lookup this
+ action.setPasswordlessDuoIntegrationLookupStrategy(prc -> integ);
+ action.setRedirectURICreationStrategy((http,duoInteg)
+ -> "https://example.com/idp/profile/Authn/Duo/2FA/callback");
+ action.setHttpServletRequestSupplier(new ConstantSupplier<>(request));
+ //set http params
+ request.addHeader("Host", "example.com");
+ request.setServerPort(443);
+ request.setScheme("https");
+ //mock
+ final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+ final DuoOIDCClient mockClient = Mockito.mock(DuoOIDCClient.class);
+ Mockito.when(mockClientRegistry.getClientOrCreate(any(DuoOIDCIntegration.class))).thenReturn(mockClient);
+
+ action.setClientRegistry(mockClientRegistry);
+ action.initialize();
+
+ final DuoPasswordlessContext passwordlessCtx =
+ prc.ensureSubcontext(AuthenticationContext.class).ensureSubcontext(DuoPasswordlessContext.class);
+ passwordlessCtx.setUsername("jdoe");
+
+ final Event event = action.execute(src);
+ //success here is a null event
+ assertNull(event);
+ assertEquals(integ.getRedirectURI(), "https://example.com/idp/profile/Authn/Duo/2FA/callback");
+ final var duoContext = ac.getSubcontext(DuoOIDCAuthenticationContext.class);
+ assertNotNull(duoContext);
+ assert duoContext != null;
+ assertEquals(duoContext
+ .getRedirectURIOverride(), "https://example.com/idp/profile/Authn/Duo/2FA/callback");
+ }
/**
* Test unsuccessful execution if there is no duo integration specified.
@@ -233,8 +326,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
public void testExecuteNullUsername() throws ComponentInitializationException,
DuoRegistryException, DuoClientException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.initialize();
+
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> null);
action.setRedirectURICreationStrategy((http,duoInteg)
@@ -259,8 +354,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
public void testExecuteNullRedirectURI() throws ComponentInitializationException,
DuoRegistryException, DuoClientException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.initialize();
+
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
action.setRedirectURICreationStrategy((http,duoInteg) -> null);
@@ -284,8 +381,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
public void testExecuteClientException() throws ComponentInitializationException,
DuoRegistryException, DuoClientException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.initialize();
+
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
action.setRedirectURICreationStrategy((http,duoInteg)
@@ -313,8 +412,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
public void testExecuteNoHttpRequest() throws ComponentInitializationException,
DuoRegistryException, DuoClientException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.initialize();
+
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
action.setRedirectURICreationStrategy((http,duoInteg)
@@ -343,8 +444,10 @@ public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
public void testExecuteNoDuoContext() throws ComponentInitializationException,
DuoRegistryException, DuoClientException {
final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+ integ.initialize();
+
//set the duo integration strategy to lookup this
- action.setDuoIntegrationLookupStrategy(prc -> integ);
+ action.setStandardDuoIntegrationLookupStrategy(prc -> integ);
//lookup a username
action.setUsernameLookupStrategy(prc -> "jdoe");
action.setRedirectURICreationStrategy((http,duoInteg)
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java
index b5f5709b..563a5436 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java
@@ -26,6 +26,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Tests for the {@link ValidateDuoResponseState} action.
*/
+ at SuppressWarnings("javadoc")
public class ValidateDuoResponseStateTest extends AbstractDuoActionTest{
/** The action to test. */
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
index 9916c1e4..d584436c 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
@@ -17,7 +17,6 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
-import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import java.security.Principal;
@@ -39,14 +38,18 @@ import org.testng.annotations.Test;
import net.shibboleth.idp.authn.AuthnEventIds;
import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.principal.UsernamePrincipal;
import net.shibboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.idp.saml.authn.principal.AuthnContextClassRefPrincipal;
+import net.shibboleth.shared.collection.CollectionSupport;
import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Tests for the {@link ValidateDuoTokenAuthenticationResult} action.
*/
+ at SuppressWarnings("javadoc")
public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionTest {
/** The action to test. */
@@ -56,10 +59,8 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
public void setUp() throws Exception {
super.setup();
action = new ValidateDuoTokenAuthenticationResult();
-
}
-
/**
* Test successful execution.
*
@@ -76,8 +77,51 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
- // success here is a null event
- assertNull(event);
+ ActionTestingSupport.assertProceedEvent(event);
+ }
+
+ /**
+ * Test successful execution with factor enforcement.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public void testExecuteSuccessWithFactorCheck() throws ComponentInitializationException {
+ addDuoContext();
+ addPasswordlessDuoIntegrationToContext(CollectionSupport.singleton("duo_push"));
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
+ dc.setUsername("jdoe");
+ action.initialize();
+
+ final Event event = action.execute(src);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ //check the correct subject has been populated.
+ final var authnResult = ac.getAuthenticationResult();
+ assert authnResult != null;
+ final Subject sbj = authnResult.getSubject();
+ assertTrue(sbj.getPrincipals().contains(new UsernamePrincipal("jdoe")));
+ }
+
+ /**
+ * Test failed execution with factor enforcement.
+ *
+ * @throws ComponentInitializationException on error.
+ */
+ @Test
+ public void testExecuteFailedWithFactorCheck() throws ComponentInitializationException {
+ addDuoContext();
+ addPasswordlessDuoIntegrationToContext(CollectionSupport.singleton("sms"));
+ addAttemptedFlow("authn/DuoOIDC");
+ dc.setAuthToken(createPlainDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+ Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now(), "api.duosecurity.com", "duo_push"));
+ dc.setUsername("jdoe");
+ action.initialize();
+
+ final Event event = action.execute(src);
+ assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
}
/**
@@ -135,8 +179,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
- // success here is a null event
- assertNull(event);
+ ActionTestingSupport.assertProceedEvent(event);
}
/**
@@ -154,7 +197,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
}
/**
- * Test validation of a token who's 2FA request was denied.
+ * Test validation of a token whose 2FA request was denied.
*
* @throws ComponentInitializationException on error.
*/
@@ -192,7 +235,6 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
-
assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
}
@@ -216,7 +258,6 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
-
assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
}
@@ -261,8 +302,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
- // success here is a null event
- assertNull(event);
+ ActionTestingSupport.assertProceedEvent(event);
//check the correct subject has been populated.
final var ac = prc.getSubcontext(AuthenticationContext.class);
@@ -318,8 +358,7 @@ public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionT
action.initialize();
final Event event = action.execute(src);
- // success here is a null event
- assertNull(event);
+ ActionTestingSupport.assertProceedEvent(event);
//check the correct subject has been populated.
final var ac = prc.getSubcontext(AuthenticationContext.class);
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
index 541c4e12..590b05c7 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenClaimsTest.java
@@ -49,6 +49,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
/**
* Tests for the {@link ValidateTokenClaims} class.
*/
+ at SuppressWarnings("javadoc")
public class ValidateTokenClaimsTest extends AbstractDuoActionTest {
/** The action to test. */
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
index f7965214..e2f0e916 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateTokenSignatureTest.java
@@ -49,6 +49,7 @@ import net.shibboleth.shared.logic.ConstraintViolationException;
/**
* Tests for the {@link ValidateTokenSignature} class.
*/
+ at SuppressWarnings("javadoc")
public class ValidateTokenSignatureTest extends AbstractDuoActionTest {
/** The action to test. */
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index 8c3fb268..13ec970d 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
@@ -52,6 +52,13 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
@NonnullAfterInit private final DuoOIDCIntegration integration;
+ /**
+ * Constructor.
+ *
+ * @param integ integration
+ *
+ * @throws DuoClientException on error
+ */
public MockDuoOIDCClient_FAIL(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
integration = integ;
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
index 08b0cfb0..50ce174d 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
@@ -49,6 +49,13 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
@NonnullAfterInit private final DuoOIDCIntegration integration;
+ /**
+ * Constructor.
+ *
+ * @param integ integration
+ *
+ * @throws DuoClientException on error
+ */
public MockDuoOIDCClient_OK(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
integration = integ;
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
index e20c2c9e..f41c1077 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK_OLD_AUTH_TIME.java
@@ -50,6 +50,13 @@ public class MockDuoOIDCClient_OK_OLD_AUTH_TIME implements DuoOIDCClient{
@NonnullAfterInit private final DuoOIDCIntegration integration;
+ /**
+ * Constructor.
+ *
+ * @param integ integration
+ *
+ * @throws DuoClientException on error
+ */
public MockDuoOIDCClient_OK_OLD_AUTH_TIME(
final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
integration = integ;
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
index e8353c7a..f82c95b1 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
@@ -49,6 +49,13 @@ public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{
@NonnullAfterInit private final DuoOIDCIntegration integration;
+ /**
+ * Constructor.
+ *
+ * @param integ integration
+ *
+ * @throws DuoClientException on error
+ */
public MockDuoOIDCClient_UNKNOWN(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
integration = integ;
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
index 003b64cb..ceff4c77 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
@@ -14,6 +14,8 @@
package net.shibboleth.idp.plugin.authn.util.mock;
+import javax.annotation.Nonnull;
+
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@@ -25,10 +27,18 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
@Configuration
public class IdPPropertyConfigurer {
+ /**
+ * Install a property placeholder configurer.
+ *
+ * @return the object
+ *
+ * @throws Exception on error
+ */
@Bean
- public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
+ @Nonnull public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
final PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
pspc.setPlaceholderPrefix("%{");
+ pspc.setPlaceholderSuffix("}");
return pspc;
}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/TestResourceConverter.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/TestResourceConverter.java
new file mode 100644
index 00000000..8863c0b4
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/TestResourceConverter.java
@@ -0,0 +1,127 @@
+/*
+ * 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.util.mock;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.core.io.Resource;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Bridging class between {@link Resource} and {@link net.shibboleth.shared.resource.Resource}.
+ */
+public final class TestResourceConverter implements net.shibboleth.shared.resource.Resource {
+
+ /** The cached Spring {@link Resource}. */
+ private Resource springResource;
+
+ /**
+ * A private for shimming the provided input.
+ *
+ * @param theResource the spring resource;
+ */
+ private TestResourceConverter(@Nonnull Resource theResource) {
+
+ springResource = Constraint.isNotNull(theResource, "provided Spring Resource should not be null");
+ }
+
+ /**
+ * Return a {@link Resource} that does all the work of the provided {@link Resource}.
+ *
+ * <p>
+ * If the input implements {@link Resource} then it is cast to the output, other a shim class is
+ * generated.
+ * </p>
+ *
+ * @param springResource the input
+ * @return a {@link Resource} which reflects what the Spring one does
+ */
+ @Nonnull public static net.shibboleth.shared.resource.Resource of(@Nonnull Resource springResource) {
+ if (springResource instanceof net.shibboleth.shared.resource.Resource) {
+ return (net.shibboleth.shared.resource.Resource) springResource;
+ }
+ return new TestResourceConverter(springResource);
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public InputStream getInputStream() throws IOException {
+ return springResource.getInputStream();
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean exists() {
+ return springResource.exists();
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isReadable() {
+ return springResource.isReadable();
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean isOpen() {
+ return springResource.isOpen();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public URL getURL() throws IOException {
+ return springResource.getURL();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public URI getURI() throws IOException {
+ return springResource.getURI();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public File getFile() throws IOException {
+ return springResource.getFile();
+ }
+
+ /** {@inheritDoc} */
+ @Override public long contentLength() throws IOException {
+ return springResource.contentLength();
+ }
+
+ /** {@inheritDoc} */
+ @Override public long lastModified() throws IOException {
+ return springResource.lastModified();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public net.shibboleth.shared.resource.Resource createRelativeResource(
+ @Nonnull String relativePath) throws IOException {
+
+ return of(springResource.createRelative(relativePath));
+ }
+
+ /** {@inheritDoc} */
+ @Override public String getFilename() {
+ return springResource.getFilename();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public String getDescription() {
+ return springResource.getDescription();
+ }
+
+}
diff --git a/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.jks b/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.jks
new file mode 100644
index 00000000..147d92bb
Binary files /dev/null and b/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.jks differ
diff --git a/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.kver b/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.kver
new file mode 100644
index 00000000..2cd48df3
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/net/shibboleth/idp/plugin/authn/duo/impl/SealerKeyStore.kver
@@ -0,0 +1 @@
+CurrentVersion = 1
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
index 7865f52f..4c98e4f2 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
@@ -13,3 +13,5 @@ idp.authn.DuoOIDC.1.src = /net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn
idp.authn.DuoOIDC.1.dest = conf/authn/duo-oidc-authn-config.xml
idp.authn.DuoOIDC.2.src = /net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
idp.authn.DuoOIDC.2.dest = conf/authn/duo-oidc.properties
+idp.authn.DuoOIDC.3.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordless.vm
+idp.authn.DuoOIDC.3.dest = views/passwordless.vm
diff --git a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
index b0d73671..e2ff44b0 100644
--- a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
+++ b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
@@ -13,3 +13,5 @@ idp.authn.DuoOIDC.1.src = /net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/du
idp.authn.DuoOIDC.1.dest = conf/authn/duo-oidc-authn-config.xml
idp.authn.DuoOIDC.2.src = /net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
idp.authn.DuoOIDC.2.dest = conf/authn/duo-oidc.properties
+idp.authn.DuoOIDC.3.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordless.vm
+idp.authn.DuoOIDC.3.dest = views/passwordless.vm
diff --git a/pom.xml b/pom.xml
index d3ce4ef9..19d18657 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
<opensaml.groupId>org.opensaml</opensaml.groupId>
<opensaml.version>5.0.0</opensaml.version>
<oidc-common.groupId>net.shibboleth.oidc</oidc-common.groupId>
- <oidc-common.version>3.0.1-SNAPSHOT</oidc-common.version>
+ <oidc-common.version>3.1.0-SNAPSHOT</oidc-common.version>
<shib-shared.groupId>net.shibboleth</shib-shared.groupId>
<shib-shared.version>9.0.0</shib-shared.version>
<shib-profile.groupId>net.shibboleth</shib-profile.groupId>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list