[java-idp-plugin-duo] 09/15: cleanup, add more tests, add basic distribution.

Phil Smart philip.smart at jisc.ac.uk
Tue Jul 14 14:29:05 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=5c19644c54e0c60d57bd18716cc2599abdc14bda

commit 5c19644c54e0c60d57bd18716cc2599abdc14bda
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jul 10 16:45:53 2020 +0100

    cleanup, add more tests, add basic distribution.
---
 .gitignore                                         |   1 +
 idp-duo-api/pom.xml                                |  20 +-
 .../authn/duo/AbstractDuoAuthenticationAction.java | 140 ++++--------
 ... => AbstractDuoClientAuthenticationAction.java} | 114 ++++------
 .../authn/duo/DefaultDuoOIDCIntegration.java       |  93 ++++----
 .../idp/plugin/authn/duo/DuoClientException.java   |  23 +-
 .../duo/DuoClientInitializationException.java      |  66 ++++++
 .../idp/plugin/authn/duo/DuoOIDCAuthAPI.java       |  52 +++++
 .../idp/plugin/authn/duo/DuoOIDCClient.java        |  25 ++-
 .../idp/plugin/authn/duo/DuoOIDCClientFactory.java |  38 ++++
 .../plugin/authn/duo/DuoOIDCClientRegistry.java    |  38 +++-
 .../idp/plugin/authn/duo/DuoOIDCIntegration.java   |  28 ++-
 .../idp/plugin/authn/duo/DuoRegistryException.java |  28 ++-
 .../plugin/authn/duo/InitializingDuoClient.java    |  18 --
 ...text.java => DuoOIDCAuthenticationContext.java} | 147 ++----------
 .../idp/plugin/authn/duo/context/package-info.java |  19 ++
 .../plugin/authn/duo/model/DuoAccessDevice.java    |  43 ++--
 .../idp/plugin/authn/duo/model/DuoApplication.java |  41 ++--
 .../idp/plugin/authn/duo/model/DuoAuthContext.java | 107 +++++----
 .../idp/plugin/authn/duo/model/DuoAuthDevice.java  |  28 +--
 .../idp/plugin/authn/duo/model/DuoAuthToken.java   | 141 +++++++-----
 .../idp/plugin/authn/duo/model/DuoHealthCheck.java |  14 +-
 .../idp/plugin/authn/duo/model/package-info.java   |  19 ++
 .../idp/plugin/authn/duo/package-info.java         |  19 ++
 .../duo/{ => principal}/DuoOIDCPrincipal.java      |   5 +-
 .../plugin/authn/duo/principal/package-info.java   |  19 ++
 .../AbstractDuoClientAuthenticationActionTest.java | 166 ++++++++++++++
 .../idp/plugin/authn/duo/DuoOIDCPrincipalTest.java |  26 ++-
 idp-duo-distribution/pom.xml                       |  44 ++++
 .../src/main/assembly/duo-assembly.xml             |  73 ++++++
 .../resources/conf/authn/duo-oidc-authn-config.xml |  13 ++
 .../resources/conf/authn/idp-duo-oidc.properties   |   5 +
 idp-duo-impl/pom.xml                               |  46 ++--
 .../idp/plugin/authn/duo/impl/CheckDuoState.java   |  72 ------
 .../duo/impl/DefaultDuoOIDCClientRegistry.java     | 129 +++++------
 .../duo/impl/DuoOIDCAuthenticationDescription.java |  63 ++++++
 .../authn/duo/impl/DuoOIDCAuthnController.java     |  41 ++--
 .../idp/plugin/authn/duo/impl/DuoSupport.java      |  22 +-
 .../authn/duo/impl/ExchangeCodeForDuoToken.java    |  44 +++-
 .../authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java  |  91 +++++---
 .../duo/impl/PopulateDuoAuthenticationContext.java |  35 ++-
 .../plugin/authn/duo/impl/ValidateDuo2FAToken.java | 144 ------------
 .../authn/duo/impl/ValidateDuoResponseState.java   |  80 +++++++
 .../authn/duo/impl/ValidateDuoTokenAudience.java   | 102 +++++++++
 .../impl/ValidateDuoTokenAuthenticationResult.java | 139 ++++++++++++
 .../impl/ValidateDuoTokenAuthenticationTime.java   | 161 +++++++++++++
 .../duo/impl/ValidateDuoTokenExpirationTime.java   | 130 +++++++++++
 .../authn/duo/impl/ValidateDuoTokenIssuedAt.java   | 141 ++++++++++++
 .../authn/duo/impl/ValidateDuoTokenIssuer.java     | 106 +++++++++
 .../authn/duo/impl/ValidateDuoTokenSubject.java    | 104 +++++++++
 .../idp/plugin/authn/duo/impl/package-info.java    |  19 ++
 .../META-INF/net.shibboleth.idp/postconfig.xml     | 166 ++++++++++++++
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   | 102 +++++++++
 .../flows/authn/DuoOIDC}/duo-oidc-authn-flow.xml   |  25 +--
 ...utilities.java.support.plugin.PluginDescription |   1 +
 .../flows/authn/duo/duo-oidc-authn-beans.xml       |  72 ------
 .../impl/AbstractAuthnXmlFlowExecutionTests.java   | 109 +++++----
 .../authn/duo/impl/AbstractDuoActionTest.java      | 210 +++++++++++++++++
 .../duo/impl/DefaultDuoOIDCClientRegistryTest.java | 119 ++++++++--
 .../authn/duo/impl/DualDuoIntegrationStrategy.java |  61 +++++
 .../plugin/authn/duo/impl/DuoAuthnFlowTest.java    | 249 +++++++++++++++------
 .../authn/duo/impl/DuoOIDCAuthnControllerTest.java | 112 +++++----
 .../duo/impl/ExchangeCodeForDuoTokenTest.java      |  89 ++++++++
 .../duo/impl/HealthCheckDuoOIDCAuthAPITest.java    | 108 +++++++++
 .../duo/impl/InMemoryCredentialValidator.java      |  47 ++--
 .../authn/duo/impl/PasswordAuthnFlowTest.java      |  87 -------
 .../impl/PopulateDuoAuthenticationContextTest.java | 100 +++++++++
 .../duo/impl/ValidateDuoResponseStateTest.java     |  90 ++++++++
 .../duo/impl/ValidateDuoTokenAudienceTest.java     |  78 +++++++
 .../ValidateDuoTokenAuthenticationResultTest.java  | 155 +++++++++++++
 .../ValidateDuoTokenAuthenticationTimeTest.java    | 122 ++++++++++
 .../impl/ValidateDuoTokenExpirationTimeTest.java   | 116 ++++++++++
 .../duo/impl/ValidateDuoTokenIssuedAtTest.java     |  98 ++++++++
 .../duo/impl/ValidateDuoTokenSubjectTest.java      |  73 ++++++
 .../mock/MockDuoOIDCClientFactory_FAIL_Client.java |  19 ++
 .../mock/MockDuoOIDCClientFactory_OK_Client.java   |  19 ++
 .../plugin/authn/mock/MockDuoOIDCClient_FAIL.java  |  60 ++---
 .../plugin/authn/mock/MockDuoOIDCClient_OK.java    |  68 +++---
 .../authn/mock/MockDuoOIDCClient_UNKNOWN.java      |  82 +++++++
 .../idp/plugin/authn/mock/MockFlowBuilder.java     |  13 +-
 ...duo-oidc-authn-config-multiple-integrations.xml |  14 ++
 .../resources/conf/authn/duo-oidc-authn-config.xml |  12 +
 idp-duo-native-client-impl/pom.xml                 |  23 +-
 .../duo/sdk/impl/DuoOIDCNativeSDKDescription.java  |  46 ++++
 .../authn/duo/sdk/impl/DuoSDKClientAdaptor.java    | 139 +++++++++---
 .../authn/duo/sdk/impl/DuoSDKClientFactory.java    |  41 ++++
 .../plugin/authn/duo/sdk/impl/package-info.java    |  19 ++
 ...utilities.java.support.plugin.PluginDescription |   1 +
 .../duo/sdk/impl/DuoSDKClientFactoryTest.java      |  50 +++++
 pom.xml                                            |  76 ++++---
 90 files changed, 4954 insertions(+), 1399 deletions(-)

diff --git a/.gitignore b/.gitignore
index 99c9e46..bcef0de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
 *.#*
 .*.swp
 .DS_store
+.DS_Store
 # Simulated Subversion default ignores end here
 # The contents of the svn:ignore property on the branch root.
 /test-output
diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index 8138e05..d410113 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -2,20 +2,28 @@
     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>
+        <groupId>net.shibboleth.plugin.idp.authn</groupId>
+        <artifactId>idp-plugin-duo-parent</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </parent>
-    <artifactId>idp-duo-api</artifactId>
+    <artifactId>idp-plugin-duo-api</artifactId>
+    <name>Shibboleth IdP :: Plugins :: Duo 2FA Login Flow API</name>
 
     <properties>
-        <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.api</automatic.module.name>
+        <automatic.module.name>net.shibboleth.plugin.idp.authn.duo.api</automatic.module.name>
     </properties>
 
 
-    <!-- TODO: API probs does not need to know about spring webflow -->
     <dependencies>
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-profile-api</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+        
+    </dependencies>
 
 
-    </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
index 0de0c2f..80223f8 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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;
 
 import java.util.function.Function;
@@ -7,86 +24,51 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 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.idp.profile.IdPEventIds;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 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.
+ * <p>A base class for Duo 2FA authentication related actions.</p>
  * 
- * In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
- * and makes available the {@link DuoOIDCClient} from a lookup from the in-flight {@link DuoIntegration}
- * attached to the {@link DuoAuthenticationContext}.
+ * <p>In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
+ * and makes available the {@link DuoOIDCAuthenticationContext}.</p>
  * 
- * Duo authentication action implementations should override
- * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)} method.
+ * <p>Duo 2FA authentication action implementations should override
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCAuthenticationContext)} 
+ * method.</p>
  * 
- * @event {@link AuthnEventIds#INVALID_AUTHN_CTX} FINISH
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
  */
-//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 {
+public abstract 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;
-    
     /** Lookup strategy to locate the Duo authentication context. */
-    @Nonnull private Function<ProfileRequestContext,DuoAuthenticationContext> duoContextLookupStrategy;
-    
-    /** The registry for locating the DuoClient for the established integration.*/
-    @NonnullAfterInit private DuoOIDCClientRegistry clientRegistry;
-    
-    /** The Duo OIDC client .*/
-    @Nonnull private DuoOIDCClient duoClient;
+    @Nonnull private Function<ProfileRequestContext,DuoOIDCAuthenticationContext> duoContextLookupStrategy;
     
     /** The Duo authentication Context.*/
-    @Nullable private DuoAuthenticationContext duoContext;
+    @Nullable private DuoOIDCAuthenticationContext duoContext;
         
     
     /** Constructor.*/
     public AbstractDuoAuthenticationAction() {
-        duoIntegrationLookupStrategy = FunctionSupport.constant(null);
-        duoContextLookupStrategy = new ChildContextLookup<>(DuoAuthenticationContext.class).
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoOIDCAuthenticationContext.class).
                 compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
-    /**
-     * Set the Duo client registry.
-     * 
-     * @param duoRegistry the registry
-     */
-    public void setClientRegistry(@Nonnull final DuoOIDCClientRegistry duoRegistry) {        
-        clientRegistry = Constraint.isNotNull(duoRegistry,"DuoClient 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 Duo authentication context lookup strategy to use.
@@ -94,33 +76,12 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      * @param strategy lookup strategy
      */
     public void setDuoContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookuplookup strategy cannot be 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");
-        }
-    }
-    
     /** {@inheritDoc} */
     @Override
     protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
@@ -129,38 +90,22 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
         if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
             return false;
         }
-        //TODO: this will not work now, as integration is in the duoContext? so arrange to after below?
-        final DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
-        if (duoIntegration == null) {
-            log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
+      
         duoContext = duoContextLookupStrategy.apply(profileRequestContext);
         if (duoContext == null) {
             log.warn("{} No Duo context returned by lookup strategy",getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
             return false;
             
-        }
-
-        //Configure the Duo client for the established integration
-        try {
-            duoClient = clientRegistry.getIntegrationClientOrCreate(duoIntegration);
-        } catch (final DuoRegistryException e) {
-            log.warn("{} No DuoClient established (located or created) for "
-                    + "this integration", getLogPrefix(),e);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        
+        }        
         
-        return doPreExecute(profileRequestContext, authenticationContext, duoClient, duoContext);
+        return doPreExecute(profileRequestContext, authenticationContext, duoContext);
     }
     
     /**
-     * Delegates to the {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)}
-     * to perform the actual authentication. Implementations can not override this method.
+     * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient, 
+     * DuoOIDCAuthenticationContext)} 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
@@ -168,7 +113,7 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
     @Override
     protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
-        doExecute(profileRequestContext,authenticationContext,duoClient,duoContext);
+        doExecute(profileRequestContext,authenticationContext,duoContext);
     }
     
     /**
@@ -176,30 +121,27 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      * 
      * @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.
      * @param context the Duo authentication context
      * 
      * @return true iff execution should continue
      */
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
-            @Nonnull final DuoAuthenticationContext context) {
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext context) {
         return true;
     }
     
-    //TODO: failry overloaded here?
     /**
      * 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.
      * @param context the Duo authentication context
      */
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
-            @Nonnull final DuoAuthenticationContext context) {
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext context) {
         
     }
 
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/AbstractDuoClientAuthenticationAction.java
similarity index 60%
copy from idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java
copy to idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoClientAuthenticationAction.java
index 0de0c2f..abddebb 100644
--- 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/AbstractDuoClientAuthenticationAction.java
@@ -1,3 +1,20 @@
+/*
+ * 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;
 
 import java.util.function.Function;
@@ -7,62 +24,51 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 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.idp.profile.IdPEventIds;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 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.
+ * <p>A base class for Duo 2FA authentication related actions.</p>
  * 
- * In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
- * and makes available the {@link DuoOIDCClient} from a lookup from the in-flight {@link DuoIntegration}
- * attached to the {@link DuoAuthenticationContext}.
+ * <p>In addition to the work performed by {@link AbstractDuoAuthenticationAction}, this action also looks up
+ * and makes available the {@link DuoOIDCClient} created or registered against the in-flight {@link DuoIntegration}
+ * attached to the {@link DuoOIDCAuthenticationContext}.</p>
  * 
- * Duo authentication action implementations should override
- * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)} method.
+ * <p>Duo 2FA authentication action implementations should override
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient, DuoOIDCAuthenticationContext)} 
+ * method.</p>
  * 
- * @event {@link AuthnEventIds#INVALID_AUTHN_CTX} FINISH
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
  */
-//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 {
+public abstract class AbstractDuoClientAuthenticationAction extends AbstractDuoAuthenticationAction {
     
     /** Class logger. */
-    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractDuoAuthenticationAction.class);
-    
-    /** Lookup strategy for Duo integration. */
-    @Nonnull private Function<ProfileRequestContext,DuoOIDCIntegration> duoIntegrationLookupStrategy;
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(AbstractDuoClientAuthenticationAction.class);
     
     /** Lookup strategy to locate the Duo authentication context. */
-    @Nonnull private Function<ProfileRequestContext,DuoAuthenticationContext> duoContextLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,DuoOIDCAuthenticationContext> duoContextLookupStrategy;
     
     /** The registry for locating the DuoClient for the established integration.*/
     @NonnullAfterInit private DuoOIDCClientRegistry clientRegistry;
     
     /** The Duo OIDC client .*/
-    @Nonnull private DuoOIDCClient duoClient;
-    
-    /** The Duo authentication Context.*/
-    @Nullable private DuoAuthenticationContext duoContext;
-        
+    @Nonnull private DuoOIDCClient duoClient;        
     
     /** Constructor.*/
-    public AbstractDuoAuthenticationAction() {
-        duoIntegrationLookupStrategy = FunctionSupport.constant(null);
-        duoContextLookupStrategy = new ChildContextLookup<>(DuoAuthenticationContext.class).
+    public AbstractDuoClientAuthenticationAction() {
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoOIDCAuthenticationContext.class).
                 compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
@@ -75,18 +81,6 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
         clientRegistry = Constraint.isNotNull(duoRegistry,"DuoClient 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 Duo authentication context lookup strategy to use.
@@ -94,23 +88,12 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      * @param strategy lookup strategy
      */
     public void setDuoContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookuplookup strategy cannot be 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 {
@@ -124,24 +107,18 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
     /** {@inheritDoc} */
     @Override
     protected final boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
 
-        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext, duoContext)) {
             return false;
         }
-        //TODO: this will not work now, as integration is in the duoContext? so arrange to after below?
-        final DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+      
+        final DuoOIDCIntegration duoIntegration = duoContext.getIntegration();
         if (duoIntegration == null) {
-            log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        duoContext = duoContextLookupStrategy.apply(profileRequestContext);
-        if (duoContext == null) {
-            log.warn("{} No Duo context returned by lookup strategy",getLogPrefix());
+            log.warn("{} No DuoIntegration attached to Duo authentication context", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
             return false;
-            
         }
 
         //Configure the Duo client for the established integration
@@ -159,15 +136,17 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
     }
     
     /**
-     * Delegates to the {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)}
-     * to perform the actual authentication. Implementations can not override this method.
+     * Delegates to {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient, 
+     * DuoOIDCAuthenticationContext)} 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) {
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
         doExecute(profileRequestContext,authenticationContext,duoClient,duoContext);
     }
     
@@ -183,11 +162,10 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      */
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
-            @Nonnull final DuoAuthenticationContext context) {
+            @Nonnull final DuoOIDCAuthenticationContext context) {
         return true;
     }
     
-    //TODO: failry overloaded here?
     /**
      * Performs this Duo authentication action using the supplied Duo client. Implementations
      * should override this method.
@@ -199,7 +177,7 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      */
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
-            @Nonnull final DuoAuthenticationContext context) {
+            @Nonnull final DuoOIDCAuthenticationContext context) {
         
     }
 
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
index 00f26d6..d0b87e0 100644
--- 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
@@ -1,10 +1,26 @@
+/*
+ * 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;
 
 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;
@@ -22,21 +38,14 @@ 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;
+    @NonnullAfterInit @NotEmpty private String clientId;
     
     /** Secret key. */
     @NonnullAfterInit @NotEmpty private String secretKey;
@@ -44,7 +53,6 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
     /** 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;
     
@@ -69,26 +77,10 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
         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
+     * @param uri the redirect_uri
      */
     public void setRedirectURI(@Nonnull @NotEmpty final String uri) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
@@ -106,19 +98,19 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
     }
 
     /** {@inheritDoc} */
-    @Nonnull @NotEmpty public String getIntegrationKey() {
-        return integrationKey;
+    @Nonnull @NotEmpty public String getClientId() {
+        return clientId;
     }
     
     /**
-     * Set the integration key to use.
+     * Set the client ID to use.
      * 
-     * @param key integration key
+     * @param id the client identifier.
      */
-    public void setIntegrationKey(@Nonnull @NotEmpty final String key) {
+    public void setClientId(@Nonnull @NotEmpty final String id) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         
-        integrationKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Integration key cannot be null or empty");
+        clientId = Constraint.isNotNull(StringSupport.trimOrNull(id), "ClientID cannot be null or empty");
     }
 
     /** {@inheritDoc} */
@@ -166,29 +158,46 @@ public class DefaultDuoOIDCIntegration extends AbstractInitializableComponent im
     /** {@inheritDoc} */
     @Override
     protected void doInitialize() throws ComponentInitializationException {
-        if (apiHost == null || applicationKey == null || integrationKey == null || secretKey == null 
+        if (apiHost == null || clientId == null || secretKey == null 
                 || redirectURI == null) {
-            throw new ComponentInitializationException("API host, integration keys and redirect_uri must be set");
+            throw new ComponentInitializationException("API host, clientId, secret key, and redirect_uri must be set");
         }
     }
 
     /** {@inheritDoc} */
     @Override
     public int hashCode() {
-        return Objects.hash(integrationKey);
+        return Objects.hash(clientId);
     }
 
     /** {@inheritDoc} */
     @Override
-    public boolean equals(Object obj) {
-        if (this == obj)
+    public boolean equals(final Object obj) {
+        if (this == obj) {
             return true;
-        if (obj == null)
+        }
+        if (obj == null) {
             return false;
-        if (getClass() != obj.getClass())
+        }
+        if (getClass() != obj.getClass()) {
             return false;
-        DefaultDuoOIDCIntegration other = (DefaultDuoOIDCIntegration) obj;
-        return Objects.equals(integrationKey, other.integrationKey);
+        }
+        final DefaultDuoOIDCIntegration other = (DefaultDuoOIDCIntegration) obj;
+        return Objects.equals(clientId, other.clientId);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("DefaultDuoOIDCIntegration [apiHost=");
+        builder.append(apiHost);
+        builder.append(", clientId=");
+        builder.append(clientId);
+        builder.append(", redirectURI=");
+        builder.append(redirectURI);
+        builder.append("]");
+        return builder.toString();
     }
     
     
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
index 59b2b04..89cab99 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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;
 
 import javax.annotation.concurrent.ThreadSafe;
@@ -26,7 +43,7 @@ public class DuoClientException extends Exception{
      * @param message exception message
      * @param cause exception to be wrapped by this one
      */
-    public DuoClientException(String message, Throwable cause) {
+    public DuoClientException(final String message, final Throwable cause) {
         super(message, cause);
         
     }
@@ -36,7 +53,7 @@ public class DuoClientException extends Exception{
      * 
      * @param message exception message
      */
-    public DuoClientException(String message) {
+    public DuoClientException(final String message) {
         super(message);
         
     }
@@ -46,7 +63,7 @@ public class DuoClientException extends Exception{
      * 
      * @param cause exception to be wrapped by this one
      */
-    public DuoClientException(Throwable cause) {
+    public DuoClientException(final Throwable cause) {
         super(cause);
         
     }
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientInitializationException.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientInitializationException.java
new file mode 100644
index 0000000..fb858d7
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoClientInitializationException.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+/**
+ * Indicates an error during registry processing.
+ */
+public class DuoClientInitializationException extends RuntimeException{
+
+
+    /** Default serialUID.  */
+    private static final long serialVersionUID = 5597692444724770271L;
+
+    /** Constructor. */
+    public DuoClientInitializationException() {
+        super();
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param cause exception to be wrapped by this one
+     */
+    public DuoClientInitializationException(final String message, final Throwable cause) {
+        super(message, cause);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public DuoClientInitializationException(final String message) {
+        super(message);
+        
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cause exception to be wrapped by this one
+     */
+    public DuoClientInitializationException(final Throwable cause) {
+        super(cause);
+        
+    }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
new file mode 100644
index 0000000..10f38fa
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCAuthAPI.java
@@ -0,0 +1,52 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Constants defined in the Duo OIDC Auth API.
+ */
+public final class DuoOIDCAuthAPI {
+
+
+    /** Duo AuthAPI auth "allow" result value. */
+    @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_ALLOW = "allow";
+
+    /** Duo AuthAPI auth "deny" result value. */
+    @Nonnull @NotEmpty public static final String DUO_AUTH_RESULT_DENY = "deny";
+
+    /** Duo AuthAPI auth "bypass" result value. */
+    @Nonnull @NotEmpty public static final String DUO_AUTH_STATUS_BYPASS = "bypass";
+
+    /** Duo AuthAPI auth "locked_out" result value. */
+    @Nonnull @NotEmpty public static final String DUO_AUTH_STATUS_LOCKED = "locked_out";
+    
+    /** Duo response failure status.*/
+    @Nonnull @NotEmpty public static final String DUO_RESPONSE_STATUS_FAIL = "FAIL";
+    
+    /** Duo response success status. */
+    @Nonnull @NotEmpty public static final String DUO_RESPONSE_STATUS_OK = "OK";
+
+    /** Constructor. */
+    private DuoOIDCAuthAPI() {
+    }
+
+}
\ No newline at end of file
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
index af0f7ac..0733fb8 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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;
 
 import javax.annotation.Nonnull;
@@ -8,9 +25,7 @@ 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?
-//TODO: These look nonnnull responses - should they be?
-public interface DuoOIDCClient extends InitializingDuoClient {
+public interface DuoOIDCClient {
     
     /**
      * Check the health of the Duo 2FA endpoint. 
@@ -29,7 +44,7 @@ public interface DuoOIDCClient extends InitializingDuoClient {
      * 
      * @return the redirect URL
      * 
-     * @throws DuoClientException
+     * @throws DuoClientException if there is an error creating the authentication URL.
      */
     @Nonnull String createAuthUrl(String username, String state) throws DuoClientException;
     
@@ -42,6 +57,8 @@ public interface DuoOIDCClient extends InitializingDuoClient {
      *              about the authentication.
      *
      * @return the token
+     * 
+     * @throws DuoClientException if there is an error exchaining the auth_code for a token result.
      */
     @Nonnull DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException;
 
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientFactory.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientFactory.java
new file mode 100644
index 0000000..d33791e
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCClientFactory.java
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Abstract factory for creating {@link DuoOIDCClient} instances.
+ */
+public interface DuoOIDCClientFactory {
+    
+    /**
+     * Create an {@link DuoOIDCClient} instance from the supplied integration.
+     * 
+     * @param integration the duo integration used to instantiate the client.
+     * 
+     * @return the created Duo client.
+     *
+     * @throws DuoClientException if there is an error creating the Duo client.
+     */
+    @Nonnull DuoOIDCClient createInstance(@Nonnull final DuoOIDCIntegration integration) 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
index f0e760f..e5b8203 100644
--- 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
@@ -1,20 +1,46 @@
+/*
+ * 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;
 
 import javax.annotation.Nonnull;
 
-//TODO: this is like a factory and a registry
+/**
+ * A registry of {@link DuoOIDCClient}s for {@link DuoOIDCIntegration}s.
+ * 
+ * TODO check this still makes sense
+ */
 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.
+     * Get a Duo client for the given Duo integration. 
+     * 
+     * @implSpec Only one client should exist per integration i.e. given the same integration, 
+     * the same client should be returned.
+     * 
+     * @implNote Clients can either be pre-registered, or lazy-initialised when first needed.
      *  
-     * @param integration the Duo integration to find a client for.
+     * @param integration the Duo integration to find a client for. Never {@literal null}.
      * 
-     * @return the existing or new Duo client.
+     * @return a {@link DuoOIDCClient} appropriate for this integration. Never {@literal null}.
      * 
      * @throws DuoRegistryException if there is an issue locating or creating a Duo client.
      */
-    @Nonnull DuoOIDCClient getIntegrationClientOrCreate(@Nonnull final DuoOIDCIntegration integration) throws DuoRegistryException;
+    @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
index ca130ca..8511164 100644
--- 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
@@ -3,15 +3,37 @@ package net.shbboleth.idp.plugin.authn.duo;
 import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.authn.principal.PrincipalSupportingComponent;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 /**
- * Extension of the {@link DuoIntegration} interface to allow OIDC specific properties.
+ * Interface to a particular Duo OIDC integration point.
  */
-public interface DuoOIDCIntegration extends DuoIntegration{
+public interface DuoOIDCIntegration extends PrincipalSupportingComponent {
+    
+    /**
+     * Get the name of the API host to contact.
+     * 
+     * @return name of API host
+     */
+    @Nonnull @NotEmpty String getAPIHost();
+
+    /**
+     * Get the clientId key.
+     * 
+     * @return the integration key
+     */
+    @Nonnull @NotEmpty String getClientId();
+
+    /**
+     * Get the secret key.
+     * 
+     * @return the secret key
+     */
+    @Nonnull @NotEmpty String getSecretKey();
     
     /**
      * 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
      */
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
index b1571dc..c5c2a56 100644
--- 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
@@ -1,9 +1,29 @@
+/*
+ * 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;
 
+/**
+ * Indicates an error during registry processing.
+ */
 public class DuoRegistryException extends Exception{
 
 
-    /** Default serialUID  */
+    /** Default serialUID.  */
     private static final long serialVersionUID = 5597692444724770271L;
 
     /** Constructor. */
@@ -18,7 +38,7 @@ public class DuoRegistryException extends Exception{
      * @param message exception message
      * @param cause exception to be wrapped by this one
      */
-    public DuoRegistryException(String message, Throwable cause) {
+    public DuoRegistryException(final String message, final Throwable cause) {
         super(message, cause);
         
     }
@@ -28,7 +48,7 @@ public class DuoRegistryException extends Exception{
      * 
      * @param message exception message
      */
-    public DuoRegistryException(String message) {
+    public DuoRegistryException(final String message) {
         super(message);
         
     }
@@ -38,7 +58,7 @@ public class DuoRegistryException extends Exception{
      * 
      * @param cause exception to be wrapped by this one
      */
-    public DuoRegistryException(Throwable cause) {
+    public DuoRegistryException(final 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
deleted file mode 100644
index 6300dcd..0000000
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/InitializingDuoClient.java
+++ /dev/null
@@ -1,18 +0,0 @@
-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/DuoOIDCAuthenticationContext.java
similarity index 52%
rename from idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java
rename to idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
index c0f8a33..f396dd9 100644
--- 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/DuoOIDCAuthenticationContext.java
@@ -17,9 +17,6 @@
 
 package net.shbboleth.idp.plugin.authn.duo.context;
 
-import java.util.HashMap;
-import java.util.Map;
-
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
@@ -28,46 +25,25 @@ import org.opensaml.messaging.context.BaseContext;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.duo.DuoIntegration;
-import net.shibboleth.utilities.java.support.annotation.constraint.Live;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
- * Context that carries Duo ...
+ * <p>Context that carries the Duo integration and Duo authentication result token.</p>
  * 
  * @parent {@link AuthenticationContext}
- * @added After ...
+ * @added After extracting the Duo integration for the given authentication request.
  */
-//TODO: do we need all of these. This name clashes with an existing IdP one.
-public final class DuoAuthenticationContext extends BaseContext {
+public final class DuoOIDCAuthenticationContext 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;
-    
     /** The selected Duo integration to use for the lifetime of this request.*/
     @Nullable private DuoOIDCIntegration integration;
     
-    /** A randomly generated minimum 32 character String sent in the Duo 2FA authorization request.*/
+    /** A randomly generated 32 character minimum String sent in the Duo 2FA authorization request.*/
     @Nullable private String requestState;
     
-    /** A randomly generated minimum 32 character String returned in the Duo 2FA authorization response.*/
+    /** A randomly generated 32 character minimum String returned in the Duo 2FA authorization response.*/
     @Nullable private String responseState;
     
     /** The authorization code return from the Duo authorization request.*/
@@ -77,10 +53,15 @@ public final class DuoAuthenticationContext extends BaseContext {
     @Nullable private DuoAuthToken authToken;
 
     /** Constructor. */
-    public DuoAuthenticationContext() {
-        pushInfo = new HashMap<>();
+    public DuoOIDCAuthenticationContext() {
+        
     }
     
+    /**
+     * Get the authorization code.
+     * 
+     * @return the authorization code.
+     */
     @Nullable public String getAuthorizationCode() {
         return authCode;
     }
@@ -92,7 +73,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * 
      * @return this context.
      */
-    @Nonnull public DuoAuthenticationContext setAuthorizationCode(@Nullable final String code) {
+    @Nonnull public DuoOIDCAuthenticationContext setAuthorizationCode(@Nullable final String code) {
         authCode = code;
         return this;
     }
@@ -104,7 +85,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public DuoAuthenticationContext setAuthToken(@Nullable final DuoAuthToken token) {
+    @Nonnull public DuoOIDCAuthenticationContext setAuthToken(@Nullable final DuoAuthToken token) {
         authToken = token;
         return this;
     }
@@ -135,7 +116,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public DuoAuthenticationContext setRequestState(@Nullable final String state) {
+    @Nonnull public DuoOIDCAuthenticationContext setRequestState(@Nullable final String state) {
         requestState = state;
         return this;
     }
@@ -156,7 +137,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public DuoAuthenticationContext setResponseState(@Nullable final String state) {
+    @Nonnull public DuoOIDCAuthenticationContext setResponseState(@Nullable final String state) {
         responseState = state;
         return this;
     }
@@ -177,7 +158,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * 
      * @return this context
      */
-    @Nonnull public DuoAuthenticationContext setUsername(@Nullable final String name) {
+    @Nonnull public DuoOIDCAuthenticationContext setUsername(@Nullable final String name) {
         username = name;
         return this;
     }
@@ -188,7 +169,7 @@ public final class DuoAuthenticationContext extends BaseContext {
      * @param duoIntegration the integration
      * @return this context
      */
-    public DuoAuthenticationContext setIntegration(@Nullable final DuoOIDCIntegration duoIntegration) {
+    public DuoOIDCAuthenticationContext setIntegration(@Nullable final DuoOIDCIntegration duoIntegration) {
         integration = duoIntegration;
         return this;
     }
@@ -202,97 +183,5 @@ public final class DuoAuthenticationContext extends BaseContext {
         return integration;
     }
 
-    /**
-     * 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/context/package-info.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/package-info.java
new file mode 100644
index 0000000..603c095
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow API context classes. */
+package net.shbboleth.idp.plugin.authn.duo.context;
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAccessDevice.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAccessDevice.java
index 108c5cf..82cb9c3 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAccessDevice.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAccessDevice.java
@@ -1,17 +1,34 @@
+/* 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.model;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 
+/** Duo Access Device.*/
 @Immutable
 public final class DuoAccessDevice {
     
     /** IP address of the device used in 2FA.*/
     @Nonnull private final String ip;
 
-    /** Name of the device e.g. phone number.*/
-    @Nonnull private final String name;
+    /** The hostname.*/
+    @Nonnull private final String hostname;
 
     /** The state where the device is located.*/
     @Nullable private final String state;
@@ -33,8 +50,8 @@ public final class DuoAccessDevice {
     /**
      * @return Returns the name.
      */
-    public final String getName() {
-        return name;
+    public final String getHostname() {
+        return hostname;
     }
 
     /**
@@ -65,7 +82,7 @@ public final class DuoAccessDevice {
      */
     private DuoAccessDevice(Builder builder) {
         this.ip = builder.ip;
-        this.name = builder.name;
+        this.hostname = builder.hostname;
         this.state = builder.state;
         this.city = builder.city;
         this.country = builder.country;
@@ -82,12 +99,12 @@ public final class DuoAccessDevice {
 
     
     public interface IIpStage {
-        public INameStage withIp(String ip);
+        public IHostnameStage withIp(String ip);
     }
 
     
-    public interface INameStage {
-        public IBuildStage withName(String name);
+    public interface IHostnameStage {
+        public IBuildStage withHostname(String host);
     }
 
     
@@ -105,10 +122,10 @@ public final class DuoAccessDevice {
      * Builder to build {@link DuoAccessDevice}.
      */
     
-    public static final class Builder implements IIpStage, INameStage, IBuildStage {
+    public static final class Builder implements IIpStage, IHostnameStage, IBuildStage {
         private String ip;
 
-        private String name;
+        private String hostname;
 
         private String state;
 
@@ -120,14 +137,14 @@ public final class DuoAccessDevice {
         }
 
         @Override
-        public INameStage withIp(String ip) {
+        public IHostnameStage withIp(String ip) {
             this.ip = ip;
             return this;
         }
 
         @Override
-        public IBuildStage withName(String name) {
-            this.name = name;
+        public IBuildStage withHostname(String host) {
+            this.hostname = host;
             return this;
         }
 
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoApplication.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoApplication.java
index 66487ca..d6cb4bc 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoApplication.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoApplication.java
@@ -1,10 +1,25 @@
+/* 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.model;
 
 import javax.annotation.Nonnull;
 import javax.annotation.concurrent.Immutable;
 
-
+/** Duo Application.*/
 @Immutable
 public final class DuoApplication {
 
@@ -13,8 +28,8 @@ public final class DuoApplication {
 
     /** Application name.*/
     @Nonnull private final String name;
-    
-    
+
+
     /**
      * @return Returns the key.
      */
@@ -34,7 +49,7 @@ public final class DuoApplication {
      *
      * @param builder the builder to build the instance with
      */
-    private DuoApplication(Builder builder) {
+    private DuoApplication(final Builder builder) {
         this.key = builder.key;
         this.name = builder.name;
     }
@@ -43,22 +58,22 @@ public final class DuoApplication {
      * Creates builder to build {@link DuoApplication}.
      * @return created builder
      */
-    
+
     public static IKeyStage builder() {
         return new Builder();
     }
 
-    
+
     public interface IKeyStage {
         public INameStage withKey(String key);
     }
 
-    
+
     public interface INameStage {
         public IBuildStage withName(String name);
     }
 
-    
+
     public interface IBuildStage {
         public DuoApplication build();
     }
@@ -66,7 +81,7 @@ public final class DuoApplication {
     /**
      * Builder to build {@link DuoApplication}.
      */
-    
+
     public static final class Builder implements IKeyStage, INameStage, IBuildStage {
         private String key;
 
@@ -76,13 +91,13 @@ public final class DuoApplication {
         }
 
         @Override
-        public INameStage withKey(String key) {
+        public INameStage withKey(final String key) {
             this.key = key;
             return this;
         }
 
         @Override
-        public IBuildStage withName(String name) {
+        public IBuildStage withName(final String name) {
             this.name = name;
             return this;
         }
@@ -92,7 +107,7 @@ public final class DuoApplication {
             return new DuoApplication(this);
         }
     }
-    
-    
+
+
 
 }
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthContext.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthContext.java
index bfbedaa..eb9ec39 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthContext.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthContext.java
@@ -1,46 +1,63 @@
+/* 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.model;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
 
 /** Duo authentication context.*/
-//FIXME: need some spec, what are these fields.
+ at Immutable
 public final class DuoAuthContext {
 
     /** Authentication result e.g. {@literal 'success'}.*/
-    @Nonnull private String result;
+    @Nonnull private final String result;
 
     /** Authentication timestamp.*/
-    @Nonnull private Integer timestamp;
+    @Nonnull private final Integer timestamp;
 
     /** Device used during 2FA.*/
-    @Nonnull private DuoAuthDevice authDevice;
+    @Nonnull private final DuoAuthDevice authDevice;
 
-    /** Authentication id.*/
-    @Nonnull private String txid;
+    /** Transaction id of the authentication event.*/
+    @Nonnull private final String txid;
 
     /** Type of event.*/
-    @Nonnull private String eventType;
+    @Nonnull private final String eventType;
 
     /** Reason for context e.g. 'user_approved'.*/
-    @Nonnull private String reason;
+    @Nonnull private final String reason;
 
     //TODO: difference between this and authdevice?
-    @Nonnull private DuoAccessDevice accessDevice;
+    @Nonnull private final DuoAccessDevice accessDevice;
 
     /** Duo 2FA application integration information.*/
-    @Nonnull private DuoApplication application;
+    @Nonnull private final DuoApplication application;
 
     /** The factor used in the 2FA request.*/
-    @Nonnull private String factor;
+    @Nonnull private final String factor;
 
     /** Username of the user for which 2FA was performed.*/
-    @Nonnull private String username;
-    
+    @Nonnull private final String username;
+
     /** Key of the user for which 2FA was preformed.*/
-    @Nonnull private String userKey;
+    @Nonnull private final String userKey;
+
 
-    
     /**
      * @return Returns the result.
      */
@@ -118,7 +135,7 @@ public final class DuoAuthContext {
         return userKey;
     }
 
-    private DuoAuthContext(Builder builder) {
+    private DuoAuthContext(final Builder builder) {
         this.result = builder.result;
         this.timestamp = builder.timestamp;
         this.authDevice = builder.authDevice;
@@ -136,67 +153,67 @@ public final class DuoAuthContext {
      * Creates builder to build {@link DuoAuthContext}.
      * @return created builder
      */
-    
+
     public static IResultStage builder() {
         return new Builder();
     }
 
-    
+
     public interface IResultStage {
         public ITimestampStage withResult(String result);
     }
 
-    
+
     public interface ITimestampStage {
         public IAuthDeviceStage withTimestamp(Integer timestamp);
     }
 
-    
+
     public interface IAuthDeviceStage {
         public ITxidStage withAuthDevice(DuoAuthDevice authDevice);
     }
 
-    
+
     public interface ITxidStage {
         public IEventTypeStage withTxid(String txid);
     }
 
-    
+
     public interface IEventTypeStage {
         public IReasonStage withEventType(String eventType);
     }
 
-    
+
     public interface IReasonStage {
         public IAccessDeviceStage withReason(String reason);
     }
 
-    
+
     public interface IAccessDeviceStage {
         public IApplicationStage withAccessDevice(DuoAccessDevice accessDevice);
     }
 
-    
+
     public interface IApplicationStage {
         public IFactorStage withApplication(DuoApplication application);
     }
 
-    
+
     public interface IFactorStage {
         public IUsernameStage withFactor(String factor);
     }
 
-    
+
     public interface IUsernameStage {
         public IUserKeyStage withUsername(String username);
     }
 
-    
+
     public interface IUserKeyStage {
         public IBuildStage withUserKey(String userKey);
     }
 
-    
+
     public interface IBuildStage {
         public DuoAuthContext build();
     }
@@ -204,10 +221,10 @@ public final class DuoAuthContext {
     /**
      * Builder to build {@link DuoAuthContext}.
      */
-    
+
     public static final class Builder
-            implements IResultStage, ITimestampStage, IAuthDeviceStage, ITxidStage, IEventTypeStage, IReasonStage,
-            IAccessDeviceStage, IApplicationStage, IFactorStage, IUsernameStage, IUserKeyStage, IBuildStage {
+    implements IResultStage, ITimestampStage, IAuthDeviceStage, ITxidStage, IEventTypeStage, IReasonStage,
+    IAccessDeviceStage, IApplicationStage, IFactorStage, IUsernameStage, IUserKeyStage, IBuildStage {
         private String result;
 
         private Integer timestamp;
@@ -234,67 +251,67 @@ public final class DuoAuthContext {
         }
 
         @Override
-        public ITimestampStage withResult(String result) {
+        public ITimestampStage withResult(final String result) {
             this.result = result;
             return this;
         }
 
         @Override
-        public IAuthDeviceStage withTimestamp(Integer timestamp) {
+        public IAuthDeviceStage withTimestamp(final Integer timestamp) {
             this.timestamp = timestamp;
             return this;
         }
 
         @Override
-        public ITxidStage withAuthDevice(DuoAuthDevice authDevice) {
+        public ITxidStage withAuthDevice(final DuoAuthDevice authDevice) {
             this.authDevice = authDevice;
             return this;
         }
 
         @Override
-        public IEventTypeStage withTxid(String txid) {
+        public IEventTypeStage withTxid(final String txid) {
             this.txid = txid;
             return this;
         }
 
         @Override
-        public IReasonStage withEventType(String eventType) {
+        public IReasonStage withEventType(final String eventType) {
             this.eventType = eventType;
             return this;
         }
 
         @Override
-        public IAccessDeviceStage withReason(String reason) {
+        public IAccessDeviceStage withReason(final String reason) {
             this.reason = reason;
             return this;
         }
 
         @Override
-        public IApplicationStage withAccessDevice(DuoAccessDevice accessDevice) {
+        public IApplicationStage withAccessDevice(final DuoAccessDevice accessDevice) {
             this.accessDevice = accessDevice;
             return this;
         }
 
         @Override
-        public IFactorStage withApplication(DuoApplication application) {
+        public IFactorStage withApplication(final DuoApplication application) {
             this.application = application;
             return this;
         }
 
         @Override
-        public IUsernameStage withFactor(String factor) {
+        public IUsernameStage withFactor(final String factor) {
             this.factor = factor;
             return this;
         }
 
         @Override
-        public IUserKeyStage withUsername(String username) {
+        public IUserKeyStage withUsername(final String username) {
             this.username = username;
             return this;
         }
 
         @Override
-        public IBuildStage withUserKey(String userKey) {
+        public IBuildStage withUserKey(final String userKey) {
             this.userKey = userKey;
             return this;
         }
@@ -304,7 +321,7 @@ public final class DuoAuthContext {
             return new DuoAuthContext(this);
         }
     }
-    
-    
+
+
 
 }
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthDevice.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthDevice.java
index f176f7f..2e660df 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthDevice.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthDevice.java
@@ -24,7 +24,7 @@ public final class DuoAuthDevice {
     /** The country where the device is located.*/
     @Nullable private final String country;
 
-    
+
     /**
      * @return Returns the ip.
      */
@@ -65,7 +65,7 @@ public final class DuoAuthDevice {
      *
      * @param builder the builder to build the instance with
      */
-    private DuoAuthDevice(Builder builder) {
+    private DuoAuthDevice(final Builder builder) {
         this.ip = builder.ip;
         this.name = builder.name;
         this.state = builder.state;
@@ -77,22 +77,22 @@ public final class DuoAuthDevice {
      * Creates builder to build {@link DuoAuthDevice}.
      * @return created builder
      */
-    
+
     public static IIpStage builder() {
         return new Builder();
     }
 
-    
+
     public interface IIpStage {
         public INameStage withIp(String ip);
     }
 
-    
+
     public interface INameStage {
         public IBuildStage withName(String name);
     }
 
-    
+
     public interface IBuildStage {
         public IBuildStage withState(String state);
 
@@ -106,7 +106,7 @@ public final class DuoAuthDevice {
     /**
      * Builder to build {@link DuoAuthDevice}.
      */
-    
+
     public static final class Builder implements IIpStage, INameStage, IBuildStage {
         private String ip;
 
@@ -122,31 +122,31 @@ public final class DuoAuthDevice {
         }
 
         @Override
-        public INameStage withIp(String ip) {
+        public INameStage withIp(final String ip) {
             this.ip = ip;
             return this;
         }
 
         @Override
-        public IBuildStage withName(String name) {
+        public IBuildStage withName(final String name) {
             this.name = name;
             return this;
         }
 
         @Override
-        public IBuildStage withState(String state) {
+        public IBuildStage withState(final String state) {
             this.state = state;
             return this;
         }
 
         @Override
-        public IBuildStage withCity(String city) {
+        public IBuildStage withCity(final String city) {
             this.city = city;
             return this;
         }
 
         @Override
-        public IBuildStage withCountry(String country) {
+        public IBuildStage withCountry(final String country) {
             this.country = country;
             return this;
         }
@@ -156,7 +156,7 @@ public final class DuoAuthDevice {
             return new DuoAuthDevice(this);
         }
     }
-    
-    
+
+
 
 }
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
index d1a4a8d..2d6a8d8 100644
--- 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
@@ -1,3 +1,19 @@
+/* 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.model;
 
 import javax.annotation.Nonnull;
@@ -10,47 +26,50 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
  * <p>A Duo authentication token which describes the result of 2FA.</p>
  * 
  * <p>Analogous in part to an OIDC token. Some descriptions are taken from the 
- * OpenID connect core 1.0 specification.</p>
+ * OpenID connect core 1.0 specification, some from the DuoAPI spec (https://duo.com/docs/authapi).</p>
  * 
  * <p>Includes a staged builder for fluent generation.</p>
+ * 
  */
-//TODO: make these immutable
 @Immutable
 public final class DuoAuthToken {
-    
+
     /** The issuer identifier for the issuer of the response.*/
-    @Nonnull @NotEmpty final private String iss;
-    
+    @Nonnull @NotEmpty private final String iss;
+
     /** the subject identifier.*/ 
-    @Nonnull @NotEmpty final private String sub;
-    
+    @Nonnull @NotEmpty private final String sub;
+
     /** Shorthand name by which the End-User wishes to be referred to as.*/
-    @Nullable private final String preferred_username;
-    
+    @Nullable private final String preferredUsername;
+
     /** The audience(s) that this token is indented for.*/
     @Nonnull @NotEmpty private final String aud;
-    
-    /** Expiration time on or after which the ID Token MUST NOT be accepted for processing.*/ 
+
+    /** 
+     * Expiration time on or after which the ID Token MUST NOT be accepted for processing.
+     * Represented as seconds since UNIX EPOCH
+     */ 
     @Nonnull @NotEmpty private final Integer exp;
-    
+
     /**  Time at which the JWT was issued. In seconds since Unix EPOCH.*/
     @Nonnull @NotEmpty private final Double iat;
-    
+
     /** Time when the End-User authentication occurred.  In seconds since Unix EPOCH.*/
-    @Nullable private final Integer auth_time;
-    
-    /** Status message after 2FA e.g. 'Login Successful'.*/
+    @Nullable private final Integer authTime;
+
+    /** Human-readable string describing the status of the authentication attempt e.g. 'Login Successful'.*/
     @Nonnull private final String authResultStatusMessage;
-    
-    /** Status of 2FA e.g. 'allow'.*/
+
+    /** String detailing the progress or outcome of the authentication attempt e.g. 'allow'.*/
     @Nonnull private final String authResultStatus;
-    
+
     /** Result of 2FA e.g. 'allow'.*/
     @Nonnull private final String authResult;
 
     /** The authentication context. */
     @Nonnull private final DuoAuthContext authContext;
-    
+
 
     /**
      * @return Returns the iss.
@@ -69,8 +88,8 @@ public final class DuoAuthToken {
     /**
      * @return Returns the preferred_username.
      */
-    public final String getPreferred_username() {
-        return preferred_username;
+    public final String getPreferredUsername() {
+        return preferredUsername;
     }
 
     /**
@@ -97,8 +116,8 @@ public final class DuoAuthToken {
     /**
      * @return Returns the auth_time.
      */
-    public final Integer getAuth_time() {
-        return auth_time;
+    public final Integer getAuthTime() {
+        return authTime;
     }
 
     /**
@@ -134,7 +153,7 @@ public final class DuoAuthToken {
      *
      * @param builder the builder to build the instance with
      */
-    private DuoAuthToken(Builder builder) {
+    private DuoAuthToken(final Builder builder) {
         this.iss = builder.iss;
         this.sub = builder.sub;
         this.aud = builder.aud;
@@ -144,69 +163,69 @@ public final class DuoAuthToken {
         this.authResultStatus = builder.authResultStatus;
         this.authResult = builder.authResult;
         this.authContext = builder.authContext;
-        this.preferred_username = builder.preferred_username;
-        this.auth_time = builder.auth_time;
+        this.preferredUsername = builder.preferredUsername;
+        this.authTime = builder.authTime;
     }
 
     /**
      * 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 IAuthResultStatusMessageStage withIat(Double iat);
     }
 
-    
+
     public interface IAuthResultStatusMessageStage {
         public IAuthResultStatusStage withAuthResultStatusMessage(String authResultStatusMessage);
     }
 
-    
+
     public interface IAuthResultStatusStage {
         public IAuthResultStage withAuthResultStatus(String authResultStatus);
     }
 
-    
+
     public interface IAuthResultStage {
         public IAuthContextStage withAuthResult(String authResult);
     }
 
-    
+
     public interface IAuthContextStage {
         public IBuildStage withAuthContext(DuoAuthContext authContext);
     }
 
-    
+
     public interface IBuildStage {
-        public IBuildStage withPreferred_username(String preferred_username);
+        public IBuildStage withPreferredUsername(String prefUsername);
 
-        public IBuildStage withAuth_time(Integer auth_time);
+        public IBuildStage withAuthTime(Integer auth_time);
 
         public DuoAuthToken build();
     }
@@ -214,9 +233,9 @@ public final class DuoAuthToken {
     /**
      * Builder to build {@link DuoAuthToken}.
      */
-    
+
     public static final class Builder implements IIssStage, ISubStage, IAudStage, IExpStage, IIatStage,
-            IAuthResultStatusMessageStage, IAuthResultStatusStage, IAuthResultStage, IAuthContextStage, IBuildStage {
+    IAuthResultStatusMessageStage, IAuthResultStatusStage, IAuthResultStage, IAuthContextStage, IBuildStage {
         private String iss;
 
         private String sub;
@@ -235,76 +254,76 @@ public final class DuoAuthToken {
 
         private DuoAuthContext authContext;
 
-        private String preferred_username;
+        private String preferredUsername;
 
-        private Integer auth_time;
+        private Integer authTime;
 
         private Builder() {
         }
 
         @Override
-        public ISubStage withIss(String iss) {
+        public ISubStage withIss(final String iss) {
             this.iss = iss;
             return this;
         }
 
         @Override
-        public IAudStage withSub(String sub) {
+        public IAudStage withSub(final String sub) {
             this.sub = sub;
             return this;
         }
 
         @Override
-        public IExpStage withAud(String aud) {
+        public IExpStage withAud(final String aud) {
             this.aud = aud;
             return this;
         }
 
         @Override
-        public IIatStage withExp(Integer exp) {
+        public IIatStage withExp(final Integer exp) {
             this.exp = exp;
             return this;
         }
 
         @Override
-        public IAuthResultStatusMessageStage withIat(Double iat) {
+        public IAuthResultStatusMessageStage withIat(final Double iat) {
             this.iat = iat;
             return this;
         }
 
         @Override
-        public IAuthResultStatusStage withAuthResultStatusMessage(String authResultStatusMessage) {
+        public IAuthResultStatusStage withAuthResultStatusMessage(final String authResultStatusMessage) {
             this.authResultStatusMessage = authResultStatusMessage;
             return this;
         }
 
         @Override
-        public IAuthResultStage withAuthResultStatus(String authResultStatus) {
+        public IAuthResultStage withAuthResultStatus(final String authResultStatus) {
             this.authResultStatus = authResultStatus;
             return this;
         }
 
         @Override
-        public IAuthContextStage withAuthResult(String authResult) {
+        public IAuthContextStage withAuthResult(final String authResult) {
             this.authResult = authResult;
             return this;
         }
 
         @Override
-        public IBuildStage withAuthContext(DuoAuthContext authContext) {
+        public IBuildStage withAuthContext(final DuoAuthContext authContext) {
             this.authContext = authContext;
             return this;
         }
 
         @Override
-        public IBuildStage withPreferred_username(String preferred_username) {
-            this.preferred_username = preferred_username;
+        public IBuildStage withPreferredUsername(final String prefUsername) {
+            this.preferredUsername = prefUsername;
             return this;
         }
 
         @Override
-        public IBuildStage withAuth_time(Integer auth_time) {
-            this.auth_time = auth_time;
+        public IBuildStage withAuthTime(final Integer authTime) {
+            this.authTime = authTime;
             return this;
         }
 
@@ -314,9 +333,9 @@ public final class DuoAuthToken {
         }
     }
 
-    
-   
-    
-    
+
+
+
+
 
 }
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
index 5833c3c..3c69d16 100644
--- 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
@@ -16,24 +16,24 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
  * <p>Is immutable, can only be built using the builder.</p>
  */
 @Immutable
-public class DuoHealthCheck {
+public final class DuoHealthCheck {
     
-    /** The health status of the 2FA endpoint.*/
+    /** A successful 'OK' or unsuccessful 'FAIL' response.*/
     @Nonnull @NotEmpty private final String status;
 
-    /** When the response was issued, as ms since Unix EPOCH.*/
+    /** When a successful response was issued, as seconds since Unix EPOCH.*/
     @Nullable private final Integer responseTimestamp;
 
-    /** The response code.*/
+    /** The error response code.*/
     @Nullable @NotEmpty private final String code;
 
-    /** The timestamp ....!!!*/
+    /** When an unsuccessful response was issued, as seconds since Unix EPOCH. */
     @Nullable private final String timestamp;
 
-    /** The response message.*/
+    /** The error response message.*/
     @Nullable private final String message;
 
-    /** The detailed response message.*/ 
+    /** The error response detailed message.*/ 
     @Nullable private final String messageDetail;
 
     
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/package-info.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/package-info.java
new file mode 100644
index 0000000..e4b99f2
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow API model classes. */
+package net.shbboleth.idp.plugin.authn.duo.model;
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/package-info.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/package-info.java
new file mode 100644
index 0000000..64be76d
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow API classes. */
+package net.shbboleth.idp.plugin.authn.duo;
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipal.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/DuoOIDCPrincipal.java
similarity index 95%
rename from idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipal.java
rename to idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/DuoOIDCPrincipal.java
index 3c3af2b..121c06e 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipal.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/DuoOIDCPrincipal.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package net.shbboleth.idp.plugin.authn.duo;
+package net.shbboleth.idp.plugin.authn.duo.principal;
 
 import javax.annotation.Nonnull;
 import javax.annotation.concurrent.Immutable;
@@ -28,7 +28,6 @@ import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 
 /** Principal based on a Duo OIDC based authentication. */
-//TODO: test the immutability and cloning.
 @Immutable
 public final class DuoOIDCPrincipal implements CloneablePrincipal {
 
@@ -79,7 +78,7 @@ public final class DuoOIDCPrincipal implements CloneablePrincipal {
     /** {@inheritDoc} */
     @Override
     public String toString() {
-        StringBuilder builder = new StringBuilder();
+        final StringBuilder builder = new StringBuilder();
         builder.append("DuoOIDCPrincipal [username=");
         builder.append(username);
         builder.append("]");
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/package-info.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/package-info.java
new file mode 100644
index 0000000..81248d9
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/principal/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow principal classes. */
+package net.shbboleth.idp.plugin.authn.duo.principal;
diff --git a/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoClientAuthenticationActionTest.java b/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoClientAuthenticationActionTest.java
new file mode 100644
index 0000000..02becbd
--- /dev/null
+++ b/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoClientAuthenticationActionTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.annotation.Nonnull;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.RequestContextBuilder;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+
+/**
+ * Tests for the {@link AbstractDuoClientAuthenticationAction}.
+ */
+public class AbstractDuoClientAuthenticationActionTest {
+    
+    /** Mock concrete implementation of an {@link AbstractDuoClientAuthenticationAction}.*/
+    private MockAbstractDuoAuthenticationAction action;
+    
+    /** The request context.*/
+    private RequestContext src;
+
+    /** The profile request context.*/
+    private ProfileRequestContext prc;
+    
+    /** The authentication context.*/
+    private  AuthenticationContext ac;
+    
+    /** The duo oidc authentication context.*/
+    private DuoOIDCAuthenticationContext dc;
+    
+    /**
+     * <p>Setup the relevant contexts per method execution.</p>
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @BeforeEach public void setup() throws ComponentInitializationException {
+        action = new MockAbstractDuoAuthenticationAction();
+        final DuoOIDCClientRegistry registry = Mockito.mock(DuoOIDCClientRegistry.class);
+        action.setClientRegistry(registry);    
+        
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
+        ac = new AuthenticationContext();
+        
+        prc.addSubcontext(ac);
+
+    }
+    
+    /** Add the Duo authentication context to the authentication context.*/
+    private void addDuoContext() {
+        dc = new DuoOIDCAuthenticationContext();
+        ac.addSubcontext(dc);
+    }
+    
+    /** Add duo integration to the duo context.*/
+    private void addDuoIntegration() {
+        final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost("host.com");
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
+        integ.setRedirectURI("http://localhost/");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        dc.setIntegration(integ);
+    }
+    
+    /**
+     * Check the {@link EventContext} in the {@link ProfileRequestContext} is a {@link String}
+     * which represents the eventId argument.
+     * 
+     * @param eventId the eventId to check.
+     */
+    private void assertEventId(@Nonnull final String eventId) {
+        final Object event = prc.getSubcontext(EventContext.class, true).getEvent();
+        assertTrue(event instanceof String);
+        assertEquals(eventId, (String)event);
+    }
+
+    /** Test setting a null client registry.*/
+    @Test 
+    @DisplayName("Test setting a null client registry") 
+    public void testSetNullClientRegistry() {               
+        Assertions.assertThrows(ConstraintViolationException.class,() -> action.setClientRegistry(null));
+    }
+
+    /** Test setting a null duo context.*/
+    @Test @DisplayName("Test setting a null duo context")  
+    public void publictestSetNullDuoContextLookupStrategy() {     
+        Assertions.assertThrows(ConstraintViolationException.class,() -> 
+            action.setDuoContextLookupStrategy(null));
+    }
+    
+    /** Test doPreExecute returns true, no failures.*/
+    @Test @DisplayName("Test doPreExecute returns true, no failures") 
+    public void testDoPreExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegration();
+        action.setDuoContextLookupStrategy(prc -> dc);
+        action.initialize();
+        assertTrue(action.doPreExecute(prc, ac));
+        
+    }
+    
+    /**
+     * Test pre-execute without a duo context.
+     *  
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test pre-execute without a duo context") 
+    public void testDoPreExecuteWithNoDuoContext() throws ComponentInitializationException {
+        action.setDuoContextLookupStrategy(prc -> dc);
+        action.initialize();
+        assertFalse(action.doPreExecute(prc, ac));
+        assertEventId(AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+    
+    /**
+     * Test pre-execute without a duo integration.
+     * 
+     * @throws ComponentInitializationException
+     */
+    @Test @DisplayName("Test pre-execute without a duo integration") 
+    public void testDoPreExecuteWithNoDuoIntegration() throws ComponentInitializationException {
+        addDuoContext();
+        action.setDuoContextLookupStrategy(prc -> dc);
+        action.initialize();
+        assertFalse(action.doPreExecute(prc, ac));
+        assertEventId(AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+
+    /** Mock concrete implementation of the AbstractDuoAuthenticationAction.*/
+    private static class MockAbstractDuoAuthenticationAction extends AbstractDuoClientAuthenticationAction{
+        
+    }
+    
+}
+
diff --git a/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipalTest.java b/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipalTest.java
index d5b5963..753f8af 100644
--- a/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipalTest.java
+++ b/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipalTest.java
@@ -2,22 +2,38 @@ package net.shbboleth.idp.plugin.authn.duo;
 
 import static org.junit.jupiter.api.Assertions.*;
 
+import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
+import net.shbboleth.idp.plugin.authn.duo.principal.DuoOIDCPrincipal;
+
+/**
+ * Test for the {@link DuoOIDCPrincipal}.
+ */
 public class DuoOIDCPrincipalTest {
 
-    @Test
+    /**
+     * Test principal cloning.
+     * 
+     * @throws CloneNotSupportedException on error
+     */
+    @Test @DisplayName("Test principal cloning")
     public void testClone() throws CloneNotSupportedException {
         final String name = "jdoe";
-        DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
-        DuoOIDCPrincipal principalTwo = principal.clone();
+        final DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
+        final DuoOIDCPrincipal principalTwo = principal.clone();
         assertEquals(principal, principalTwo);       
     }
     
-    @Test
+    /**
+     * Test name is not mutable.
+     * 
+     * @throws CloneNotSupportedException on error
+     */
+    @Test @DisplayName("Test name is not mutable")
     public void testImmutable() throws CloneNotSupportedException {
         final String name = "jdoe";
-        DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
+        final DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
         //should be obvious as string is not mutable. But dbl check.
         String nameInPrincipal = principal.getName();
         nameInPrincipal = "jdoe-modified";
diff --git a/idp-duo-distribution/pom.xml b/idp-duo-distribution/pom.xml
new file mode 100644
index 0000000..e00d92f
--- /dev/null
+++ b/idp-duo-distribution/pom.xml
@@ -0,0 +1,44 @@
+<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.idp.authn</groupId>
+        <artifactId>idp-plugin-duo-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    <artifactId>idp-plugin-duo-dist</artifactId>
+    <name>Shibboleth IdP :: Plugins :: Duo 2FA Login Flow Distribution</name>
+    <packaging>pom</packaging>
+
+    <properties>
+        <dist.finalName>shibboleth-idp-plugin-duo-${project.version}</dist.finalName>
+    </properties>
+
+    <build>
+        <plugins>
+            <!-- Assemble -->
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>make-assembly</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <appendAssemblyId>false</appendAssemblyId>
+                    <descriptors>
+                        <descriptor>src/main/assembly/duo-assembly.xml</descriptor>
+                    </descriptors>
+                    <finalName>${dist.finalName}</finalName>
+                    <tarLongFileMode>gnu</tarLongFileMode>
+                </configuration>
+            </plugin>
+        </plugins>
+
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/idp-duo-distribution/src/main/assembly/duo-assembly.xml b/idp-duo-distribution/src/main/assembly/duo-assembly.xml
new file mode 100644
index 0000000..15e4c30
--- /dev/null
+++ b/idp-duo-distribution/src/main/assembly/duo-assembly.xml
@@ -0,0 +1,73 @@
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <id>bin</id>
+    <formats>
+        <format>tar.gz</format>
+        <format>tar.bz2</format>
+        <format>zip</format>
+    </formats>
+    <fileSets>
+        <fileSet>
+            <directory>../idp-duo-impl/src/main/resources/flows</directory>
+            <outputDirectory>flows</outputDirectory>
+            <includes>
+                <include>**/*</include>
+            </includes>
+        </fileSet>
+        <fileSet>
+            <directory>../idp-duo-impl/target/dependency</directory>
+            <outputDirectory>edit-webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>../idp-duo-impl/target</directory>
+            <outputDirectory>edit-webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                <include>idp-plugin-duo-impl-*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+                <exclude>*javadoc.jar</exclude>
+                <exclude>*sources.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>../idp-duo-api/target</directory>
+            <outputDirectory>edit-webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                <include>idp-plugin-duo-api-*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+                <exclude>*javadoc.jar</exclude>
+                <exclude>*sources.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>../idp-duo-native-client-impl/target</directory>
+            <outputDirectory>edit-webapp/WEB-INF/lib</outputDirectory>
+            <includes>
+                <include>idp-plugin-duo-native-client-impl-*.jar</include>
+            </includes>
+            <excludes>
+                <exclude>*test*.jar</exclude>
+                <exclude>*javadoc.jar</exclude>
+                <exclude>*sources.jar</exclude>
+            </excludes>
+        </fileSet>
+        <fileSet>
+            <directory>src/main/resources/</directory>
+            <outputDirectory></outputDirectory>
+            <includes>
+                <include>conf/*</include>
+                <include>conf/authn/*</include>                
+            </includes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/idp-duo-distribution/src/main/resources/conf/authn/duo-oidc-authn-config.xml b/idp-duo-distribution/src/main/resources/conf/authn/duo-oidc-authn-config.xml
new file mode 100644
index 0000000..f7e9f4a
--- /dev/null
+++ b/idp-duo-distribution/src/main/resources/conf/authn/duo-oidc-authn-config.xml
@@ -0,0 +1,13 @@
+<?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">
+
+
+
+</beans>
diff --git a/idp-duo-distribution/src/main/resources/conf/authn/idp-duo-oidc.properties b/idp-duo-distribution/src/main/resources/conf/authn/idp-duo-oidc.properties
new file mode 100644
index 0000000..bd66221
--- /dev/null
+++ b/idp-duo-distribution/src/main/resources/conf/authn/idp-duo-oidc.properties
@@ -0,0 +1,5 @@
+## Duo OIDC 2FA integration settings
+idp.duo.oidc.apiHost = hostname
+idp.duo.oidc.clientId = clientID
+idp.duo.oidc.secretKey = key
+idp.duo.oidc.redirectUri = redirect
\ No newline at end of file
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index e9d8856..d2cbdd4 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -2,13 +2,15 @@
     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>
 
+    <name>Shibboleth IdP :: Plugins :: Duo 2FA Login Flow Impl</name>
     <parent>
-        <groupId>net.shibboleth.plugin.authn</groupId>
-        <artifactId>idp-duo-parent</artifactId>
+        <groupId>net.shibboleth.plugin.idp.authn</groupId>
+        <artifactId>idp-plugin-duo-parent</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </parent>
 
-    <artifactId>idp-duo-impl</artifactId>
+
+    <artifactId>idp-plugin-duo-impl</artifactId>
 
     <properties>
         <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.impl</automatic.module.name>
@@ -22,15 +24,11 @@
 
         <!-- compile time depedencies -->
         <dependency>
-            <groupId>net.shibboleth.plugin.authn</groupId>
-            <artifactId>idp-duo-api</artifactId>
+            <groupId>net.shibboleth.plugin.idp.authn</groupId>
+            <artifactId>idp-plugin-duo-api</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
-            <version>${apache.commons.lang.version}</version>
-        </dependency>
+
 
         <!-- Provided compile time dependencies the project builds against -->
 
@@ -45,13 +43,24 @@
             </exclusions>
             <scope>provided</scope>
         </dependency>
+        
+        <dependency> 
+            <groupId>net.shibboleth.ext</groupId>
+            <artifactId>spring-extensions</artifactId>            
+            <scope>provided</scope>
+        </dependency>
 
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>${apache.commons.lang.version}</version>
+            <scope>provided</scope>
+        </dependency>
 
-
-        <!-- Duo client implementation is a runtime depedancy that can be swapped out -->
+        <!-- Duo client implementation is a runtime dependency that can be swapped out -->
         <dependency>
-            <groupId>net.shibboleth.plugin.authn</groupId>
-            <artifactId>idp-duo-native-client-impl</artifactId>
+            <groupId>net.shibboleth.plugin.idp.authn</groupId>
+            <artifactId>idp-plugin-duo-native-client-impl</artifactId>
             <scope>runtime</scope>
         </dependency>
 
@@ -82,6 +91,14 @@
 
         </dependency>
 
+
+        <dependency>
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-profile-api</artifactId>
+            <scope>test</scope>
+            <type>test-jar</type>
+        </dependency>
+
         <!-- Spring webflow tests require Junit4 / will junit-vintage-engine help? -->
         <dependency>
             <groupId>junit</groupId>
@@ -121,6 +138,7 @@
                     </execution>
                 </executions>
             </plugin>
+            
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckDuoState.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckDuoState.java
deleted file mode 100644
index d170c14..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckDuoState.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
-import net.shibboleth.idp.authn.AbstractAuthenticationAction;
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-
-/**
- * Validate the state (which is required in the Duo flow) sent in the Duo 2FA request matches that in the 2FA response.
- * 
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
- * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null</pre>
- */
-public class CheckDuoState extends AbstractAuthenticationAction {    
-    
-    /** Class logger. */
-    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(CheckDuoState.class);
-    
-    /** The Duo authentication context which stores information about the Duo 2FA request.*/
-    @Nonnull @NotEmpty private DuoAuthenticationContext duoContext;
-    
-    
-    /** {@inheritDoc} */
-    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
-
-        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
-            return false;
-        }
-
-        duoContext = authenticationContext.getSubcontext(DuoAuthenticationContext.class);
-        if (duoContext == null) {
-            log.info("{} No DuoAuthenticationContext available", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
-            return false;
-        }
-        return true;
-    }
-    
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
-        
-        final String requestState = duoContext.getRequestState();
-        final String responseState = duoContext.getResponseState();
-        log.trace("Duo 2FA request state '{}' was returned in the response as '{}'",requestState,responseState );
-        
-        if (requestState == null || responseState == null) {
-            log.error("State was not present in either the request or response, state is mandatory for Duo 2FA requests");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
-            return;
-        }
-        if (!requestState.equals(responseState)) {
-            log.error("Duo request state did not match response state, has it been tampered with!");
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
-            return;
-        } else {
-            log.trace("Duo 2FA request and response state match");
-        }
-        //state is fine.
-        
-    }
-
-}
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
index cbe0ff0..421d33e 100644
--- 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
@@ -17,8 +17,7 @@
 
 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;
@@ -29,117 +28,113 @@ import javax.annotation.concurrent.ThreadSafe;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoClientInitializationException;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
 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.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
 /**
- * Default DuoClient registry.
+ * <p>The default Duo Client registry for mapping a {@link DuoOIDCIntegration} to either a new
+ * or existing {@link DuoOIDCClient} instance.</p>
  * 
- * Instance of registry used for auto-creation of
- *
- * Supports lazy generation of {@link DuoOIDCClient}s. Clients are tied to and 
- * hence reused per {@link DuoOIDCIntegration}.
+ * <p>Clients are created and registered against a {@link DuoOIDCIntegration}. Once created the client is
+ * re-used for the lifetime of the IdP. The {@link DuoOIDCIntegration} should decide it's own business key (using the
+ * equals and hashcode method appropriately), the {@link DefaultDuoOIDCIntegration} key is the clientID.</p>
+ * 
+ * <p>Support lazy intilization of clients when they are first requested. A configurable client factory 
+ * is called to initilize new clients.</p>
  * 
- * TODO: FINISH WHEN WE DECIDE IF THIS IS GOOD.
  */
 @ThreadSafe
-//TODO: this is like a factory and a registry. See PrincipalEvalPredicateFactoryRegistry.
-public class DefaultDuoOIDCClientRegistry extends AbstractIdentifiableInitializableComponent 
-    implements DuoOIDCClientRegistry{
+public class DefaultDuoOIDCClientRegistry extends AbstractIdentifiableInitializableComponent
+             implements DuoOIDCClientRegistry {
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultDuoOIDCClientRegistry.class);
     
+    /** Factory to produce Duo clients.*/
+    @Nonnull private DuoOIDCClientFactory clientFactory;
+    
     /** Registry of Duo client to Duo integration.*/
     @Nonnull @NonnullElements private final ConcurrentMap<DuoOIDCIntegration, DuoOIDCClient> clientRegistry;
     
-    /** Create clients of this type.*/
-    @NonnullAfterInit private Class<? extends DuoOIDCClient> clientType;
-    
     /** Function for creating a DuoClient from a DuoIntegration. */
     @Nonnull private Function<DuoOIDCIntegration, DuoOIDCClient> clientRegistryMappingFunction; 
     
-    /** Constructor .*/
+    /** 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");
-        }
+        clientRegistry = new ConcurrentHashMap<DuoOIDCIntegration, DuoOIDCClient>(1);
+        clientRegistryMappingFunction = new CreateNewClientMappingFunction();
     }
     
     /**
-     * Set the Duo OIDC client type to be used for all Duo integrations.
+     * Set the client factory to use.
      * 
-     * @param type the class name as a string. 
+     * @param factory the factory.
      */
-    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 (final 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");
-        }
+    public void setClientFactory(@Nonnull final DuoOIDCClientFactory factory) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(factory, "Duo client factory can not be null");
+        clientFactory = factory;
     }
+
     
-    //TODO: review the 'business key' of the integration used to lookup the client. Should that be immutable?
-    /** {@link Inherited}.*/
+    @Override
     @Nonnull public DuoOIDCClient getIntegrationClientOrCreate(@Nonnull final DuoOIDCIntegration integration) 
             throws DuoRegistryException {
         Constraint.isNotNull(integration, "Duo integration can not be null");
-        //this is an atomic call, avoiding the need to synchronise here.
-        final DuoOIDCClient client = clientRegistry.computeIfAbsent(integration,clientRegistryMappingFunction);
-        if (client == null) {
-            throw new DuoRegistryException("DuoClient could not be found or created in the registry");
+
+        try {
+            //this is an atomic call, avoiding the need to synchronise here e.g. two clients should never 
+            //be created for the same integration.
+            final DuoOIDCClient client = clientRegistry.computeIfAbsent(integration,clientRegistryMappingFunction);
+            log.debug("Retrieved existing Duo client for integration '{}'",integration);
+            return client;
+        } catch (final DuoClientInitializationException e) {
+            throw new DuoRegistryException("DuoClient could not be found or created in the registry",e);
+        }        
+        
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (clientFactory == null) {
+            throw new ComponentInitializationException("Duo Client Factory cannot be null");
         }
-        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.
+     * A function for creating a new Duo client from the configured client factory for the given Duo integration.
+     * 
+     * @throws DuoClientInitializationException if the factory can not create the client.
      */
-    private class CreateNewClientRegistryMappingFunction implements Function<DuoOIDCIntegration, DuoOIDCClient> {
+    private class CreateNewClientMappingFunction implements Function<DuoOIDCIntegration, DuoOIDCClient> {
         
         /** Class logger. */
-        @Nonnull private final Logger log = LoggerFactory.getLogger(CreateNewClientRegistryMappingFunction.class);
+        @Nonnull private final Logger log = LoggerFactory.getLogger(CreateNewClientMappingFunction.class);
         
         @Override
         public DuoOIDCClient apply(@Nonnull final DuoOIDCIntegration integration) {
-//            try {
-//                final DuoOIDCClient client = clientType.getDeclaredConstructor().newInstance();
-//                client.initialize(integration);
-//                log.debug("Created new client {}",client);
-//                return client;
-//            } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException
-//                    | InvocationTargetException | NoSuchMethodException | SecurityException e) {
-//                log.warn("Could not create a DuoClient instance for the integration '{}'",
-//                        integration.getClientId(),e);
-//                
-//            } catch (final DuoClientException e) {
-//                log.warn("Could not initialise the DuoClient for the integration '{}'",
-//                        integration.getClientId(),e);  
-//            }
-            //nothing added, nothing returned.
-            return null;
+            
+            try {
+                log.debug("Creating a new Duo client for integration '{}'",integration);
+                return clientFactory.createInstance(integration);
+            } catch (final DuoClientException e) {
+                //wrap the exception in a runtime exception.
+                throw new DuoClientInitializationException("Could not initialise "
+                        + "the DuoClient for the client "+integration.getClientId(),e);
+            }           
         }
         
     }
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthenticationDescription.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthenticationDescription.java
new file mode 100644
index 0000000..b6060d1
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthenticationDescription.java
@@ -0,0 +1,63 @@
+/*
+ * 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.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import net.shibboleth.utilities.java.support.plugin.AbstractPluginDescription;
+
+/**
+ * Details about the Duo OIDC 2FA plugin.
+ */
+public class DuoOIDCAuthenticationDescription extends AbstractPluginDescription{
+
+    @Override
+    public String getPluginId() {
+        return "net.shibboleth.plugin.idp.authn.duo";
+    }
+    
+    @Override
+    public List<String> getAdditionalPropertyFiles(){
+        return List.of("/conf/authn/idp-duo-oidc.properties");
+    }
+
+
+    @Override
+    public int getMajorVersion() {
+        return 0;
+    }
+
+    @Override
+    public int getMinorVersion() {
+        return 0;
+    }
+
+    @Override
+    public int getPatchVersion() {
+        return 1;
+    }
+
+    @Override
+    public List<URL> getUpdateURLs() throws IOException {
+        return Collections.emptyList();
+    }
+
+}
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
index 84b207f..9afbcb1 100644
--- 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
 import java.io.IOException;
@@ -41,7 +40,7 @@ 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.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.idp.authn.ExternalAuthentication;
 import net.shibboleth.idp.authn.ExternalAuthenticationException;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -51,34 +50,34 @@ import net.shibboleth.utilities.java.support.component.AbstractInitializableComp
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-
+//FIXME: This class needs work e.g. around SWF key storage etc. 
 /**
  * 
- * MVC controller for managing the Duo 2FA exchanges implemented as an {@link ExternalAuthentication} mechanism
+ * <p>MVC controller for managing the Duo 2FA exchanges implemented as an {@link ExternalAuthentication} mechanism</p>
  * 
- * MVC controller used to initiate the Duo OIDC flow and collect the authorisation response.
- * FINISH
+ * <p>The controller initiates the Duo OIDC flow and collects the authorisation response.</p>
+ * TODO: FINISH
  * 
  */
 @Controller
- at RequestMapping("%{idp.authn.duo.OIDC.externalAuthnPath:/Authn/Duo}")
+ at RequestMapping("%{idp.authn.duo.OIDC.externalAuthnPath:/Authn/Duo/2FA}")
 public class DuoOIDCAuthnController extends AbstractInitializableComponent{
     
     /** The name of the Http parameter that stores the authorisation code.*/
-    @Nonnull @NotEmpty public final static String CODE_PARAMETER = "code";
+    @Nonnull @NotEmpty public static final String CODE_PARAMETER = "code";
     
     /** The name of the Http parameter that stores the state value.*/
-    @Nonnull @NotEmpty public final static String STATE_PARAMETER = "state";
+    @Nonnull @NotEmpty public static final String STATE_PARAMETER = "state";
     
     /** The name of the session attribute that stores the flow execution key.*/ 
-    @Nonnull @NotEmpty public final static String KEY_SESSION_ATTRIBUTE = "key";
+    @Nonnull @NotEmpty public static final String KEY_SESSION_ATTRIBUTE = "key";
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DuoOIDCAuthnController.class);
     
     
     /** Lookup strategy to locate the Duo authentication context. */
-    @Nonnull private Function<ProfileRequestContext,DuoAuthenticationContext> duoContextLookupStrategy;
+    @Nonnull private Function<ProfileRequestContext,DuoOIDCAuthenticationContext> duoContextLookupStrategy;
 
     
     /** The registry for locating the DuoClient for the established integration.*/
@@ -87,7 +86,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
     /** Constructor. */
     public DuoOIDCAuthnController() {
         // PRC -> AC -> DuoAuthenticationContext
-        duoContextLookupStrategy = new ChildContextLookup<>(DuoAuthenticationContext.class).
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoOIDCAuthenticationContext.class).
                 compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
@@ -97,7 +96,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
      * @param strategy lookup strategy
      */
     public void setDuoContextLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
         duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookuplookup strategy cannot be null");
     }
     
@@ -120,7 +119,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         }
     }
     
-    @GetMapping("/2FA/authorize")
+    @GetMapping("/authorize")
     public void authorizationRequest(@Nonnull final HttpServletRequest httpRequest,
             @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException, IOException {
         
@@ -136,7 +135,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         
         final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
         
-        final DuoAuthenticationContext duoContext = duoContextLookupStrategy.apply(prc);
+        final DuoOIDCAuthenticationContext duoContext = duoContextLookupStrategy.apply(prc);
         if (duoContext == null) {
             log.error("No Duo context to use in initiating a Duo 2FA request");
             httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
@@ -145,7 +144,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
             
         }
         final DuoOIDCIntegration integration = duoContext.getIntegration();
-        log.trace("Starting Duo 2FA for integration '{}' and user '{}'",integration.getIntegrationKey(),
+        log.trace("Starting Duo 2FA for integration client '{}' and user '{}'",integration.getClientId(),
                 duoContext.getUsername());
         try {
             final DuoOIDCClient client = clientRegistry.getIntegrationClientOrCreate(integration);            
@@ -163,7 +162,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         
     }
 
-    @GetMapping("/2FA/authorize-callback")
+    @GetMapping("/duo-callback")
     public void authorizationCallback(@Nonnull final HttpServletRequest httpRequest,
             @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException, IOException {
         
@@ -180,15 +179,15 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         
         final Object keyObject = session.getAttribute(KEY_SESSION_ATTRIBUTE);
         if (keyObject == null || !(keyObject instanceof String)) {
-            throw new ExternalAuthenticationException("Flow execution key could not be found in the Http session, unable"
-                    + " to resume the flow execution");
+            throw new ExternalAuthenticationException("Flow execution key could not be found in the "
+                    + "Http session, unable to resume the flow execution");
         }
-        //for this point onwards, any error can be added back into the flow.
+        //from this point onwards, any error can be added back into the flow.
         final String key = StringUtils.trim((String)keyObject);
         
         final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
         
-        final DuoAuthenticationContext duoContext = duoContextLookupStrategy.apply(prc);
+        final DuoOIDCAuthenticationContext duoContext = duoContextLookupStrategy.apply(prc);
         if (duoContext == null) {
             log.error("No Duo authentication context to store the Duo 2FA response");
             httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY, EventIds.INVALID_PROFILE_CTX);
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
index c30e768..6ffccf8 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -1,3 +1,20 @@
+/*
+ * 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.security.SecureRandom;
@@ -16,12 +33,11 @@ public final class DuoSupport {
     private DuoSupport() {
 
     }
-    
-    
+   
     /**
      * Generates a random identifier to be used as the state variable in Duo 2FA requests.
      *  
-     * @param length the length of the parameter, Duo require a minimum 32 character state value.
+     * @param length the length of the parameter, Duo requires a minimum 32 character state value.
      * 
      * @return the randomly generated state value.
      */
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
index b3a83dd..3bd0f78 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
@@ -1,3 +1,20 @@
+/*
+ * 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 javax.annotation.Nonnull;
@@ -7,20 +24,26 @@ 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.AbstractDuoClientAuthenticationAction;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 
 
 /**
- * Exchange the authorization code in the Duo 2FA response for a Duo token that describes the result
+ * Action to exchange the authorization code in the Duo 2FA response for a Duo id_token that describes the result
  * of 2FA. Adds the token to the Duo context.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null and 
+ * AuthenticationContext.getSubcontect(DuoOIDCAuthenticationContext.class,false)!=null</pre>
+ * @post Add the Duo authentication token to the context. 
  */
-public class ExchangeCodeForDuoToken extends AbstractDuoAuthenticationAction{
+public class ExchangeCodeForDuoToken extends AbstractDuoClientAuthenticationAction{
     
     /** Class logger.*/
     @Nonnull private final Logger log = LoggerFactory.getLogger(ExchangeCodeForDuoToken.class);
@@ -29,25 +52,26 @@ public class ExchangeCodeForDuoToken extends AbstractDuoAuthenticationAction{
     /** {@inheritDoc} */
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
-            @Nonnull final DuoAuthenticationContext duoContext) {    
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {    
         
         final String code = duoContext.getAuthorizationCode();
         
         if (code == null) {
-            log.error("{} Duo 2FA code is not available in the response",getLogPrefix());
+            log.error("{} Duo 2FA authorization code is not available in the response",getLogPrefix());
             //FIXME: maybe our own exception and switch in the flow here.
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
         }
         
         try {
             final DuoAuthToken token = client.exchangeAuthorizationCodeFor2FAResult(code);
-            log.trace("{} Duo 2FA token received '{}'",getLogPrefix(),token);
-            duoContext.setAuthToken(token);
-            
+            log.info("{} Duo 2FA token received for subject '{}'",getLogPrefix(),token.getSub());
+            duoContext.setAuthToken(token);            
+            //success
         } catch (final DuoClientException e) {
             log.error("{} Unable to exchange authorisation code for 2FA result",getLogPrefix(),e);
-            //FIXME: our own event?
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
         }
         
     }
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
index 6da837b..a0c27e1 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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 javax.annotation.Nonnull;
@@ -8,59 +25,81 @@ 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.AbstractDuoClientAuthenticationAction;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 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. 
+ * An {@link AuthnEventIds#AUTHN_EXCEPTION} event is emitted if the health point is unreachable or unhealthy.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#IO_ERROR}
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
  */
-//TODO: Support multiple integrations (multiplexing) e.g. the strategy.
-//TODO: abstract the duo action integration and client lookup? done?
-public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoAuthenticationAction{
+public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoClientAuthenticationAction{
     
     /** 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";
+    @Nonnull @NotEmpty public static final String DUO_UNAVAILABLE = "DuoUnavailable";        
     
     /** 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,
-            @Nonnull final DuoAuthenticationContext duoContext) {        
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {        
         
         try {
+            //Native duo client will throw an exception if anything other than OK is returned. 
             final DuoHealthCheck healthCheckResponse = client.healthCheck();            
-            log.trace("Duo health check response '{}'",healthCheckResponse);
+            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");
+                log.info("{} No Duo 2FA health check response", getLogPrefix());
+                throw new DuoClientException("Duo 2FA is unavailable, null 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;
+            //These checks are redundant if using v1.0.0 of the native Duo client as it throws an exception if not 'OK'.
+            //They are still included  to be compatible with other implementations which return the full response. 
+            if (DuoOIDCAuthAPI.DUO_RESPONSE_STATUS_OK.equalsIgnoreCase(healthCheckResponse.getStatus())) {
+                log.trace("{} Duo 2FA endpoints are healthy!",getLogPrefix());
+                return;
+                
+            } else if (DuoOIDCAuthAPI.DUO_RESPONSE_STATUS_FAIL.equalsIgnoreCase(healthCheckResponse.getStatus())) {
+                //2FA is unavailable for the given client integration. 
+                log.info("{} Duo 2FA health check failed, current status '{}',"
+                        + "code '{}', message '{}', message detail '{}'",getLogPrefix(),
+                        healthCheckResponse.getStatus(),healthCheckResponse.getCode(),
+                        healthCheckResponse.getMessage(),healthCheckResponse.getMessageDetail());
+                throw new DuoClientException("Duo 2FA health check responded with a failure status"+
+                        healthCheckResponse.getMessage());
             
+            } else {
+                log.info("{} Duo health check response contained an unknown status of '{}', "
+                        + "code '{}', message '{}', message detail '{}'",getLogPrefix(),
+                        healthCheckResponse.getStatus(),healthCheckResponse.getCode(),
+                        healthCheckResponse.getMessage(),healthCheckResponse.getMessageDetail());
+                throw new DuoClientException("Duo 2FA health check returned an unknown status response: "+
+                        healthCheckResponse.getMessage());
+            }           
+   
         } 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);
+           log.error("{} Duo API health check failed",getLogPrefix(),e);
+           ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
            return;
         }
         
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
index 6737096..c027e1b 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
@@ -1,3 +1,19 @@
+/*
+ * 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;
 
@@ -12,7 +28,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.idp.authn.AbstractAuthenticationAction;
 import net.shibboleth.idp.authn.AuthnEventIds;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
@@ -23,7 +39,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 
 /**
- * An action to create (or lookup if configured) and populate the {@link DuoAuthenticationContext} 
+ * An action to create (or lookup) and populate the {@link DuoOIDCAuthenticationContext} 
  * with the username and chosen {@link DuoIntegration} appropriate for this request. 
  * 
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
@@ -36,8 +52,8 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateDuoAuthenticationContext.class);
     
-    /** Strategy used to locate or create the {@link DuoAuthenticationContext} to populate. */
-    @Nonnull private Function<ProfileRequestContext,DuoAuthenticationContext> duoAuthContextCreationStrategy;
+    /** Strategy used to locate or create the {@link DuoOIDCAuthenticationContext} to populate. */
+    @Nonnull private Function<ProfileRequestContext,DuoOIDCAuthenticationContext> duoAuthContextCreationStrategy;
 
     /** Lookup strategy for username to match against Duo identity. */
     @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
@@ -47,8 +63,11 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
 
     /** Constructor.*/
     public PopulateDuoAuthenticationContext() {
+        //default creates duo authentication context under authentication context.
         duoAuthContextCreationStrategy =
-                new ChildContextLookup<>(DuoAuthenticationContext.class, true);
+                new ChildContextLookup<>(DuoOIDCAuthenticationContext.class, true).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
+        
         usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
         duoIntegrationLookupStrategy = FunctionSupport.constant(null);
     }
@@ -65,12 +84,12 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
     }
     
     /**
-     * Set the strategy used to locate the {@link DuoAuthenticationContext} to operate on.
+     * Set the strategy used to locate the {@link DuoOIDCAuthenticationContext} to operate on.
      * 
      * @param strategy lookup strategy
      */
     public void setDuoContextCreationStrategy(
-            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+            @Nonnull final Function<ProfileRequestContext,DuoOIDCAuthenticationContext> strategy) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
 
         duoAuthContextCreationStrategy = Constraint.isNotNull(strategy, "DuoAuthenticationContext"
@@ -93,7 +112,7 @@ public class PopulateDuoAuthenticationContext extends AbstractAuthenticationActi
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
 
-        final DuoAuthenticationContext context = duoAuthContextCreationStrategy.apply(profileRequestContext);
+        final DuoOIDCAuthenticationContext context = duoAuthContextCreationStrategy.apply(profileRequestContext);
         if (context == null) {
             log.error("{} Error creating DuoAuthenticationContext", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuo2FAToken.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuo2FAToken.java
deleted file mode 100644
index 6b661c9..0000000
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuo2FAToken.java
+++ /dev/null
@@ -1,144 +0,0 @@
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.security.auth.Subject;
-
-import org.opensaml.profile.action.ActionSupport;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import net.shbboleth.idp.plugin.authn.duo.DuoOIDCPrincipal;
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
-import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
-import net.shibboleth.idp.authn.AbstractValidationAction;
-import net.shibboleth.idp.authn.AuthenticationResult;
-import net.shibboleth.idp.authn.AuthnEventIds;
-import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
-/**
- * An action that checks for a a validate {@link DuoAuthToken} and directly produces an
- * {@link AuthenticationResult} based on the identity described by the token.
- * 
- * TODO: finish events and pre and post.
- */
-public class ValidateDuo2FAToken extends AbstractValidationAction{
-    
-    /** Class logger.*/
-    @Nonnull private final Logger log = LoggerFactory.getLogger(ExchangeCodeForDuoToken.class);
-    
-    
-    /** DuoApi context for tokens. **/
-    @Nonnull @NotEmpty private DuoAuthenticationContext duoContext;
-    
-    /** Attempted username. */
-    @Nullable @NotEmpty private String username;
-    
-    /** 
-     * <p>The allowed authentication result status values.</p>
-     * 
-     * <p> The status is the string pertaining to the success of the authentication. The case should not
-     * be considered when matching to the response.</p>
-     */
-    //TODO: not clear what the other status are? Looks like it only returns if the authn was succesful.
-    //TODO: See DuoAuthAPI for constants we could use or replicate?
-    public enum AuthResultStatus{       
-        
-        /** 2FA success.*/
-        Success("allow");
-        
-        /** The result string associated with this status.*/
-        @Nonnull @NotEmpty private String status;
-        
-        /**
-         * Constructor.
-         *
-         * @param statusValue the status string.
-         */
-        private AuthResultStatus(@Nonnull @NotEmpty final String statusValue) {
-            status = Constraint.isNotEmpty(statusValue, "the same-site attribute value can not be empty");
-         }
-
-        /**
-         * Get the status name.
-         * 
-         * @return Returns the name.
-         */
-        @Nonnull public String getStatus() {
-            return status;
-        }
-        
-    }
-    
-    //TODO: Consider the duo factors etc.
-    /** {@inheritDoc} */
-    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
-
-        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
-            return false;
-        }
-
-        duoContext = authenticationContext.getSubcontext(DuoAuthenticationContext.class);
-        if (duoContext == null) {
-            log.info("{} No DuoAuthenticationContext available", getLogPrefix());
-            handleError(profileRequestContext, authenticationContext, "No DuoAuthenticationContext context available",
-                    AuthnEventIds.INVALID_AUTHN_CTX);
-            recordFailure();
-            return false;
-        } 
-        username = duoContext.getUsername();
-        return true;
-    }
-
-    
-    /** {@inheritDoc} */
-    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {    
-        
-        final DuoAuthToken token = duoContext.getAuthToken();
-        if (token == null) {
-            log.error("{} Duo 2FA token is not available",getLogPrefix());
-            //FIXME: maybe our own exception and switch in the flow here.
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
-            recordFailure();
-            return;
-        }
-        
-        //check success of 2fa.
-        if (!AuthResultStatus.Success.getStatus().equalsIgnoreCase(token.getAuthResultStatus())){
-            log.error("{} Duo 2FA was not successful, status is '{}'",getLogPrefix(),
-                    token.getAuthResultStatusMessage());
-            //FIXME: maybe our own exception and switch in the flow here.
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
-            recordFailure();
-            return;
-        }
-        log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
-        recordSuccess();
-        buildAuthenticationResult(profileRequestContext, authenticationContext);
-    }
-    
-    /** {@inheritDoc} */
-    @Override protected Subject populateSubject(@Nonnull final Subject subject) {
-        subject.getPrincipals().add(new DuoOIDCPrincipal(username));
-        //FIXME: We will need this?
-        //subject.getPrincipals().addAll(duoIntegration.getSupportedPrincipals(Principal.class));
-        return subject;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext) {
-        super.buildAuthenticationResult(profileRequestContext, authenticationContext);
-
-        //TODO: is this still the case for this MFA plugin? or should it support a mode where the username is not already supplied c14n'ed.
-        // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
-        profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(username);
-    }
-
-}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
new file mode 100644
index 0000000..2fd4a94
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseState.java
@@ -0,0 +1,80 @@
+/*
+ * 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 javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Authentication action that validates the Duo response state parameter (which is <b>required</b> in the Duo flow) 
+ * matches that in the 2FA request.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ */
+public class ValidateDuoResponseState extends AbstractDuoAuthenticationAction {    
+    
+    /** Class logger. */
+    @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(ValidateDuoResponseState.class);
+    
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext, 
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+        
+        log.trace("{} Duo 2FA request state '{}' was returned in the response as '{}'",
+                getLogPrefix(),duoContext.getRequestState(),duoContext.getResponseState() );
+        
+        if (duoContext.getRequestState() == null || duoContext.getResponseState() == null) {
+            log.error("{} The state parameter was not present in either the request or response, "
+                    + "state is mandatory for Duo 2FA requests",getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        if (!duoContext.getRequestState().equals(duoContext.getResponseState())) {
+            log.error("{} Duo request state did not match response state, has it been tampered with!",
+                    getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        } 
+        // else state is fine.
+        log.debug("{} Duo 2FA request and response state match, continuing",getLogPrefix());
+        
+        
+        
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
new file mode 100644
index 0000000..953be95
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudience.java
@@ -0,0 +1,102 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * An action that verifies the Audience (aud) claim in the Duo token contains the client_id of this client (as
+ * registered at the issuer). See section 3.1.3.7 of OpenID Connect core 1.0.
+ * 
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre n<pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ */
+public class ValidateDuoTokenAudience extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAudience.class);
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        final String audience = token.getAud();
+        if (audience == null) {
+            log.error("{} No audience found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        if (duoContext.getIntegration() == null) {
+            log.error("{} No Duo integration found in the Duo context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        if (!audience.equals(duoContext.getIntegration().getClientId())) {
+            log.error("{} Client is not the intended audience", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        log.debug("{} Token has the correct audience '{}' for this client",getLogPrefix(),audience);
+        //audience is fine
+
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
new file mode 100644
index 0000000..e6f39c9
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResult.java
@@ -0,0 +1,139 @@
+/*
+ *  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.security.Principal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.principal.DuoOIDCPrincipal;
+import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthenticationResult;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * A validation action that checks for a validate {@link DuoAuthToken} and directly produces an
+ * {@link AuthenticationResult} based on the identity described by the token.
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @event {@link AuthnEventIds#INVALID_AUTHN_CTX}
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ */
+public class ValidateDuoTokenAuthenticationResult extends AbstractValidationAction{
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAuthenticationResult.class);
+       
+    /** Duo authentiction context. **/
+    @Nonnull private DuoOIDCAuthenticationContext duoContext;
+    
+    /** Attempted username. */
+    @Nullable @NotEmpty private String username;
+    
+    
+    /** {@inheritDoc} */
+    @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+
+        duoContext = authenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class);
+        if (duoContext == null) {
+            log.info("{} No DuoAuthenticationContext available", getLogPrefix());
+            handleError(profileRequestContext, authenticationContext, "No DuoAuthenticationContext context available",
+                    AuthnEventIds.INVALID_AUTHN_CTX);
+            recordFailure();
+            return false;
+        }         
+        //we get username from the original context, not the duo response.
+        username = duoContext.getUsername();        
+        return true;
+    }
+
+    
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {    
+        
+        final DuoAuthToken token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available",getLogPrefix());
+            handleError(profileRequestContext, authenticationContext,"Duo 2FA result not available",
+                    AuthnEventIds.INVALID_CREDENTIALS);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            recordFailure();
+            return;
+        }
+        
+        if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW.equalsIgnoreCase(token.getAuthResultStatus())){
+            log.debug("{} Duo 2FA authentication succeeded for '{}'",getLogPrefix(),duoContext.getUsername());
+            recordSuccess();
+            buildAuthenticationResult(profileRequestContext, authenticationContext);
+        } else if (DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY.equalsIgnoreCase(token.getAuthResultStatus())) {
+            log.error("{} Duo 2FA failed for '{}', 2FA status '{}'",getLogPrefix(), username,
+                    token.getAuthResultStatusMessage());
+            handleError(profileRequestContext, authenticationContext, token.getAuthResultStatus(),
+                    AuthnEventIds.INVALID_CREDENTIALS);
+            recordFailure();
+        } else {        
+            log.error("{} Duo 2FA access failed for '{}', unknown response", getLogPrefix(), username);
+            handleError(profileRequestContext, authenticationContext,"Unexepected Authentication Response", 
+                    AuthnEventIds.AUTHN_EXCEPTION);
+            recordFailure();
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override protected Subject populateSubject(@Nonnull final Subject subject) {
+        subject.getPrincipals().add(new DuoOIDCPrincipal(username));
+        subject.getPrincipals().addAll(duoContext.getIntegration().getSupportedPrincipals(Principal.class));
+        return subject;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+
+        //TODO: is this still the case for this MFA plugin? or should it support a mode where the username is not already supplied c14n'ed.
+        // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
+        profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(username);
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
new file mode 100644
index 0000000..ff62ef1
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTime.java
@@ -0,0 +1,161 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * 
+ * <p> An action that checks if auth_time (when the End-User authentication took place) stored within the id_token
+ * is within a valid expiration window.</p>
+ * 
+ * <p> This only applies to forced authentication requests, to ensure active 2FA was performed.</p> 
+ * 
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_AUTHN_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ */
+public class ValidateDuoTokenAuthenticationTime extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAuthenticationTime.class);
+    
+    /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
+    @Nonnull private Duration clockSkew;
+       
+    /** Amount of time for which a token is valid after if it was issued. (Default value: 1 minute) */
+    @Nonnull private Duration authnLifetime;
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+    
+    /** Constructor. */
+    public ValidateDuoTokenAuthenticationTime() {        
+        authnLifetime = Duration.ofMinutes(1);
+        clockSkew = Duration.ofMinutes(1);
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid.
+     * 
+     * @param lifetime amount of time for which a token is valid
+     */
+    public synchronized void setAuthnLifetime(@Nonnull final Duration lifetime) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(lifetime, "Token authentication lifetime cannot be null");
+        Constraint.isFalse(lifetime.isNegative(), "Token authentication lifetime cannot be negative");
+        
+        authnLifetime = lifetime;
+    }
+    
+    /**
+     * Set the clock skew.
+     * 
+     * @param skew clock skew to set
+     */
+    public void setClockSkew(@Nonnull final Duration skew) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+                
+        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+    }
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+ 
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+        
+        if (!authenticationContext.isForceAuthn()) {
+            return;
+        }
+        
+        //auth_time in seconds (as integer) since Unix EPOCH.
+        final Integer authTimeEpochSeconds = token.getAuthTime();
+        if (authTimeEpochSeconds == null) {
+            log.error("{} No auth_time found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return; 
+        }
+        
+        final Instant authTime = Instant.ofEpochSecond(authTimeEpochSeconds);
+        final Instant now = Instant.now();
+        final Instant latestValid = now.plus(clockSkew.abs());
+        final Instant expiration = authTime.plus(clockSkew.abs()).plus(authnLifetime);
+
+        // Check time of authentication wasn't in the future
+        if (authTime.isAfter(latestValid)) {
+            log.warn("{} Authentication is not yet valid: auth_time was {}, latest valid is: {}", getLogPrefix(),
+                    authTime, latestValid);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+
+        // Check time of authentication has not expired
+        if (expiration.isBefore(now)) {
+            log.warn(
+                    "{} Authentication has expired: auth_time was '{}', expired at: '{}', current time: '{}'",
+                    getLogPrefix(), authTime, expiration, now);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+        }
+        
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
new file mode 100644
index 0000000..bcc1273
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTime.java
@@ -0,0 +1,130 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * An action that verifies the expiration time (exp) of an id_token. If the expiration time has past
+ * the id_token must not be accepted. A few minutes of leeway is allowed - clock skew.
+ * See section 3.1.3.7 of OpenID Connect core 1.0.
+ * 
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ */
+public class ValidateDuoTokenExpirationTime extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenExpirationTime.class);
+    
+    /** Positive clock skew adjustment to consider still acceptable (Default value: 1 minute). */
+    @Nonnull private Duration clockSkew;
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+    
+    /** Constructor. */
+    public ValidateDuoTokenExpirationTime() {
+        clockSkew = Duration.ofMinutes(1);
+    }
+    
+    /**
+     * Set the clock skew.
+     * 
+     * @param skew clock skew to set
+     */
+    public void setClockSkew(@Nonnull final Duration skew) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+                
+        clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+    }
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+ 
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        //exp is time in seconds since EPOCH.
+        final Integer expEpochSeconds = token.getExp();
+        if (expEpochSeconds == null) {
+            log.error("{} No expiry found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return; 
+        }
+        
+        final Instant now = Instant.now();
+        final Instant latestExpired = Instant.ofEpochSecond(expEpochSeconds).plus(clockSkew.abs());
+        
+        log.trace("{} Token expires at '{}', current time is '{}'",getLogPrefix(),latestExpired,now);
+        if (now.isAfter(latestExpired)) {
+            log.warn("{} Token is past expiry date: message expired at: '{}', current time: '{}'", getLogPrefix(),
+                    latestExpired, now);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        //has not expired.
+        
+
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
new file mode 100644
index 0000000..31d359b
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAt.java
@@ -0,0 +1,141 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * 
+ * An action that rejects tokens that were issued (iat) to far away from the current time. 
+ * See section 3.1.3.7 of OpenID Connect core 1.0.
+ * 
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ */
+public class ValidateDuoTokenIssuedAt extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuedAt.class);
+       
+    /**
+     *  Maximum amount (in either direction from now) of clock skew for which a token is valid after 
+     *  it is issued (Default value: 3 minutes). 
+     */
+    @Nonnull private Duration iatMaxClockSkew;
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+    
+    /** Constructor. */
+    public ValidateDuoTokenIssuedAt() {        
+        iatMaxClockSkew = Duration.ofMinutes(3);
+    }
+    
+    /**
+     * Sets the amount of time for which a token is valid.
+     * 
+     * @param clockSkew amount of time for which a token is valid
+     */
+    public synchronized void setIatMaxClockSkew(@Nonnull final Duration clockSkew) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        Constraint.isNotNull(clockSkew, "Token issued at clock skew cannot be null");
+        Constraint.isFalse(clockSkew.isNegative(), "Token issued at clock skew cannot be negative");
+        
+        iatMaxClockSkew = clockSkew;
+    }
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+ 
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        //iat is time in seconds (as double) since EPOCH.
+        final Double iatEpochSeconds = token.getIat();
+        if (iatEpochSeconds == null) {
+            log.error("{} No expiry found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return; 
+        }
+        
+        final Instant now = Instant.now();
+        //double to long conversation with rounding!
+        final Instant iat = Instant.ofEpochSecond(Math.round(iatEpochSeconds));
+        
+        final Duration iatDifference = Duration.between(now, iat).abs();
+        
+        log.trace("{} Difference between when the token was issued '{}' and now '{}' is '{}'",
+                getLogPrefix(),iat, now, iatDifference);
+        
+        if (iatMaxClockSkew.compareTo(iatDifference)  < 0) {
+            log.error("{} Token issued at '{}' was too far away from the current time '{}' with acceptable "
+                    + " deviation of '{}', difference is '{}'",
+                    getLogPrefix(), iat, now, iatMaxClockSkew, iatDifference);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        //is within token lifetime window.
+        
+
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
new file mode 100644
index 0000000..0e2de20
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuer.java
@@ -0,0 +1,106 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * An action that verifies the issuer (iss) of the id_token exactly matches that of the configured 
+ * Duo token provider. See section 3.1.3.7 of OpenID Connect core 1.0.
+ * 
+ * * @pre
+ * 
+ * <pre>
+ * ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ * </pre>
+ * 
+ * @pre
+ * 
+ *      <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre
+ * 
+ *      <pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#AUTHN_EXCEPTION}
+ * 
+ * TODO: Can we do this for Duo (The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) MUST exactly match the value of the iss (issuer) Claim.)
+ */
+public class ValidateDuoTokenIssuer extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenIssuer.class);
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        final String issuer = token.getIss();
+        if (issuer == null) {
+            log.error("{} No issuer found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        if (duoContext.getIntegration() == null) {
+            log.error("{} No Duo integration found in the Duo context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        }
+        
+       //TODO: NOT IMPLEMENTED.
+        //audience is fine
+
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
new file mode 100644
index 0000000..1fd0cd5
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubject.java
@@ -0,0 +1,104 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * An action that verifies the Subject (sub) claim in the Duo token matches that user which
+ * is currently authenticating. This validation is suggested by the Duo Modern MFA developer
+ * documentation.
+ * 
+ * @pre <pre>
+ *      ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre <pre>
+ *      AuthenticationContext.getSubcontext(DuoOIDCAuthenticationContext.class, false) != null
+ *      </pre>
+ * 
+ * @pre n<pre>
+ *      DuoOIDCAuthenticationContext.getAuthToken() != null
+ *      </pre>
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#INVALID_CREDENTIALS}
+ */
+public class ValidateDuoTokenSubject extends AbstractDuoAuthenticationAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenSubject.class);
+
+    /** The Duo authentication token. */
+    @Nullable private DuoAuthToken token;
+
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        token = duoContext.getAuthToken();
+        if (token == null) {
+            log.error("{} Duo 2FA token is not available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
+            return false;
+        }
+        return true;
+
+    }
+
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext,
+            @Nonnull final DuoOIDCAuthenticationContext duoContext) {
+
+        final String subject = token.getSub();
+        if (subject == null) {
+            log.error("{} No subject found in the token response", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
+        final String username = duoContext.getUsername();
+        if (username == null) {
+            log.error("{} No username found in the Duo context", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
+        if (!username.equals(subject)) {
+            log.error("{} Username in the Duo context does not match with the subject of the"
+                    + " Duo token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_CREDENTIALS);
+            return;
+        }
+        
+
+    }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/package-info.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/package-info.java
new file mode 100644
index 0000000..c16e22c
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow implementation classes. */
+package net.shibboleth.idp.plugin.authn.duo.impl;
diff --git a/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
new file mode 100644
index 0000000..9803918
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -0,0 +1,166 @@
+<?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">
+
+    <!-- System beans needed for extension to function, loaded after global.xml -->
+
+    <!-- Controller implementation -->
+    <bean id="shibboleth.DuoOIDCAuthnController"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.DuoOIDCAuthnController"
+        p:clientRegistry-ref="shibboleth.authn.duo.OIDC.clientRegistry" />
+
+
+    <!-- Duo Client factory and bean registry -->
+    <bean id="shibboleth.authn.duo.OIDC.clientRegistry" scope="singleton"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
+        p:clientFactory="#{getObject('shibboleth.authn.duo.OIDC.clientFactory') ?: getObject('shibboleth.authn.duo.OIDC.internal.clientFactory')}" />
+
+    <!-- Need the client factory from a different context e.g. webflow context which is a child context 
+    so added here for now.-->
+    <bean id="shibboleth.authn.duo.OIDC.internal.clientFactory"
+        class="net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientFactory" scope="singleton">
+        <!-- certficate pins -->
+        <property name="caCerts">
+            <list>
+                <value>sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=</value>
+                <value>sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</value>
+                <value>sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=</value>
+                <value>sha256/dykHF2FLJfEpZOvbOLX4PKrcD2w2sHd/iA/G3uHTOcw=</value>
+                <value>sha256/JZaQTcTWma4gws703OR/KFk313RkrDcHRvUt6na6DCg=</value>
+                <value>sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=</value>
+                <value>sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE=</value>
+                <value>sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k=</value>
+                <value>sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U=</value>
+                <value>sha256/j9ESw8g3DxR9XM06fYZeuN1UB4O6xp/GAIjjdD/zM3g=</value>
+            </list>
+        </property>
+    </bean>
+
+    <util:list id="shibboleth.AvailableAuthenticationFlows">
+
+        <bean id="authn/IPAddress" parent="shibboleth.AuthenticationFlow" p:passiveAuthenticationSupported="true"
+            p:lifetime="PT60S" p:inactivityTimeout="PT60S">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/SPNEGO" parent="shibboleth.AuthenticationFlow" p:nonBrowserSupported="false">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="urn:ietf:rfc:1510" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/Function" parent="shibboleth.AuthenticationFlow" />
+
+        <bean id="authn/X509" parent="shibboleth.AuthenticationFlow" p:nonBrowserSupported="false">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:X509" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="urn:ietf:rfc:2246" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/X509Internal" parent="shibboleth.AuthenticationFlow">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:X509" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod" c:method="urn:ietf:rfc:2246" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/Password" parent="shibboleth.AuthenticationFlow" p:passiveAuthenticationSupported="true"
+            p:forcedAuthenticationSupported="true" />
+
+        <bean id="authn/Duo" parent="shibboleth.AuthenticationFlow" p:forcedAuthenticationSupported="true"
+            p:nonBrowserSupported="false"> <!-- put this back, false is original -->
+            <!-- The list below should be changed to reflect whatever locally- or community-defined values are appropriate 
+                to represent MFA. It is strongly advised that the value not be specific to Duo or any particular technology. -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="http://example.org/ac/classes/mfa" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="http://example.org/ac/classes/mfa" />
+                </list>
+            </property>
+        </bean>
+
+        <!-- new for Duo OIDC -->
+        <bean id="authn/DuoOIDC" parent="shibboleth.AuthenticationFlow" p:forcedAuthenticationSupported="true"
+            p:passiveAuthenticationSupported="false" p:nonBrowserSupported="false"> <!-- no nonbrowser, passive? -->
+            <!-- The list below should be changed to reflect whatever locally- or community-defined values are appropriate 
+                to represent MFA. It is strongly advised that the value not be specific to Duo or any particular technology. -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="http://example.org/ac/classes/mfa" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="http://example.org/ac/classes/mfa" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/MFA" parent="shibboleth.AuthenticationFlow" p:passiveAuthenticationSupported="true"
+            p:forcedAuthenticationSupported="true">
+            <!-- The list below almost certainly requires changes, and should generally be the union of any of the separate 
+                factors you combine in your particular MFA flow rules. The example corresponds to the example in mfa-authn-config.xml that 
+                combines IPAddress with Password. -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol" />
+                    <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" />
+                </list>
+            </property>
+        </bean>
+
+        <!-- This is a flow for proxied SAML authentication to another IdP. -->
+
+        <bean id="authn/SAML" parent="shibboleth.AuthenticationFlow" p:nonBrowserSupported="false"
+            p:passiveAuthenticationSupported="true" p:forcedAuthenticationSupported="true" p:proxyScopingEnforced="true"
+            p:discoveryRequired="true" />
+
+        <!-- These flows are often, though not exclusively, used to proxy authentication, so may need the proxyScopingEnforced 
+            flag enabled by hand to honor RP/local proxy count limits, and may optionally trigger discovery via the discoveryRequired 
+            flag. -->
+
+        <bean id="authn/External" parent="shibboleth.AuthenticationFlow" p:nonBrowserSupported="false" />
+
+        <bean id="authn/RemoteUser" parent="shibboleth.AuthenticationFlow" p:nonBrowserSupported="false" />
+
+        <bean id="authn/RemoteUserInternal" parent="shibboleth.AuthenticationFlow" />
+
+    </util:list>
+
+
+
+
+</beans>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
new file mode 100644
index 0000000..8d226d6
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -0,0 +1,102 @@
+<?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.duo.OIDC.externalAuthnPath:/Authn/Duo/2FA/}" />
+
+    <!-- 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:clientId="%{idp.duo.oidc.clientId:none}"
+        p:secretKey="%{idp.duo.oidc.secretKey:none}" 
+        p:redirectURI="%{idp.duo.oidc.redirectUri:none}" />
+        
+    <bean id="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
+        c:target-ref="shibboleth.authn.duo.OIDC.DuoIntegration" />
+        
+     <!-- Default username comes from previous c14n or session. -->        
+    <bean id="shibboleth.authn.duo.OIDC.UsernameLookupStrategy"
+        class="net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy" />
+        
+    <!-- FIXME: For current SWF flow tests the client registry and factory beans from postconfig.xml
+     need to be copied into here, will fix. -->
+        
+        
+    <!-- Can override one or more of the beans above. 
+        TODO: uses a property placeholder not SpEL as import happens before SpEL eval. Also this
+        is different to the internal ones, which are not changeable. Also, what should the default
+        be.-->
+   
+    <import resource="${idp.duo.oidc.config.user:../../../conf/authn/duo-oidc-authn-config.xml}" /> 
+
+   
+    <!-- Prototype per conversation beans -->
+    <bean id="PopulateDuoAuthenticationContext" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.PopulateDuoAuthenticationContext"
+        p:duoIntegrationLookupStrategy-ref="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy"
+        p:usernameLookupStrategy-ref="shibboleth.authn.duo.OIDC.UsernameLookupStrategy" />
+
+    <bean id="HealthCheckDuoOIDCAuthAPI" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.HealthCheckDuoOIDCAuthAPI"
+        parent="shibboleth.authn.duo.OIDC.DuoClientActions" />
+
+    <bean id="ValidateDuoResponseState" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoResponseState" />
+        
+     <bean id="ValidateDuoTokenAudience" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAudience" />
+        
+     <bean id="ValidateDuoTokenExpirationTime" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenExpirationTime" />
+        
+      <bean id="ValidateDuoTokenAuthenticationTime" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationTime" />
+        
+     <bean id="ValidateDuoTokenIssuedAt" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenIssuedAt" />
+
+    <bean id="ExchangeCodeForDuoToken" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken"
+        parent="shibboleth.authn.duo.OIDC.DuoClientActions" />
+
+    <bean id="ValidateDuoTokenAuthenticationResult" scope="prototype" 
+        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoTokenAuthenticationResult" 
+        p:addDefaultPrincipals="#{getObject('shibboleth.authn.duo.OIDC.addDefaultPrincipals') ?: true}"
+        p:classifiedMessages="#{getObject('shibboleth.authn.duo.OIDC.ClassifiedMessageMap') ?: getObject('shibboleth.authn.duo.OIDC.DefaultClassifiedMessageMap')}"
+        p:resultCachingPredicate="#{getObject('shibboleth.authn.duo.OIDC.resultCachingPredicate')}"
+        />
+               
+        <!-- TODO may not be the correct values for the new API -->
+     <util:map id="shibboleth.authn.duo.OIDC.DefaultClassifiedMessageMap">
+        <entry key="AccountLocked">
+            <list>
+                <value>locked_out</value>
+                <value>Your two-factor account is disabled.</value>
+            </list>
+        </entry>
+    </util:map>
+
+    <!-- parent to aid in the configuration of AbstractDuoClientAuthenticationAction beans -->
+    <bean id="shibboleth.authn.duo.OIDC.DuoClientActions" abstract="true"
+        p:clientRegistry-ref="shibboleth.authn.duo.OIDC.clientRegistry" />
+
+
+</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/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
similarity index 74%
rename from idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
rename to idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
index 745061c..2e86855 100644
--- a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml
@@ -14,23 +14,19 @@
     
 
     <view-state id="Duo2FAAuthorizationRequest"
-        view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.duo.OIDC.externalAuthnPathStrategy').apply(opensamlProfileRequestContext), flowExecutionContext.getKey().toString())}">
+        view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.duo.OIDC.externalAuthnPathStrategy').apply(opensamlProfileRequestContext) + '/authorize', flowExecutionContext.getKey().toString())}">
         <attribute name="csrf_excluded" value="true" type="boolean" />
-       <!--  <on-render>
+        <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> -->
+        </on-render> 
 
         <transition on="proceed" to="ValidateDuoResponse" />
     </view-state>
 
     <!-- match the response state to the request state, fail if error  -->
     <action-state id="ValidateDuoResponse">
-        <evaluate expression="CheckDuoState"/>
+        <evaluate expression="ValidateDuoResponseState"/>
         <!-- switch here if no match-->   
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="ExchangeCodeForDuoToken" />
@@ -38,13 +34,16 @@
     
     <action-state id="ExchangeCodeForDuoToken">
         <evaluate expression="ExchangeCodeForDuoToken"/>
-        <!-- validate the response and set principal -->
-        <evaluate expression="ValidateDuo2FAToken"/>       
+        <!-- validate the token and set principal -->
+        <evaluate expression="ValidateDuoTokenAudience"/>
+        <evaluate expression="ValidateDuoTokenExpirationTime"/>
+        <evaluate expression="ValidateDuoTokenAuthenticationTime"/>
+        <evaluate expression="ValidateDuoTokenIssuedAt"/>
+        <!-- final validation of the response status to build an authn result -->
+        <evaluate expression="ValidateDuoTokenAuthenticationResult"/>       
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="proceed" />
-    </action-state>
-
-    
+    </action-state>    
 
     <bean-import resource="duo-oidc-authn-beans.xml" />
 </flow>
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription b/idp-duo-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
new file mode 100644
index 0000000..f879a67
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
@@ -0,0 +1 @@
+net.shibboleth.idp.plugin.authn.duo.impl.DuoOIDCAuthenticationDescription
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
deleted file mode 100644
index f459057..0000000
--- a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-beans.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?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}"/>
-    
-    
-    <!--  Prototype per conversation beans -->
-    <bean id="PopulateDuoAuthenticationContext" scope="prototype" 
-        class="net.shibboleth.idp.plugin.authn.duo.impl.PopulateDuoAuthenticationContext"
-        p:duoIntegrationLookupStrategy-ref="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy" />
-    
-    <bean id="HealthCheckDuoOIDCAuthAPI" scope="prototype" 
-        class="net.shibboleth.idp.plugin.authn.duo.impl.HealthCheckDuoOIDCAuthAPI"                 
-         parent="shibboleth.authn.duo.OIDC.DuoClientActions"/>
-         
-     <bean id="CheckDuoState" scope="prototype" class="net.shibboleth.idp.plugin.authn.duo.impl.CheckDuoState"/>
-     
-     <bean id="ExchangeCodeForDuoToken" scope="prototype" 
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ExchangeCodeForDuoToken"
-        parent="shibboleth.authn.duo.OIDC.DuoClientActions"/>
-        
-     <bean id="ValidateDuo2FAToken" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuo2FAToken"/>
-         
-     <!-- 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/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
index 4570dd1..3ef6819 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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.Base64;
@@ -40,6 +57,8 @@ import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
 import com.codahale.metrics.MetricRegistry;
 import com.google.common.net.HttpHeaders;
 
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
@@ -48,6 +67,7 @@ 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.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
 
 
@@ -57,9 +77,10 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
  * <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
+ * 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{
@@ -72,7 +93,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     
     /**
      *  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?!
+     *  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;
@@ -83,6 +104,9 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     /** Map of properties to set.*/
     @Nonnull private Map<String,String> mockProperties;
     
+    /** The Duo client factory to use.*/
+    @Nonnull private DuoOIDCClientFactory clientFactory;
+    
     /** Mock request. */
     @Nonnull protected MockHttpServletRequest mockRequest;
 
@@ -94,7 +118,6 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     
  
     /** Constructor. */
-    //TODO: what should be initialised once per concrete test class, what per test.
     public AbstractAuthnXmlFlowExecutionTests() {        
         flowModelResources = Collections.emptyMap();
         subflows = Collections.emptyList();    
@@ -111,6 +134,16 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
         flowModelResources = testFlowModelResources;
     }
     
+    /**
+     * Set the Duo client factory to use.
+     * 
+     * @param factory the Duo client factory.
+     */
+    public void setClientFactory(@Nonnull final DuoOIDCClientFactory factory) {
+        Assertions.assertNotNull(factory);
+        clientFactory = factory;
+    }
+    
     /**
      * Set the path to the flow to test.
      * 
@@ -121,6 +154,11 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
         flowPath = testFlowPath;
     }
     
+    /**
+     * Set the mock properties to use within a property source.
+     * 
+     * @param properties
+     */
     public void setMockProperties(@Nonnull final Map<String,String> properties) {
         Assertions.assertNotNull(properties);
         mockProperties = properties;
@@ -131,12 +169,18 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * 
      * @param mockSubflows the mock subflows.
      */
-    public void setSubflows(@Nonnull @NonnullElements List<Flow> mockSubflows) {
+    public void setSubflows(@Nonnull @NonnullElements final List<Flow> mockSubflows) {
         Assertions.assertNotNull(mockSubflows);
         subflows = mockSubflows;
     }
     
    
+    /**
+     * Add HTTP basic authentication headers to the request.
+     * 
+     * @param username the username
+     * @param password the password
+     */
     public void addHttpBasicAuthHeader(@Nonnull final String username, @Nonnull final String password) {
         Assertions.assertNotNull(username);
         Assertions.assertNotNull(password);
@@ -149,6 +193,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      */
     //TODO: should this be initialised once, or once before test?
     @BeforeEach public void initializeMocks() {
+        log.info("Mocks are being created");
         mockRequest = new MockHttpServletRequest();        
         mockResponse = new MockHttpServletResponse();
         externalContext = new MockExternalContext();
@@ -157,19 +202,19 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     }
 
     @Override
-    protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
+    protected FlowDefinitionResource getResource(final FlowDefinitionResourceFactory resourceFactory) {
         return resourceFactory.createClassPathResource(flowPath, getClass());
     }
 
     @Override
-    protected FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory) {
+    protected FlowDefinitionResource[] getModelResources(final FlowDefinitionResourceFactory resourceFactory) {
         
         if (!flowModelResources.isEmpty()) {
 
             final FlowDefinitionResource[] resources = new FlowDefinitionResource[flowModelResources.size()];
             
             int rIndex = 0;
-            for (Map.Entry<String, String> entry : flowModelResources.entrySet()) {
+            for (final Map.Entry<String, String> entry : flowModelResources.entrySet()) {
                 resources[rIndex++] = resourceFactory.createResource(entry.getKey(), null,entry.getValue());
             }            
             return resources;            
@@ -178,7 +223,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     }
     
     @Override
-    protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
+    protected void configureFlowBuilderContext(final MockFlowBuilderContext builderContext) {
         registerMockBeanDefinitions(builderContext);   
         registerMockPropertySource(builderContext);
         registerMockSubflows(builderContext);
@@ -200,10 +245,10 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * but are required for authentication flows to proceed. 
      * 
      * @param builderContext the context used to register the beans.
+     * @throws ComponentInitializationException 
      */
     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);
@@ -213,34 +258,20 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
         //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 a disabled metric registry in case we are using IdP v4.0.0 and not v4.0.1 or greater.
+        final FilteredMetricRegistry filterRegistry = new FilteredMetricRegistry();
+        ConfigurationService.register(MetricRegistry.class, filterRegistry);
         
         //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));
-    
-        
+        //register the client registry and factory
+        final DefaultDuoOIDCClientRegistry registry = new DefaultDuoOIDCClientRegistry();
+        builderContext.registerBean("shibboleth.authn.duo.OIDC.clientRegistry", registry);
+        registry.setClientFactory(clientFactory);
+               
     }
     
     
@@ -302,19 +333,19 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      */
     @Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final boolean forceAuthn) {
         
-        ProfileRequestContext prc = new ProfileRequestContext();
-        AuthenticationContext ac = new AuthenticationContext();
-        AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final AuthenticationContext ac = new AuthenticationContext();
+        final 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();
+        final SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
         scc.setPrincipalName("jdoe");
         prc.addSubcontext(scc);
-        SessionContext sc = new SessionContext();
-        IdPSession session = Mockito.mock(IdPSession.class);
+        final SessionContext sc = new SessionContext();
+        final IdPSession session = Mockito.mock(IdPSession.class);
         Mockito.when(session.getPrincipalName()).thenReturn("jdoe");
         sc.setIdPSession(session);
         prc.addSubcontext(sc);
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
new file mode 100644
index 0000000..517551c
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoActionTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.core.config.ConfigurationService;
+import org.opensaml.core.metrics.FilteredMetricRegistry;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+
+import com.codahale.metrics.MetricRegistry;
+
+import org.junit.jupiter.api.Assertions;
+
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAccessDevice;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoApplication;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.profile.RequestContextBuilder;
+
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Abstract class for Duo 2FA OIDC Action tests.
+ */
+public abstract class AbstractDuoActionTest {
+    
+    
+    protected static final String CLIENT_ID = "DIU6GEFWG5LIUBVV2M3P";
+    
+    protected static final String REDIRECT_URI = "http://localhost/";
+    
+    protected static final String API_HOST = "host.com";
+    
+    protected static final String SECRET = "rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh";
+       
+    protected RequestContext src;
+
+    protected ProfileRequestContext prc;
+    
+    protected  AuthenticationContext ac;
+    
+    protected DuoOIDCAuthenticationContext dc;
+    
+    /**
+     * <p>Setup the relevant contexts per method execution.</p>
+     * 
+     * <p>Is not inherited, so must be enabled in concrete test classes, e.g.
+     * add a setup method and call super.</p>
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    public void setup() throws ComponentInitializationException {
+       
+        src = new RequestContextBuilder().buildRequestContext();
+        prc = new WebflowRequestContextProfileRequestContextLookup().apply(src);
+        ac = new AuthenticationContext();
+        
+        prc.addSubcontext(ac);
+        
+        //setup required for IdP 4.0.0, is no longer required in 4.0.1.
+        FilteredMetricRegistry registry = new FilteredMetricRegistry();
+        ConfigurationService.register(MetricRegistry.class, registry);
+
+    }
+    
+    /**
+     * Create a dummy {@link DuoAuthToken}.
+     * 
+     * @param authResult the authentication result e.g. allow.
+     * @param authResultMessage a user friendly result message.
+     * @param aud audience
+     * @param exp expiration time.
+     * @param iat issued at.
+     * @param authTime the auth time.
+     * 
+     * @return the duo auth token.
+     */
+    protected DuoAuthToken createDummyToken(@Nonnull final String authResult, 
+            @Nonnull final String authResultMessage, @Nonnull final String aud,
+            @Nonnull final Instant exp, @Nonnull final Instant iat, 
+            @Nonnull final Instant authTime) {
+        return DuoAuthToken.builder()
+                .withIss("https://api.duosecurity.com/oauth/v1/token")
+                .withSub("jdoe")
+                .withAud(aud)
+                .withExp(Math.toIntExact(exp.getEpochSecond()))
+                .withIat((double)iat.getEpochSecond())
+                .withAuthResultStatusMessage(authResultMessage)
+                .withAuthResultStatus(authResult)
+                .withAuthResult(authResult)
+                .withAuthContext(DuoAuthContext.builder()
+                        .withResult("success")
+                        .withTimestamp(1590070939)
+                        .withAuthDevice(DuoAuthDevice.builder()
+                                .withIp("192.168.0.1")
+                                .withName("99999999")
+                                .build())
+                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b")
+                        .withEventType("authenticaiton")
+                        .withReason("user_approved")
+                        .withAccessDevice(DuoAccessDevice.builder()
+                                .withIp("192.168.0.1")
+                                .withHostname("99999999")
+                                .build())
+                        .withApplication(DuoApplication.builder()
+                                .withKey("DIU6GEFXXXXXXX")
+                                .withName("Test")
+                                .build())
+                        .withFactor("duo_push")
+                        .withUsername("jdoe")
+                        .withUserKey("XXXXXXX")
+                        .build()).withAuthTime(Math.toIntExact(authTime.getEpochSecond()))
+                .build();
+    }
+    
+    /**
+     * Add an {@link AuthenticationFlowDescriptor} to the {@link AuthenticationContext} 
+     * with the specified flow ID.
+     * 
+     * @param flowId the flow ID.
+     */
+    protected void addAttemptedFlow(@Nonnull final String flowId) {
+        Assertions.assertNotNull(flowId);
+        final AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+        afd.setId(flowId);
+        ac.setAttemptedFlow(afd);
+    }
+    
+    /**
+     * Check the {@link EventContext} in the {@link ProfileRequestContext} is a {@link String}
+     * which represents the eventId argument.
+     * 
+     * @param eventId the eventId to check.
+     */
+    protected void assertEventIdFromProfileRequestContext(@Nonnull final String eventId) {
+        final Object event = prc.getSubcontext(EventContext.class, true).getEvent();
+        Assertions.assertNotNull(event);
+        Assertions.assertTrue(event instanceof String,"Event is not a string is: "+event.getClass());
+        Assertions.assertEquals(eventId, (String)event);
+
+    }
+    
+    /**
+     * Check the event ID matches the eventId.
+     * 
+     * @param event the event to check the ID of.
+     * @param eventId the ID to match.
+     */
+    protected void assertEventId(@Nullable final Event event, @Nonnull final String eventId) {
+        Assertions.assertNotNull(event);
+        Assertions.assertEquals(eventId, event.getId());  
+    }
+    
+    /** Add the Duo authentication context to the authentication context.*/
+    protected void addDuoContext() {
+        dc = new DuoOIDCAuthenticationContext();
+        ac.addSubcontext(dc);
+    }
+    
+    /** Add fabricated duo integration to the duo context.*/
+    protected void addDuoIntegrationToContext() {    
+        Assertions.assertNotNull(dc,"try addDuoContext() before adding the duo integration");
+        dc.setIntegration(createDummyDuoIntegration());
+    }
+    
+    /**
+     * Create a dummy Duo integration.
+     * 
+     * @return a dummy Duo integration.
+     */
+    @Nonnull protected DefaultDuoOIDCIntegration createDummyDuoIntegration() {
+        final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost(API_HOST);       
+        integ.setClientId(CLIENT_ID);
+        integ.setRedirectURI(REDIRECT_URI);
+        integ.setSecretKey(SECRET);
+        return integ;
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
index 8935005..68c1d82 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
@@ -1,50 +1,121 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
 
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 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.mock.MockDuoOIDCClientFactory_OK_Client;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link DefaultDuoOIDCClientRegistry}.
+ */
+public class DefaultDuoOIDCClientRegistryTest {
 
-import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK;
-import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+    /** The registry to test. */
+    private DefaultDuoOIDCClientRegistry registry;
 
-class DefaultDuoOIDCClientRegistryTest {
+    @BeforeEach
+    public void setUp() throws Exception {
+        registry = new DefaultDuoOIDCClientRegistry();
+        registry.setId("Test registry");
+    }
 
     @Test
-    void test() throws DuoRegistryException {
-        final DefaultDuoOIDCClientRegistry factory = new DefaultDuoOIDCClientRegistry();
-        factory.setClientType(MockDuoOIDCClient_OK.class.getName());
+    @DisplayName("Correct registration and caching of client integrations")
+    public final void testCaching() throws DuoRegistryException, ComponentInitializationException {
+
+        // needs a real mock to test proper caching.
+        final MockDuoOIDCClientFactory_OK_Client factory = new MockDuoOIDCClientFactory_OK_Client();
+
+        registry.setClientFactory(factory);
+        registry.initialize();
+
         final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
         integ.setAPIHost("host.com");
         integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
         integ.setRedirectURI("http://localhost/");
         integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
 
-        final DuoOIDCClient client = factory.getIntegrationClientOrCreate(integ);
-        final DuoOIDCClient clientTwo = factory.getIntegrationClientOrCreate(integ);
-        
+        final DuoOIDCClient client = registry.getIntegrationClientOrCreate(integ);
+        final DuoOIDCClient clientTwo = registry.getIntegrationClientOrCreate(integ);
+
         Assertions.assertEquals(client, clientTwo);
-        
-        //set to a different value
+
+        // set to a different value
         integ.setClientId("DIU6GEFWG5LIUBVV2M3B");
-        //should get a new client.
-        final DuoOIDCClient clientThree = factory.getIntegrationClientOrCreate(integ);
-        
-        System.out.println("Client: "+client+" Client2: "+clientTwo+" Client3: "+clientThree);
+        // should get a new client.
+        final DuoOIDCClient clientThree = registry.getIntegrationClientOrCreate(integ);
+
+        System.out.println("Client: " + client + " Client2: " + clientTwo + " Client3: " + clientThree);
         Assertions.assertNotSame(client, clientThree);
-        
     }
-    
-    @Test public void testUknownClientType() {
-        final DefaultDuoOIDCClientRegistry factory = new DefaultDuoOIDCClientRegistry();
-        Assertions.assertThrows(ConstraintViolationException.class,() -> 
-            factory.setClientType("no.such.client.DuoClient"));
+
+    /* Manually step debugging of the registry thread safety, works fine.*/
+    //@Test
+    @DisplayName("Correct registration and caching of client integrations, thread safe")
+    public final void testThreadSafetyCaching() throws DuoRegistryException, ComponentInitializationException, InterruptedException, ExecutionException {
+
+        // needs a real mock to test proper caching.
+        MockDuoOIDCClientFactory_OK_Client factory = new MockDuoOIDCClientFactory_OK_Client();
+
+        registry.setClientFactory(factory);
+        registry.initialize();
+
+        final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost("host.com");
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
+        integ.setRedirectURI("http://localhost/");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+
+        ExecutorService service = Executors.newFixedThreadPool(2);
+        Collection<Future<DuoOIDCClient>> futures = new ArrayList<>(2);
+        for (int t = 0; t < 2; ++t) {
+            
+            futures.add(service.submit(()->registry.getIntegrationClientOrCreate(integ)));
+        }
+        for (Future<DuoOIDCClient> f : futures) {
+            DuoOIDCClient client = f.get();
+            System.out.println("client:"+client);
+          }
+     
+
+        
+
+       
+
     }
-    
-    
-    
 
 }
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DualDuoIntegrationStrategy.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DualDuoIntegrationStrategy.java
new file mode 100644
index 0000000..e3663b4
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DualDuoIntegrationStrategy.java
@@ -0,0 +1,61 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
+
+/**
+ * Function for testing multiple duo integrations.
+ */
+public class DualDuoIntegrationStrategy implements Function<ProfileRequestContext,DuoOIDCIntegration>{
+
+    @Override
+    public DuoOIDCIntegration apply(final ProfileRequestContext prc) {
+        
+        final DefaultDuoOIDCIntegration int1 = new DefaultDuoOIDCIntegration();
+        int1.setAPIHost("host.com");
+        int1.setClientId(DuoAuthnFlowTest.FIRST_INTEGRATION_CLIENT_ID);
+        int1.setRedirectURI("http://localhost/");
+        int1.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        
+        final DefaultDuoOIDCIntegration int2 = new DefaultDuoOIDCIntegration();
+        int2.setAPIHost("host.com");
+        int2.setClientId(DuoAuthnFlowTest.SECOND_INTEGRATION_CLIENT_ID);
+        int2.setRedirectURI("http://localhost/");
+        int2.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        
+        if (DuoAuthnFlowTest.FIRST_INTEGRATION_SP.equals(
+                prc.getSubcontext(RelyingPartyContext.class).getRelyingPartyId())) {
+            return int1;
+        } else if (DuoAuthnFlowTest.SECOND_INTEGRATION_SP.equals(
+                prc.getSubcontext(RelyingPartyContext.class).getRelyingPartyId())){
+            return int2;
+        }
+        //fail if none chosen.
+        Assertions.fail();
+        return null;
+    }
+
+}
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
index bb99fca..ff60e29 100644
--- 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
@@ -1,101 +1,126 @@
+/*
+ * 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 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.Assertions;
+import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.webflow.core.collection.LocalAttributeMap;
 import org.springframework.webflow.engine.Flow;
 import org.springframework.webflow.engine.impl.FlowExecutionImpl;
 import org.springframework.webflow.execution.FlowExecution;
-import org.springframework.webflow.execution.FlowExecutionKey;
-import org.springframework.webflow.execution.repository.snapshot.SimpleFlowExecutionSnapshotFactory;
-import org.springframework.webflow.execution.repository.support.CompositeFlowExecutionKey;
-import org.springframework.webflow.test.MockFlowExecutionContext;
-import org.springframework.webflow.test.MockFlowExecutionKey;
 
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
 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.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClientFactory_FAIL_Client;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClientFactory_OK_Client;
 import net.shibboleth.idp.plugin.authn.mock.MockFlowBuilder;
+import net.shibboleth.idp.profile.context.RelyingPartyContext;
 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?
+/**
+ * Test the Duo 2FA flow using SWF flow testing.
+ */
 public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     
-
+    /** RP ID of the SP that requires the first duo integration.*/
+    @Nonnull public static final String FIRST_INTEGRATION_SP ="https://sp.example.com/requires-first-integration"; 
     
-    /** Path to the flow to be tested.*/
-    @Nonnull final private static String FLOW = "/flows/authn/duo/duo-oidc-authn-flow.xml";
+    /** RP ID of the SP that requires the second duo integration.*/
+    @Nonnull public static final String SECOND_INTEGRATION_SP ="https://sp.example.com/requires-second-integration"; 
     
-    /** 
-     * 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"));
+    /** Duo client ID of the integration needed for the {@link #FIRST_INTEGRATION_SP}.*/
+    @Nonnull public static final String FIRST_INTEGRATION_CLIENT_ID = "FIRST_INTEGRATION";
+    
+    /** Duo client ID of the integration needed for the {@link #SECOND_INTEGRATION_SP}.*/
+    @Nonnull public static final String SECOND_INTEGRATION_CLIENT_ID = "SECOND_INTEGRATION";
     
     /** The username of the user to test.*/
-    @Nonnull private final static String USERNAME = "jdoe";
+    @Nonnull private static final String USERNAME = "jdoe";
     
     /** The password of the user to test.*/
-    @Nonnull private final static String PASSWORD = "changeit";
+    @Nonnull private static final String PASSWORD = "changeit";
+    
+    /** Path to the flow to be tested.*/
+    @Nonnull private static final String FLOW = 
+            "/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-flow.xml";
     
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DuoAuthnFlowTest.class);
     
-    /**
-     * Runs the flow to fail.
-     * <ol>
-     * <li>Uses baisc auth, so skips username and password login view</li>
-     * </ol>
+    /** List of mocked subflows.*/
+    @Nonnull @NonnullElements @Unmodifiable private final List<Flow> subflows = 
+            List.of(MockFlowBuilder.MockNoOpFlow("c14n"));
+    
+    /** 
+     * Map of flow resources that support building the flow to test.
+     * These are only for parent flows, not subflows.
      */
-    @Test public void testDuoAuthnFlowUnavailable() {
+    @Nonnull @NonnullElements @Unmodifiable private 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");
+
+
+    /** Test the Duo flow when the health check returns unhealthy.*/
+    @Test @DisplayName("Test Duo 2FA flow where the Duo 2FA endpoint is unhealthy") 
+    public void testDuoAuthnFlowDuoEndpointUnhealthy() {
         
         setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-       //addHttpBasicAuthHeader(USERNAME,PASSWORD);
+        setClientFactory(new MockDuoOIDCClientFactory_FAIL_Client());
         
        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.apiHost","api-c9f24c5a.duosecurity.com",
+                "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
                 "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
-                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_FAIL");
+                "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
         
         setMockProperties(mockProperties);
         
-        LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+        final LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
         inputMap.put("calledAsSubflow", true);
        
-        //you must do this bit, not startFlow?
-        FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
+        final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
         flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
         updateFlowExecution(flowExecution);
         flowExecution.start(inputMap, externalContext);         
@@ -103,63 +128,156 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
 
     }
     
-    @Test public void testDuoAuthnFlowToAuthorizationRequest() {
+    /** Test the Duo flow up to the external authorization request.*/
+    @Test @DisplayName("Test Duo 2FA flow up to the Duo 2FA authorization request") 
+    public void testDuoAuthnFlowToAuthorizationRequest() {
         
         setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-       // addHttpBasicAuthHeader(USERNAME,PASSWORD);
+        setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
         
        final Map<String,String> mockProperties = Map.of(
                 "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
-                "idp.duo.oidc.apiHost","https://api.duosecurity.com/oauth/v1/token",
-                "idp.duo.oidc.integrationKey","DIU6GEFWG5LIUBVV2M3P",
+                "idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
+                "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
                 "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
-                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
+                "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
         
         setMockProperties(mockProperties);
         
-        LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+        final LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
         inputMap.put("calledAsSubflow", true);
        
-        //you must do this bit, not startFlow?
-        FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
+        final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
         flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
-        updateFlowExecution(flowExecution);
+        updateFlowExecution(flowExecution);         
         flowExecution.start(inputMap, externalContext);         
         assertFlowExecutionActive();
         assertCurrentStateEquals("Duo2FAAuthorizationRequest");
 
     }
     
-    @Test public void testDuoAuthnFlowFromAuthorizationCallback() {
+    /** Test the Duo flow up to the external authorization request using the dynamically selected
+     * first Duo integration.*/
+    @Test @DisplayName("Test Duo 2FA flow up to the Duo 2FA authorization "
+            + "request with multiple integrations, first") 
+    public void testDuoAuthnFlowToAuthorizationRequestMultipleDuoIntegrationsFirst() {
         
         setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-        //addHttpBasicAuthHeader(USERNAME,PASSWORD);
+        setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
         
-        //TODO: this is repeated.
        final Map<String,String> mockProperties = Map.of(
                 "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
-                "idp.duo.oidc.apiHost","https://api.duosecurity.com/oauth/v1/token",
-                "idp.duo.oidc.integrationKey","DIU6GEFWG5LIUBVV2M3P",
+                "idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
+                "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
                 "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
-                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
+                "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
         
         setMockProperties(mockProperties);
         
+        final LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+        inputMap.put("calledAsSubflow", true);
+       
+        final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
+        final ProfileRequestContext prcOne =  buildProfileRequestContext(false);
+        final RelyingPartyContext rpcOne = new RelyingPartyContext();
+        rpcOne.setRelyingPartyId(FIRST_INTEGRATION_SP);
+        prcOne.addSubcontext(rpcOne);
+        flowExecution.getConversationScope().put("opensamlProfileRequestContext",prcOne);
+        updateFlowExecution(flowExecution);         
+        flowExecution.start(inputMap, externalContext);         
+        assertFlowExecutionActive();
+        assertCurrentStateEquals("Duo2FAAuthorizationRequest");
+        
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class));
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class).getSubcontext(
+                DuoOIDCAuthenticationContext.class));
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class).getSubcontext(
+                DuoOIDCAuthenticationContext.class).getIntegration());
+        Assertions.assertEquals(FIRST_INTEGRATION_CLIENT_ID,prcOne.getSubcontext(AuthenticationContext.class)
+                .getSubcontext(DuoOIDCAuthenticationContext.class).getIntegration().getClientId());
+
+    }
+    
+    /** Test the Duo flow up to the external authorization request using the dynamically selected
+     * second Duo integration.*/
+    @Test @DisplayName("Test Duo 2FA flow up to the Duo 2FA authorization "
+            + "request with multiple integrations, second") 
+    public void testDuoAuthnFlowToAuthorizationRequestMultipleDuoIntegrationsSecond() {
+        
+        setFlowPath(FLOW);
+        setFlowModelResources(flowResources);
+        setSubflows(subflows);
+        setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
+        
+       final Map<String,String> mockProperties = Map.of(
+                "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
+                "idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
+                "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
+                "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
+                "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
+        
+        setMockProperties(mockProperties);
+        
+        final LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+        inputMap.put("calledAsSubflow", true);
+       
+        final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());  
+        final ProfileRequestContext prcOne =  buildProfileRequestContext(false);
+        final RelyingPartyContext rpcOne = new RelyingPartyContext();
+        rpcOne.setRelyingPartyId(SECOND_INTEGRATION_SP);
+        prcOne.addSubcontext(rpcOne);
+        flowExecution.getConversationScope().put("opensamlProfileRequestContext",prcOne);
+        updateFlowExecution(flowExecution);         
+        flowExecution.start(inputMap, externalContext);         
+        assertFlowExecutionActive();
+        assertCurrentStateEquals("Duo2FAAuthorizationRequest");
+        
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class));
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class).getSubcontext(
+                DuoOIDCAuthenticationContext.class));
+        Assertions.assertNotNull(prcOne.getSubcontext(AuthenticationContext.class).getSubcontext(
+                DuoOIDCAuthenticationContext.class).getIntegration());
+        Assertions.assertEquals(SECOND_INTEGRATION_CLIENT_ID,prcOne.getSubcontext(AuthenticationContext.class)
+                .getSubcontext(DuoOIDCAuthenticationContext.class).getIntegration().getClientId());
+
+    }
+    
+   
+    /** Test the Duo flow from the external authorization request to the end of the flow.*/
+    @Test @DisplayName("Test Duo 2FA flow from (not including) authorization callback") 
+    public void testDuoAuthnFlowFromAuthorizationCallback() {
+        
+        setFlowPath(FLOW);
+        setFlowModelResources(flowResources);
+        setSubflows(subflows);
+        setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
+        
+        final Map<String,String> mockProperties = Map.of(
+                "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+        
+        setMockProperties(mockProperties);
 
         final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
                 .createFlowExecution(getFlowDefinition());
         final ProfileRequestContext prc =  buildProfileRequestContext(false);
         //add a DuoContext
-        final DuoAuthenticationContext duoContext = new DuoAuthenticationContext();
+        final DuoOIDCAuthenticationContext duoContext = new DuoOIDCAuthenticationContext();
         final String state = DuoSupport.generateState(32);
         duoContext.setAuthorizationCode("adummycode");
         duoContext.setRequestState(state);
         duoContext.setResponseState(state);
         duoContext.setUsername("jdoe");
+        final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
+        integ.setAPIHost("api-c9f24c5a.duosecurity.com");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        integ.setRedirectURI("http://localhost/authorization-callback");
+        
+        duoContext.setIntegration(integ);
         prc.getSubcontext(AuthenticationContext.class).addSubcontext(duoContext);
         flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
         updateFlowExecution(flowExecution);
@@ -171,13 +289,14 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         
         //assert success conditions
         assertFlowExecutionEnded();
-        assertNotNull(prc.getSubcontext(AuthenticationContext.class));
-        assertNotNull(prc.getSubcontext(AuthenticationContext.class).getSubcontext(DuoAuthenticationContext.class));
-        final DuoAuthenticationContext contextFromPrc = prc.getSubcontext(AuthenticationContext.class).
-                getSubcontext(DuoAuthenticationContext.class);
-        assertNotNull(contextFromPrc.getAuthToken());
-        assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
-        assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
+        Assertions.assertNotNull(prc.getSubcontext(AuthenticationContext.class));
+        Assertions.assertNotNull(prc.getSubcontext(AuthenticationContext.class).
+                getSubcontext(DuoOIDCAuthenticationContext.class));
+        final DuoOIDCAuthenticationContext contextFromPrc = prc.getSubcontext(AuthenticationContext.class).
+                getSubcontext(DuoOIDCAuthenticationContext.class);
+        Assertions.assertNotNull(contextFromPrc.getAuthToken());
+        Assertions.assertNotNull(prc.getSubcontext(SubjectCanonicalizationContext.class));
+        Assertions.assertEquals(prc.getSubcontext(SubjectCanonicalizationContext.class).getPrincipalName(),"jdoe");
         
     }
     
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
index ac12807..037bd5c 100644
--- 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
@@ -1,9 +1,9 @@
 /*
- * Licensed to the University Corporation for Advanced Internet Development, 
- * Inc. (UCAID) under one or more contributor license agreements.  See the 
+ * 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 
+ * 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
@@ -53,7 +53,7 @@ import org.springframework.webflow.execution.repository.support.CompositeFlowExe
 import org.springframework.webflow.executor.FlowExecutorImpl;
 
 import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
-import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
 import net.shibboleth.idp.authn.ExternalAuthentication;
 import net.shibboleth.idp.authn.ExternalAuthenticationException;
@@ -61,6 +61,7 @@ import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.ExternalAuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
 import net.shibboleth.idp.authn.impl.ExternalAuthenticationImpl;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClientFactory_OK_Client;
 import net.shibboleth.idp.plugin.authn.util.mock.IdPPropertyConfigurer;
 import net.shibboleth.idp.session.IdPSession;
 import net.shibboleth.idp.session.context.SessionContext;
@@ -72,7 +73,7 @@ import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
 @ExtendWith(SpringExtension.class)
 @ContextConfiguration(classes = {DuoOIDCAuthnController.class, IdPPropertyConfigurer.class})
 @WebAppConfiguration
- at TestPropertySource(properties = {"idp.authn.duo.OIDC.externalAuthnPath=/Authn/Duo",})
+ at TestPropertySource(properties = {"idp.authn.duo.OIDC.externalAuthnPath=/Authn/Duo/2FA",})
 public class DuoOIDCAuthnControllerTest {
 
     /** Class logger. */
@@ -90,16 +91,21 @@ public class DuoOIDCAuthnControllerTest {
     /** The mock http session.*/
     @Nonnull private MockHttpSession session;
 
+    /**
+     * Setup. 
+     * 
+     * @throws Exception on error.
+     */
     @BeforeEach
     public void setUp() throws Exception {
         
         // check controller is instantiated.
-        DuoOIDCAuthnController controller = webApplicationContext.getBean(DuoOIDCAuthnController.class);        
+        final DuoOIDCAuthnController controller = webApplicationContext.getBean(DuoOIDCAuthnController.class);        
         Assertions.assertNotNull(controller);
 
         // add a Duo client registry
-        DefaultDuoOIDCClientRegistry registry = new DefaultDuoOIDCClientRegistry();
-        registry.setClientType("net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
+        final DefaultDuoOIDCClientRegistry registry = new DefaultDuoOIDCClientRegistry();
+        registry.setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
         registry.setId("TestDefaultClientRegistry");
         registry.initialize();
         controller.setClientRegistry(registry);
@@ -136,12 +142,18 @@ public class DuoOIDCAuthnControllerTest {
     public void testSuccessfulCallback() throws Exception {
 
         session.setAttribute(DuoOIDCAuthnController.KEY_SESSION_ATTRIBUTE, "e1s1");
-        mockMvc.perform(get("/Authn/Duo/2FA/authorize-callback").session(session).param("code", "db87f45083db5aea16eb45b7ace685c8").param("state",
-                "8821febf7b3237fc3974f76a75f37e8c")).andDo(print()).andExpect(status().is3xxRedirection());
+        mockMvc.perform(get("/Authn/Duo/2FA/duo-callback").
+                session(session).
+                param("code", "db87f45083db5aea16eb45b7ace685c8").
+                param("state",
+                "8821febf7b3237fc3974f76a75f37e8c")).
+        andDo(print()).
+        andExpect(status().
+                is3xxRedirection());
         
         //check the duo context is populated correctly.
        
-        DuoAuthenticationContext duoContext = extractDuoContext();
+        DuoOIDCAuthenticationContext duoContext = extractDuoContext();
         Assertions.assertEquals("db87f45083db5aea16eb45b7ace685c8",duoContext.getAuthorizationCode());
         Assertions.assertEquals("8821febf7b3237fc3974f76a75f37e8c", duoContext.getResponseState());
 
@@ -158,10 +170,17 @@ public class DuoOIDCAuthnControllerTest {
 
         removeDuoContext();
         session.setAttribute(DuoOIDCAuthnController.KEY_SESSION_ATTRIBUTE, "e1s1");
-        MvcResult result = mockMvc.perform(get("/Authn/Duo/2FA/authorize-callback").session(session)
-                .param("code", "db87f45083db5aea16eb45b7ace685c8").param("state",
-                "8821febf7b3237fc3974f76a75f37e8c")).andDo(print()).andExpect(status().is3xxRedirection()).andReturn();
-        Object extAuthErrorObject = result.getRequest().getAttribute(ExternalAuthentication.AUTHENTICATION_ERROR_KEY);
+        final MvcResult result = mockMvc.perform(get("/Authn/Duo/2FA/duo-callback").
+                session(session)
+                .param("code", "db87f45083db5aea16eb45b7ace685c8").
+                param("state",
+                "8821febf7b3237fc3974f76a75f37e8c")).
+                andDo(print()).
+                andExpect(status().
+                        is3xxRedirection()).
+                andReturn();
+        final Object extAuthErrorObject = result.getRequest().getAttribute(
+                ExternalAuthentication.AUTHENTICATION_ERROR_KEY);
         Assertions.assertNotNull(extAuthErrorObject);
         Assertions.assertTrue(extAuthErrorObject instanceof String);
         Assertions.assertEquals(EventIds.INVALID_PROFILE_CTX, (String)extAuthErrorObject);
@@ -171,14 +190,14 @@ public class DuoOIDCAuthnControllerTest {
     }
 
     /**
-     * Test an exception is thrown if the callback does not contain a auth_code parameter.
+     * Test an exception is thrown if the callback does not contain an auth_code parameter.
      * 
      * @throws Exception on exception.
      */
     @Test
     public void testCallbackNoCode() throws Exception {
-        Exception exception = Assertions.assertThrows(NestedServletException.class, () -> {
-            mockMvc.perform(get("/Authn/Duo/2FA/authorize-callback").param("state",
+        final Exception exception = Assertions.assertThrows(NestedServletException.class, () -> {
+            mockMvc.perform(get("/Authn/Duo/2FA/duo-callback").param("state",
                     "8821febf7b3237fc3974f76a75f37e8c")).andDo(print());
         });
         Assertions.assertTrue(exception.getCause() instanceof ExternalAuthenticationException); 
@@ -191,8 +210,10 @@ public class DuoOIDCAuthnControllerTest {
      */
     @Test
     public void testCallbackNoState() throws Exception {
-        Exception exception = Assertions.assertThrows(NestedServletException.class, () -> {
-            mockMvc.perform(get("/Authn/Duo/2FA/authorize-callback").param("code", "db87f45083db5aea16eb45b7ace685c8")).andDo(print());
+        final Exception exception = Assertions.assertThrows(NestedServletException.class, () -> {
+            mockMvc.perform(get("/Authn/Duo/2FA/duo-callback").
+                    param("code", "db87f45083db5aea16eb45b7ace685c8")).
+            andDo(print());
         });
         Assertions.assertTrue(exception.getCause() instanceof ExternalAuthenticationException); 
     }
@@ -203,15 +224,15 @@ public class DuoOIDCAuthnControllerTest {
      */
     private void exportServletContextAttributes() {
 
-        FlowExecutorImpl mockFlowExecutor = Mockito.mock(FlowExecutorImpl.class);
-        FlowExecutionRepository mockFlowExecutionRepo = Mockito.mock(FlowExecutionRepository.class);
-        FlowExecution mockFlowExecution = Mockito.mock(FlowExecution.class);
+        final FlowExecutorImpl mockFlowExecutor = Mockito.mock(FlowExecutorImpl.class);
+        final FlowExecutionRepository mockFlowExecutionRepo = Mockito.mock(FlowExecutionRepository.class);
+        final FlowExecution mockFlowExecution = Mockito.mock(FlowExecution.class);
 
-        MutableAttributeMap<Object> map = new LocalAttributeMap<Object>();
+        final MutableAttributeMap<Object> map = new LocalAttributeMap<Object>();
         map.put(ProfileRequestContext.BINDING_KEY, buildProfileRequestContext());
 
         Mockito.when(mockFlowExecutor.getExecutionRepository()).thenReturn(mockFlowExecutionRepo);
-        CompositeFlowExecutionKey key = new CompositeFlowExecutionKey("1", "1");
+        final CompositeFlowExecutionKey key = new CompositeFlowExecutionKey("1", "1");
         Mockito.when(mockFlowExecutionRepo.parseFlowExecutionKey("e1s1")).thenReturn(key);
         Mockito.when(mockFlowExecutionRepo.getFlowExecution(key)).thenReturn(mockFlowExecution);
         Mockito.when(mockFlowExecution.getConversationScope()).thenReturn(map);
@@ -228,36 +249,35 @@ public class DuoOIDCAuthnControllerTest {
      */
     @Nonnull private ProfileRequestContext buildProfileRequestContext() {
 
-        ProfileRequestContext prc = new ProfileRequestContext();
-        AuthenticationContext ac = new AuthenticationContext();
-        ExternalAuthenticationContext ec = new ExternalAuthenticationContext(new ExternalAuthenticationImpl());
-        DuoAuthenticationContext dc = new DuoAuthenticationContext();
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final AuthenticationContext ac = new AuthenticationContext();
+        final ExternalAuthenticationContext ec = new ExternalAuthenticationContext(new ExternalAuthenticationImpl());
+        final DuoOIDCAuthenticationContext dc = new DuoOIDCAuthenticationContext();
 
         // will redirect here once finished or in error.
         ec.setFlowExecutionUrl("http://localhost/idp/profile/SSO&_eventId_proceed=1");
 
-        DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        final DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
         integ.setAPIHost("host.com");
-        integ.setApplicationKey("SDFGHJKLPOIUYTREWQZXCVBNMLKJHGFAQWERTYU");
-        integ.setIntegrationKey("DIU6GEFWG5LIUBVV2M3P");
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
         integ.setRedirectURI("http://localhost/");
         integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
         dc.setUsername("jdoe");
         dc.setIntegration(integ);
 
-        AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+        final AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
         afd.setId("authn/DuoOIDC");
         ac.setAttemptedFlow(afd);
         ac.addSubcontext(new RelyingPartyUIContext());
         ac.setForceAuthn(false);
 
         // do we need this populated for the Controller - as username is in the Duo context.
-        SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
+        final SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
         scc.setPrincipalName("jdoe");
-        SessionContext sc = new SessionContext();
-        IdPSession session = Mockito.mock(IdPSession.class);
-        Mockito.when(session.getPrincipalName()).thenReturn("jdoe");
-        sc.setIdPSession(session);
+        final SessionContext sc = new SessionContext();
+        final IdPSession idpSession = Mockito.mock(IdPSession.class);
+        Mockito.when(idpSession.getPrincipalName()).thenReturn("jdoe");
+        sc.setIdPSession(idpSession);
 
         ac.addSubcontext(dc);
         ac.addSubcontext(ec);
@@ -275,33 +295,33 @@ public class DuoOIDCAuthnControllerTest {
         final Object flowExecutorObject = servletContext.getAttribute(ExternalAuthentication.SWF_KEY);
         Assertions.assertTrue(flowExecutorObject instanceof FlowExecutorImpl);
         
-        Object prcObject = ((FlowExecutorImpl)flowExecutorObject).getExecutionRepository().
+        final Object prcObject = ((FlowExecutorImpl)flowExecutorObject).getExecutionRepository().
                 getFlowExecution(new CompositeFlowExecutionKey("1", "1")).
                 getConversationScope().get(ProfileRequestContext.BINDING_KEY);
         
         ((ProfileRequestContext)prcObject).getSubcontext(AuthenticationContext.class).
-        removeSubcontext(DuoAuthenticationContext.class);
+        removeSubcontext(DuoOIDCAuthenticationContext.class);
     
     }
     
     
     /**
-     * Extract the {@link DuoAuthenticationContext} from the {@link ProfileRequestContext} ultimately 
+     * Extract the {@link DuoOIDCAuthenticationContext} from the {@link ProfileRequestContext} ultimately 
      * stored in the {@link ServletContext}.
      * 
      * @return the Duo authentication context extracted from the profile request context.
      */
-    @Nonnull private DuoAuthenticationContext extractDuoContext() {
+    @Nonnull private DuoOIDCAuthenticationContext extractDuoContext() {
         final Object flowExecutorObject = servletContext.getAttribute(ExternalAuthentication.SWF_KEY);
         Assertions.assertTrue(flowExecutorObject instanceof FlowExecutorImpl);
         
-        Object prcObject = ((FlowExecutorImpl)flowExecutorObject).getExecutionRepository().
+        final Object prcObject = ((FlowExecutorImpl)flowExecutorObject).getExecutionRepository().
                 getFlowExecution(new CompositeFlowExecutionKey("1", "1")).
                 getConversationScope().get(ProfileRequestContext.BINDING_KEY);
         Assertions.assertTrue(prcObject instanceof ProfileRequestContext);
        
-        DuoAuthenticationContext duoContext = ((ProfileRequestContext)prcObject).getSubcontext(AuthenticationContext.class).
-                getSubcontext(DuoAuthenticationContext.class);
+        final DuoOIDCAuthenticationContext duoContext = ((ProfileRequestContext)prcObject).
+                getSubcontext(AuthenticationContext.class).getSubcontext(DuoOIDCAuthenticationContext.class);
         Assertions.assertNotNull(duoContext);
         
         return duoContext;
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java
new file mode 100644
index 0000000..c66501a
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoTokenTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
+import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for {@link ExchangeCodeForDuoToken}.
+ */
+public class ExchangeCodeForDuoTokenTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ExchangeCodeForDuoToken action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ExchangeCodeForDuoToken();
+
+    }
+
+    @Test
+    @DisplayName("Test ExchangeCodeForDuoToken executes succesfully")
+    public void testExecuteSuccess() throws ComponentInitializationException, DuoRegistryException, DuoClientException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        //add an auth code
+        dc.setAuthorizationCode("testcode");
+        final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+
+        Mockito.when(mockClientRegistry.getIntegrationClientOrCreate(dc.getIntegration()))
+                .thenReturn(new MockDuoOIDCClient_OK(dc.getIntegration()));
+        action.setClientRegistry(mockClientRegistry);
+        action.initialize();
+        final Event event = action.execute(src);
+        // success here is a null event
+        Assertions.assertNull(event);
+    }
+
+    @Test
+    @DisplayName("Test ExchangeCodeForDuoToken with no authorization code")
+    public void testExecuteNoAuthCode()
+            throws ComponentInitializationException, DuoRegistryException, DuoClientException {
+        
+        addDuoContext();
+        addDuoIntegrationToContext();
+        // blank the auth code.
+        dc.setAuthorizationCode(null);
+        final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+
+        Mockito.when(mockClientRegistry.getIntegrationClientOrCreate(dc.getIntegration()))
+                .thenReturn(new MockDuoOIDCClient_OK(dc.getIntegration()));
+        action.setClientRegistry(mockClientRegistry);
+        action.initialize();
+        final Event event = action.execute(src);
+
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPITest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPITest.java
new file mode 100644
index 0000000..82210c8
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/HealthCheckDuoOIDCAuthAPITest.java
@@ -0,0 +1,108 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
+import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_FAIL;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK;
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_UNKNOWN;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+class HealthCheckDuoOIDCAuthAPITest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private HealthCheckDuoOIDCAuthAPI healthCheckAction;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        healthCheckAction = new HealthCheckDuoOIDCAuthAPI();
+
+    }
+
+    /* Test for a healthy endpoint. */
+    @Test @DisplayName("Test for an OK health check")
+    void testDoExecuteHealthy() throws DuoRegistryException, ComponentInitializationException, DuoClientException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+
+        final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+
+        Mockito.when(mockClientRegistry.getIntegrationClientOrCreate(dc.getIntegration()))
+                .thenReturn(new MockDuoOIDCClient_OK(dc.getIntegration()));
+        healthCheckAction.setClientRegistry(mockClientRegistry);
+        healthCheckAction.initialize();
+        final Event event = healthCheckAction.execute(src);
+        // success here is a null event
+        Assertions.assertNull(event);
+
+    }
+
+    /* Test for an unhealthy endpoint. */
+    @Test @DisplayName("Test for an unhealthy/failed health check")
+    void testDoExecuteUnHealthy() throws DuoRegistryException, ComponentInitializationException, DuoClientException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+
+        final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+
+        Mockito.when(mockClientRegistry.getIntegrationClientOrCreate(dc.getIntegration()))
+                .thenReturn(new MockDuoOIDCClient_FAIL(dc.getIntegration()));
+        healthCheckAction.setClientRegistry(mockClientRegistry);
+        healthCheckAction.initialize();
+        final Event event = healthCheckAction.execute(src);
+        // success here is a null event
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+    
+    /* Test for an unknown response from the health endpoint. */
+    @Test @DisplayName("Test for an unknown health check response")
+    void testDoExecuteUnknown() throws DuoRegistryException, ComponentInitializationException, DuoClientException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+
+        final DuoOIDCClientRegistry mockClientRegistry = Mockito.mock(DuoOIDCClientRegistry.class);
+
+        Mockito.when(mockClientRegistry.getIntegrationClientOrCreate(dc.getIntegration()))
+                .thenReturn(new MockDuoOIDCClient_UNKNOWN(dc.getIntegration()));
+        healthCheckAction.setClientRegistry(mockClientRegistry);
+        healthCheckAction.initialize();
+        final Event event = healthCheckAction.execute(src);
+        // success here is a null event
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+
+    // TODO: Test timeout in some way of the client?
+
+}
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
index 1933395..37930ef 100644
--- 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
@@ -1,3 +1,20 @@
+/*
+ * 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 javax.annotation.Nonnull;
@@ -23,21 +40,21 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
  */
 @ThreadSafe
 public class InMemoryCredentialValidator extends AbstractUsernamePasswordCredentialValidator{
-    
+
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(InMemoryCredentialValidator.class);
-    
+
     /** The configured username.*/
-    @Nonnull private String username;
-    
+    @Nonnull private final String username;
+
     /** The configured password.*/
-    @Nonnull private String password;
-    
+    @Nonnull private final 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 {
@@ -45,23 +62,23 @@ public class InMemoryCredentialValidator extends AbstractUsernamePasswordCredent
     }
 
     @Override
-    protected Subject doValidate(ProfileRequestContext profileRequestContext,
-            AuthenticationContext authenticationContext, UsernamePasswordContext usernamePasswordContext,
-            WarningHandler warningHandler, ErrorHandler errorHandler) throws Exception {
-        
+    protected Subject doValidate(final ProfileRequestContext profileRequestContext,
+            final AuthenticationContext authenticationContext, final UsernamePasswordContext usernamePasswordContext,
+            final WarningHandler warningHandler, final 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,
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
deleted file mode 100644
index 92ea6ec..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-
-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.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";
-    
-    
-    @Test public void nothing() {
-        
-    }
-
-    /**
-     * 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/PopulateDuoAuthenticationContextTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java
new file mode 100644
index 0000000..8bd7f36
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContextTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link PopulateDuoAuthenticationContext} strategy.
+ */
+public class PopulateDuoAuthenticationContextTest extends AbstractDuoActionTest{
+
+    /** The action to test. */
+    private PopulateDuoAuthenticationContext action;
+
+    /**
+     * Setup. 
+     * 
+     * @throws Exception on exception.
+     */
+    @BeforeEach public void setUp() throws Exception {
+        super.setup();
+        action = new PopulateDuoAuthenticationContext();
+
+    }
+    
+    /**
+     * Test successful execution.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test populate duo context execution, success")
+    public void testExecuteSuccess() throws ComponentInitializationException {
+        final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+        //set the duo integration strategy to lookup this 
+        action.setDuoIntegrationLookupStrategy(prc -> integ);
+        //lookup a username
+        action.setUsernameLookupStrategy(prc -> "jdoe");
+        action.initialize();
+        final Event event = action.execute(src);
+        //success here is a null event
+        Assertions.assertNull(event);
+    }
+    
+    /**
+     * Test unsuccessful execution if there is no duo integration specified.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test populate duo context, no Duo integration")
+    public void testExecuteNoDuoIntegration() throws ComponentInitializationException {
+        
+        action.initialize();
+        final Event event = action.execute(src);
+        //success here is a null event
+        assertEventId(event, EventIds.INVALID_PROFILE_CTX);
+    }
+    
+    /**
+     * Test unsuccessful execution if there is a null username.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test populate duo context, null username")
+    public void testExecuteNullUsername() throws ComponentInitializationException {
+        final DefaultDuoOIDCIntegration integ = createDummyDuoIntegration();
+        //set the duo integration strategy to lookup this 
+        action.setDuoIntegrationLookupStrategy(prc -> integ);
+        //lookup a username
+        action.setUsernameLookupStrategy(prc -> null);
+        action.initialize();
+        final Event event = action.execute(src);
+        //success here is a null event
+        assertEventId(event, AuthnEventIds.NO_CREDENTIALS);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java
new file mode 100644
index 0000000..d6448a5
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoResponseStateTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoResponseState} action.
+ */
+public class ValidateDuoResponseStateTest extends AbstractDuoActionTest{
+
+    /** The action to test. */
+    private ValidateDuoResponseState action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoResponseState();
+
+    }
+
+    @Test @DisplayName("Test Duo 2FA response validation, success.")
+    public void testExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        final String state = DuoSupport.generateState(32);
+        dc.setRequestState(state);
+        dc.setResponseState(state);
+        action.initialize();
+        final Event event = action.execute(src);
+        //null event is success.
+        Assertions.assertNull(event);
+    }
+    
+    @Test @DisplayName("Test Duo 2FA response validation, no state in either request or response.")
+    public void testExecuteNoStateInRequestOrResponse() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event,AuthnEventIds.AUTHN_EXCEPTION);
+    }
+    
+    @Test @DisplayName("Test Duo 2FA response validation, no state in response.")
+    public void testExecuteNoStateInResponse() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setRequestState(DuoSupport.generateState(32));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event,AuthnEventIds.AUTHN_EXCEPTION);
+    }
+    
+    @Test @DisplayName("Test Duo 2FA response validation, no state in request.")
+    public void testExecuteNoStateInRequest() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setResponseState(DuoSupport.generateState(32));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event,AuthnEventIds.AUTHN_EXCEPTION);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
new file mode 100644
index 0000000..8071fd4
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAudienceTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAudience} class.
+ */
+public class ValidateDuoTokenAudienceTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenAudience action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenAudience();
+
+    }
+
+    @Test
+    @DisplayName("Test Duo 2FA token response audience validation, success.")
+    public final void testDoExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response audience validation, wrong audience.")
+    public final void testDoExecuteWrongAudience() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful","WRONG",
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        assertEventId(event,AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
new file mode 100644
index 0000000..eca9d9e
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationResultTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAuthenticationResult} action.
+ */
+public class ValidateDuoTokenAuthenticationResultTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenAuthenticationResult action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenAuthenticationResult();
+
+    }
+  
+
+    /**
+     * Test successful execution.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    @DisplayName("Test validation of Duo 2FA token execution, success")
+    public void testExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID,
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        dc.setUsername("jdoe");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        // success here is a null event
+        Assertions.assertNull(event);
+    }
+    
+    /**
+     * Test the correct invalid authentication state is signalled when no Duo context is supplied.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test validation, no Duo Context")
+    public void testNoDuoContext() throws ComponentInitializationException {
+        addAttemptedFlow("authn/DuoOIDC");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.INVALID_AUTHN_CTX);
+    }
+    
+    /**
+     * Test validation of a token whoes 2FA request was denied.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test @DisplayName("Test validation of Duo 2FA token with classifed message, request denied")
+    public void testExecuteRequestDenied() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        
+        final Map<String, Collection<String>> classifiedMessages = new HashMap<String,Collection<String>>();
+        classifiedMessages.put(AuthnEventIds.ACCOUNT_LOCKED, Arrays.asList(new String[] {"deny"}));
+        action.setClassifiedMessages(classifiedMessages);
+        
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Account locked",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        dc.setUsername("jdoe");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.ACCOUNT_LOCKED);
+    }
+    
+    /**
+     * Test unsuccessful execution, when there is no Duo authentication token.
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    @DisplayName("Test validation of Duo 2FA token when the token is null")
+    public void testExecuteNoDuoAuthToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setUsername("jdoe");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+     
+        assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+    
+    /**
+     * Test unsuccessful execution when the token specifies a 2FA failure. Note,
+     * this is unlikely to ever happen in practice, as failure is not reported back 
+     * to the IdP. 
+     * 
+     * @throws ComponentInitializationException on error.
+     */
+    @Test
+    @DisplayName("Test validation of Duo 2FA token which has failed 2FA")
+    public void testExecuteFailedToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        //made up fail message, TODO: do they describe this anywhere.
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_DENY,"Login Failed",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        dc.setUsername("jdoe");
+        action.initialize();
+        
+        final Event event = action.execute(src);
+     
+        assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
new file mode 100644
index 0000000..6a4738a
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenAuthenticationTimeTest.java
@@ -0,0 +1,122 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Tests for the {@link ValidateDuoTokenAuthenticationTime} action.
+ */
+public class ValidateDuoTokenAuthenticationTimeTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenAuthenticationTime action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenAuthenticationTime();
+
+    }
+
+    @Test
+    @DisplayName("Test Duo 2FA token response authentication time validation, success.")
+    public final void testDoExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response authentication time validation, not forced auth.")
+    public final void testDoExecuteSuccessNotForcedAuth() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(false);
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response authentication time validation, outside window but not force authn.")
+    public final void testDoExecuteSuccessOutsideLifetimeNotForcedAuthn() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(false);
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES))));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response authentication time validation, outside lifetime window.")
+    public final void testDoExecuteOutsideLifetime() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().minus(Duration.of(10, ChronoUnit.MINUTES))));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response authentication time validation, in the future.")
+    public final void testDoExecuteInTheFuture() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        ac.setForceAuthn(true);
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        action.setAuthnLifetime(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now().plus(Duration.of(10, ChronoUnit.MINUTES))));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
new file mode 100644
index 0000000..111747e
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenExpirationTimeTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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 java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/** Tests for ValidateDuoTokenExpirationTime class.*/
+public class ValidateDuoTokenExpirationTimeTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenExpirationTime action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenExpirationTime();
+
+    }
+
+
+    @Test
+    @DisplayName("Test Duo 2FA token response expiration time validation, success.")
+    public final void testDoExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response expiration time validation, expired token.")
+    public final void testDoExecuteExpiredToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        //create token, set expiry 1 day in the past.
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().minus(1,ChronoUnit.DAYS),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response expiration time validation, within clock skew, success.")
+    public final void testDoExecuteWithinClockSkewSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+
+        //allow a 1 minute clock skew - should be default
+        action.setClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        //create token, set expiry 50 second in the past.
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().minus(50,ChronoUnit.SECONDS),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response expiration time validation, set zero clock skew expired token.")
+    public final void testDoExecuteSetZeroClockSkewExpiredToken() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        //do not allow a clock skew on the expiry
+        action.setClockSkew(Duration.of(0, ChronoUnit.SECONDS));
+        //create token, set expiry 1 second in the past.
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().minus(1,ChronoUnit.SECONDS),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
new file mode 100644
index 0000000..af62bb0
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenIssuedAtTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Test the {@link ValidateDuoTokenIssuedAt} class.
+ */
+public class ValidateDuoTokenIssuedAtTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenIssuedAt action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenIssuedAt();
+
+    }
+
+    @Test
+    @DisplayName("Test Duo 2FA token response token lifetime (issuedAt) validation, success.")
+    public final void testDoExecuteSuccess() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response token lifetime (issuedAt) validation, to far in the past.")
+    public final void testDoExecuteToFarInThePast() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now().minus(3,ChronoUnit.MINUTES), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+       
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+    
+    @Test
+    @DisplayName("Test Duo 2FA token response token lifetime (issuedAt) validation, to far in the future.")
+    public final void testDoExecuteToFarInTheFuture() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        action.setIatMaxClockSkew(Duration.of(1, ChronoUnit.MINUTES));
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now(),Instant.now().plus(3,ChronoUnit.MINUTES), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+       
+        assertEventId(event, AuthnEventIds.AUTHN_EXCEPTION);
+
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
new file mode 100644
index 0000000..7477391
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoTokenSubjectTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.webflow.execution.Event;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCAuthAPI;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+class ValidateDuoTokenSubjectTest extends AbstractDuoActionTest {
+
+    /** The action to test. */
+    private ValidateDuoTokenSubject action;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        super.setup();
+        action = new ValidateDuoTokenSubject();
+
+    }
+
+    @Test @DisplayName("Test Duo 2FA token subject validation, success")
+    public final void testExecuteSuccesful() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setUsername("jdoe");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        Assertions.assertNull(event);
+    }
+    
+    @Test @DisplayName("Test Duo 2FA token subject validation, failure")
+    public final void testExecuteInvalidSubject() throws ComponentInitializationException {
+        addDuoContext();
+        addDuoIntegrationToContext();
+        addAttemptedFlow("authn/DuoOIDC");
+        dc.setUsername("different-user");
+        dc.setAuthToken(createDummyToken(DuoOIDCAuthAPI.DUO_AUTH_RESULT_ALLOW,"Login Succesful",CLIENT_ID, 
+                Instant.now().plus(1,ChronoUnit.MINUTES),Instant.now(), Instant.now()));
+        action.initialize();
+        final Event event = action.execute(src);
+        // null event is success.
+        assertEventId(event, AuthnEventIds.INVALID_CREDENTIALS);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_FAIL_Client.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_FAIL_Client.java
new file mode 100644
index 0000000..a07e5f4
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_FAIL_Client.java
@@ -0,0 +1,19 @@
+
+package net.shibboleth.idp.plugin.authn.mock;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+
+/**
+ * Mock client factory that returns a {@link MockDuoOIDCClient_OK} client.
+ */
+public class MockDuoOIDCClientFactory_FAIL_Client implements DuoOIDCClientFactory {
+
+    @Override
+    public DuoOIDCClient createInstance(final DuoOIDCIntegration integration) throws DuoClientException {
+        return new MockDuoOIDCClient_FAIL(integration);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_OK_Client.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_OK_Client.java
new file mode 100644
index 0000000..fdfcd34
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClientFactory_OK_Client.java
@@ -0,0 +1,19 @@
+
+package net.shibboleth.idp.plugin.authn.mock;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+
+/**
+ * Mock client factory that returns a {@link MockDuoOIDCClient_OK} client.
+ */
+public class MockDuoOIDCClientFactory_OK_Client implements DuoOIDCClientFactory {
+
+    @Override
+    public DuoOIDCClient createInstance(final DuoOIDCIntegration integration) throws DuoClientException {
+        return new MockDuoOIDCClient_OK(integration);
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index 02246cf..a34f75c 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
@@ -2,6 +2,9 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 import javax.annotation.Nonnull;
 
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
@@ -14,6 +17,7 @@ import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
  * Mock a Duo client which is not available (unhealthy).
@@ -26,27 +30,12 @@ public 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;
-    
     @NonnullAfterInit private DuoOIDCIntegration integration;
-    
-    //TODO: finish values for the token.
-
-    public MockDuoOIDCClient_FAIL() {
-    }
 
-    @Override
-    public void initialize(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
-       integration = integ;
-        
+    public MockDuoOIDCClient_FAIL(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
+        integration = integ;        
     }
 
     @Override
@@ -58,19 +47,38 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
     @Override
     public String createAuthUrl(final String username, final String state) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
-                integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(), AUD, "JWT");
+                integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
+                integration.getClientId(), "JWT");
     }
 
     @Override
     public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code) throws DuoClientException {
-        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).withAuthResultStatusMessage("Login Failed").withAuthResultStatus("fail").
-                withAuthResult("fail").withAuthContext(DuoAuthContext.builder().withResult("fail").
-                        withTimestamp(1590070939).withAuthDevice(DuoAuthDevice.builder().withIp("192.168.0.1").
-                                withName("99999999").build()).withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton").withReason("user_approved").
-                        withAccessDevice(DuoAccessDevice.builder().withIp("192.168.0.1").withName("99999999").build()).
-                        withApplication(DuoApplication.builder().withKey("DIU6GEFXXXXXXX").withName("Test").build()).
+        return DuoAuthToken.builder()
+                .withIss("https://api.duosecurity.com/oauth/v1/token")
+                .withSub(SUB)
+                .withAud(integration.getClientId())
+                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
+                .withIat((double)Instant.now().getEpochSecond())
+                .withAuthResultStatusMessage("Login Failed")
+                .withAuthResultStatus("fail")
+                .withAuthResult("fail")
+                .withAuthContext(DuoAuthContext.builder()
+                        .withResult("fail").
+                        withTimestamp(1590070939)
+                        .withAuthDevice(DuoAuthDevice.builder()
+                                .withIp("192.168.0.1").
+                                withName("99999999")
+                                .build())
+                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
+                        withEventType("authenticaiton")
+                        .withReason("user_approved").
+                        withAccessDevice(DuoAccessDevice.builder()
+                                .withIp("192.168.0.1")
+                                .withHostname("localhost")
+                                .build()).
+                        withApplication(DuoApplication.builder()
+                                .withKey("DIU6GEFXXXXXXX")
+                                .withName("Test").build()).
                         withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).build();
     }
 
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
index e5671d5..03f1807 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
@@ -2,6 +2,9 @@ package net.shibboleth.idp.plugin.authn.mock;
 
 import static java.lang.String.format;
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
 import javax.annotation.Nonnull;
 
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
@@ -14,6 +17,7 @@ import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /**
  * Mock a Duo client which is available and returns a valid response.
@@ -22,27 +26,13 @@ public 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 SUB = "jdoe";    
     
     @NonnullAfterInit private DuoOIDCIntegration integration;
     
-    //TODO: finish values for the token.
-
-    public MockDuoOIDCClient_OK() {
-    }
-
-    @Override
-    public void initialize(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
-       integration = integ;
-        
+    public MockDuoOIDCClient_OK(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {       
+        integration = integ;        
     }
 
     @Override
@@ -54,20 +44,44 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
     @Override
     public String createAuthUrl(final String username, final String state) throws DuoClientException {
         return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
-                integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(), AUD, "JWT");
+                integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(), 
+                integration.getClientId(), "JWT");
     }
 
     @Override
     public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code) throws DuoClientException {
-        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).withAuthResultStatusMessage("Login Successful").withAuthResultStatus("allow").
-                withAuthResult("allow").withAuthContext(DuoAuthContext.builder().withResult("success").
-                        withTimestamp(1590070939).withAuthDevice(DuoAuthDevice.builder().withIp("192.168.0.1").
-                                withName("99999999").build()).withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
-                        withEventType("authenticaiton").withReason("user_approved").
-                        withAccessDevice(DuoAccessDevice.builder().withIp("192.168.0.1").withName("99999999").build()).
-                        withApplication(DuoApplication.builder().withKey("DIU6GEFXXXXXXX").withName("Test").build()).
-                        withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).build();
+        return DuoAuthToken.builder()
+                .withIss("https://api.duosecurity.com/oauth/v1/token")
+                .withSub(SUB)
+                .withAud(integration.getClientId())
+                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
+                .withIat((double)Instant.now().getEpochSecond())
+                .withAuthResultStatusMessage("Login Successful")
+                .withAuthResultStatus("allow")
+                .withAuthResult("allow")
+                .withAuthContext(DuoAuthContext.builder()
+                        .withResult("success")
+                        .withTimestamp(1590070939)
+                        .withAuthDevice(DuoAuthDevice.builder()
+                                .withIp("192.168.0.1").
+                                withName("99999999")
+                                .build())
+                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
+                        withEventType("authenticaiton")
+                        .withReason("user_approved")
+                        .withAccessDevice(DuoAccessDevice.builder()
+                                .withIp("192.168.0.1")
+                                .withHostname("localhost")
+                                .build())
+                       .withApplication(DuoApplication.builder()
+                                .withKey("DIU6GEFXXXXXXX")
+                                .withName("Test")
+                                .build()).
+                        withFactor("duo_push")
+                        .withUsername("jdoe")
+                        .withUserKey("XXXXXXX")
+                        .build())
+                .build();
     }
 
 }
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
new file mode 100644
index 0000000..f7e1dbe
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_UNKNOWN.java
@@ -0,0 +1,82 @@
+package net.shibboleth.idp.plugin.authn.mock;
+
+import static java.lang.String.format;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import javax.annotation.Nonnull;
+
+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.DuoAccessDevice;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoApplication;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Mock a Duo client which returns an unknown response for the health check and a login failure in the
+ * id_token.
+ */
+public class MockDuoOIDCClient_UNKNOWN implements DuoOIDCClient{    
+    
+    @Nonnull private static final String STATUS = "UNKNOWN";
+    
+    @Nonnull private static final String SUB = "jdoe"; 
+    
+    @NonnullAfterInit private DuoOIDCIntegration integration;
+
+    public MockDuoOIDCClient_UNKNOWN(final @Nonnull DuoOIDCIntegration integ) throws DuoClientException {
+        integration = integ;        
+    }
+
+    @Override
+    public DuoHealthCheck healthCheck() throws DuoClientException {
+        return DuoHealthCheck.builder().withStatus(STATUS).
+                withTimestamp(Long.toString(System.currentTimeMillis())).build();
+    }
+
+    @Override
+    public String createAuthUrl(final String username, final String state) throws DuoClientException {
+        return format("https://%s%s?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
+                integration.getAPIHost(),"/oauth/v1/authorize", integration.getRedirectURI(),
+                integration.getClientId(), "JWT");
+    }
+
+    @Override
+    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code) throws DuoClientException {
+        return DuoAuthToken.builder()
+                .withIss("https://api.duosecurity.com/oauth/v1/token")
+                .withSub(SUB)
+                .withAud(integration.getClientId())
+                .withExp(Math.toIntExact(Instant.now().plus(10,ChronoUnit.MINUTES).getEpochSecond()))
+                .withIat((double)Instant.now().getEpochSecond())
+                .withAuthResultStatusMessage("Login Failed")
+                .withAuthResultStatus("fail")
+                .withAuthResult("fail")
+                .withAuthContext(DuoAuthContext.builder()
+                        .withResult("fail").
+                        withTimestamp(1590070939)
+                        .withAuthDevice(DuoAuthDevice.builder()
+                                .withIp("192.168.0.1").
+                                withName("99999999")
+                                .build())
+                        .withTxid("b1287968-1dd1-4488-bb3c-0c72fc398b8b").
+                        withEventType("authenticaiton")
+                        .withReason("user_approved").
+                        withAccessDevice(DuoAccessDevice.builder()
+                                .withIp("192.168.0.1")
+                                .withHostname("localhost")
+                                .build()).
+                        withApplication(DuoApplication.builder()
+                                .withKey("DIU6GEFXXXXXXX")
+                                .withName("Test").build()).
+                        withFactor("duo_push").withUsername("jdoe").withUserKey("XXXXXXX").build()).build();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
index 870b8b5..b61f03c 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
@@ -15,11 +15,16 @@ 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 builder for generating simple mock flows. Mostly used to generate stub subflows when testing
  * a flow.
  */
 public final class MockFlowBuilder {
     
+    /** Constructor.*/
+    private MockFlowBuilder() {
+        
+    }
+    
     /**
      * Create a simple flow with one start-state which transitions to an
      * end-state that ends the flow. The flow does nothing.
@@ -27,11 +32,11 @@ public final class MockFlowBuilder {
     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") {
+        final Flow flow = new Flow(flowId);
+        final ActionState endingStartState = new ActionState(flow,"noop-start-start") {
             
             @Override
-            protected void doEnter(RequestControlContext context) throws FlowExecutionException {
+            protected void doEnter(final RequestControlContext context) throws FlowExecutionException {
                 context.handleEvent(new Event(this, "proceed"));
                 
             }
diff --git a/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-multiple-integrations.xml b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-multiple-integrations.xml
new file mode 100644
index 0000000..751c539
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-multiple-integrations.xml
@@ -0,0 +1,14 @@
+<?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 id="shibboleth.authn.duo.OIDC.DuoIntegrationStrategy" 
+        class="net.shibboleth.idp.plugin.authn.duo.impl.DualDuoIntegrationStrategy" />
+
+</beans>
diff --git a/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config.xml b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config.xml
new file mode 100644
index 0000000..0d20579
--- /dev/null
+++ b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config.xml
@@ -0,0 +1,12 @@
+<?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">
+
+
+</beans>
diff --git a/idp-duo-native-client-impl/pom.xml b/idp-duo-native-client-impl/pom.xml
index f22b8e1..607746f 100644
--- a/idp-duo-native-client-impl/pom.xml
+++ b/idp-duo-native-client-impl/pom.xml
@@ -2,27 +2,32 @@
     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>
+        <groupId>net.shibboleth.plugin.idp.authn</groupId>
+        <artifactId>idp-plugin-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>
+    <artifactId>idp-plugin-duo-native-client-impl</artifactId>
+    <name>Shibboleth IdP :: Plugins :: Native Duo SDK implementation</name>
 
     <properties>
         <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.sdk.impl</automatic.module.name>
     </properties>
 
-
+    <!-- FIXME: LOTS of unwanted inherited from parent deps!! -->
     <dependencies>
         <dependency>
             <groupId>com.duosecurity</groupId>
-            <artifactId>duo-client</artifactId>
-            <scope>provided</scope>
+            <artifactId>duo-client</artifactId>      
         </dependency>
         <dependency>
-            <groupId>net.shibboleth.plugin.authn</groupId>
-            <artifactId>idp-duo-api</artifactId>
+            <groupId>net.shibboleth.plugin.idp.authn</groupId>
+            <artifactId>idp-plugin-duo-api</artifactId>
+        </dependency>
+        
+        <!-- provided deps -->
+        <dependency> 
+            <groupId>net.shibboleth.ext</groupId>
+            <artifactId>spring-extensions</artifactId>            
             <scope>provided</scope>
         </dependency>
     </dependencies>
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoOIDCNativeSDKDescription.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoOIDCNativeSDKDescription.java
new file mode 100644
index 0000000..e0b8272
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoOIDCNativeSDKDescription.java
@@ -0,0 +1,46 @@
+package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.core.io.ClassPathResource;
+
+import net.shibboleth.ext.spring.resource.ResourceHelper;
+import net.shibboleth.utilities.java.support.plugin.AbstractPluginDescription;
+import net.shibboleth.utilities.java.support.plugin.PluginDescription;
+import net.shibboleth.utilities.java.support.resource.Resource;
+
+
+/**
+ * Details about the native Duo SDK client implementation.
+ */
+public class DuoOIDCNativeSDKDescription extends AbstractPluginDescription{
+
+    @Override
+    public String getPluginId() {
+        return "net.shibboleth.plugin.idp.authn.duo.native.sdk";
+    }
+    
+    @Override
+    public int getMajorVersion() {
+        return 0;
+    }
+
+    @Override
+    public int getMinorVersion() {
+        return 0;
+    }
+
+    @Override
+    public int getPatchVersion() {
+        return 1;
+    }
+
+    @Override
+    public List<URL> getUpdateURLs() throws IOException {
+        return Collections.emptyList();
+    }
+
+}
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
index af030bc..7184b18 100644
--- 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
@@ -1,26 +1,34 @@
 package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
 
+import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import com.duosecurity.Client;
 import com.duosecurity.exception.DuoException;
 import com.duosecurity.model.HealthCheckResponse;
+import com.duosecurity.model.Token;
 
 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.DuoAccessDevice;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoApplication;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthContext;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthDevice;
 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.
+ * <p>An object adaptor class for bridging between the Duo SDK implementation 
+ * and the internal {@link DuoOIDCClient} interface.</p>
+ * 
+ * <p>This is package private, and can only be instantiated by the {@link DuoSDKClientFactory}.</p>
  */
-//TODO find a way to stop a client from being created other than through the registry?
-public class DuoSDKClientAdaptor implements DuoOIDCClient{
+final class DuoSDKClientAdaptor implements DuoOIDCClient{
     
     /** The wrapped Duo native client.*/
     @Nonnull private Client client;
@@ -28,31 +36,40 @@ public class DuoSDKClientAdaptor implements DuoOIDCClient{
     /** Function to map the native Duo {@link HealthCheckResponse} object to the interface {@link DuoHealthCheck} object.*/
     @Nonnull private Function<HealthCheckResponse,DuoHealthCheck> healthCheckResponseConverter;
     
-    /** Constructor.
+    /** Function to map the native Duo {@link Token} object to the interface {@link DuoAuthToken} object.*/
+    @Nonnull private Function<Token, DuoAuthToken> tokenResponseConverter;    
+
+    
+    /**
+     * 
+     * Constructor. Initialises the native Duo SDK client.
+     *
+     * @param integration the Duo integration to initilize the client from. Never {@literal null}.
+     * @param caCerts the list of CA Certificates used to validate connections to Duo. Can be {@literal null}.
      * 
      * @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() {
+    protected DuoSDKClientAdaptor(@Nonnull final DuoOIDCIntegration integration, @Nullable List<String> caCerts) 
+            throws DuoClientException {  
+        Constraint.isNotNull(integration,"Duo SDK Client requires a non-null Duo Integration");
         healthCheckResponseConverter = new DefaultHealthCheckResponseConverter();
-    }
-    
-    @Override
-    public void initialize(DuoOIDCIntegration integration) throws DuoClientException{
+        tokenResponseConverter = new DefaultTokenResponseConverter();
         try {
-            client = new Client(integration.getIntegrationKey(), integration.getSecretKey(),
+            if (caCerts == null) {
+                client = new Client(integration.getClientId(), integration.getSecretKey(),
                     integration.getAPIHost(), integration.getRedirectURI());
-        } catch (DuoException e) {
+            }
+            else {
+                client = new Client(integration.getClientId(), integration.getSecretKey(),
+                        integration.getAPIHost(), integration.getRedirectURI(), 
+                        caCerts.toArray(new String[caCerts.size()]));
+            }
+        } catch (final DuoException e) {
             throw new DuoClientException(e);
-        } 
+        }
+ 
     }
-    
+
     
     /**
      * Set the health check response converter.
@@ -65,16 +82,16 @@ public class DuoSDKClientAdaptor implements DuoOIDCClient{
     }
 
     @Override
-    public DuoHealthCheck healthCheck() throws DuoClientException {        
+    @Nonnull public DuoHealthCheck healthCheck() throws DuoClientException {        
         try {
             final HealthCheckResponse response = client.healthCheck();
-            if (response==null) {
-                throw new DuoClientException("Duo health check response was null!");
+            if (response == null) {
+                throw new DuoClientException("Duo health check response was null");
             }
             return healthCheckResponseConverter.apply(response);
             
             
-        } catch (DuoException e) {
+        } catch (final DuoException e) {
           //wrap duo specific exception.
            throw new DuoClientException(e);
         }
@@ -82,24 +99,33 @@ public class DuoSDKClientAdaptor implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
+    @Nonnull public String createAuthUrl(@Nonnull final String username, @Nonnull final String state) 
+            throws DuoClientException {
         try {
             return client.createAuthUrl(username, state);
-        } catch (DuoException e) {
+        } catch (final 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;
+    @Nonnull public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(@Nonnull final String code) 
+            throws DuoClientException {
+        try {
+            final Token token = client.exchangeAuthorizationCodeFor2FAResult(code);
+            if (token == null) {
+                throw new DuoClientException("Duo token was null");
+            }
+            return tokenResponseConverter.apply(token);
+         } catch (final DuoException e) {
+          //wrap duo specific exception.
+            throw new DuoClientException(e);
+        }
     }
-    
-    /**
-     * Default health check response matter.
-     */
+
+
+    /** Default health check response converter. */
     //TODO do we need this extensible in an adaptor?
     private class DefaultHealthCheckResponseConverter implements Function<HealthCheckResponse,DuoHealthCheck>{
 
@@ -116,6 +142,51 @@ public class DuoSDKClientAdaptor implements DuoOIDCClient{
         }
         
     }
+    //TODO: optional parts of the builder e.g. city are not being built optionally
+    //TODO: no null checks on nested classes?
+    //TODO: builders are a marginal mess?
+    /** Default Duo token converter.*/
+    private class DefaultTokenResponseConverter implements Function<Token, DuoAuthToken>{
+
+        @Override
+        public DuoAuthToken apply(Token t) {
+            return DuoAuthToken.builder().
+            withIss(t.getIss()).
+            withSub(t.getSub()).
+            withAud(t.getAud()).
+            withExp(t.getExp()).
+            withIat(t.getIat()).
+            withAuthResultStatusMessage(t.getAuth_result().getStatus_msg()).
+            withAuthResultStatus(t.getAuth_result().getStatus()).
+            withAuthResult(t.getAuth_result().getResult())
+            .withAuthContext(DuoAuthContext.builder().
+                    withResult(t.getAuth_context().getResult()).
+                    withTimestamp(t.getAuth_context().getTimestamp()).
+                    withAuthDevice(DuoAuthDevice.builder().
+                            withIp(t.getAuth_context().getAuth_device().getIp()).
+                            withName(t.getAuth_context().getAuth_device().getName()).
+                            withCity(t.getAuth_context().getAuth_device().getLocation().getCity()).
+                            withCountry(t.getAuth_context().getAuth_device().getLocation().getCountry()).
+                            withState(t.getAuth_context().getAuth_device().getLocation().getState()).build()).
+                    withTxid(t.getAuth_context().getTxid()).
+                    withEventType(t.getAuth_context().getEvent_type()).
+                    withReason(t.getAuth_context().getReason()).
+                    withAccessDevice(DuoAccessDevice.builder().
+                            withIp(t.getAuth_context().getAccess_device().getIp()).
+                            withHostname(t.getAuth_context().getAccess_device().getHostname()).
+                            withCity(t.getAuth_context().getAccess_device().getLocation().getCity()).
+                            withCountry(t.getAuth_context().getAccess_device().getLocation().getCountry()).
+                            withState(t.getAuth_context().getAccess_device().getLocation().getState()).build()).
+                    withApplication(DuoApplication.builder().withKey(t.getAuth_context().getApplication().getKey()).
+                            withName(t.getAuth_context().getApplication().getName()).build()).
+                    withFactor(t.getAuth_context().getFactor()).
+                    withUsername(t.getAuth_context().getUser().getName()).
+                    withUserKey(t.getAuth_context().getUser().getKey()).build()).
+            withAuthTime(t.getAuth_time()).
+            withPreferredUsername(t.getPreferred_username()).build();
+        }
+        
+    }
 
 
 
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
new file mode 100644
index 0000000..ff6ded5
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
@@ -0,0 +1,41 @@
+package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+
+
+/**
+ * Abstract factory implementation for the {@link DuoSDKClientAdaptor}.
+ */
+public class DuoSDKClientFactory implements DuoOIDCClientFactory{
+    
+    
+    /** List of CA Certificate pins. If null, the clients default set are used.*/
+    @Nullable private List<String> caCerts;
+    
+    
+    /**
+     * Sets the list of CA certificate pins used to verify the Duo client connection
+     * to the API host.
+     * 
+     * @param certs the list of certificate pins.
+     */
+    public void setCaCerts(@Nullable List<String> certs) {
+        caCerts = certs;
+    }
+
+    @Override
+    public DuoOIDCClient createInstance(@Nonnull final DuoOIDCIntegration integration) throws DuoClientException {
+        //every integration shares the same list of caCert pins.
+        return new DuoSDKClientAdaptor(integration, caCerts);
+    }
+
+}
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/package-info.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/package-info.java
new file mode 100644
index 0000000..b84a17a
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Native Duo OIDC 2FA client adaptor implementation classes. */
+package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
diff --git a/idp-duo-native-client-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription b/idp-duo-native-client-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
new file mode 100644
index 0000000..24fc198
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/resources/META-INF/services/net.shibboleth.utilities.java.support.plugin.PluginDescription
@@ -0,0 +1 @@
+net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoOIDCNativeSDKDescription
diff --git a/idp-duo-native-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactoryTest.java b/idp-duo-native-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactoryTest.java
new file mode 100644
index 0000000..cd6eb84
--- /dev/null
+++ b/idp-duo-native-client-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactoryTest.java
@@ -0,0 +1,50 @@
+package net.shibboleth.idp.plugin.authn.duo.sdk.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+
+public class DuoSDKClientFactoryTest {
+    
+    private DuoSDKClientFactory factory;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        factory = new DuoSDKClientFactory();
+        
+    }
+
+    @Test @DisplayName("Test succesful set of null CA certs")
+    public final void testSetNullCaCerts() {
+        factory.setCaCerts(null);
+    }
+    
+    @Test @DisplayName("Test succesful set of CA certs")
+    public final void testSetCaCerts() {
+        final List<String> certs = new ArrayList<>();
+        certs.add("sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=");
+        factory.setCaCerts(certs);
+    }
+
+    @Test
+    public final void testCreateInstance() throws DuoClientException {
+        final List<String> certs = new ArrayList<>();
+        certs.add("sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=");
+        DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost("host.com");
+        integ.setClientId("DIU6GEFWG5LIUBVV2M3P");
+        integ.setRedirectURI("http://localhost/");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        DuoOIDCClient client = factory.createInstance(integ);
+        Assertions.assertNotNull(client);
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index e61a335..708eb74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,19 +1,20 @@
 <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">
 
+    <!-- TODO: The POM is a mess, and is not yet properly following the current guidance -->
 
     <modelVersion>4.0.0</modelVersion>
-    <groupId>net.shibboleth.plugin.authn</groupId>
-    <artifactId>idp-duo-parent</artifactId>
+    <groupId>net.shibboleth.plugin.idp.authn</groupId>
+    <artifactId>idp-plugin-duo-parent</artifactId>
     <version>0.0.1-SNAPSHOT</version>
-    <name>Shibbileth IdP v4 Duo AuthN Parent</name>
+    <name>Shibboleth IdP :: Plugins :: Duo 2FA Login Flow</name>
     <packaging>pom</packaging>
 
-    <properties>
+    <properties> <!-- TODO most of these not needed if parent is idp-parent -->
         <maven.compiler.source>11</maven.compiler.source>
         <maven.compiler.target>11</maven.compiler.target>
         <maven.compiler.release>11</maven.compiler.release>
-        <shib.idp.version>4.0.0</shib.idp.version>
+        <shib.idp.version>4.1.0-SNAPSHOT</shib.idp.version>
         <opensaml.version>4.0.0</opensaml.version>
         <opensaml.groupId>org.opensaml</opensaml.groupId>
         <idp.groupId>net.shibboleth.idp</idp.groupId>
@@ -22,21 +23,27 @@
         <slf4j.groupId>org.slf4j</slf4j.groupId>
         <slf4j.version>1.7.30</slf4j.version>
         <spring.groupId>org.springframework</spring.groupId>
-        <java-support.version>8.0.0</java-support.version>
+        <java-support.version>8.1.0-SNAPSHOT</java-support.version>
         <spring-webflow.groupId>org.springframework.webflow</spring-webflow.groupId>
         <spring-webflow.version>2.5.1.RELEASE</spring-webflow.version>
         <checkstyle.configLocation>${project.basedir}/../resources/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
-    <!-- TODO: if you do not import the shib parent, there are errors as you need to import the SWF stuff explicitly really? -->
     <!-- required for general project properties -->
 
-    <parent> <groupId>net.shibboleth.idp</groupId> <artifactId>idp-parent</artifactId> <version>4.0.0</version> </parent> 
+    <parent>
+        <groupId>net.shibboleth.idp</groupId>
+        <artifactId>idp-parent</artifactId>
+        <version>4.1.0-SNAPSHOT</version>
+    </parent>
 
     <modules>
+        <!-- switch to? <module>idp-plugin-duo-api</module> <module>idp-plugin-duo-impl</module> <module>idp-plugin-duo-native-client-impl</module> 
+            <module>idp-plugin-duo-dist</module> -->
         <module>idp-duo-api</module>
         <module>idp-duo-impl</module>
         <module>idp-duo-native-client-impl</module>
+        <module>idp-duo-distribution</module>
     </modules>
 
     <repositories>
@@ -155,6 +162,7 @@
         <dependency>
             <groupId>com.google.code.findbugs</groupId>
             <artifactId>jsr305</artifactId>
+            <scope>provided</scope>
         </dependency>
 
 
@@ -167,16 +175,12 @@
 
         <dependency>
             <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-api</artifactId>
-            <version>${junit.jupitar.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-engine</artifactId>
+            <artifactId>junit-jupiter</artifactId>
             <version>${junit.jupitar.version}</version>
             <scope>test</scope>
         </dependency>
+
+
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
@@ -186,8 +190,8 @@
 
     <dependencyManagement>
         <dependencies>
-
-            <!-- IdP BOM when importing IdP deps TODO: this inherists properties from java-parent as well -->
+            <!-- REMOVE THESE -->
+            <!-- IdP BOM when importing IdP deps TODO: this inherits properties from java-parent as well -->
             <dependency>
                 <groupId>net.shibboleth.idp</groupId>
                 <artifactId>idp-bom</artifactId>
@@ -214,17 +218,25 @@
             </dependency>
 
             <dependency>
-                <groupId>net.shibboleth.plugin.authn</groupId>
-                <artifactId>idp-duo-api</artifactId>
+                <groupId>net.shibboleth.plugin.idp.authn</groupId>
+                <artifactId>idp-plugin-duo-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
             <dependency>
-                <groupId>net.shibboleth.plugin.authn</groupId>
-                <artifactId>idp-duo-native-client-impl</artifactId>
+                <groupId>net.shibboleth.plugin.idp.authn</groupId>
+                <artifactId>idp-plugin-duo-native-client-impl</artifactId>
                 <version>${project.version}</version>
             </dependency>
 
+            <!-- test dependencies -->
 
+            <dependency>
+                <groupId>net.shibboleth.idp</groupId>
+                <version>${shib.idp.version}</version>
+                <artifactId>idp-profile-api</artifactId>
+                <type>test-jar</type>
+                <scope>test</scope>
+            </dependency>
 
 
         </dependencies>
@@ -233,16 +245,20 @@
     <build>
         <plugins>
             <plugin>
+                <!-- FIXME: Exclude testng tests, include junit -->
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>3.0.0-M4</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>org.junit.jupiter</groupId>
-                        <artifactId>junit-jupiter-engine</artifactId>
-                        <version>5.3.2</version>
-                    </dependency>
-                </dependencies>
+                <configuration>
+                <properties>
+                    <property>
+                        <!-- stop the inherited testng surefire plugin
+                        from running junit tests and failing. -->
+                        <name>junit</name>
+                        <value>false</value>
+                    </property>
+                </properties>
+                </configuration>
+             
             </plugin>
             <plugin>
                 <groupId>org.codehaus.mojo</groupId>
@@ -253,4 +269,4 @@
         </plugins>
     </build>
 
-</project>
\ No newline at end of file
+</project>

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list