[java-idp-plugin-duo] 03/15: Early version
Phil Smart
philip.smart at jisc.ac.uk
Tue Jul 14 14:28:59 UTC 2020
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch master
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=51621f046297684be46090a6ef30187597c4c36b
commit 51621f046297684be46090a6ef30187597c4c36b
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu May 28 13:58:57 2020 +0100
Early version
---
.gitignore | 3 +
idp-duo-api/.gitignore | 1 +
idp-duo-api/.sts4-cache/.gitignore | 1 +
idp-duo-api/pom.xml | 25 +-
.../authn/duo/AbstractDuoAuthenticationAction.java | 169 ++++++++++
.../authn/duo/DefaultDuoOIDCIntegration.java | 196 +++++++++++
.../idp/plugin/authn/duo/DuoClientException.java | 57 ++++
.../idp/plugin/authn/duo/DuoOIDCClient.java | 45 +++
.../plugin/authn/duo/DuoOIDCClientRegistry.java | 19 ++
.../idp/plugin/authn/duo/DuoOIDCIntegration.java | 21 ++
.../idp/plugin/authn/duo/DuoRegistryException.java | 46 +++
.../plugin/authn/duo/InitializingDuoClient.java | 18 ++
.../duo/context/DuoAuthenticationContext.java | 177 ++++++++++
.../idp/plugin/authn/duo/model/DuoAuthToken.java | 220 +++++++++++++
.../idp/plugin/authn/duo/model/DuoHealthCheck.java | 204 ++++++++++++
idp-duo-impl/.gitignore | 1 +
idp-duo-impl/.sts4-cache/.gitignore | 1 +
idp-duo-impl/pom.xml | 298 ++++++++++++++++-
.../duo/impl/DefaultDuoOIDCClientRegistry.java | 120 +++++++
.../authn/duo/impl/DuoOIDCAuthnController.java | 114 +++++++
.../authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java | 69 ++++
.../flows/authn/duo/duo-oidc-authn-beans.xml | 55 ++++
.../flows/authn/duo/duo-oidc-authn-flow.xml | 38 +++
.../impl/AbstractAuthnXmlFlowExecutionTests.java | 326 +++++++++++++++++++
.../duo/impl/AbstractDuoSAML2SSOFlowTest.java | 42 +++
.../duo/impl/DefaultDuoOIDCClientRegistryTest.java | 50 +++
.../plugin/authn/duo/impl/DuoAuthnFlowTest.java | 223 +++++++++++++
.../authn/duo/impl/DuoOIDCAuthnControllerTest.java | 65 ++++
.../plugin/authn/duo/impl/DuoSAML2FlowTest.java | 128 ++++++++
.../duo/impl/InMemoryCredentialValidator.java | 73 +++++
.../authn/duo/impl/PasswordAuthnFlowTest.java | 82 +++++
...rtySourcesPlaceholderConfigurerInitializer.java | 41 +++
.../plugin/authn/util/mock/MockFlowBuilder.java | 47 +++
.../META-INF/net/shibboleth/idp/StoredIdStore.sql | 11 +
.../idp/flows/resolveAndFilter/beans.xml | 66 ++++
.../resolveAndFilter/resolveAndFilter-flow.xml | 43 +++
.../resources/conf/authn/password-authn-config.xml | 147 +++++++++
idp-duo-impl/src/test/resources/conf/global.xml | 77 +++++
.../resources/conf/logback-include-console.xml | 22 ++
.../src/test/resources/conf/metadata-filters.xml | 42 +++
.../src/test/resources/conf/metadata-providers.xml | 67 ++++
.../src/test/resources/conf/relying-party.xml | 54 ++++
.../src/test/resources/credentials/ldap-server.crt | 13 +
idp-duo-impl/src/test/resources/logback-test.xml | 38 +++
.../test/resources/metadata/example-metadata.xml | 140 ++++++++
.../resources/metadata/example-sp123-metadata.xml | 358 +++++++++++++++++++++
.../.gitignore | 1 +
idp-duo-native-client-impl/.sts4-cache/.gitignore | 1 +
idp-duo-native-client-impl/pom.xml | 24 ++
.../authn/duo/sdk/impl/DuoSDKClientAdaptor.java | 122 +++++++
pom.xml | 185 ++++++++++-
src/main/resources/.gitignore | 1 +
src/main/resources/checkstyle/checkstyle.xml | 117 +++++++
53 files changed, 4479 insertions(+), 25 deletions(-)
diff --git a/.gitignore b/.gitignore
index 823af2a..99c9e46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,6 @@
.project/
.classpath
.project
+classpath:
+/.checkstyle
+/.DS_Store
diff --git a/idp-duo-api/.gitignore b/idp-duo-api/.gitignore
index b322941..87c484d 100644
--- a/idp-duo-api/.gitignore
+++ b/idp-duo-api/.gitignore
@@ -1,2 +1,3 @@
/target
/test-output
+/.checkstyle
diff --git a/idp-duo-api/.sts4-cache/.gitignore b/idp-duo-api/.sts4-cache/.gitignore
new file mode 100644
index 0000000..602f6bb
--- /dev/null
+++ b/idp-duo-api/.sts4-cache/.gitignore
@@ -0,0 +1 @@
+/classpath-data.json
diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index 804af35..d01d26e 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -1,9 +1,32 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
- <groupId>net.shibboleth.extensions.authn</groupId>
+ <groupId>net.shibboleth.plugin.authn</groupId>
<artifactId>idp-duo-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>idp-duo-api</artifactId>
+
+
+
+ <dependencies>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-authn-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>${slf4j.groupId}</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Provided Dependencies -->
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ </dependencies>
</project>
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java
new file mode 100644
index 0000000..6bf6828
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java
@@ -0,0 +1,169 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+/**
+ * A base class for Duo 2FA authentication related actions.
+ *
+ * In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
+ * and makes available the {@link DuoOIDCClient} from the looked up in-flight {@link DuoIntegration}.
+ *
+ * Duo authentication action implementations should override
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext)}
+ *
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX} FINISH
+ */
+//TODO: Can the instance variables be protected and used by the implementation? this does not seem to be best practice in the IdP.
+public class AbstractDuoAuthenticationAction extends AbstractAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractDuoAuthenticationAction.class);
+
+ /** Lookup strategy for Duo integration. */
+ @Nonnull private Function<ProfileRequestContext,DuoOIDCIntegration> duoIntegrationLookupStrategy;
+
+ /** The registry for locating the DuoClient for the established integration.*/
+ @Nonnull private DuoOIDCClientRegistry clientRegistry;
+
+ /** The Duo OIDC client .*/
+ @Nonnull private DuoOIDCClient duoClient;
+
+
+ /** Constructor.*/
+ public AbstractDuoAuthenticationAction() {
+ duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+ }
+
+ /**
+ * Set the Duo client registry.
+ *
+ * @param duoRegistry the registry
+ */
+ public void setClientRegistry(@Nonnull final DuoOIDCClientRegistry duoRegistry) {
+ clientRegistry = Constraint.isNotNull(duoRegistry,"DuoCient registry can not be null");
+ }
+
+ /**
+ * Set DuoIntegration details to use directly.
+ *
+ * @param duo Duo integration details
+ */
+ //TODO: support this?
+ public void setDuoIntegration(@Nonnull final DuoOIDCIntegration duo) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ Constraint.isNotNull(duo, "DuoIntegration cannot be null");
+ duoIntegrationLookupStrategy = FunctionSupport.constant(duo);
+ }
+
+ /**
+ * Set DuoIntegration lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDuoIntegrationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoOIDCIntegration> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ duoIntegrationLookupStrategy = Constraint.isNotNull(strategy, "DuoIntegration lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (clientRegistry == null) {
+ throw new ComponentInitializationException("Duo Client Registry cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ return false;
+ }
+
+ DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+ if (duoIntegration == null) {
+ log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ //Configure the Duo client for the established integration
+ try {
+ duoClient = clientRegistry.getIntegrationClientOrCreate(duoIntegration);
+ } catch (DuoRegistryException e) {
+ log.warn("{} No DuoClient established (located or created) for "
+ + "this integration", getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+ return false;
+ }
+
+ return doPreExecute(profileRequestContext, authenticationContext, duoClient);
+ }
+
+ /**
+ * Delegates to the {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)}
+ * to perform the actual authentication. Implementations can not override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ */
+ @Override
+ protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+ doExecute(profileRequestContext,authenticationContext,duoClient);
+ }
+
+ /**
+ * Performs this authentication action's pre-execute step. Default implementation just returns true.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ * @param DuoOIDCClient the Duo OIDC client appropriate for the located Duo integration.
+ *
+ * @return true iff execution should continue
+ */
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client) {
+ return true;
+ }
+
+
+ /**
+ * Performs this Duo authentication action using the supplied Duo client. Implementations
+ * should override this method.
+ *
+ * @param profileRequestContext the current IdP profile request context
+ * @param authenticationContext the current authentication context
+ * @param client the Duo OIDC client appropriate for the located Duo integration.
+ */
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client) {
+
+ }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
new file mode 100644
index 0000000..00f26d6
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DefaultDuoOIDCIntegration.java
@@ -0,0 +1,196 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+import java.net.URI;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Wrapper for use with Duo OIDC integrations.
+ *
+ * @since 4.0.1
+ */
+//TODO: Should this be a context class e.g. see OIDCAuthenticationResponseContext
+public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent implements DuoOIDCIntegration{
+
+ /** API host. */
+ @NonnullAfterInit @NotEmpty private String apiHost;
+
+ //TODO: do we need this?
+ /** Application key. */
+ @NonnullAfterInit @NotEmpty private String applicationKey;
+
+ /** Integration key. */
+ @NonnullAfterInit @NotEmpty private String integrationKey;
+
+ /** Secret key. */
+ @NonnullAfterInit @NotEmpty private String secretKey;
+
+ /** The redirect_uri to send the client after authorisation .*/
+ @NonnullAfterInit @NotEmpty private String redirectURI;
+
+ //TODO: check usage
+ /** Container for supported principals. */
+ @Nonnull private final Subject supportedPrincipals;
+
+ /** Constructor. */
+ public DefaultDuoOIDCIntegration() {
+ supportedPrincipals = new Subject();
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getAPIHost() {
+ return apiHost;
+ }
+
+ /**
+ * Set the API host to use.
+ *
+ * @param host API host
+ */
+ public void setAPIHost(@Nonnull @NotEmpty final String host) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ apiHost = Constraint.isNotNull(StringSupport.trimOrNull(host), "API host cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getApplicationKey() {
+ return applicationKey;
+ }
+
+ /**
+ * Set the application key to use.
+ *
+ * @param key application key
+ */
+ public void setApplicationKey(@Nonnull @NotEmpty final String key) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ applicationKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Application key cannot be null or empty");
+ }
+
+ /**
+ * Set the redirect_uri to use.
+ *
+ * @param redirectURI the redirect_uri
+ */
+ public void setRedirectURI(@Nonnull @NotEmpty final String uri) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ redirectURI = Constraint.isNotEmpty(uri, "Redirect_uri cannot be null or empty");
+ }
+
+ /**
+ * Get the redirect_uri.
+ *
+ * @return the redirect_uri.
+ */
+ @Nonnull @NotEmpty public String getRedirectURI() {
+ return redirectURI;
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getIntegrationKey() {
+ return integrationKey;
+ }
+
+ /**
+ * Set the integration key to use.
+ *
+ * @param key integration key
+ */
+ public void setIntegrationKey(@Nonnull @NotEmpty final String key) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ integrationKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Integration key cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getSecretKey() {
+ return secretKey;
+ }
+
+ /**
+ * Set the secret key to use.
+ *
+ * @param key secret key
+ */
+ public void setSecretKey(@Nonnull @NotEmpty final String key) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ secretKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Secret key cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NonnullElements @Unmodifiable
+ public <T extends Principal> Set<T> getSupportedPrincipals(@Nonnull final Class<T> c) {
+ return supportedPrincipals.getPrincipals(c);
+ }
+
+ /**
+ * Set supported non-user-specific principals that the action will include in the subjects
+ * it generates, in place of any default principals from the flow.
+ *
+ * <p>Setting to a null or empty collection will maintain the default behavior of relying on the flow.</p>
+ *
+ * @param <T> a type of principal to add, if not generic
+ * @param principals supported principals to include
+ */
+ public <T extends Principal> void setSupportedPrincipals(
+ @Nullable @NonnullElements final Collection<T> principals) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ supportedPrincipals.getPrincipals().clear();
+
+ if (principals != null && !principals.isEmpty()) {
+ supportedPrincipals.getPrincipals().addAll(Set.copyOf(principals));
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ if (apiHost == null || applicationKey == null || integrationKey == null || secretKey == null
+ || redirectURI == null) {
+ throw new ComponentInitializationException("API host, integration keys and redirect_uri must be set");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return Objects.hash(integrationKey);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ DefaultDuoOIDCIntegration other = (DefaultDuoOIDCIntegration) obj;
+ return Objects.equals(integrationKey, other.integrationKey);
+ }
+
+
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientException.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientException.java
new file mode 100644
index 0000000..59b2b04
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientException.java
@@ -0,0 +1,57 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An exception to signal an error condition during execution of a Duo client.
+ */
+ at ThreadSafe
+public class DuoClientException extends Exception{
+
+ /** Serial UID. */
+ private static final long serialVersionUID = -2380145079984333546L;
+
+ /**
+ * Constructor.
+ *
+ */
+ public DuoClientException() {
+ super();
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception to be wrapped by this one
+ */
+ public DuoClientException(String message, Throwable cause) {
+ super(message, cause);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public DuoClientException(String message) {
+ super(message);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception to be wrapped by this one
+ */
+ public DuoClientException(Throwable cause) {
+ super(cause);
+
+ }
+
+
+
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClient.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClient.java
new file mode 100644
index 0000000..bc4d738
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClient.java
@@ -0,0 +1,45 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+
+/**
+ * A client for handling Duo OIDC 2FA interactions.
+ */
+//TODO: Implementations must have a no-arg constructor?
+public interface DuoOIDCClient extends InitializingDuoClient {
+
+ /**
+ * Check the health of the Duo 2FA endpoint.
+ *
+ * @return the heath check response
+ *
+ * @throws DuoClientException if there is an error returning the health check response
+ */
+ DuoHealthCheck healthCheck() throws DuoClientException;
+
+ /**
+ * Constructs a redirection URL string with query parameters required to initiate a Duo 2FA request.
+ *
+ * @param username The user to be authenticated by Duo.
+ * @param state A randomly generated 32 character String, which is relied back to the client.
+ *
+ * @return the redirect URL
+ *
+ * @throws DuoClientException
+ */
+ String createAuthUrl(String username, String state) throws DuoClientException;
+
+ /**
+ * Verifies the code returned by Duo and exchanges it for a token which contains information pertaining to
+ * the authentication.
+ *
+ * @param code An authentication identifier which is exchanged (per OAuth2.0 spec) with Duo for a token.
+ * the token can be used to determine if authentication was successful as well as obtain meta-data
+ * about the authentication.
+ *
+ * @return the token
+ */
+ DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException;
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientRegistry.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientRegistry.java
new file mode 100644
index 0000000..35d1a85
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientRegistry.java
@@ -0,0 +1,19 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import javax.annotation.Nonnull;
+
+public interface DuoOIDCClientRegistry {
+
+ /**
+ * Get a Duo client for the given Duo integration. If the client does not exist for the given integration
+ * a new one is created and stored for later re-use. Only one client is created per integration.
+ *
+ * @param integration the Duo integration to find a client for.
+ *
+ * @return the existing or new Duo client.
+ *
+ * @throws DuoRegistryException if there is an issue locating or creating a Duo client.
+ */
+ @Nonnull DuoOIDCClient getIntegrationClientOrCreate(@Nonnull final DuoOIDCIntegration integration) throws DuoRegistryException;
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
new file mode 100644
index 0000000..ca130ca
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCIntegration.java
@@ -0,0 +1,21 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+
+/**
+ * Extension of the {@link DuoIntegration} interface to allow OIDC specific properties.
+ */
+public interface DuoOIDCIntegration extends DuoIntegration{
+
+ /**
+ * Get the redirect_uri to direct the client to after authorisation.
+ * Note, is a string to be compatible with the Duo SDK.
+ *
+ * @return the redirect_uri
+ */
+ @Nonnull String getRedirectURI();
+
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoRegistryException.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoRegistryException.java
new file mode 100644
index 0000000..b1571dc
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoRegistryException.java
@@ -0,0 +1,46 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+public class DuoRegistryException extends Exception{
+
+
+ /** Default serialUID */
+ private static final long serialVersionUID = 5597692444724770271L;
+
+ /** Constructor. */
+ public DuoRegistryException() {
+ super();
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ * @param cause exception to be wrapped by this one
+ */
+ public DuoRegistryException(String message, Throwable cause) {
+ super(message, cause);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param message exception message
+ */
+ public DuoRegistryException(String message) {
+ super(message);
+
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param cause exception to be wrapped by this one
+ */
+ public DuoRegistryException(Throwable cause) {
+ super(cause);
+
+ }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/InitializingDuoClient.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/InitializingDuoClient.java
new file mode 100644
index 0000000..6300dcd
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/InitializingDuoClient.java
@@ -0,0 +1,18 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import javax.annotation.Nonnull;
+
+/**
+ * <p>Initialize the client using the selected Duo integration.<p>
+ */
+public interface InitializingDuoClient {
+
+ /**
+ * Initialize this DuoClient using the integration provided. Will be called on creation of the client.
+ *
+ * @param integration
+ * @throws DuoClientException
+ */
+ void initialize(@Nonnull final DuoOIDCIntegration integration) throws DuoClientException;
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java
new file mode 100644
index 0000000..1f3e490
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.shbboleth.idp.plugin.authn.duo.context;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.Live;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+/**
+ * Context that carries Duo ...
+ *
+ * @parent {@link AuthenticationContext}
+ * @added After ...
+ */
+public final class DuoAuthenticationContext extends BaseContext {
+
+ /** Username. */
+ @Nullable private String username;
+
+ /** Client address. */
+ @Nullable private String clientAddress;
+
+ /** Factor. */
+ @Nullable private String duoFactor;
+
+ /** Device ID. */
+ @Nullable private String duoDevice;
+
+ /** Passcode. */
+ @Nullable private String duoPasscode;
+
+ /** PushInfo data. */
+ @Nullable private Map<String,String> pushInfo;
+
+ /** Constructor. */
+ public DuoAuthenticationContext() {
+ pushInfo = new HashMap<>();
+ }
+
+ /**
+ * Get the username.
+ *
+ * @return username
+ */
+ @Nullable public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Set the username.
+ *
+ * @param name username
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setUsername(@Nullable final String name) {
+ username = name;
+ return this;
+ }
+
+ /**
+ * Get the client address.
+ *
+ * @return address
+ */
+ @Nullable public String getClientAddress() {
+ return clientAddress;
+ }
+
+ /**
+ * Set the client address.
+ *
+ * @param address client address
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setClientAddress(@Nullable final String address) {
+ clientAddress = address;
+ return this;
+ }
+
+ /**
+ * Get the device ID.
+ *
+ * @return the Duo device identifier
+ */
+ @Nullable public String getDeviceID() {
+ return duoDevice;
+ }
+
+ /**
+ * Set the device ID.
+ *
+ * @param deviceId the Duo device identifier
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setDeviceID(@Nullable final String deviceId) {
+ duoDevice = deviceId;
+ return this;
+ }
+
+ /**
+ * Get the factor to use.
+ *
+ * @return the factor to use
+ */
+ @Nullable public String getFactor() {
+ return duoFactor;
+ }
+
+ /**
+ * Set the factor to use.
+ *
+ * @param factor the Duo factor
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setFactor(@Nullable final String factor) {
+ duoFactor = factor;
+ return this;
+ }
+
+ /**
+ * Get the passcode.
+ *
+ * @return the passcode
+ */
+ @Nullable public String getPasscode() {
+ return duoPasscode;
+ }
+
+ /**
+ * Set the passcode.
+ *
+ * @param passcode the passcode
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setPasscode(@Nullable final String passcode) {
+ duoPasscode = passcode;
+ return this;
+ }
+
+ /**
+ * Get the pushinfo.
+ *
+ * @return the pushinfo
+ */
+ @Nonnull @NonnullElements @Live public Map<String,String> getPushInfo() {
+ return pushInfo;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthToken.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthToken.java
new file mode 100644
index 0000000..d6909a5
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthToken.java
@@ -0,0 +1,220 @@
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * <p>A Duo authentication token which describes the result of 2FA.</p>
+ *
+ * <p>Analogous to an OIDC token. Descriptions are taken from the
+ * OpenID connect core 1.0 spec.</p>
+ *
+ * <p>Includes a staged builder for fluent generation.</p>
+ */
+public class DuoAuthToken {
+
+ /** The issuer identifier for the issuer of the response.*/
+ @Nonnull @NotEmpty private String iss;
+
+ /** the subject identifier.*/
+ @Nonnull @NotEmpty private String sub;
+
+ /** Shorthand name by which the End-User wishes to be referred to as.*/
+ @Nullable private String preferred_username;
+
+ /** The audience(s) that this token is indented for.*/
+ @Nonnull @NotEmpty private String aud;
+
+ /** Expiration time on or after which the ID Token MUST NOT be accepted for processing.*/
+ @Nonnull @NotEmpty private Integer exp;
+
+ /** Time at which the JWT was issued. In seconds since Unix EPOCH.*/
+ @Nonnull @NotEmpty private Double iat;
+
+ /** Time when the End-User authentication occurred. In seconds since Unix EPOCH.*/
+ @Nullable private Integer auth_time;
+
+// private AuthResult auth_result;
+// private AuthContext auth_context;
+
+
+ private DuoAuthToken(Builder builder) {
+ this.iss = builder.iss;
+ this.sub = builder.sub;
+ this.aud = builder.aud;
+ this.exp = builder.exp;
+ this.iat = builder.iat;
+ this.preferred_username = builder.preferred_username;
+ this.auth_time = builder.auth_time;
+ }
+
+ /**
+ * @return Returns the iss.
+ */
+ public String getIss() {
+ return iss;
+ }
+
+ /**
+ * @return Returns the sub.
+ */
+ public String getSub() {
+ return sub;
+ }
+
+ /**
+ * @return Returns the preferred_username.
+ */
+ public String getPreferred_username() {
+ return preferred_username;
+ }
+
+ /**
+ * @return Returns the aud.
+ */
+ public String getAud() {
+ return aud;
+ }
+
+ /**
+ * @return Returns the exp.
+ */
+ public Integer getExp() {
+ return exp;
+ }
+
+ /**
+ * @return Returns the iat.
+ */
+ public Double getIat() {
+ return iat;
+ }
+
+ /**
+ * @return Returns the auth_time.
+ */
+ public Integer getAuth_time() {
+ return auth_time;
+ }
+
+ /**
+ * Creates builder to build {@link DuoAuthToken}.
+ * @return created builder
+ */
+
+ public static IIssStage builder() {
+ return new Builder();
+ }
+
+
+ public interface IIssStage {
+ public ISubStage withIss(String iss);
+ }
+
+
+ public interface ISubStage {
+ public IAudStage withSub(String sub);
+ }
+
+
+ public interface IAudStage {
+ public IExpStage withAud(String aud);
+ }
+
+
+ public interface IExpStage {
+ public IIatStage withExp(Integer exp);
+ }
+
+
+ public interface IIatStage {
+ public IBuildStage withIat(Double iat);
+ }
+
+
+ public interface IBuildStage {
+ public IBuildStage withPreferred_username(String preferred_username);
+
+ public IBuildStage withAuth_time(Integer auth_time);
+
+ public DuoAuthToken build();
+ }
+
+ /**
+ * Builder to build {@link DuoAuthToken}.
+ */
+
+ public static final class Builder implements IIssStage, ISubStage, IAudStage, IExpStage, IIatStage, IBuildStage {
+ private String iss;
+
+ private String sub;
+
+ private String aud;
+
+ private Integer exp;
+
+ private Double iat;
+
+ private String preferred_username;
+
+ private Integer auth_time;
+
+ private Builder() {
+ }
+
+ @Override
+ public ISubStage withIss(String iss) {
+ this.iss = iss;
+ return this;
+ }
+
+ @Override
+ public IAudStage withSub(String sub) {
+ this.sub = sub;
+ return this;
+ }
+
+ @Override
+ public IExpStage withAud(String aud) {
+ this.aud = aud;
+ return this;
+ }
+
+ @Override
+ public IIatStage withExp(Integer exp) {
+ this.exp = exp;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withIat(Double iat) {
+ this.iat = iat;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withPreferred_username(String preferred_username) {
+ this.preferred_username = preferred_username;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withAuth_time(Integer auth_time) {
+ this.auth_time = auth_time;
+ return this;
+ }
+
+ @Override
+ public DuoAuthToken build() {
+ return new DuoAuthToken(this);
+ }
+ }
+
+
+
+
+
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
new file mode 100644
index 0000000..39ff993
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
@@ -0,0 +1,204 @@
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+
+
+/**
+ * <p>Represents a health check response from Duo's 2FA endpoint.</p>
+ *
+ * <p>Includes a staged builder for fluent generation.</p>
+ */
+ at Immutable
+public class DuoHealthCheck {
+
+ /** The health status of the 2FA endpoint.*/
+ @Nonnull @NotEmpty final private String status;
+
+ /** When the response was issued, as ms since Unix EPOCH.*/
+ @Nullable final private Integer responseTimestamp;
+
+ /** The response code.*/
+ @Nullable @NotEmpty final private String code;
+
+ /** The timestamp ....!!!*/
+ @Nullable final private String timestamp;
+
+ /** The response message.*/
+ @Nullable final private String message;
+
+ /** The detailed response message.*/
+ @Nullable final private String messageDetail;
+
+
+ /**
+ * Get the health status of the 2FA endpoint.
+ *
+ * @return Returns the status.
+ */
+ @Nonnull public String getStatus() {
+ return status;
+ }
+
+ /**
+ * Get the timestamp of when the response was issued.
+ *
+ * @return Returns the responseTimestamp.
+ */
+ @Nullable public Integer getResponseTimestamp() {
+ return responseTimestamp;
+ }
+
+ /**
+ * Get the response code.
+ *
+ * @return Returns the code.
+ */
+ @Nullable public String getCode() {
+ return code;
+ }
+
+ /**
+ * Get the timestamp!!
+ *
+ * @return Returns the timestamp.
+ */
+ @Nullable public String getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Get the response message.
+ *
+ * @return Returns the message.
+ */
+ @Nullable public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Get a detailed response message.
+ *
+ * @return Returns the messageDetail.
+ */
+ @Nullable public String getMessageDetail() {
+ return messageDetail;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return String.format(
+ "DuoHealthCheck [status=%s, responseTimestamp=%s, code=%s, timestamp=%s, message=%s, messageDetail=%s]",
+ status, responseTimestamp, code, timestamp, message, messageDetail);
+ }
+
+
+
+ //TODO: Add Javadoc below.
+ private DuoHealthCheck(Builder builder) {
+ this.status = builder.status;
+ this.responseTimestamp = builder.responseTimestamp;
+ this.code = builder.code;
+ this.timestamp = builder.timestamp;
+ this.message = builder.message;
+ this.messageDetail = builder.messageDetail;
+ }
+
+ /**
+ * Creates builder to build {@link DuoHealthCheck}.
+ * @return created builder
+ */
+
+ public static IStatusStage builder() {
+ return new Builder();
+ }
+
+
+ public interface IStatusStage {
+ public IBuildStage withStatus(String status);
+ }
+
+
+ public interface IBuildStage {
+ public IBuildStage withResponseTimestamp(Integer responseTimestamp);
+
+ public IBuildStage withCode(String code);
+
+ public IBuildStage withTimestamp(String timestamp);
+
+ public IBuildStage withMessage(String message);
+
+ public IBuildStage withMessageDetail(String messageDetail);
+
+ public DuoHealthCheck build();
+ }
+
+ /**
+ * Builder to build {@link DuoHealthCheck}.
+ */
+ public static final class Builder implements IStatusStage, IBuildStage {
+ private String status;
+
+ private Integer responseTimestamp;
+
+ private String code;
+
+ private String timestamp;
+
+ private String message;
+
+ private String messageDetail;
+
+ private Builder() {
+ }
+
+ @Override
+ public IBuildStage withStatus(String status) {
+ this.status = status;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withResponseTimestamp(Integer responseTimestamp) {
+ this.responseTimestamp = responseTimestamp;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withCode(String code) {
+ this.code = code;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withTimestamp(String timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ @Override
+ public IBuildStage withMessageDetail(String messageDetail) {
+ this.messageDetail = messageDetail;
+ return this;
+ }
+
+ @Override
+ public DuoHealthCheck build() {
+ return new DuoHealthCheck(this);
+ }
+ }
+
+
+
+}
diff --git a/idp-duo-impl/.gitignore b/idp-duo-impl/.gitignore
index b322941..1052633 100644
--- a/idp-duo-impl/.gitignore
+++ b/idp-duo-impl/.gitignore
@@ -1,2 +1,3 @@
/target
/test-output
+/.DS_Store
diff --git a/idp-duo-impl/.sts4-cache/.gitignore b/idp-duo-impl/.sts4-cache/.gitignore
new file mode 100644
index 0000000..602f6bb
--- /dev/null
+++ b/idp-duo-impl/.sts4-cache/.gitignore
@@ -0,0 +1 @@
+/classpath-data.json
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index b442121..f838071 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -1,9 +1,291 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>net.shibboleth.extensions.authn</groupId>
- <artifactId>idp-duo-parent</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- </parent>
- <artifactId>idp-duo-impl</artifactId>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-parent</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ </parent>
+
+
+ <artifactId>idp-duo-impl</artifactId>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-authn-api</artifactId>
+ </dependency>
+
+
+ <dependency>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-api</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-native-client-impl</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+
+ <!-- Provided Dependencies -->
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>${slf4j.groupId}</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ <!-- <scope>provided</scope> -->
+ </dependency>
+
+ <!-- Test Dependencies -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>5.6.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>5.6.2</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-filter-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-filter-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-filter-spring</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-filter-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-resolver-spring</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-consent-impl</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.sun.mail</groupId>
+ <artifactId>jakarta.mail</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.janino</groupId>
+ <artifactId>janino</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-profile-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-filter-spring</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-core</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-attribute-resolver-spring</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-authn-impl</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-authn-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-authn-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-conf</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-conf</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-profile-impl</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-profile-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-profile-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-storage-api</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-messaging-impl</artifactId>
+ <optional>true</optional>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.unboundid</groupId>
+ <artifactId>unboundid-ldapsdk</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${spring.groupId}</groupId>
+ <artifactId>spring-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-session-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-cas-impl</artifactId>
+ <scope>test</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-admin-impl</artifactId>
+ <scope>test</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.ext</groupId>
+ <artifactId>spring-extensions</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+
+
+
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+
+ <!-- <execution> <id>copy-dependencies-test</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal>
+ </goals> <configuration> <outputDirectory>${project.target.directory}</outputDirectory> </configuration> </execution> -->
+ <execution>
+ <id>unpack-test</id>
+ <phase>generate-resources</phase>
+ <goals>
+ <goal>unpack-dependencies</goal>
+ </goals>
+ <configuration>
+ <includeGroupIds>net.shibboleth.idp</includeGroupIds>
+ <includeArtifactIds>idp-conf</includeArtifactIds>
+ <outputDirectory>${project.build.directory}/classes</outputDirectory>
+ <includes>system/**,conf/**,credentials/**</includes>
+ <excludes>conf/authn/jaas-authn-config.xml</excludes>
+ <scope>test</scope>
+ <includeScope>test</includeScope>
+ <type>test-jar</type>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <excludes>
+ <exclude>system/**</exclude>
+ <exclude>conf/**</exclude>
+ <exclude>credentials/**</exclude>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistry.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistry.java
new file mode 100644
index 0000000..8b1efe8
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistry.java
@@ -0,0 +1,120 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.lang.annotation.Inherited;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+/**
+ * Default DuoClient registry.
+ *
+ * Supports lazy generation of {@link DuoOIDCClient}s. Clients are tied to and hence reused per {@link DuoOIDCIntegration}.
+ */
+ at ThreadSafe
+public class DefaultDuoOIDCClientRegistry extends AbstractIdentifiableInitializableComponent implements DuoOIDCClientRegistry{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultDuoOIDCClientRegistry.class);
+
+ /** Registry of Duo client to Duo integration.*/
+ @Nonnull @NonnullElements private final ConcurrentMap<DuoOIDCIntegration, DuoOIDCClient> clientRegistry;
+
+ /** Create clients of the specific type.*/
+ @NonnullAfterInit private Class<? extends DuoOIDCClient> clientType;
+
+ /** Function for creating a DuoClient from a DuoIntegration. */
+ @Nonnull private Function<DuoOIDCIntegration, DuoOIDCClient> clientRegistryMappingFunction;
+
+ /** Constructor .*/
+ public DefaultDuoOIDCClientRegistry() {
+ clientRegistryMappingFunction = new CreateNewClientRegistryMappingFunction();
+ clientRegistry = new ConcurrentHashMap<DuoOIDCIntegration, DuoOIDCClient>();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (clientType == null) {
+ throw new ComponentInitializationException("Duo clientType cannot be null");
+ }
+ }
+
+ /**
+ * Set the Duo OIDC client type to be used for all Duo integrations.
+ *
+ * @param type the class name as a string.
+ */
+ public void setClientType(@Nonnull @NotEmpty final String type) {
+ Constraint.isNotEmpty(type, "OIDC Client type can not be null or empty");
+ try {
+ clientType = Class.forName(type).asSubclass(DuoOIDCClient.class);
+ } catch (ClassNotFoundException | ClassCastException e) {
+ log.error("Duo client type '{}' could not be initialised",type,e);
+ throw new ConstraintViolationException("Duo Client type '"+type+"' could not be located or is"
+ + " the wrong type");
+ }
+ }
+
+ /** {@link Inherited}.*/
+ //TODO: review the 'business key' of the integration used to lookup the client. Should that be immutable?
+ @Nonnull public DuoOIDCClient getIntegrationClientOrCreate(@Nonnull final DuoOIDCIntegration integration)
+ throws DuoRegistryException {
+ //this is an atomic call, avoiding the need to synchronise here
+ DuoOIDCClient client = clientRegistry.computeIfAbsent(integration,clientRegistryMappingFunction);
+ if (client == null) {
+ throw new DuoRegistryException("DuoClient could not be found or created in the registry");
+ }
+ return client;
+ }
+
+ /**
+ * An inner class function (other implementations are not supported) for creating a new Duo client for
+ * the given Duo integration and initializing it.
+ */
+ private class CreateNewClientRegistryMappingFunction implements Function<DuoOIDCIntegration, DuoOIDCClient> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(CreateNewClientRegistryMappingFunction.class);
+
+ @Override
+ public DuoOIDCClient apply(DuoOIDCIntegration integration) {
+ try {
+ DuoOIDCClient client = clientType.getDeclaredConstructor().newInstance();
+ client.initialize(integration);
+ log.debug("Created new client {}",client);
+ return client;
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
+ | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+ log.warn("Could not create a DuoClient instance for the integration '{}'",integration.getIntegrationKey(),e);
+
+ } catch (DuoClientException e) {
+ log.warn("Could not initialise the DuoClient for the integration '{}'",integration.getIntegrationKey(),e);
+ }
+ //nothing added, nothing returned.
+ return null;
+ }
+
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
new file mode 100644
index 0000000..946dec7
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.Function;
+
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.view.RedirectView;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.idp.authn.ExternalAuthentication;
+import net.shibboleth.idp.authn.ExternalAuthenticationException;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+
+/**
+ *
+ * MVC controller for managing the Duo 2FA exchanges implemented as an {@link ExternalAuthentication} mechanism
+ *
+ * MVC controller used to initiate the Duo OIDC flow and collect the authorisation response.
+ * FINISH
+ *
+ */
+ at Controller
+ at RequestMapping("%{idp.authn.duo.OIDC.externalAuthnPath:/Authn/Duo")
+public class DuoOIDCAuthnController extends AbstractInitializableComponent{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DuoOIDCAuthnController.class);
+
+ /** Lookup strategy for Duo integration. */
+ @Nonnull private Function<ProfileRequestContext,DuoOIDCIntegration> duoIntegrationLookupStrategy;
+
+ /** The registry for locating the DuoClient for the established integration.*/
+ @Nonnull private DuoOIDCClientRegistry clientRegistry;
+
+ /** Constructor.*/
+ public DuoOIDCAuthnController() {
+ duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+ }
+
+ /**
+ * Set DuoIntegration lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDuoIntegrationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoOIDCIntegration> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ duoIntegrationLookupStrategy = Constraint.isNotNull(strategy, "DuoIntegration lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (clientRegistry == null) {
+ throw new ComponentInitializationException("Duo Client Registry cannot be null");
+ }
+ }
+
+ @GetMapping("/2FA/start")
+ public RedirectView start2FA(@Nonnull final HttpServletRequest httpRequest,
+ @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException {
+
+ final String key = ExternalAuthentication.startExternalAuthentication(httpRequest);
+ final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
+
+
+
+ }
+
+ @GetMapping("/2FA/end")
+ public void end2FA(@Nonnull final HttpServletRequest httpRequest,
+ @Nonnull final HttpServletResponse httpResponse,
+ @RequestParam("code") String code, @RequestParam("state") String state) {
+
+ // ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java
new file mode 100644
index 0000000..005a286
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java
@@ -0,0 +1,69 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shbboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * An action that checks the health of the Duo 2FA endpoint for the established Duo integration.
+ */
+//TODO: Support multiple integrations (multiplexing) e.g. the strategy.
+//TODO: Not a validation action - this is just checking the health of the endpoint change.
+//TODO: abstract the duo action integration and client lookup? done?
+public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoAuthenticationAction{
+
+ /** Event signalling that the Duo 2FA endpoints are not available. */
+ @Nonnull @NotEmpty public static final String DUO_UNAVAILABLE = "DuoUnavailable";
+
+ /** The response to look for in the duo health check response to indicate 2FA is not available.*/
+ @Nonnull @NotEmpty private static final String DUO_HEALTH_CHECK_FAIL = "FAIL";
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(HealthCheckDuoOIDCAuthAPI.class);
+
+ //TODO: do you need the integration here as well as the client?
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client) {
+
+ try {
+ final DuoHealthCheck healthCheckResponse = client.healthCheck();
+ log.trace("Duo health check response '{}'",healthCheckResponse);
+
+ if (healthCheckResponse == null) {
+ //no response.
+ log.warn("{} No Duo 2FA health check response", getLogPrefix());
+ throw new DuoClientException("Duo 2FA is unavailable, no response from the health check endpoint");
+ }
+ if (DUO_HEALTH_CHECK_FAIL.equalsIgnoreCase(healthCheckResponse.getStatus())) {
+ //2FA is unavailable.
+ log.warn("{} Duo 2FA endpoints are unhealthy, current status '{}'",getLogPrefix(),healthCheckResponse.getStatus());
+ throw new DuoClientException("Duo 2FA endpoints are unhealthy");
+ }
+ //2FA available
+ log.trace("Duo 2FA endpoints are healthy!");
+ return;
+
+ } catch (final DuoClientException e) {
+ //TODO: how best to fail, do we need to handleError() to add stuff to the context see ValidateDuoAuth
+ log.error("{} Duo API health check access failed, endpoint unavailable",getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ return;
+ }
+
+ }
+
+
+}
diff --git a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-beans.xml
new file mode 100644
index 0000000..f62375b
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-beans.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer" p:placeholderPrefix="%{"
+ p:placeholderSuffix="}" />
+
+ <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+ <bean class="net.shibboleth.idp.profile.impl.ProfileActionBeanPostProcessor" />
+
+ <bean id="shibboleth.authn.duo.OIDC.externalAuthnPath" class="java.lang.String"
+ c:_0="servletRelative:%{idp.authn.saml.externalAuthnPath:/Authn/Duo/2FA/start}" />
+
+ <!-- Default strategy function to obtain the external path. -->
+ <bean id="shibboleth.authn.duo.OIDC.externalAuthnPathStrategy" parent="shibboleth.Functions.Constant"
+ c:target-ref="shibboleth.authn.duo.OIDC.externalAuthnPath" />
+
+ <bean id="shibboleth.authn.duo.OIDC.DuoIntegration" class="net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration"
+ p:APIHost="%{idp.duo.oidc.apiHost:none}"
+ p:applicationKey="%{idp.duo.oidc.applicationKey:none}"
+ p:integrationKey="%{idp.duo.oidc.integrationKey:none}"
+ p:secretKey="%{idp.duo.oidc.secretKey:none}"
+ p:redirectURI="%{idp.duo.oidc.redirectUri}"/>
+ <bean id="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
+ c:target-ref="shibboleth.authn.duo.OIDC.DuoIntegration" />
+
+ <!-- Singleton client registry for attaching Duo client instances to Duo integrations -->
+ <bean id="shibboleth.authn.duo.OIDC.clientRegistry" scope="singleton"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
+ p:clientType="%{idp.duo.oidc.clientType:net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientAdaptor}"/>
+
+ <bean id="HealthCheckDuoOIDCAuthAPI" scope="prototype" class="net.shibboleth.idp.plugin.authn.duo.impl.HealthCheckDuoOIDCAuthAPI"
+ parent="shibboleth.authn.duo.OIDC.DuoClientActions"/>
+
+ <!-- parent to aid in the configuration of AbstractDuoAuthenticationAction beans-->
+ <bean id="shibboleth.authn.duo.OIDC.DuoClientActions" abstract="true"
+ p:duoIntegrationLookupStrategy-ref="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy"
+ p:clientRegistry-ref="shibboleth.authn.duo.OIDC.clientRegistry"/>
+
+ <!-- <bean id="ValidateDuoAuthAPI" scope="prototype" class="net.shibboleth.idp.ext.authn.duo.impl.ValidateDuoOIDCAuthAPI"
+ p:usernameLookupStrategy-ref="shibboleth.authn.Duo.UsernameLookupStrategy"
+ p:duoIntegrationLookupStrategy-ref="shibboleth.authn.Duo.NonBrowser.DuoIntegrationStrategy"
+ p:addDefaultPrincipals="#{getObject('shibboleth.authn.Duo.addDefaultPrincipals') ?: true}"
+ p:preauthAuthenticator-ref="DuoPreauthAuthenticator" p:authAuthenticator-ref="DuoAuthAuthenticator"
+ p:classifiedMessages="#{getObject('shibboleth.authn.Duo.ClassifiedMessageMap') ?: getObject('shibboleth.authn.Duo.DefaultClassifiedMessageMap')}"
+ p:resultCachingPredicate="#{getObject('shibboleth.authn.Duo.resultCachingPredicate')}" /> -->
+
+</beans>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
new file mode 100644
index 0000000..5f9390a
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
@@ -0,0 +1,38 @@
+<flow xmlns="http://www.springframework.org/schema/webflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="authn.abstract">
+
+ <!-- This is a login flow for the Duo OIDC authentication flow -->
+
+ <!-- TODO: throws an AuthnException if the endpoint is not healthy, no backoff etc. -->
+ <action-state id="CheckDuoOIDCAuthAPI">
+ <evaluate expression="HealthCheckDuoOIDCAuthAPI" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="StartDuo2FA" />
+ </action-state>
+
+
+ <view-state id="StartDuo2FA"
+ view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.duo.OIDC.externalAuthnPathStrategy').apply(opensamlProfileRequestContext), flowExecutionContext.getKey().toString())}">
+ <attribute name="csrf_excluded" value="true" type="boolean" />
+ <!-- <on-render>
+ <evaluate
+ expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).addSubcontext(new net.shibboleth.idp.authn.context.ExternalAuthenticationContext(new net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl(false)), true).setFlowExecutionUrl(flowExecutionUrl + '&_eventId_proceed=1')" />
+ <evaluate
+ expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).addSubcontext(new net.shibboleth.idp.saml.saml2.profile.impl.SAMLAuthnContext(EncodeMessage, messageDecoderFactory))"
+ result="flowScope.samlContext" />
+ <evaluate expression="flowScope.samlContext.setOutboundMessageHandler(PreEncodeMessageHandler)" />
+ </on-render> -->
+
+ <transition on="proceed" to="ProcessResponse" />
+ </view-state>
+
+ <view-state id="DisplayDuoUsernamePasswordPage" view="duo-oidc">
+
+ </view-state>
+
+ <!-- We only need the username -->
+ <!-- <action-state id="ExtractUsernameFromFormRequest"> </action-state> -->
+
+ <bean-import resource="duo-oidc-authn-beans.xml" />
+</flow>
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
new file mode 100644
index 0000000..82fae20
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
@@ -0,0 +1,326 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.annotation.Nonnull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mockito;
+import org.opensaml.core.config.ConfigurationService;
+import org.opensaml.core.metrics.FilteredMetricRegistry;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.mock.env.MockPropertySource;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.config.FlowDefinitionResource;
+import org.springframework.webflow.config.FlowDefinitionResourceFactory;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.test.MockExternalContext;
+import org.springframework.webflow.test.MockFlowBuilderContext;
+import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecutionTests;
+import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.net.HttpHeaders;
+
+import junit.framework.Assert;
+import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+
+
+/**
+ * <p>Base test class for FINISH</p>
+ *
+ * <p>One flow per concrete implementation!? One initalisation, so one set of variables?!</p>
+ *
+ * <p>Note, you can not use test context creation annotations e.g. @SpringJUnitWebConfig to help
+ * build the application context, because the {@link AbstractExternalizedFlowExecutionTests} creates a new {@link StaticApplicationContext}
+ * when building the flow (see buildFlow). This is different from the {@link ApplicationContext} created by {@link ContextConfiguration}
+ * annotation processors. As a result, those bean configurations and environment variables need to be injected
+ * inside {@link #configureFlowBuilderContext(MockFlowBuilderContext)}</p>
+ */
+public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlowExecutionTests{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthnXmlFlowExecutionTests.class);
+
+ /** The path resource to the authentication flow to be tested. */
+ @Nonnull private String flowPath;
+
+ /**
+ * Map of additional flow resources used to support the construction of the test flow.
+ * These are only possible parents of the flow, and can not be used to load other flows?!
+ * <p>The key is the path to the resource, the value is the flowID.</p>
+ */
+ @Nonnull private Map<String,String> flowModelResources;
+
+ /** Mock flows to add as stub subflows when executing the main test flow. */
+ @Nonnull private List<Flow> subflows;
+
+ /** Map of properties to set.*/
+ @Nonnull private Map<String,String> mockProperties;
+
+ /** Mock request. */
+ @Nonnull protected MockHttpServletRequest mockRequest;
+
+ /** Mock response. */
+ @Nonnull protected MockHttpServletResponse mockResponse;
+
+ /** Mock external context. */
+ @Nonnull protected MockExternalContext externalContext;
+
+
+ /** Constructor. */
+ //TODO: what should be initialised once per concrete test class, what per test.
+ public AbstractAuthnXmlFlowExecutionTests() {
+ flowModelResources = Collections.emptyMap();
+ subflows = Collections.emptyList();
+ mockProperties = Collections.emptyMap();
+ }
+
+ /**
+ * Set the flow model resources (parent flows) that support the assembly of the flow to test.
+ *
+ * @param testFlowModelResources the parent flows.
+ */
+ public void setFlowModelResources(@Nonnull final Map<String,String> testFlowModelResources) {
+ Assert.assertNotNull(testFlowModelResources);
+ flowModelResources = testFlowModelResources;
+ }
+
+ /**
+ * Set the path to the flow to test.
+ *
+ * @param testFlowPath the path to the file to test.
+ */
+ public void setFlowPath(@Nonnull @NotEmpty final String testFlowPath) {
+ Assert.assertNotNull(testFlowPath);
+ flowPath = testFlowPath;
+ }
+
+ public void setMockProperties(@Nonnull final Map<String,String> properties) {
+ Assert.assertNotNull(properties);
+ mockProperties = properties;
+ }
+
+ /**
+ * Set the mock subflows to be used with the flow to test.
+ *
+ * @param mockSubflows the mock subflows.
+ */
+ public void setSubflows(@Nonnull @NonnullElements List<Flow> mockSubflows) {
+ Assert.assertNotNull(mockSubflows);
+ subflows = mockSubflows;
+ }
+
+
+ public void addHttpBasicAuthHeader(@Nonnull final String username, @Nonnull final String password) {
+ Assert.assertNotNull(username);
+ Assert.assertNotNull(password);
+ mockRequest.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+
+ Base64.getEncoder().encodeToString((username+":"+password).getBytes()));
+ }
+
+ /**
+ * Setup the mock objects before each method.
+ */
+ //TODO: should this be initialised once, or once before test?
+ @BeforeEach public void initializeMocks() {
+ mockRequest = new MockHttpServletRequest();
+ mockResponse = new MockHttpServletResponse();
+ externalContext = new MockExternalContext();
+ externalContext.setNativeRequest(mockRequest);
+ externalContext.setNativeResponse(mockResponse);
+ }
+
+ @Override
+ protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
+ return resourceFactory.createClassPathResource(flowPath, getClass());
+ }
+
+ @Override
+ protected FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory) {
+
+ if (!flowModelResources.isEmpty()) {
+
+ final FlowDefinitionResource[] resources = new FlowDefinitionResource[flowModelResources.size()];
+
+ int rIndex = 0;
+ for (Map.Entry<String, String> entry : flowModelResources.entrySet()) {
+ resources[rIndex++] = resourceFactory.createResource(entry.getKey(), null,entry.getValue());
+ }
+ return resources;
+ }
+ return null;
+ }
+
+ @Override
+ protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
+ registerMockBeanDefinitions(builderContext);
+ registerMockPropertySource(builderContext);
+ registerMockSubflows(builderContext);
+ }
+
+ /**
+ * Register the subflows with the builder context.
+ *
+ * @param builderContext the builder context to register the subflows with.
+ */
+ private void registerMockSubflows(@Nonnull final MockFlowBuilderContext builderContext) {
+ Assert.assertNotNull(builderContext);
+
+ subflows.forEach(flow -> builderContext.registerSubflow(flow));
+ }
+
+ /**
+ * Register mock beans that are not necessary to configure for authentication testing
+ * but are required for authentication flows to proceed.
+ *
+ * @param builderContext the context used to register the beans.
+ */
+ private void registerMockBeanDefinitions(@Nonnull final MockFlowBuilderContext builderContext) {
+
+ // builderContext.registerBean("shibboleth.AttributeResolverService", Mockito.mock(ReloadableService.class));
+ builderContext.registerBean("shibboleth.DefaultIdentifierGenerationStrategy",
+ new SecureRandomIdentifierGenerationStrategy());
+ builderContext.registerBean("shibboleth.HttpServletRequest",mockRequest);
+ builderContext.registerBean("shibboleth.HttpServletResponse", mockResponse);
+ builderContext.registerBean("shibboleth.PostLoginSubjectCanonicalizationFlows", Collections.emptyList());
+
+ //create a real JSON parser
+ builderContext.registerBean("shibboleth.JSONObjectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
+
+ //register a disabled metric registry.
+ //TODO: why do we have to create a metric registry - should be allowed to be null?
+ FilteredMetricRegistry registry = new FilteredMetricRegistry();
+ ConfigurationService.register(MetricRegistry.class, registry);
+
+ //register shibboleth function constants.
+ //TODO: most the beans needed come from utilities.xml - maybe try to register those?
+ addBeanDefinition(builderContext, "shibboleth.Functions.Constant",BeanDefinitionBuilder.
+ genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+ .setFactoryMethod("constant").setAbstract(true).getBeanDefinition());
+
+ //add commaDelimStringArray bean
+// addBeanDefinition(builderContext, "shibboleth.CommaDelimStringArray",
+// BeanDefinitionBuilder.genericBeanDefinition(org.springframework.util.StringUtils.class)
+// .setAbstract(true).setFactoryMethod("commaDelimitedListToStringArray").getBeanDefinition());
+
+ //add conversionService bean.
+// Set<Converter<?,?>> converters = Set.of(new StringToIPRangeConverter(),new BooleanToPredicateConverter(),
+// new StringBooleanToPredicateConverter(), new StringToResourceConverter(), new FunctionToFunctionConverter(),
+// new PredicateToPredicateConverter(), new StringToDurationConverter());
+// addBeanDefinition(builderContext,"conversionService",
+// BeanDefinitionBuilder.genericBeanDefinition(org.springframework.context.support.ConversionServiceFactoryBean.class)
+// .addPropertyValue("converters", converters).getBeanDefinition());
+
+ //mock the velocity template
+ // builderContext.registerBean("shibboleth.VelocityEngine", Mockito.mock(net.shibboleth.ext.spring.velocity.VelocityEngineFactoryBean.class));
+
+
+ }
+
+
+ /**
+ * Adds the bean to the {@link StaticApplicationContext} contained in the builder context.
+ *
+ * @param builderContext to add the bean to.
+ * @param beanName the name of the bean.
+ * @param bean the bean.
+ */
+ private void addBeanDefinition(@Nonnull final MockFlowBuilderContext builderContext,
+ @Nonnull final String beanName, @Nonnull final BeanDefinition bean) {
+
+ Assert.assertNotNull(builderContext);
+ Assert.assertNotNull(beanName);
+ Assert.assertNotNull(bean);
+ Assert.assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
+
+ BeanFactory factory = ((ConfigurableApplicationContext) builderContext.getApplicationContext()).getBeanFactory();
+
+ Assert.assertNotNull(factory);
+ Assert.assertTrue(factory instanceof DefaultListableBeanFactory);
+
+ ((DefaultListableBeanFactory)factory).registerBeanDefinition(beanName, bean);;
+
+ }
+
+ /**
+ * Register a {@link MockPropertySource} with the {@link Environment} of the current flow builders
+ * {@link ApplicationContext}. Set properties required to run an authentication flow.
+ *
+ * @param builderContext the builder context to attach the mock property source to.
+ */
+ private void registerMockPropertySource(@Nonnull final MockFlowBuilderContext builderContext) {
+
+ Assert.assertNotNull(builderContext);
+ Assert.assertNotNull(builderContext.getApplicationContext());
+ Assert.assertNotNull(builderContext.getApplicationContext().getEnvironment());
+ Assert.assertTrue(builderContext.getApplicationContext().getEnvironment() instanceof StandardEnvironment);
+
+ final MockPropertySource mock = new MockPropertySource();
+
+ //add the standard mocks to enable IdP function.
+ mock.setProperty("idp.home", "classpath:");
+ mock.setProperty("idp.webflows", "classpath*:/flows");
+
+ //add test specific mocks
+ mockProperties.forEach((key,value) -> mock.setProperty(key, value));
+
+ ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).getPropertySources().addFirst(mock);
+
+ }
+
+ /**
+ * Build a {@link ProfileRequestContext} by configuring a suitable context tree e.g. a {@link AuthenticationContext}.
+ *
+ * @param forceAuthn force authentication
+ * @return a profile request context.
+ */
+ @Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final boolean forceAuthn) {
+
+ ProfileRequestContext prc = new ProfileRequestContext();
+ AuthenticationContext ac = new AuthenticationContext();
+ AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+ afd.setId("authn/DuoOIDC");
+ ac.setAttemptedFlow(afd);
+ ac.addSubcontext(new RelyingPartyUIContext());
+ ac.setForceAuthn(forceAuthn);
+ prc.addSubcontext(ac);
+ SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
+ scc.setPrincipalName("jdoe");
+ prc.addSubcontext(scc);
+ SessionContext sc = new SessionContext();
+ IdPSession session = Mockito.mock(IdPSession.class);
+ Mockito.when(session.getPrincipalName()).thenReturn("jdoe");
+ sc.setIdPSession(session);
+ prc.addSubcontext(sc);
+
+ return prc;
+ }
+
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java
new file mode 100644
index 0000000..b515a58
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java
@@ -0,0 +1,42 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.net.URL;
+
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.classic.util.ContextInitializer;
+import ch.qos.logback.core.joran.spi.JoranException;
+import ch.qos.logback.core.util.StatusPrinter;
+import net.shibboleth.idp.test.flows.saml2.AbstractSAML2SSOFlowTest;
+
+public class AbstractDuoSAML2SSOFlowTest extends AbstractSAML2SSOFlowTest{
+
+
+ /**
+ * Reload the configuration of the active logger context. This is useful if a logback-text.xml
+ * has been provided for the plugin which would otherwise be reset by the <code>shibboleth.LoggingService</code>
+ * on initialisation of the IdP.
+ */
+ protected void reloadLogger() {
+ LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+
+ ContextInitializer ci = new ContextInitializer(loggerContext);
+ URL url = ci.findURLOfDefaultConfigurationFile(true);
+
+ try {
+ JoranConfigurator configurator = new JoranConfigurator();
+ configurator.setContext(loggerContext);
+ loggerContext.reset();
+ configurator.doConfigure(url);
+ loggerContext.start();
+ } catch (JoranException je) {
+ // StatusPrinter will handle this
+ }
+ StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
+ }
+
+}
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
new file mode 100644
index 0000000..a2ec73b
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
@@ -0,0 +1,50 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import junit.framework.Assert;
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
+import net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientAdaptor;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+class DefaultDuoOIDCClientRegistryTest {
+
+ @Test
+ void test() throws DuoRegistryException {
+ DefaultDuoOIDCClientRegistry factory = new DefaultDuoOIDCClientRegistry();
+ factory.setClientType(DuoSDKClientAdaptor.class.getName());
+ DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+ integ.setAPIHost("host.com");
+ integ.setApplicationKey("SDFGHJKLPOIUYTREWQZXCVBNMLKJHGFAQWERTYU");
+ integ.setIntegrationKey("DIU6GEFWG5LIUBVV2M3P");
+ integ.setRedirectURI("http://localhost/");
+ integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+
+ DuoOIDCClient client = factory.getIntegrationClientOrCreate(integ);
+ DuoOIDCClient clientTwo = factory.getIntegrationClientOrCreate(integ);
+
+ Assert.assertEquals(client, clientTwo);
+
+ //set to a different value
+ integ.setIntegrationKey("DIU6GEFWG5LIUBVV2M3B");
+ //should get a new client.
+ DuoOIDCClient clientThree = factory.getIntegrationClientOrCreate(integ);
+
+ System.out.println("Client: "+client+" Client2: "+clientTwo+" Client3: "+clientThree);
+ Assert.assertNotSame(client, clientThree);
+
+ }
+
+ @Test public void testUknownClientType() {
+ DefaultDuoOIDCClientRegistry factory = new DefaultDuoOIDCClientRegistry();
+ Assertions.assertThrows(ConstraintViolationException.class,() -> factory.setClientType("no.such.client.DuoClient"));
+ }
+
+
+
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthnFlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthnFlowTest.java
new file mode 100644
index 0000000..79b6c24
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthnFlowTest.java
@@ -0,0 +1,223 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import static java.lang.String.format;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.annotation.Nonnull;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.execution.FlowExecution;
+
+import junit.framework.Assert;
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.idp.plugin.authn.util.mock.MockFlowBuilder;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+
+
+//TODO: IS THIS A ONE FLOW TEST? if so, maybe set FLOW to test in constructor and the supporting stuff in the test method?
+public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DuoAuthnFlowTest.class);
+
+ /** Path to the flow to be tested.*/
+ @Nonnull final String flowToTest = "/flows/authn/duo/duo-oidc-authn-flow.xml";
+
+ /**
+ * Map of flow resources that support building the flow to test.
+ * These are only for parent flows, not subflows.
+ */
+ @Nonnull @NonnullElements @Unmodifiable final Map<String,String> flowResources = Map.of(
+ "/system/flows/authn/authn-abstract-flow.xml","authn.abstract",
+ "/conf/authn/authn-events-flow.xml","authn.events",
+ "/flows/authn/conditions/conditions-flow.xml","authn/conditions");
+
+ /** List of mocked subflows.*/
+ @Nonnull @NonnullElements @Unmodifiable private final List<Flow> subflows = List.of(MockFlowBuilder.MockNoOpFlow("c14n"));
+
+ /** The username of the user to test.*/
+ @Nonnull private final static String USERNAME = "jdoe";
+
+ /** The password of the user to test.*/
+ @Nonnull private final static String PASSWORD = "changeit";
+
+ /**
+ * Runs the flow to fail:
+ * <ol>
+ * <li>Uses baisc auth, so skips username and password login view</li>
+ * </ol>
+ */
+ @Test public void testDuoAuthnFlowUnavailable() {
+
+ setFlowPath(flowToTest);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
+ addHttpBasicAuthHeader(USERNAME,PASSWORD);
+
+ final Map<String,String> mockProperties = Map.of(
+ "idp.duo.oidc.redirectUri","http://localhost/callback",
+ "idp.duo.oidc.apiHost","https://api.duosecurity.com/oauth/v1/token",
+ "idp.duo.oidc.integrationKey","DIU6GEFWG5LIUBVV2M3P",
+ "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
+ "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.duo.impl.MockDuoOIDCClient_FAIL");
+
+ setMockProperties(mockProperties);
+
+ LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+ inputMap.put("calledAsSubflow", true);
+
+ //you must do this bit, not startFlow?
+ FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
+ flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
+ updateFlowExecution(flowExecution);
+ flowExecution.start(inputMap, externalContext);
+ assertFlowExecutionEnded();
+
+ }
+
+ @Test public void testDuoAuthnFlow() {
+
+ setFlowPath(flowToTest);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
+ addHttpBasicAuthHeader(USERNAME,PASSWORD);
+
+ final Map<String,String> mockProperties = Map.of(
+ "idp.duo.oidc.redirectUri","http://localhost/callback",
+ "idp.duo.oidc.apiHost","https://api.duosecurity.com/oauth/v1/token",
+ "idp.duo.oidc.integrationKey","DIU6GEFWG5LIUBVV2M3P",
+ "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
+ "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.duo.impl.MockDuoOIDCClient_OK");
+
+ setMockProperties(mockProperties);
+
+ LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+ inputMap.put("calledAsSubflow", true);
+
+ //you must do this bit, not startFlow?
+ FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
+ flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
+ updateFlowExecution(flowExecution);
+ flowExecution.start(inputMap, externalContext);
+ assertFlowExecutionActive();
+ assertCurrentStateEquals("StartDuo2FA");
+
+ }
+
+
+}
+
+/**
+ * Mock a Duo client which is available and returns a valid response.
+ */
+class MockDuoOIDCClient_OK implements DuoOIDCClient{
+
+ @Nonnull private static final String STATUS = "OK";
+
+ @Nonnull private static final String ISS = "https://api.duosecurity.com/oauth/v1/token";
+
+ @Nonnull private static final String SUB = "jdoe";
+
+ @Nonnull private static final String AUD = "INT_KEY";
+
+ @Nonnull private static final Integer EXP = 1590070939;
+
+ @Nonnull private static final Double IAT = 1590070939d;
+
+ @Nonnull private static final String REDIRECT_URI = "http://localhost:8443/callback";
+
+ //TODO: finish values for the token.
+
+ public MockDuoOIDCClient_OK() {
+ }
+
+ @Override
+ public void initialize(DuoOIDCIntegration integration) throws DuoClientException {
+ //no-op method, nothing to initalise
+
+ }
+
+ @Override
+ public DuoHealthCheck healthCheck() throws DuoClientException {
+ return DuoHealthCheck.builder().withStatus(STATUS).
+ withTimestamp(Long.toString(System.currentTimeMillis())).build();
+ }
+
+ @Override
+ public String createAuthUrl(String username, String state) throws DuoClientException {
+ return format("?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
+ REDIRECT_URI, AUD, "JWT");
+ }
+
+ @Override
+ public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+ return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
+ withExp(EXP).withIat(IAT).build();
+ }
+
+}
+
+/**
+ * Mock a Duo client which is not available (unhealthy).
+ */
+class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
+
+ @Nonnull private static final String STATUS = "FAIL";
+
+ @Nonnull private static final String ISS = "https://api.duosecurity.com/oauth/v1/token";
+
+ @Nonnull private static final String SUB = "jdoe";
+
+ @Nonnull private static final String AUD = "INT_KEY";
+
+ @Nonnull private static final Integer EXP = 1590070939;
+
+ @Nonnull private static final Double IAT = 1590070939d;
+
+ @Nonnull private static final String REDIRECT_URI = "http://localhost:8443/callback";
+
+ //TODO: finish values for the token.
+
+ public MockDuoOIDCClient_FAIL() {
+ }
+
+ @Override
+ public void initialize(DuoOIDCIntegration integration) throws DuoClientException {
+ //no-op method, nothing to initalise
+
+ }
+
+ @Override
+ public DuoHealthCheck healthCheck() throws DuoClientException {
+ return DuoHealthCheck.builder().withStatus(STATUS).
+ withTimestamp(Long.toString(System.currentTimeMillis())).build();
+ }
+
+ @Override
+ public String createAuthUrl(String username, String state) throws DuoClientException {
+ return format("?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
+ REDIRECT_URI, AUD, "JWT");
+ }
+
+ @Override
+ public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+ return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
+ withExp(EXP).withIat(IAT).build();
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
new file mode 100644
index 0000000..3e4e079
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ *
+ */
+public class DuoOIDCAuthnControllerTest {
+
+ /**
+ * @throws java.lang.Exception
+ */
+ @BeforeAll
+ static void setUpBeforeClass() throws Exception {
+ }
+
+ /**
+ * @throws java.lang.Exception
+ */
+ @AfterAll
+ static void tearDownAfterClass() throws Exception {
+ }
+
+ /**
+ * @throws java.lang.Exception
+ */
+ @BeforeEach
+ void setUp() throws Exception {
+ }
+
+ /**
+ * @throws java.lang.Exception
+ */
+ @AfterEach
+ void tearDown() throws Exception {
+ }
+
+ @Test
+ void test() {
+ fail("Not yet implemented");
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java
new file mode 100644
index 0000000..313066e
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.io.UnsupportedEncodingException;
+import java.net.URL;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.xml.io.MarshallingException;
+import org.opensaml.core.xml.util.XMLObjectSupport;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.saml.common.SAMLObject;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.core.AuthnRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.annotations.Test;
+import org.w3c.dom.Element;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.classic.util.ContextInitializer;
+import ch.qos.logback.core.joran.spi.JoranException;
+import ch.qos.logback.core.util.StatusPrinter;
+import net.shibboleth.idp.log.LogbackLoggingService;
+import net.shibboleth.idp.test.flows.saml2.AbstractSAML2SSOFlowTest;
+import net.shibboleth.utilities.java.support.codec.Base64Support;
+import net.shibboleth.utilities.java.support.codec.EncodingException;
+import net.shibboleth.utilities.java.support.xml.SerializeSupport;
+
+
+/**
+ * Temporary class to see if a flow can complete.
+ */
+ at ContextConfiguration(initializers = PropertySourcesPlaceholderConfigurerInitializer.class)
+public class DuoSAML2FlowTest extends AbstractDuoSAML2SSOFlowTest{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DuoSAML2FlowTest.class);
+
+ /** Flow id. */
+ @Nonnull public final static String FLOW_ID = "SAML2/POST/SSO";
+
+ /**
+ *
+ * End to end SAML2 SSO POST flow test.
+ *
+ * @throws Exception if an exception occurs.
+ */
+ @Test public void testSAML2SSOFlow() throws Exception {
+
+ //TODO: do this automatically?
+ reloadLogger();
+
+ buildRequest();
+
+ overrideEndStateOutput(FLOW_ID);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+
+ validateResult(result, FLOW_ID);
+
+ }
+
+ /**
+ * Build the {@link MockHttpServletRequest}.
+ *
+ * @throws Exception if an error occurs
+ */
+ //TODO: this is in the SAML2POSTSSOFlowTest class as well, should we extend here etc.
+ public void buildRequest() throws Exception {
+
+ request.setMethod("POST");
+ request.setRequestURI("/idp/profile/" + FLOW_ID);
+
+ final AuthnRequest authnRequest = buildAuthnRequest(request);
+ authnRequest.setDestination(getDestinationPost(request));
+
+ final MessageContext messageContext =
+ buildOutboundMessageContext(authnRequest, SAMLConstants.SAML2_POST_BINDING_URI);
+ final SAMLObject message = (SAMLObject) messageContext.getMessage();
+ final String encodedMessage = encodeMessage(message);
+ request.addParameter("SAMLRequest", encodedMessage);
+ }
+
+
+
+ /**
+ * Base64 the given SAML message.
+ *
+ * @param message the SAML message
+ * @return Base64 encoded message
+ * @throws MarshallingException if there is a problem marshalling the XMLObject
+ * @throws UnsupportedEncodingException If the named charset is not supported
+ * @throws EncodingException if the message can not be base64 encoded
+ */
+ @Nonnull public String encodeMessage(@Nonnull final SAMLObject message) throws MarshallingException,
+ UnsupportedEncodingException, EncodingException {
+ final Element domMessage = XMLObjectSupport.marshall(message);
+ final String messageXML = SerializeSupport.nodeToString(domMessage);
+
+ return Base64Support.encode(messageXML.getBytes("UTF-8"), Base64Support.UNCHUNKED);
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/InMemoryCredentialValidator.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/InMemoryCredentialValidator.java
new file mode 100644
index 0000000..1933395
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/InMemoryCredentialValidator.java
@@ -0,0 +1,73 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginException;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.idp.authn.AbstractUsernamePasswordCredentialValidator;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.UsernamePasswordContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Simple in-memory credential validator that validates the username and password against a single
+ * set of configured credentials. Used for testing.
+ */
+ at ThreadSafe
+public class InMemoryCredentialValidator extends AbstractUsernamePasswordCredentialValidator{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(InMemoryCredentialValidator.class);
+
+ /** The configured username.*/
+ @Nonnull private String username;
+
+ /** The configured password.*/
+ @Nonnull private String password;
+
+ public InMemoryCredentialValidator(@Nonnull @NotEmpty final String uname, @Nonnull @NotEmpty final String pssword) {
+ username = Constraint.isNotEmpty(uname, "Username can not be null or empty");
+ password = Constraint.isNotEmpty(pssword, "Password can not be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ }
+
+ @Override
+ protected Subject doValidate(ProfileRequestContext profileRequestContext,
+ AuthenticationContext authenticationContext, UsernamePasswordContext usernamePasswordContext,
+ WarningHandler warningHandler, ErrorHandler errorHandler) throws Exception {
+
+
+ final String requestUsername = usernamePasswordContext.getTransformedUsername();
+ final String requestPassword = usernamePasswordContext.getPassword();
+
+ log.debug("{} Attempting to authenticate user '{}' ", getLogPrefix(), username);
+
+ if (password.equals(requestPassword) && username.equals(requestUsername)) {
+ log.info("{} Login by '{}' succeeded", getLogPrefix(), username);
+ return populateSubject(new Subject(), usernamePasswordContext);
+ }
+
+ log.info("{} Login by '{}' failed", getLogPrefix(), username);
+
+ final LoginException e = new LoginException(AuthnEventIds.INVALID_CREDENTIALS);
+ if (errorHandler != null) {
+ errorHandler.handleError(profileRequestContext, authenticationContext, e,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ }
+ throw e;
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java
new file mode 100644
index 0000000..b2df852
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java
@@ -0,0 +1,82 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.execution.FlowExecution;
+
+import net.shibboleth.idp.plugin.authn.util.mock.MockFlowBuilder;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+
+//TODO: IS THIS A ONE FLOW TEST? if so, maybe set FLOW to test in constructor and the supporting stuff in the test method?
+public class PasswordAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(PasswordAuthnFlowTest.class);
+
+ /** Path to the flow to be tested.*/
+ @Nonnull final String flowToTest = "/system/flows/authn/password-authn-flow.xml";
+
+ /**
+ * Map of flow resources that support building the flow to test.
+ * These are only for parent flows, not subflows.
+ */
+ @Nonnull @NonnullElements @Unmodifiable final Map<String,String> flowResources = Map.of(
+ "/system/flows/authn/authn-abstract-flow.xml","authn.abstract",
+ "/conf/authn/authn-events-flow.xml","authn.events",
+ "/flows/authn/conditions/conditions-flow.xml","authn/conditions");
+
+ /** List of mocked subflows.*/
+ @Nonnull @NonnullElements @Unmodifiable final List<Flow> subflows = List.of(MockFlowBuilder.MockNoOpFlow("c14n"));
+
+ /** The username of the user to test.*/
+ @Nonnull final static String USERNAME = "jdoe";
+
+ /** The password of the user to test.*/
+ @Nonnull final static String PASSWORD = "changeit";
+
+
+ /**
+ * Runs the flow:
+ * <ol>
+ * <li>Uses baisc auth, so skips username and password login view</li>
+ * </ol>
+ */
+ @Test public void testPasswordAuthnFlow() {
+
+ setFlowPath(flowToTest);
+ setFlowModelResources(flowResources);
+ setSubflows(subflows);
+ addHttpBasicAuthHeader(USERNAME,PASSWORD);
+
+ LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+ inputMap.put("calledAsSubflow", true);
+ //you must do this bit, not startFlow?
+ FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
+
+ flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
+ flowExecution.start(inputMap, externalContext);
+
+
+ }
+
+
+
+
+
+
+
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java
new file mode 100644
index 0000000..fd008fa
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java
@@ -0,0 +1,41 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContextInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.mock.env.MockPropertySource;
+import org.springframework.stereotype.Component;
+
+import net.shibboleth.idp.test.TestEnvironmentApplicationContextInitializer;
+
+//TODO: remove this if not used.
+ at Deprecated
+public class PropertySourcesPlaceholderConfigurerInitializer
+ implements ApplicationContextInitializer<ConfigurableApplicationContext> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(TestEnvironmentApplicationContextInitializer.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
+
+ final MockPropertySource mock = new MockPropertySource();
+ mock.setProperty("idp.home", "classpath:");
+ mock.setProperty("idp.webflows", "classpath*:/flows");
+// mock.setProperty("idp.storage.htmlLocalStorage", "false");
+// mock.setProperty("idp.session.trackSPSessions", "false");
+// mock.setProperty("idp.session.secondaryServiceIndex", "false");
+// mock.setProperty("idp.service.metadata.resources", "testbed.MetadataResolverResources");
+ applicationContext.getEnvironment().getPropertySources().addFirst(mock);
+ log.info("Prepending properties '{}'", mock.getSource());
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java
new file mode 100644
index 0000000..ac1c5e9
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java
@@ -0,0 +1,47 @@
+package net.shibboleth.idp.plugin.authn.util.mock;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.util.Assert;
+import org.springframework.webflow.engine.ActionState;
+import org.springframework.webflow.engine.EndState;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.engine.RequestControlContext;
+import org.springframework.webflow.engine.Transition;
+import org.springframework.webflow.engine.support.DefaultTargetStateResolver;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.FlowExecutionException;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * A builder for generating simple mock flows. Mostly used generate stub subflows when testing
+ * a flow.
+ */
+public final class MockFlowBuilder {
+
+ /**
+ * Create a simple flow with one start-state which transitions to an
+ * end-state that ends the flow. The flow does nothing.
+ */
+ public static Flow MockNoOpFlow(@Nonnull @NotEmpty final String flowId) {
+ Assert.notNull(flowId, "Flow to mock must have an ID");
+
+ Flow flow = new Flow(flowId);
+ ActionState endingStartState = new ActionState(flow,"noop-start-start") {
+
+ @Override
+ protected void doEnter(RequestControlContext context) throws FlowExecutionException {
+ context.handleEvent(new Event(this, "proceed"));
+
+ }
+ };
+ //add a transition on any event to the 'proceed' end-state
+ endingStartState.getTransitionSet().add(new Transition(new DefaultTargetStateResolver("proceed")));
+ //add a 'proceed' end state to finish the sublow.
+ new EndState(flow, "proceed");
+ flow.setStartState(endingStartState);
+ return flow;
+ }
+
+}
diff --git a/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/StoredIdStore.sql b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/StoredIdStore.sql
new file mode 100644
index 0000000..294b108
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/StoredIdStore.sql
@@ -0,0 +1,11 @@
+CREATE TABLE shibpid (
+ localEntity VARCHAR(100) NOT NULL,
+ peerEntity VARCHAR(100) NOT NULL,
+ persistentId VARCHAR(50) NOT NULL,
+ principalName VARCHAR(50) NOT NULL,
+ localId VARCHAR(50) NOT NULL,
+ peerProvidedId VARCHAR(50),
+ creationDate TIMESTAMP NOT NULL,
+ deactivationDate TIMESTAMP,
+ PRIMARY KEY (localEntity, peerEntity, persistentId)
+ );
diff --git a/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/beans.xml b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/beans.xml
new file mode 100644
index 0000000..209bf0b
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/beans.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+ default-init-method="initialize" default-destroy-method="destroy">
+
+ <!-- Beans from the standard flows -->
+
+ <bean class="net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor" />
+
+ <bean id="InitializeProfileRequestContext"
+ class="net.shibboleth.idp.profile.impl.InitializeProfileRequestContext"
+ scope="prototype" p:profileId="test_flow" />
+
+ <bean id="ResolveAttributes" class="net.shibboleth.idp.profile.impl.ResolveAttributes"
+ scope="prototype" c:resolverService-ref="shibboleth.AttributeResolverService" />
+
+ <bean id="FilterAttributes" class="net.shibboleth.idp.profile.impl.FilterAttributes"
+ scope="prototype" c:filterService-ref="shibboleth.AttributeFilterService" />
+
+ <bean id="SAMLMetadataLookup"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="INBOUND">
+ <constructor-arg name="messageHandler">
+ <bean
+ class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler"
+ scope="prototype">
+ <property name="roleDescriptorResolver">
+ <bean
+ class="org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver"
+ c:mdResolver-ref="shibboleth.MetadataResolver" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="SAMLPopulateACS"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="INBOUND">
+ <constructor-arg name="messageHandler">
+ <bean
+ class="org.opensaml.saml.common.binding.impl.SAMLAddAttributeConsumingServiceHandler"
+ scope="prototype" />
+ </constructor-arg>
+ </bean>
+
+ <!-- Common test flow bean -->
+ <bean id="PopulateResolution" class="net.shibboleth.idp.test.flows.SetupForResolver"
+ scope="prototype" />
+
+
+ <!-- Specific test flow beans -->
+ <bean id="SetupSAMLMessage" class="net.shibboleth.idp.test.flows.mapper.SpoofSAMLMessage"
+ scope="prototype" />
+
+
+ <bean id="TestAfterFilter" class="net.shibboleth.idp.test.flows.mapper.TestAfterFilter"
+ scope="prototype" />
+
+
+
+</beans>
diff --git a/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/resolveAndFilter-flow.xml b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/resolveAndFilter-flow.xml
new file mode 100644
index 0000000..369cc17
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/META-INF/net/shibboleth/idp/flows/resolveAndFilter/resolveAndFilter-flow.xml
@@ -0,0 +1,43 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd">
+
+ <on-start>
+ <evaluate expression="InitializeProfileRequestContext" />
+ </on-start>
+
+ <action-state id="StartTest">
+ <evaluate expression="SetupSAMLMessage" />
+ <evaluate expression="SAMLMetadataLookup" />
+ <evaluate expression="SAMLPopulateACS" />
+ <evaluate expression="PopulateResolution" />
+ <evaluate expression="ResolveAttributes" />
+ <evaluate expression="FilterAttributes" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="AfterTest" />
+ </action-state>
+
+ <action-state id="AfterTest">
+ <evaluate expression="TestAfterFilter" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="end" />
+ </action-state>
+
+ <end-state id="end">
+ <!-- TODO: Probably remove later. For tests, map the profile request context
+ as an output attribute -->
+ <output name="ProfileRequestContext" value="opensamlProfileRequestContext" />
+ </end-state>
+
+ <end-state id="error" />
+
+ <global-transitions>
+ <transition on="#{!'proceed'.equals(currentEvent.id)}" to="error" />
+ </global-transitions>
+
+ <bean-import resource="beans.xml" />
+
+</flow>
+
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/resources/conf/authn/password-authn-config.xml b/idp-duo-impl/src/test/resources/conf/authn/password-authn-config.xml
new file mode 100644
index 0000000..7825d9e
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/authn/password-authn-config.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <!--
+ You can optionally comment out anything you don't need, but make sure not to
+ reference the corresponding validator in the list below if you do remove any.
+ -->
+ <!-- TODO: Make this list configurable per test? otherwise your stuck with the TestValidator -->
+ <!-- <import resource="jaas-authn-config.xml" />
+ <import resource="krb5-authn-config.xml" />
+ <import resource="ldap-authn-config.xml" /> -->
+
+ <!-- Ordered list of CredentialValidators to apply to a request. -->
+ <util:list id="shibboleth.authn.Password.Validators">
+ <!-- Test Validator for static in-memory username and password validation -->
+ <ref bean="TestValidator" />
+ </util:list>
+
+ <!-- In-memory test validator -->
+ <bean id="TestValidator" parent="shibboleth.CredentialValidator" lazy-init="false"
+ class="net.shibboleth.idp.ext.authn.duo.impl.InMemoryCredentialValidator">
+ <constructor-arg index="0" value='jdoe'></constructor-arg>
+ <constructor-arg index="1" value='changeit'></constructor-arg>
+ </bean>
+
+ <!-- Controls whether all validators in the above bean have to succeed, or just one. -->
+ <util:constant id="shibboleth.authn.Password.RequireAll" static-field="java.lang.Boolean.FALSE"/>
+
+
+ <!-- Names of form fields to pull username and password from. -->
+ <bean id="shibboleth.authn.Password.UsernameFieldName" class="java.lang.String" c:_0="j_username" />
+ <bean id="shibboleth.authn.Password.PasswordFieldName" class="java.lang.String" c:_0="j_password" />
+ <bean id="shibboleth.authn.Password.SSOBypassFieldName" class="java.lang.String" c:_0="donotcache" />
+
+ <!-- Simple transforms to apply to username before validation. -->
+ <util:constant id="shibboleth.authn.Password.Lowercase" static-field="java.lang.Boolean.FALSE"/>
+ <util:constant id="shibboleth.authn.Password.Uppercase" static-field="java.lang.Boolean.FALSE"/>
+ <util:constant id="shibboleth.authn.Password.Trim" static-field="java.lang.Boolean.TRUE"/>
+
+ <!-- Set to TRUE if you want the password kept in the resulting Subject as a private credential. -->
+ <util:constant id="shibboleth.authn.Password.RetainAsPrivateCredential" static-field="java.lang.Boolean.FALSE"/>
+
+ <!-- Apply any regular expression replacement pairs to username before validation. -->
+ <util:list id="shibboleth.authn.Password.Transforms">
+ <!--
+ <bean parent="shibboleth.Pair" p:first="^(.+)@example\.org$" p:second="$1" />
+ -->
+ </util:list>
+
+ <!-- Uncomment to configure account lockout backed by in-memory storage. -->
+ <!--
+ <bean id="shibboleth.authn.Password.AccountLockoutManager"
+ parent="shibboleth.StorageBackedAccountLockoutManager"
+ p:maxAttempts="5"
+ p:counterInterval="PT5M"
+ p:lockoutDuration="PT5M"
+ p:extendLockoutDuration="false" />
+ -->
+
+ <!--
+ Define entries here to map error messages detected by validation actions and classify them as particular
+ kinds of errors for use in your templates and as events in flows.
+
+ Keys are events to signal, values are error codes.
+ -->
+ <util:map id="shibboleth.authn.Password.ClassifiedMessageMap">
+ <entry key="UnknownUsername">
+ <list>
+ <value>NoCredentials</value>
+ <value>UnknownUsername</value>
+ <value>CLIENT_NOT_FOUND</value>
+ <value>Client not found</value>
+ <value>DN_RESOLUTION_FAILURE</value>
+ </list>
+ </entry>
+ <entry key="InvalidPassword">
+ <list>
+ <value>InvalidCredentials</value>
+ <value>PREAUTH_FAILED</value>
+ <value>INVALID_CREDENTIALS</value>
+ <value>Checksum failed</value>
+ </list>
+ </entry>
+ <entry key="AccountLocked">
+ <list>
+ <value>AccountLocked</value>
+ <value>Clients credentials have been revoked</value>
+ </list>
+ </entry>
+ <entry key="ExpiredPassword">
+ <list>
+ <value>PASSWORD_EXPIRED</value>
+ </list>
+ </entry>
+ <entry key="ExpiringPassword">
+ <list>
+ <value>ACCOUNT_WARNING</value>
+ </list>
+ </entry>
+ <entry key="RequestUnsupported">
+ <list>
+ <value>RequestUnsupported</value>
+ </list>
+ </entry>
+ </util:map>
+
+ <!--
+ Configuration of "extended" login methods to offer in the password login form.
+
+ The String bean is a regular expression identifying the flows to offer. These flows
+ must also be enabled at the "top" level to be available for use.
+
+ The ExtendedFlowParameters bean can be used to transfer custom parameters from the
+ login form into the context tree for use later by other flows.
+
+ The last bean provides the set of custom Principals to use for results produced by the
+ Password flow itself. You would use this if you need the Password flow to run as a shell
+ to run the "extended" login methods, but want to limit its own results more narrowly.
+ -->
+ <!--
+ <bean id="shibboleth.authn.Password.ExtendedFlows" class="java.lang.String" c:_0="" />
+
+ <util:list id="shibboleth.authn.Password.ExtendedFlowParameters">
+ </util:list>
+
+ <util:list id="shibboleth.authn.Password.PrincipalOverride">
+ <bean parent="shibboleth.SAML2AuthnContextClassRef"
+ c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
+ <bean parent="shibboleth.SAML2AuthnContextClassRef"
+ c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Password" />
+ <bean parent="shibboleth.SAML1AuthenticationMethod"
+ c:method="urn:oasis:names:tc:SAML:1.0:am:password" />
+ </util:list>
+ -->
+
+</beans>
diff --git a/idp-duo-impl/src/test/resources/conf/global.xml b/idp-duo-impl/src/test/resources/conf/global.xml
new file mode 100644
index 0000000..7b79f7a
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/global.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:jdbc="http://www.springframework.org/schema/jdbc"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <!-- Use this file to define any custom beans needed globally. -->
+
+ <!--
+ Algorithm whitelists and blacklists that override or merge with library defaults. Normally you can leave
+ these empty or commented and use the system defaults, but you can override those defaults using these lists.
+ Each <value> element is an algorithm URI, or you can use <util:constant> elements in place of literal values.
+ -->
+
+ <!--
+ <util:list id="shibboleth.SignatureWhitelist">
+ </util:list>
+
+ <util:list id="shibboleth.SignatureBlacklist">
+ </util:list>
+
+ <util:list id="shibboleth.EncryptionWhitelist">
+ </util:list>
+
+ <util:list id="shibboleth.EncryptionBlacklist">
+ </util:list>
+ -->
+
+ <!--
+ If you need to define and inject custom Java object(s) into the various views used throughout the
+ system (errors, login, logout, etc.), you can uncomment and define the bean below to be of any
+ type required. It will appear in the view scope as a variable named "custom".
+
+ The example below defines the bean as a map, which allows you to inject multiple objects under
+ named keys to expand the feature to support multiple injected objects.
+ -->
+
+ <!--
+ <util:map id="shibboleth.CustomViewContext">
+ <entry key="foo" value="bar"/>
+ </util:map>
+ -->
+
+ <util:set id="testbed.MetadataIndexes">
+ <bean class="org.opensaml.saml.metadata.resolver.index.impl.SAMLArtifactMetadataIndex" />
+ <ref bean="shibboleth.CASMetadataIndices" />
+ </util:set>
+
+ <jdbc:embedded-database id="testbed.PairwiseIdDataSource" generate-name="true">
+ <jdbc:script location="classpath:/META-INF/net/shibboleth/idp/StoredIdStore.sql"/>
+ </jdbc:embedded-database>
+
+ <bean id="exampleMetadata" class="org.springframework.core.io.ClassPathResource">
+ <constructor-arg value="/metadata/example-metadata.xml"/>
+ </bean>
+
+ <bean id="exampleMetadata-sp123" class="org.springframework.core.io.ClassPathResource">
+ <constructor-arg value="/metadata/example-sp123-metadata.xml"/>
+ </bean>
+
+ <util:list id="testbed.MetadataResolverResources">
+ <value>%{idp.home}/conf/metadata-providers.xml</value>
+ <value>%{idp.home}/conf/metadata-filters.xml</value>
+ <value>%{idp.home}/system/conf/metadata-providers-system.xml</value>
+ </util:list>
+
+</beans>
diff --git a/idp-duo-impl/src/test/resources/conf/logback-include-console.xml b/idp-duo-impl/src/test/resources/conf/logback-include-console.xml
new file mode 100644
index 0000000..d9d9f68
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/logback-include-console.xml
@@ -0,0 +1,22 @@
+<included>
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
+ <evaluator>
+ <matcher>
+ <Name>VelocityStatusViewFilter</Name>
+ <regex>ResourceManager : unable to find resource 'status.vm' in any resource loader.</regex>
+ </matcher>
+ <expression>VelocityStatusViewFilter.matches(formattedMessage)</expression>
+ </evaluator>
+ <OnMatch>DENY</OnMatch>
+ </filter>
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <charset>UTF-8</charset>
+ <Pattern>%date{ISO8601} - %level [%logger:%line] - %msg%n</Pattern>
+ </encoder>
+ </appender>
+
+ <root level="INFO">
+ <appender-ref ref="CONSOLE" />
+ </root>
+</included>
diff --git a/idp-duo-impl/src/test/resources/conf/metadata-filters.xml b/idp-duo-impl/src/test/resources/conf/metadata-filters.xml
new file mode 100644
index 0000000..9a16b61
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/metadata-filters.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MetadataFilter xsi:type="ByReference"
+ xmlns="urn:mace:shibboleth:2.0:metadata"
+ xmlns:security="urn:mace:shibboleth:2.0:security"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+ xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:ds11="http://www.w3.org/2009/xmldsig11#"
+ xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
+ xmlns:enc11="http://www.w3.org/2009/xmlenc11#"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd
+ urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd
+ urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd
+ urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd
+ urn:oasis:names:tc:SAML:metadata:algsupport http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-algsupport-v1.0.xsd
+ http://www.w3.org/2000/09/xmldsig# http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd
+ http://www.w3.org/2009/xmldsig11# http://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/xmldsig11-schema.xsd
+ http://www.w3.org/2001/04/xmlenc# http://www.w3.org/TR/xmlenc-core/xenc-schema.xsd
+ http://www.w3.org/2009/xmlenc11# http://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/xenc-schema-11.xsd">
+
+ <MetadataFilters providerRef="URLMD">
+
+ <MetadataFilter xsi:type="EntityAttributes">
+ <saml:Attribute Name="https://sp.example.org/tagname">
+ <saml:AttributeValue>foo</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="https://sp.example.org/tagname2">
+ <saml:AttributeValue>foo</saml:AttributeValue>
+ <saml:AttributeValue>bar</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="http://shibboleth.net/ns/profiles/saml1/sso/browser/includeAttributeStatement"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>1</saml:AttributeValue>
+ </saml:Attribute>
+ <Entity>https://sp.example.org</Entity>
+ </MetadataFilter>
+
+ </MetadataFilters>
+
+</MetadataFilter>
diff --git a/idp-duo-impl/src/test/resources/conf/metadata-providers.xml b/idp-duo-impl/src/test/resources/conf/metadata-providers.xml
new file mode 100644
index 0000000..2df93e0
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/metadata-providers.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MetadataProvider id="ShibbolethMetadata" xsi:type="ChainingMetadataProvider"
+ xmlns="urn:mace:shibboleth:2.0:metadata"
+ xmlns:security="urn:mace:shibboleth:2.0:security"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:alg="urn:oasis:names:tc:SAML:metadata:algsupport"
+ xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+ xmlns:ds11="http://www.w3.org/2009/xmldsig11#"
+ xmlns:enc="http://www.w3.org/2001/04/xmlenc#"
+ xmlns:enc11="http://www.w3.org/2009/xmlenc11#"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd
+ urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd
+ urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd
+ urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd
+ urn:oasis:names:tc:SAML:metadata:algsupport http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-algsupport-v1.0.xsd
+ http://www.w3.org/2000/09/xmldsig# http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd
+ http://www.w3.org/2009/xmldsig11# http://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/xmldsig11-schema.xsd
+ http://www.w3.org/2001/04/xmlenc# http://www.w3.org/TR/xmlenc-core/xenc-schema.xsd
+ http://www.w3.org/2009/xmlenc11# http://www.w3.org/TR/2013/REC-xmlenc-core1-20130411/xenc-schema-11.xsd">
+
+ <!-- ========================================== -->
+ <!-- Metadata Configuration -->
+ <!-- ========================================== -->
+
+ <!-- Example metadata provider. -->
+
+ <MetadataProvider id="URLMD" xsi:type="ResourceBackedMetadataProvider" maxRefreshDelay="PT5M" indexesRef="testbed.MetadataIndexes" resourceRef="exampleMetadata">
+ <MetadataFilter xsi:type="SignatureValidation" requireSignedRoot="false"> <!-- TODO -->
+ <PublicKey>
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxg0TyQAP/tIvOH89EtaX
+ uRRn8SYzTj7W1TbNY4VvBmobjkRmSkki4hH9x4sQpi635wn6WtXTN/FNNmkTK3N/
+ LspmBWxfZS+n+cc7I82E5yvCAPX67QsZgqgglp2W5dvK/FsMMCS6X6SVqzBLMP88
+ NenXKxY+HMxMs0sT0UKYh1cAEqadrHRBO65aDBcm5a0sBVYt9K6pgaOHrp/zSIbh
+ nR5tFFLjBbtFktDpHL3AdGBH3OYidNGKBO3tJ3Ms7LeKXsM0+0Y4P+9fHZINL2X3
+ E2N6GVnKs5PZTg9sP0FtIpAbYm/+zCx7Yj1ET/Er8mDd6tNVGSQsn9s5xUBwGqn1
+ 4wIDAQAB
+ </PublicKey>
+ </MetadataFilter>
+ <MetadataFilter xsi:type="SchemaValidation"/>
+ <MetadataFilter xsi:type="EntityRoleWhiteList">
+ <RetainedRole>md:SPSSODescriptor</RetainedRole>
+ </MetadataFilter>
+
+ <MetadataFilter xsi:type="Algorithm">
+ <md:EncryptionMethod Algorithm="http://www.w3.org/2009/xmlenc11#rsa-oaep">
+ <MGF xmlns="http://www.w3.org/2009/xmlenc11#"
+ Algorithm="http://www.w3.org/2009/xmlenc11#mgf1sha256" />
+ <DigestMethod xmlns="http://www.w3.org/2000/09/xmldsig#"
+ Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
+ </md:EncryptionMethod>
+ <Entity>https://sp.example.org</Entity>
+ </MetadataFilter>
+ </MetadataProvider>
+
+ <MetadataProvider id="SP123MD" xsi:type="ResourceBackedMetadataProvider" maxRefreshDelay="PT5M" indexesRef="testbed.MetadataIndexes" resourceRef="exampleMetadata-sp123"/>
+
+ <!--
+ <MetadataProvider id="ICMDQ" xsi:type="DynamicHTTPMetadataProvider"
+ minCacheDuration="PT5M" maxCacheDuration="PT24H" maxIdleEntityData="PT1H">
+ <MetadataFilter xsi:type="RequiredValidUntil" maxValidityInterval="P14D" />
+ <MetadataQueryProtocol>https://mdq.incommon.org/</MetadataQueryProtocol>
+ </MetadataProvider>
+ -->
+
+</MetadataProvider>
diff --git a/idp-duo-impl/src/test/resources/conf/relying-party.xml b/idp-duo-impl/src/test/resources/conf/relying-party.xml
new file mode 100644
index 0000000..f467896
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/relying-party.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <!--
+ Unverified RP configuration, defaults to no support for any profiles. Add <ref> elements to the list
+ to enable specific default profile settings (as below), or create new beans inline to override defaults.
+
+ "Unverified" typically means the IdP has no metadata, or equivalent way of assuring the identity and
+ legitimacy of a requesting system. To run an "open" IdP, you can enable profiles here.
+ -->
+
+ <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+ <property name="profileConfigurations">
+ <list>
+ <!-- <bean parent="SAML2.SSO" p:encryptAssertions="false" /> -->
+ </list>
+ </property>
+ </bean>
+
+ <!-- Default configuration, with default settings applied for all profiles. -->
+
+ <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty.MDDriven">
+ <property name="profileConfigurations">
+ <list>
+ <ref bean="CAS.LoginConfiguration.MDDriven" />
+ <ref bean="CAS.ValidateConfiguration.MDDriven" />
+ <ref bean="Shibboleth.SSO.MDDriven" />
+ <ref bean="SAML1.AttributeQuery.MDDriven" />
+ <ref bean="SAML1.ArtifactResolution.MDDriven" />
+ <ref bean="SAML2.SSO.MDDriven" />
+ <ref bean="SAML2.ECP.MDDriven" />
+ <ref bean="SAML2.Logout.MDDriven" />
+ <ref bean="SAML2.AttributeQuery.MDDriven" />
+ <ref bean="SAML2.ArtifactResolution.MDDriven" />
+ </list>
+ </property>
+ </bean>
+
+ <util:list id="shibboleth.RelyingPartyOverrides">
+
+ </util:list>
+
+</beans>
diff --git a/idp-duo-impl/src/test/resources/credentials/ldap-server.crt b/idp-duo-impl/src/test/resources/credentials/ldap-server.crt
new file mode 100644
index 0000000..e6390b3
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/credentials/ldap-server.crt
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIICATCCAWoCCQCZy4ovDlQTvDANBgkqhkiG9w0BAQUFADBFMRMwEQYKCZImiZPy
+LGQBGRYDbmV0MRowGAYKCZImiZPyLGQBGRYKc2hpYmJvbGV0aDESMBAGA1UEAxMJ
+bG9jYWxob3N0MB4XDTEzMDkyMDA0NDE1OFoXDTIzMDkxODA0NDE1OFowRTETMBEG
+CgmSJomT8ixkARkWA25ldDEaMBgGCgmSJomT8ixkARkWCnNoaWJib2xldGgxEjAQ
+BgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA+Pj+
+Rk0FosrK8tdNJLASSbfRFEJDf0498bnwtor76PONwkTLjnm7paTMAy7oGIAccuBf
+n7183mlo/yh22x+7Fdux4ESvdp1FWl7nGlBBWUrLZzXwwGybC0qv3/Cmvfkvm6zh
+zgY7YyQYlA/RCjTk/pUfrXaIIaKwd+B2miBDrDUCAwEAATANBgkqhkiG9w0BAQUF
+AAOBgQCKQsndxYjsGIvK3lnlztIuKQmRlj7IHL/T6abBYwmQcyMa5o3hv6/Rx+Ze
+NSRfyhakPY4v5yUBi1fyIVIGcAbnak+GQIfsO8uDhzZt6DwshzHrnCbHTLQx6NTB
+4mvLjAdt7riBIl5y5n3tbtrZdpTgFhQxjLWTBpWIO0XKxqtBhw==
+-----END CERTIFICATE-----
diff --git a/idp-duo-impl/src/test/resources/logback-test.xml b/idp-duo-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..62dbda8
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%level [%logger:%line] - %msg%n</pattern>
+ <charset>UTF-8</charset>
+ </encoder>
+ </appender>
+
+ <logger name="net.shibboleth" level="DEBUG" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="net.shibboleth.idp.plugin.authn.duo" level="TRACE" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
+ <logger name="net.shibboleth.idp.ext.authn.duo.impl.Saml2FlowTest" level="DEBUG" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <logger name="org.springframework.webflow" level="TRACE" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+
+ <logger name="org.springframework.binding.mapping.impl" level="DEBUG" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
+
+ <root>
+ <level value="INFO" />
+ <appender-ref ref="STDOUT" />
+ </root>
+
+</configuration>
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/resources/metadata/example-metadata.xml b/idp-duo-impl/src/test/resources/metadata/example-metadata.xml
new file mode 100644
index 0000000..478828b
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/metadata/example-metadata.xml
@@ -0,0 +1,140 @@
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute"
+ entityID="https://sp.example.org">
+ <Extensions>
+ <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <saml:Attribute Name="http://macedir.org/entity-category"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>http://refeds.org/category/research-and-scholarship
+ </saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="urn:oasis:names:tc:SAML:profiles:subject-id:req"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>none</saml:AttributeValue>
+ </saml:Attribute>
+ <!--
+ <saml:Attribute Name="http://shibboleth.net/ns/profiles/saml2/sso/browser/postAuthenticationFlows"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>terms-of-use</saml:AttributeValue>
+ <saml:AttributeValue>attribute-release</saml:AttributeValue>
+ </saml:Attribute>
+ -->
+ <saml:Attribute Name="http://shibboleth.net/ns/profiles/saml2/sso/browser/encryptAssertions"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>true</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="http://shibboleth.net/ns/attributes/releaseAllValues"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>eduPersonPrincipalName</saml:AttributeValue>
+ <saml:AttributeValue>mail</saml:AttributeValue>
+ </saml:Attribute>
+ </mdattr:EntityAttributes>
+ </Extensions>
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol https://www.apereo.org/cas/protocol">
+ <Extensions>
+ <mdui:UIInfo>
+ <mdui:DisplayName xml:lang="en">TEST SP (display Name)</mdui:DisplayName>
+ <mdui:Description xml:lang="en">
+ TEST SP (description)Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ </mdui:Description>
+ <mdui:InformationURL xml:lang="en">https://www.example.org</mdui:InformationURL>
+ <mdui:PrivacyStatementURL xml:lang="en">https://www.example.org/privacy</mdui:PrivacyStatementURL>
+ <mdui:Logo height="65" width="150">https://shibboleth.net/images/shibboleth.png</mdui:Logo>
+ </mdui:UIInfo>
+ </Extensions>
+
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDDzCCAfegAwIBAgIJAN5ThWwNrvJxMA0GCSqGSIb3DQEBBQUAMCMxITAfBgNV
+BAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVkdTAeFw0xMzExMDYxNjUyMzBaFw0y
+MzExMDQxNjUyMzBaMCMxITAfBgNVBAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVk
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODCRHObLNAJ6xaxDmeA
+sJl2Mw3pTGCrWXe2H77n8TjZz68I9fgYLeamCONHFx1qaQUZXim/TANhzOSMCIs+
+/Ma/XVaB8H1lB2EJlob0gDa3wqpnd+ltUI+pirG0t1CDrP8RhBWaZWTRB8Oj9uMV
+lBarznvvSuJIOYnEt5OjLpHe3fbpstiI6dlkViB/jFMOT1G+MSfkPRXQopJ5PfnM
+LhrQa1iJ2hEwE4xviQUHbtqtIcw0xMOlse/GvdV6WMixxQ5Yr9tksb/hzOFG29Nk
+E+ZNxEoxeFQNl73pU3DuA8ogjBpGYDX9jGt0zvyDRlUyXferSIxZy4lauYaxw10o
+WyUCAwEAAaNGMEQwIwYDVR0RBBwwGoIYc25vd2RvZy1wcm8uZWxhbi5vc3UuZWR1
+MB0GA1UdDgQWBBS1KTq0OfsmdgBoqLkINa0wTnM9FzANBgkqhkiG9w0BAQUFAAOC
+AQEApclRWD8hdX+PvBS/8AZnMQFx0TP6UipoegT9GNiZTfbqUeaaepiUzLvrAa81
+IuxZKneg6deoeVhsvNxLAPlPR6OK32A3NadS4KU8d69H1zPKOd7nc+k9WzdFygwB
+DaWTdr2idWU2DpMkZpmDNnwsMpda3zONCJHiBqiCFbq2vsMhFa9X3SFasYIU0H9T
+osjFNFTQVwLeZyjf/fuKnfqbcs3JW+OM9fsxxAmUQQyLz8aKywOaMlQgTFTL9r9u
+Jnnr5zUpZZh6WVFIFS9QmNE4zJMZH6vIDk2zKsO0YJVaogxFML35hKlWen4DLbyT
+wJUl9NY7VG+HjvHrr6xeiDAq0g==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <KeyDescriptor>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDCTCCAfGgAwIBAgIJAN5X7xMzDu64MA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV
+BAMTDnNwLmV4YW1wbGUub3JnMB4XDTEzMDUyNDIxMzU0NloXDTIzMDUyMjIxMzU0
+NlowGTEXMBUGA1UEAxMOc3AuZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDAVA9a1ip1mvd5rQJe9xhL6TXvIHxPm+FLxabvQJA48sJh
+XsXmLAsyGzSm/HviqdkdR3VxyFhzLoj03TqqPP8/IoTFA2Lp+q9PnaeEHKaYeJGH
+g4RozQ0UKhiuyppWptO2Syls302haIaWWXz+p3iOgjuZKg/ESs1udGEJvAC/OUVu
+Eo08b6BuySxO/KsX9kWYoCF6vm+tN1kpOrMTceCqcJlRDyFzxUo4JTaku/81k88v
+iF+5/b0p7McB3IGqCFHR6MhZol880Dgxeze3o6/hmvXwGbJgOeWc46jgKareWwYn
+CSQ7/08TDvX0RhCca39IJXqSP/Wv4QOkALTdobQhAgMBAAGjVDBSMDEGA1UdEQQq
+MCiCDnNwLmV4YW1wbGUub3JnhhZodHRwczovL3NwLmV4YW1wbGUub3JnMB0GA1Ud
+DgQWBBQ1FR+wdqXqtICqb7G0fms7ELdt1zANBgkqhkiG9w0BAQUFAAOCAQEAoW/5
+uKnQMuQJfEx11uVCItAvRNmjWSmGFXpQ3rown3XbRkdX78PztcIdgwwtNZoTlMmA
+122Kdb8ViXw2tsZaB1iOaBiavMtLgdxbg9j6B6UmQrzERQm+zq5rBAzSTh6VRyDP
+kCMYnlX4+PyjWXGVADI65cRBbFBnED6By0NhJesDfaH8VpY19LN9AYpXVJRv4/Dc
+zlnQds4vPKzOaJGvt01Ni9jqZtcUDzKt9UdmDATbukoStvCKTwcr4gAPvfCfNZgo
+CUaW4GEmfyDkCahd5qNlWw0++Z2kNoSRyPA1PTJIkZhagv+VbO85ua2SDJWVgF4I
+h+XNZ5oAql/cjrwDWw==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/SAML2/Redirect/SLO"/>
+
+ <SingleLogoutService Binding="https://www.apereo.org/cas/protocol/logout" Location="urn:mace:shibboleth:profile:CAS:logout"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/SAML2/POST/ACS" index="1"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/sp/SAML2/POST/ACS" index="2"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/sp/SAML2/POST/ACS" index="3"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/sp/SAML2/POST/ACS" index="4"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://localhost:8443/sp/SAML2/Artifact/ACS" index="5"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://localhost:8443/sp/SAML2/PAOS/ACS" index="6"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="http://localhost:8080/sp/SAML1/POST/ACS" index="9"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://localhost:8443/sp/SAML1/POST/ACS" index="10"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://localhost:8443/sp/SAML1/Artifact/ACS" index="11"/>
+
+ <AssertionConsumerService Binding="https://www.apereo.org/cas/protocol/login" Location="https://localhost:8443/sp" index="12"/>
+
+ <AttributeConsumingService isDefault="true" index="1">
+ <ServiceName xml:lang="en">Service Name</ServiceName>
+ <RequestedAttribute FriendlyName="displayName"
+ Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ <RequestedAttribute FriendlyName="displayName" isRequired="true"
+ Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" />
+ <RequestedAttribute FriendlyName="eduPersonPrincipalName"
+ Name="urn:mace:dir:attribute-def:eduPersonPrincipalName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ </AttributeConsumingService>
+
+ </SPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">TSC</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">The Shibboleth Consortium</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://shibboleth.net/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="support">
+ <GivenName>J Random</GivenName>
+ <SurName>Engineer</SurName>
+ <EmailAddress>mailto:jre at example.org</EmailAddress>
+ </ContactPerson>
+</EntityDescriptor>
diff --git a/idp-duo-impl/src/test/resources/metadata/example-sp123-metadata.xml b/idp-duo-impl/src/test/resources/metadata/example-sp123-metadata.xml
new file mode 100644
index 0000000..b2e75b2
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/metadata/example-sp123-metadata.xml
@@ -0,0 +1,358 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Example metadata for mock SPs with entityIDs of form 'https://sp[123].example.org'.
+ -->
+<EntitiesDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+ xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui"
+ xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+ xmlns:mdattr="urn:oasis:names:tc:SAML:metadata:attribute">
+
+<EntityDescriptor entityID="https://affiliation.example.org">
+ <AffiliationDescriptor affiliationOwnerID="https://affiliation.example.org">
+ <AffiliateMember>https://sp1.example.org</AffiliateMember>
+ <AffiliateMember>https://sp2.example.org</AffiliateMember>
+ <AffiliateMember>https://sp3.example.org</AffiliateMember>
+ </AffiliationDescriptor>
+</EntityDescriptor>
+
+<EntityDescriptor entityID="https://sp1.example.org">
+ <Extensions>
+ <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <saml:Attribute Name="http://macedir.org/entity-category"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>http://refeds.org/category/research-and-scholarship
+ </saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name="http://shibboleth.net/ns/profiles/saml2/logout/signSOAPRequests"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>true</saml:AttributeValue>
+ </saml:Attribute>
+ </mdattr:EntityAttributes>
+ </Extensions>
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol" AuthnRequestsSigned="false">
+ <Extensions>
+ <mdui:UIInfo>
+ <mdui:DisplayName xml:lang="en">TEST SP1 (display Name)</mdui:DisplayName>
+ <mdui:Description xml:lang="en">
+ TEST SP1 (description)Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ </mdui:Description>
+ <mdui:InformationURL xml:lang="en">https://www.example.org</mdui:InformationURL>
+ <mdui:PrivacyStatementURL xml:lang="en">https://www.example.org/privacy</mdui:PrivacyStatementURL>
+ <mdui:Logo height="65" width="150">https://shibboleth.net/images/shibboleth.png</mdui:Logo>
+ </mdui:UIInfo>
+ </Extensions>
+
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDDzCCAfegAwIBAgIJAN5ThWwNrvJxMA0GCSqGSIb3DQEBBQUAMCMxITAfBgNV
+BAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVkdTAeFw0xMzExMDYxNjUyMzBaFw0y
+MzExMDQxNjUyMzBaMCMxITAfBgNVBAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVk
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODCRHObLNAJ6xaxDmeA
+sJl2Mw3pTGCrWXe2H77n8TjZz68I9fgYLeamCONHFx1qaQUZXim/TANhzOSMCIs+
+/Ma/XVaB8H1lB2EJlob0gDa3wqpnd+ltUI+pirG0t1CDrP8RhBWaZWTRB8Oj9uMV
+lBarznvvSuJIOYnEt5OjLpHe3fbpstiI6dlkViB/jFMOT1G+MSfkPRXQopJ5PfnM
+LhrQa1iJ2hEwE4xviQUHbtqtIcw0xMOlse/GvdV6WMixxQ5Yr9tksb/hzOFG29Nk
+E+ZNxEoxeFQNl73pU3DuA8ogjBpGYDX9jGt0zvyDRlUyXferSIxZy4lauYaxw10o
+WyUCAwEAAaNGMEQwIwYDVR0RBBwwGoIYc25vd2RvZy1wcm8uZWxhbi5vc3UuZWR1
+MB0GA1UdDgQWBBS1KTq0OfsmdgBoqLkINa0wTnM9FzANBgkqhkiG9w0BAQUFAAOC
+AQEApclRWD8hdX+PvBS/8AZnMQFx0TP6UipoegT9GNiZTfbqUeaaepiUzLvrAa81
+IuxZKneg6deoeVhsvNxLAPlPR6OK32A3NadS4KU8d69H1zPKOd7nc+k9WzdFygwB
+DaWTdr2idWU2DpMkZpmDNnwsMpda3zONCJHiBqiCFbq2vsMhFa9X3SFasYIU0H9T
+osjFNFTQVwLeZyjf/fuKnfqbcs3JW+OM9fsxxAmUQQyLz8aKywOaMlQgTFTL9r9u
+Jnnr5zUpZZh6WVFIFS9QmNE4zJMZH6vIDk2zKsO0YJVaogxFML35hKlWen4DLbyT
+wJUl9NY7VG+HjvHrr6xeiDAq0g==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <KeyDescriptor>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDCTCCAfGgAwIBAgIJAN5X7xMzDu64MA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV
+BAMTDnNwLmV4YW1wbGUub3JnMB4XDTEzMDUyNDIxMzU0NloXDTIzMDUyMjIxMzU0
+NlowGTEXMBUGA1UEAxMOc3AuZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDAVA9a1ip1mvd5rQJe9xhL6TXvIHxPm+FLxabvQJA48sJh
+XsXmLAsyGzSm/HviqdkdR3VxyFhzLoj03TqqPP8/IoTFA2Lp+q9PnaeEHKaYeJGH
+g4RozQ0UKhiuyppWptO2Syls302haIaWWXz+p3iOgjuZKg/ESs1udGEJvAC/OUVu
+Eo08b6BuySxO/KsX9kWYoCF6vm+tN1kpOrMTceCqcJlRDyFzxUo4JTaku/81k88v
+iF+5/b0p7McB3IGqCFHR6MhZol880Dgxeze3o6/hmvXwGbJgOeWc46jgKareWwYn
+CSQ7/08TDvX0RhCca39IJXqSP/Wv4QOkALTdobQhAgMBAAGjVDBSMDEGA1UdEQQq
+MCiCDnNwLmV4YW1wbGUub3JnhhZodHRwczovL3NwLmV4YW1wbGUub3JnMB0GA1Ud
+DgQWBBQ1FR+wdqXqtICqb7G0fms7ELdt1zANBgkqhkiG9w0BAQUFAAOCAQEAoW/5
+uKnQMuQJfEx11uVCItAvRNmjWSmGFXpQ3rown3XbRkdX78PztcIdgwwtNZoTlMmA
+122Kdb8ViXw2tsZaB1iOaBiavMtLgdxbg9j6B6UmQrzERQm+zq5rBAzSTh6VRyDP
+kCMYnlX4+PyjWXGVADI65cRBbFBnED6By0NhJesDfaH8VpY19LN9AYpXVJRv4/Dc
+zlnQds4vPKzOaJGvt01Ni9jqZtcUDzKt9UdmDATbukoStvCKTwcr4gAPvfCfNZgo
+CUaW4GEmfyDkCahd5qNlWw0++Z2kNoSRyPA1PTJIkZhagv+VbO85ua2SDJWVgF4I
+h+XNZ5oAql/cjrwDWw==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp1/SAML2/SOAP/SLO"/>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp1/SAML2/Redirect/SLO"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp1/SAML2/POST/ACS" index="1"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/sp/sp1/SAML2/POST/ACS" index="2"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/sp/sp1/SAML2/POST/ACS" index="3"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/sp/sp1/SAML2/POST/ACS" index="4"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://localhost:8443/sp/sp1/SAML2/Artifact/ACS" index="5"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://localhost:8443/sp/sp1/SAML2/PAOS/ACS" index="6"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="http://localhost:8080/sp/sp1/SAML1/POST/ACS" index="9"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://localhost:8443/sp/sp1/SAML1/POST/ACS" index="10"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://localhost:8443/sp/sp1/SAML1/Artifact/ACS" index="11"/>
+ <AttributeConsumingService isDefault="true" index="1">
+ <ServiceName xml:lang="en">Service Name</ServiceName>
+ <RequestedAttribute FriendlyName="displayName"
+ Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ <RequestedAttribute FriendlyName="displayName" isRequired="true"
+ Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" />
+ <RequestedAttribute FriendlyName="eduPersonPrincipalName"
+ Name="urn:mace:dir:attribute-def:eduPersonPrincipalName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ </AttributeConsumingService>
+
+ </SPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">TSC</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">The Shibboleth Consortium</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://shibboleth.net/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="support">
+ <GivenName>J Random</GivenName>
+ <SurName>Engineer</SurName>
+ <EmailAddress>mailto:jre at example.org</EmailAddress>
+ </ContactPerson>
+</EntityDescriptor>
+<EntityDescriptor entityID="https://sp2.example.org">
+ <Extensions>
+ <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <saml:Attribute Name="http://macedir.org/entity-category"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>http://refeds.org/category/research-and-scholarship
+ </saml:AttributeValue>
+ </saml:Attribute>
+ </mdattr:EntityAttributes>
+ </Extensions>
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol" AuthnRequestsSigned="false">
+ <Extensions>
+ <mdui:UIInfo>
+ <mdui:DisplayName xml:lang="en">TEST SP2 (display Name)</mdui:DisplayName>
+ <mdui:Description xml:lang="en">
+ TEST SP2 (description)Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ </mdui:Description>
+ <mdui:InformationURL xml:lang="en">https://www.example.org</mdui:InformationURL>
+ <mdui:PrivacyStatementURL xml:lang="en">https://www.example.org/privacy</mdui:PrivacyStatementURL>
+ <mdui:Logo height="65" width="150">https://shibboleth.net/images/shibboleth.png</mdui:Logo>
+ </mdui:UIInfo>
+ </Extensions>
+
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDDzCCAfegAwIBAgIJAN5ThWwNrvJxMA0GCSqGSIb3DQEBBQUAMCMxITAfBgNV
+BAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVkdTAeFw0xMzExMDYxNjUyMzBaFw0y
+MzExMDQxNjUyMzBaMCMxITAfBgNVBAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVk
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODCRHObLNAJ6xaxDmeA
+sJl2Mw3pTGCrWXe2H77n8TjZz68I9fgYLeamCONHFx1qaQUZXim/TANhzOSMCIs+
+/Ma/XVaB8H1lB2EJlob0gDa3wqpnd+ltUI+pirG0t1CDrP8RhBWaZWTRB8Oj9uMV
+lBarznvvSuJIOYnEt5OjLpHe3fbpstiI6dlkViB/jFMOT1G+MSfkPRXQopJ5PfnM
+LhrQa1iJ2hEwE4xviQUHbtqtIcw0xMOlse/GvdV6WMixxQ5Yr9tksb/hzOFG29Nk
+E+ZNxEoxeFQNl73pU3DuA8ogjBpGYDX9jGt0zvyDRlUyXferSIxZy4lauYaxw10o
+WyUCAwEAAaNGMEQwIwYDVR0RBBwwGoIYc25vd2RvZy1wcm8uZWxhbi5vc3UuZWR1
+MB0GA1UdDgQWBBS1KTq0OfsmdgBoqLkINa0wTnM9FzANBgkqhkiG9w0BAQUFAAOC
+AQEApclRWD8hdX+PvBS/8AZnMQFx0TP6UipoegT9GNiZTfbqUeaaepiUzLvrAa81
+IuxZKneg6deoeVhsvNxLAPlPR6OK32A3NadS4KU8d69H1zPKOd7nc+k9WzdFygwB
+DaWTdr2idWU2DpMkZpmDNnwsMpda3zONCJHiBqiCFbq2vsMhFa9X3SFasYIU0H9T
+osjFNFTQVwLeZyjf/fuKnfqbcs3JW+OM9fsxxAmUQQyLz8aKywOaMlQgTFTL9r9u
+Jnnr5zUpZZh6WVFIFS9QmNE4zJMZH6vIDk2zKsO0YJVaogxFML35hKlWen4DLbyT
+wJUl9NY7VG+HjvHrr6xeiDAq0g==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <KeyDescriptor>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDCTCCAfGgAwIBAgIJAN5X7xMzDu64MA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV
+BAMTDnNwLmV4YW1wbGUub3JnMB4XDTEzMDUyNDIxMzU0NloXDTIzMDUyMjIxMzU0
+NlowGTEXMBUGA1UEAxMOc3AuZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDAVA9a1ip1mvd5rQJe9xhL6TXvIHxPm+FLxabvQJA48sJh
+XsXmLAsyGzSm/HviqdkdR3VxyFhzLoj03TqqPP8/IoTFA2Lp+q9PnaeEHKaYeJGH
+g4RozQ0UKhiuyppWptO2Syls302haIaWWXz+p3iOgjuZKg/ESs1udGEJvAC/OUVu
+Eo08b6BuySxO/KsX9kWYoCF6vm+tN1kpOrMTceCqcJlRDyFzxUo4JTaku/81k88v
+iF+5/b0p7McB3IGqCFHR6MhZol880Dgxeze3o6/hmvXwGbJgOeWc46jgKareWwYn
+CSQ7/08TDvX0RhCca39IJXqSP/Wv4QOkALTdobQhAgMBAAGjVDBSMDEGA1UdEQQq
+MCiCDnNwLmV4YW1wbGUub3JnhhZodHRwczovL3NwLmV4YW1wbGUub3JnMB0GA1Ud
+DgQWBBQ1FR+wdqXqtICqb7G0fms7ELdt1zANBgkqhkiG9w0BAQUFAAOCAQEAoW/5
+uKnQMuQJfEx11uVCItAvRNmjWSmGFXpQ3rown3XbRkdX78PztcIdgwwtNZoTlMmA
+122Kdb8ViXw2tsZaB1iOaBiavMtLgdxbg9j6B6UmQrzERQm+zq5rBAzSTh6VRyDP
+kCMYnlX4+PyjWXGVADI65cRBbFBnED6By0NhJesDfaH8VpY19LN9AYpXVJRv4/Dc
+zlnQds4vPKzOaJGvt01Ni9jqZtcUDzKt9UdmDATbukoStvCKTwcr4gAPvfCfNZgo
+CUaW4GEmfyDkCahd5qNlWw0++Z2kNoSRyPA1PTJIkZhagv+VbO85ua2SDJWVgF4I
+h+XNZ5oAql/cjrwDWw==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp2/SAML2/SOAP/SLO"/>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp2/SAML2/Redirect/SLO"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp2/SAML2/POST/ACS" index="1"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/sp/sp2/SAML2/POST/ACS" index="2"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/sp/sp2/SAML2/POST/ACS" index="3"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/sp/sp2/SAML2/POST/ACS" index="4"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://localhost:8443/sp/sp2/SAML2/Artifact/ACS" index="5"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://localhost:8443/sp/sp2/SAML2/PAOS/ACS" index="6"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="http://localhost:8080/sp/sp2/SAML1/POST/ACS" index="9"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://localhost:8443/sp/sp2/SAML1/POST/ACS" index="10"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://localhost:8443/sp/sp2/SAML1/Artifact/ACS" index="11"/>
+ <AttributeConsumingService isDefault="true" index="1">
+ <ServiceName xml:lang="en">Service Name</ServiceName>
+ <RequestedAttribute FriendlyName="displayName"
+ Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ <RequestedAttribute FriendlyName="displayName" isRequired="true"
+ Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" />
+ <RequestedAttribute FriendlyName="eduPersonPrincipalName"
+ Name="urn:mace:dir:attribute-def:eduPersonPrincipalName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ </AttributeConsumingService>
+
+ </SPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">TSC</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">The Shibboleth Consortium</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://shibboleth.net/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="support">
+ <GivenName>J Random</GivenName>
+ <SurName>Engineer</SurName>
+ <EmailAddress>mailto:jre at example.org</EmailAddress>
+ </ContactPerson>
+</EntityDescriptor>
+<EntityDescriptor entityID="https://sp3.example.org">
+ <Extensions>
+ <mdattr:EntityAttributes xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+ <saml:Attribute Name="http://macedir.org/entity-category"
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
+ <saml:AttributeValue>http://refeds.org/category/research-and-scholarship
+ </saml:AttributeValue>
+ </saml:Attribute>
+ </mdattr:EntityAttributes>
+ </Extensions>
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol urn:oasis:names:tc:SAML:1.1:protocol" AuthnRequestsSigned="false">
+ <Extensions>
+ <mdui:UIInfo>
+ <mdui:DisplayName xml:lang="en">TEST SP3 (display Name)</mdui:DisplayName>
+ <mdui:Description xml:lang="en">
+ TEST SP3 (description)Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.
+ </mdui:Description>
+ <mdui:InformationURL xml:lang="en">https://www.example.org</mdui:InformationURL>
+ <mdui:PrivacyStatementURL xml:lang="en">https://www.example.org/privacy</mdui:PrivacyStatementURL>
+ <mdui:Logo height="65" width="150">https://shibboleth.net/images/shibboleth.png</mdui:Logo>
+ </mdui:UIInfo>
+ </Extensions>
+
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDDzCCAfegAwIBAgIJAN5ThWwNrvJxMA0GCSqGSIb3DQEBBQUAMCMxITAfBgNV
+BAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVkdTAeFw0xMzExMDYxNjUyMzBaFw0y
+MzExMDQxNjUyMzBaMCMxITAfBgNVBAMTGHNub3dkb2ctcHJvLmVsYW4ub3N1LmVk
+dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODCRHObLNAJ6xaxDmeA
+sJl2Mw3pTGCrWXe2H77n8TjZz68I9fgYLeamCONHFx1qaQUZXim/TANhzOSMCIs+
+/Ma/XVaB8H1lB2EJlob0gDa3wqpnd+ltUI+pirG0t1CDrP8RhBWaZWTRB8Oj9uMV
+lBarznvvSuJIOYnEt5OjLpHe3fbpstiI6dlkViB/jFMOT1G+MSfkPRXQopJ5PfnM
+LhrQa1iJ2hEwE4xviQUHbtqtIcw0xMOlse/GvdV6WMixxQ5Yr9tksb/hzOFG29Nk
+E+ZNxEoxeFQNl73pU3DuA8ogjBpGYDX9jGt0zvyDRlUyXferSIxZy4lauYaxw10o
+WyUCAwEAAaNGMEQwIwYDVR0RBBwwGoIYc25vd2RvZy1wcm8uZWxhbi5vc3UuZWR1
+MB0GA1UdDgQWBBS1KTq0OfsmdgBoqLkINa0wTnM9FzANBgkqhkiG9w0BAQUFAAOC
+AQEApclRWD8hdX+PvBS/8AZnMQFx0TP6UipoegT9GNiZTfbqUeaaepiUzLvrAa81
+IuxZKneg6deoeVhsvNxLAPlPR6OK32A3NadS4KU8d69H1zPKOd7nc+k9WzdFygwB
+DaWTdr2idWU2DpMkZpmDNnwsMpda3zONCJHiBqiCFbq2vsMhFa9X3SFasYIU0H9T
+osjFNFTQVwLeZyjf/fuKnfqbcs3JW+OM9fsxxAmUQQyLz8aKywOaMlQgTFTL9r9u
+Jnnr5zUpZZh6WVFIFS9QmNE4zJMZH6vIDk2zKsO0YJVaogxFML35hKlWen4DLbyT
+wJUl9NY7VG+HjvHrr6xeiDAq0g==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <KeyDescriptor>
+ <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+ <X509Data>
+ <X509Certificate>
+MIIDCTCCAfGgAwIBAgIJAN5X7xMzDu64MA0GCSqGSIb3DQEBBQUAMBkxFzAVBgNV
+BAMTDnNwLmV4YW1wbGUub3JnMB4XDTEzMDUyNDIxMzU0NloXDTIzMDUyMjIxMzU0
+NlowGTEXMBUGA1UEAxMOc3AuZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDAVA9a1ip1mvd5rQJe9xhL6TXvIHxPm+FLxabvQJA48sJh
+XsXmLAsyGzSm/HviqdkdR3VxyFhzLoj03TqqPP8/IoTFA2Lp+q9PnaeEHKaYeJGH
+g4RozQ0UKhiuyppWptO2Syls302haIaWWXz+p3iOgjuZKg/ESs1udGEJvAC/OUVu
+Eo08b6BuySxO/KsX9kWYoCF6vm+tN1kpOrMTceCqcJlRDyFzxUo4JTaku/81k88v
+iF+5/b0p7McB3IGqCFHR6MhZol880Dgxeze3o6/hmvXwGbJgOeWc46jgKareWwYn
+CSQ7/08TDvX0RhCca39IJXqSP/Wv4QOkALTdobQhAgMBAAGjVDBSMDEGA1UdEQQq
+MCiCDnNwLmV4YW1wbGUub3JnhhZodHRwczovL3NwLmV4YW1wbGUub3JnMB0GA1Ud
+DgQWBBQ1FR+wdqXqtICqb7G0fms7ELdt1zANBgkqhkiG9w0BAQUFAAOCAQEAoW/5
+uKnQMuQJfEx11uVCItAvRNmjWSmGFXpQ3rown3XbRkdX78PztcIdgwwtNZoTlMmA
+122Kdb8ViXw2tsZaB1iOaBiavMtLgdxbg9j6B6UmQrzERQm+zq5rBAzSTh6VRyDP
+kCMYnlX4+PyjWXGVADI65cRBbFBnED6By0NhJesDfaH8VpY19LN9AYpXVJRv4/Dc
+zlnQds4vPKzOaJGvt01Ni9jqZtcUDzKt9UdmDATbukoStvCKTwcr4gAPvfCfNZgo
+CUaW4GEmfyDkCahd5qNlWw0++Z2kNoSRyPA1PTJIkZhagv+VbO85ua2SDJWVgF4I
+h+XNZ5oAql/cjrwDWw==
+ </X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://localhost:8443/sp/sp3/SAML2/SOAP/SLO"/>
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://localhost:8443/sp/sp3/SAML2/Redirect/SLO"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost/sp/sp3/SAML2/POST/ACS" index="1"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://localhost:8443/sp/sp3/SAML2/POST/ACS" index="2"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost/sp/sp3/SAML2/POST/ACS" index="3"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="http://localhost:8080/sp/sp3/SAML2/POST/ACS" index="4"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://localhost:8443/sp/sp3/SAML2/Artifact/ACS" index="5"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://localhost:8443/sp/sp3/SAML2/PAOS/ACS" index="6"/>
+
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="http://localhost:8080/sp/sp3/SAML1/POST/ACS" index="9"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:browser-post" Location="https://localhost:8443/sp/sp3/SAML1/POST/ACS" index="10"/>
+ <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:1.0:profiles:artifact-01" Location="https://localhost:8443/sp/sp3/SAML1/Artifact/ACS" index="11"/>
+ <AttributeConsumingService isDefault="true" index="1">
+ <ServiceName xml:lang="en">Service Name</ServiceName>
+ <RequestedAttribute FriendlyName="displayName"
+ Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ <RequestedAttribute FriendlyName="displayName" isRequired="true"
+ Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" />
+ <RequestedAttribute FriendlyName="eduPersonPrincipalName"
+ Name="urn:mace:dir:attribute-def:eduPersonPrincipalName" NameFormat="urn:mace:shibboleth:1.0:attributeNamespace:uri" />
+ </AttributeConsumingService>
+
+ </SPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">TSC</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">The Shibboleth Consortium</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://shibboleth.net/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="support">
+ <GivenName>J Random</GivenName>
+ <SurName>Engineer</SurName>
+ <EmailAddress>mailto:jre at example.org</EmailAddress>
+ </ContactPerson>
+</EntityDescriptor>
+</EntitiesDescriptor>
diff --git a/idp-duo-api/.gitignore b/idp-duo-native-client-impl/.gitignore
similarity index 61%
copy from idp-duo-api/.gitignore
copy to idp-duo-native-client-impl/.gitignore
index b322941..87c484d 100644
--- a/idp-duo-api/.gitignore
+++ b/idp-duo-native-client-impl/.gitignore
@@ -1,2 +1,3 @@
/target
/test-output
+/.checkstyle
diff --git a/idp-duo-native-client-impl/.sts4-cache/.gitignore b/idp-duo-native-client-impl/.sts4-cache/.gitignore
new file mode 100644
index 0000000..602f6bb
--- /dev/null
+++ b/idp-duo-native-client-impl/.sts4-cache/.gitignore
@@ -0,0 +1 @@
+/classpath-data.json
diff --git a/idp-duo-native-client-impl/pom.xml b/idp-duo-native-client-impl/pom.xml
new file mode 100644
index 0000000..3da5cca
--- /dev/null
+++ b/idp-duo-native-client-impl/pom.xml
@@ -0,0 +1,24 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-parent</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ </parent>
+ <artifactId>idp-duo-native-client-impl</artifactId>
+ <description>Native Duo SDK implementation for the Duo OIDC 2FA service</description>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>com.duosecurity</groupId>
+ <artifactId>duo-client</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-api</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
new file mode 100644
index 0000000..af030bc
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientAdaptor.java
@@ -0,0 +1,122 @@
+package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import com.duosecurity.Client;
+import com.duosecurity.exception.DuoException;
+import com.duosecurity.model.HealthCheckResponse;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An object adaptor class for bridging between the Duo SDK implementation
+ * and the internal {@link DuoOIDCClient} interface.
+ */
+//TODO find a way to stop a client from being created other than through the registry?
+public class DuoSDKClientAdaptor implements DuoOIDCClient{
+
+ /** The wrapped Duo native client.*/
+ @Nonnull private Client client;
+
+ /** Function to map the native Duo {@link HealthCheckResponse} object to the interface {@link DuoHealthCheck} object.*/
+ @Nonnull private Function<HealthCheckResponse,DuoHealthCheck> healthCheckResponseConverter;
+
+ /** Constructor.
+ *
+ * @throws DuoClientException if there is an error instantiating the client
+ */
+// public DuoSDKClientAdaptor(@Nonnull final String ikey, at Nonnull final String skey,
+// @Nonnull final String apiHost, @Nonnull final String redirectUri) throws DuoClientException {
+//
+// healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
+//
+// }
+ // Has to have this do not want to mandate this?
+ public DuoSDKClientAdaptor() {
+ healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
+ }
+
+ @Override
+ public void initialize(DuoOIDCIntegration integration) throws DuoClientException{
+ try {
+ client = new Client(integration.getIntegrationKey(), integration.getSecretKey(),
+ integration.getAPIHost(), integration.getRedirectURI());
+ } catch (DuoException e) {
+ throw new DuoClientException(e);
+ }
+ }
+
+
+ /**
+ * Set the health check response converter.
+ *
+ * @param converter the converter.
+ */
+ public void setHealthCheckResponseConverter(@Nonnull final
+ Function<HealthCheckResponse,DuoHealthCheck> converter) {
+ healthCheckResponseConverter = Constraint.isNotNull(converter, "Health Check response converter can not be null");
+ }
+
+ @Override
+ public DuoHealthCheck healthCheck() throws DuoClientException {
+ try {
+ final HealthCheckResponse response = client.healthCheck();
+ if (response==null) {
+ throw new DuoClientException("Duo health check response was null!");
+ }
+ return healthCheckResponseConverter.apply(response);
+
+
+ } catch (DuoException e) {
+ //wrap duo specific exception.
+ throw new DuoClientException(e);
+ }
+
+ }
+
+ @Override
+ public String createAuthUrl(String username, String state) throws DuoClientException {
+ try {
+ return client.createAuthUrl(username, state);
+ } catch (DuoException e) {
+ //wrap duo specific exception.
+ throw new DuoClientException(e);
+ }
+ }
+
+ @Override
+ public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ /**
+ * Default health check response matter.
+ */
+ //TODO do we need this extensible in an adaptor?
+ private class DefaultHealthCheckResponseConverter implements Function<HealthCheckResponse,DuoHealthCheck>{
+
+ @Override
+ public DuoHealthCheck apply(HealthCheckResponse response) {
+
+ Integer responseTimestamp = null;
+ if (response.getResponse()!=null) {
+ responseTimestamp = response.getResponse().getTimestamp();
+ }
+ return DuoHealthCheck.builder().withStatus(response.getStat()).withCode(response.getCode())
+ .withMessage(response.getMessage()).withMessageDetail(response.getMessage_detail())
+ .withResponseTimestamp(responseTimestamp).withTimestamp(response.getTimestamp()).build();
+ }
+
+ }
+
+
+
+}
diff --git a/pom.xml b/pom.xml
index 646108a..15b7af9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,17 +1,170 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-
-
- <modelVersion>4.0.0</modelVersion>
- <groupId>net.shibboleth.extensions.authn</groupId>
- <artifactId>idp-duo-parent</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>Shibbileth IdP v4 Duo AuthN Parent</name>
- <packaging>pom</packaging>
-
-
- <modules>
- <module>idp-duo-api</module>
- <module>idp-duo-impl</module>
- </modules>
-
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-parent</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>Shibbileth IdP v4 Duo AuthN Parent</name>
+ <packaging>pom</packaging>
+
+ <properties>
+ <shib.idp.version>4.0.0</shib.idp.version>
+ <opensaml.version>4.0.0</opensaml.version>
+ <duo.client.version>1.0-SNAPSHOT</duo.client.version>
+ <checkstyle.configLocation>${project.basedir}/../resources/checkstyle.xml</checkstyle.configLocation>
+ </properties>
+
+ <!-- required for general project properties -->
+ <parent>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-parent</artifactId>
+ <version>4.0.0</version>
+ </parent>
+
+ <modules>
+ <module>idp-duo-api</module>
+ <module>idp-duo-impl</module>
+ <module>idp-duo-native-client-impl</module>
+ </modules>
+
+ <repositories>
+ <repository>
+ <id>shib-release</id>
+ <url>https://build.shibboleth.net/nexus/content/groups/public</url>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </repository>
+ <repository>
+ <id>shib-snapshot</id>
+ <url>https://build.shibboleth.net/nexus/content/repositories/snapshots</url>
+ <releases>
+ <enabled>false</enabled>
+ </releases>
+ </repository>
+ </repositories>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <artifactId>idp-bom</artifactId>
+ <version>${shib.idp.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.duosecurity</groupId>
+ <artifactId>duo-client</artifactId>
+ <version>${duo.client.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.plugin.authn</groupId>
+ <artifactId>idp-duo-native-client-impl</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Test Dependencies -->
+
+ <!-- Do we need these here? -->
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.6</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <artifactId>opensaml-core</artifactId>
+ <version>${opensaml.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-attribute-filter-spring</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-profile-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-attribute-resolver-spring</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-authn-impl</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-authn-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-authn-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-conf</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.shibboleth.idp</groupId>
+ <version>${shib.idp.version}</version>
+ <artifactId>idp-conf</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <version>${opensaml.version}</version>
+ <artifactId>opensaml-profile-impl</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <version>${opensaml.version}</version>
+ <artifactId>opensaml-profile-api</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.opensaml</groupId>
+ <version>${opensaml.version}</version>
+ <artifactId>opensaml-storage-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+
+ </dependencies>
+ </dependencyManagement>
+
</project>
\ No newline at end of file
diff --git a/src/main/resources/.gitignore b/src/main/resources/.gitignore
new file mode 100644
index 0000000..9bb88d3
--- /dev/null
+++ b/src/main/resources/.gitignore
@@ -0,0 +1 @@
+/.DS_Store
diff --git a/src/main/resources/checkstyle/checkstyle.xml b/src/main/resources/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..f3f13f6
--- /dev/null
+++ b/src/main/resources/checkstyle/checkstyle.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!--
+ This configuration file was written by the eclipse-cs plugin configuration editor
+-->
+<!--
+ Checkstyle-Configuration: Shibboleth Checkstyle
+ Description: none
+-->
+<module name="Checker">
+ <property name="severity" value="warning"/>
+ <module name="TreeWalker">
+ <property name="tabWidth" value="4"/>
+ <module name="JavadocMethod"/>
+ <module name="JavadocType">
+ <property name="allowUnknownTags" value="true"/>
+ </module>
+ <module name="JavadocVariable"/>
+ <module name="JavadocStyle">
+ <property name="checkEmptyJavadoc" value="true"/>
+ </module>
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/>
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+ <module name="MethodLength">
+ <property name="max" value="70"/>
+ </module>
+ <module name="ParameterNumber">
+ <property name="max" value="5"/>
+ </module>
+ <module name="EmptyForIteratorPad"/>
+ <module name="MethodParamPad"/>
+ <module name="ModifierOrder"/>
+ <module name="AvoidNestedBlocks"/>
+ <module name="LeftCurly"/>
+ <module name="NeedBraces"/>
+ <module name="RightCurly"/>
+ <module name="EmptyStatement"/>
+ <module name="EqualsHashCode"/>
+ <module name="HiddenField"/>
+ <module name="IllegalInstantiation"/>
+ <module name="InnerAssignment"/>
+ <module name="MissingSwitchDefault"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor"/>
+ <module name="VisibilityModifier"/>
+ <module name="ArrayTypeStyle"/>
+ <module name="UpperEll"/>
+ <module name="AnonInnerLength"/>
+ <module name="EmptyForInitializerPad"/>
+ <module name="CovariantEquals"/>
+ <module name="DefaultComesLast"/>
+ <module name="DeclarationOrder"/>
+ <module name="ExplicitInitialization"/>
+ <module name="FallThrough"/>
+ <module name="IllegalThrows"/>
+ <module name="MultipleVariableDeclarations"/>
+ <module name="PackageDeclaration"/>
+ <module name="ParameterAssignment"/>
+ <module name="ReturnCount">
+ <property name="max" value="8"/>
+ <property name="maxForVoid" value="8"/>
+ </module>
+ <module name="StringLiteralEquality"/>
+ <module name="SuperFinalize"/>
+ <module name="ArrayTrailingComma"/>
+ <module name="UnnecessaryParentheses"/>
+ <module name="MutableException"/>
+ <module name="ThrowsCount">
+ <property name="max" value="3"/>
+ </module>
+ <module name="CyclomaticComplexity"/>
+ <module name="TrailingComment"/>
+ <module name="EqualsAvoidNull"/>
+ <module name="ModifiedControlVariable"/>
+ <module name="FinalParameters">
+ <property name="tokens" value="METHOD_DEF,CTOR_DEF,LITERAL_CATCH"/>
+ </module>
+ <module name="FinalLocalVariable">
+ <property name="tokens" value="PARAMETER_DEF,VARIABLE_DEF"/>
+ <property name="validateEnhancedForLoopVariable" value="true"/>
+ </module>
+ <module name="SuppressionCommentFilter">
+ <property name="offCommentFormat" value="\bCheck[Ss]tyle:\s*([\w|]+)\s+OFF\b"/>
+ <property name="onCommentFormat" value="\bCheck[Ss]tyle:\s*([\w|]+)\s+ON\b"/>
+ <property name="checkFormat" value="$1"/>
+ </module>
+ <module name="MissingJavadocMethod"/>
+ <module name="MissingJavadocPackage"/>
+ <module name="MissingJavadocType"/>
+ <module name="InvalidJavadocPosition"/>
+ </module>
+ <module name="FileTabCharacter"/>
+ <module name="FileLength">
+ <property name="max" value="1000"/>
+ </module>
+ <module name="Header">
+ <property name="header" value="/*\n * Licensed to the University Corporation for Advanced Internet Development,\n * Inc. (UCAID) under one or more contributor license agreements. See the\n * NOTICE file distributed with this work for additional information regarding\n * copyright ownership. The UCAID licenses this file to You under the Apache\n * License, Version 2.0 (the "License"); you may not use this file except in\n * compliance with the License. You may obtain a cop [...]
+ </module>
+ <module name="JavadocPackage"/>
+ <module name="LineLength">
+ <property name="max" value="120"/>
+ </module>
+</module>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list