[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