[java-idp-plugin-duo] 05/15: Add basics before pom cleanup

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

commit c095a81b176145836980a4ed198bc8510b56b640
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Jun 9 16:19:08 2020 +0100

    Add basics before pom cleanup
---
 idp-duo-api/pom.xml                                |  38 ++-
 .../authn/duo/AbstractDuoAuthenticationAction.java |   4 +-
 .../plugin/authn/duo/DuoOIDCClientRegistry.java    |   1 +
 .../duo/context/DuoAuthenticationContext.java      |  95 +++++++
 idp-duo-impl/pom.xml                               | 195 ++------------
 .../idp/plugin/authn/duo/impl/CheckDuoState.java   |  70 +++++
 .../duo/impl/DefaultDuoOIDCClientRegistry.java     |   3 +
 .../authn/duo/impl/DuoOIDCAuthnController.java     | 151 +++++++++--
 .../plugin/authn/duo/impl/PopulateDuoContext.java  |  96 +++++++
 .../flows/authn/duo/duo-oidc-authn-flow.xml        |  24 +-
 .../plugin/authn/duo/impl/DuoAuthnFlowTest.java    | 136 +++-------
 .../authn/duo/impl/DuoOIDCAuthnControllerTest.java | 287 ++++++++++++++++++---
 .../authn/duo/impl/PasswordAuthnFlowTest.java      |   2 +-
 .../plugin/authn/mock/MockDuoOIDCClient_FAIL.java  |  62 +++++
 .../plugin/authn/mock/MockDuoOIDCClient_OK.java    |  62 +++++
 .../authn/{util => }/mock/MockFlowBuilder.java     |   2 +-
 ...yConfigurer.java => IdPPropertyConfigurer.java} |   2 +-
 idp-duo-native-client-impl/pom.xml                 |   6 +
 pom.xml                                            |  95 +------
 19 files changed, 884 insertions(+), 447 deletions(-)

diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index d01d26e..9a6c0f4 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -1,32 +1,30 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-  <parent>
-    <groupId>net.shibboleth.plugin.authn</groupId>
-    <artifactId>idp-duo-parent</artifactId>
-    <version>0.0.1-SNAPSHOT</version>
-  </parent>
-  <artifactId>idp-duo-api</artifactId>
-  
-  
-  
-   <dependencies>            
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>net.shibboleth.plugin.authn</groupId>
+        <artifactId>idp-duo-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+    </parent>
+    <artifactId>idp-duo-api</artifactId>
+
+    <properties>
+        <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.api</automatic.module.name>
+    </properties>
+
+
+    <dependencies>
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
             <artifactId>idp-authn-api</artifactId>
-        </dependency>      
-       
-        <dependency>
-            <groupId>${slf4j.groupId}</groupId>
-            <artifactId>jcl-over-slf4j</artifactId>
-            <scope>provided</scope>
         </dependency>
-       
+
         <!-- Provided Dependencies -->
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
             <scope>provided</scope>
         </dependency>
-       
+
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/AbstractDuoAuthenticationAction.java
index 6bf6828..e56ffaf 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
@@ -26,10 +26,10 @@ import net.shibboleth.utilities.java.support.logic.FunctionSupport;
  * A base class for Duo 2FA authentication related actions.
  * 
  * In addition to the work performed by {@link AbstractAuthenticationAction}, this action also looks up
- * and makes available the {@link DuoOIDCClient} from the looked up in-flight {@link DuoIntegration}.
+ * and makes available the {@link DuoOIDCClient} from a lookup from the in-flight {@link DuoIntegration}.
  * 
  * Duo authentication action implementations should override
- * {@link #doExecute(ProfileRequestContext, AuthenticationContext)}
+ * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)} method.
  * 
  * @event {@link AuthnEventIds#INVALID_AUTHN_CTX} FINISH
  */
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 35d1a85..f0e760f 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
@@ -2,6 +2,7 @@ package net.shbboleth.idp.plugin.authn.duo;
 
 import javax.annotation.Nonnull;
 
+//TODO: this is like a factory and a registry
 public interface DuoOIDCClientRegistry {
     
     /**
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoAuthenticationContext.java
index 1f3e490..414bc96 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/DuoAuthenticationContext.java
@@ -25,9 +25,13 @@ import javax.annotation.Nullable;
 
 import org.opensaml.messaging.context.BaseContext;
 
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 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 ...
@@ -35,6 +39,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
  * @parent {@link AuthenticationContext}
  * @added After ...
  */
+//TODO: do we need all of these. This name clashes with an existing IdP one.
 public final class DuoAuthenticationContext extends BaseContext {
 
     /** Username. */
@@ -54,12 +59,82 @@ public final class DuoAuthenticationContext extends BaseContext {
     
     /** 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.*/
+    @Nullable private String requestState;
+    
+    /** A randomly generated minimum 32 character String returned in the Duo 2FA authorization response.*/
+    @Nullable private String responseState;
+    
+    /** The authorization code return from the Duo authorization request.*/
+    @Nullable private String authCode;
 
     /** Constructor. */
     public DuoAuthenticationContext() {
         pushInfo = new HashMap<>();
     }
     
+    @Nullable public String getAuthorizationCode() {
+        return authCode;
+    }
+    
+    /**
+     * Set the authorization code.
+     * 
+     * @param code the authorization code.
+     * 
+     * @return this context.
+     */
+    @Nonnull public DuoAuthenticationContext setAuthorizationCode(@Nullable final String code) {
+        authCode = code;
+        return this;
+    }
+    
+    /**
+     * Get the request state.
+     * 
+     * @return the state
+     */
+    @Nullable public String getRequestState() {
+        return requestState;
+    }
+    
+    /**
+     * Set the request state.
+     * 
+     * @param state the state.
+     * 
+     * @return this context
+     */
+    @Nonnull public DuoAuthenticationContext setRequestState(@Nullable final String state) {
+        requestState = state;
+        return this;
+    }
+    
+    /**
+     * Get the response state.
+     * 
+     * @return the state
+     */
+    @Nullable public String getResponseState() {
+        return responseState;
+    }
+    
+    /**
+     * Set the response state.
+     * 
+     * @param state the state.
+     * 
+     * @return this context
+     */
+    @Nonnull public DuoAuthenticationContext setResponseState(@Nullable final String state) {
+        responseState = state;
+        return this;
+    }
+    
     /**
      * Get the username.
      * 
@@ -80,6 +155,26 @@ public final class DuoAuthenticationContext extends BaseContext {
         username = name;
         return this;
     }
+    
+    /**
+     * Set the Duo integration.
+     * 
+     * @param duoIntegration the integration
+     * @return this context
+     */
+    public DuoAuthenticationContext setIntegration(@Nullable final DuoOIDCIntegration duoIntegration) {
+        integration = duoIntegration;
+        return this;
+    }
+    
+    /**
+     * Get the Duo integration.
+     * 
+     * @return the Duo integration
+     */
+    @Nullable public DuoOIDCIntegration getIntegration() {
+        return integration;
+    }
 
     /**
      * Get the client address.
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index f838071..07efa35 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -8,29 +8,44 @@
         <version>0.0.1-SNAPSHOT</version>
     </parent>
 
-
     <artifactId>idp-duo-impl</artifactId>
+    
+    <properties>
+        <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.impl</automatic.module.name>
+    </properties>
 
 
+    <!--  TODO: make sure any classes we are using explicitly in our code are directly imported, and not transitively imported for best
+    practice. -->
     <dependencies>
+
+        <!-- Compile time dependencies the project builds against -->
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
             <artifactId>idp-authn-api</artifactId>
+            <scope>Provided</scope>
+        </dependency>
+
+        <dependency> <!-- Provides some lookup strategies -->
+            <groupId>net.shibboleth.idp</groupId>
+            <artifactId>idp-session-api</artifactId>
+             <scope>Provided</scope>
         </dependency>
 
-        
         <dependency>
             <groupId>net.shibboleth.plugin.authn</groupId>
             <artifactId>idp-duo-api</artifactId>
+             <scope>Provided</scope>
         </dependency>
-        
+
+        <!-- Duo client implementation is a runtime depedancy that can be swapped out -->
         <dependency>
             <groupId>net.shibboleth.plugin.authn</groupId>
             <artifactId>idp-duo-native-client-impl</artifactId>
             <scope>runtime</scope>
         </dependency>
 
-        <!-- Provided Dependencies -->
+        <!-- Provided/Test Dependencies -->
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
@@ -40,70 +55,18 @@
         <dependency>
             <groupId>${slf4j.groupId}</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
-            <!-- <scope>provided</scope> -->
+            <scope>provided</scope>
         </dependency>
 
         <!-- Test Dependencies -->
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
-            <version>5.6.2</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-engine</artifactId>
-            <version>5.6.2</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.hsqldb</groupId>
-            <artifactId>hsqldb</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-filter-api</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-filter-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-filter-spring</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-filter-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-resolver-spring</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-consent-impl</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>commons-io</groupId>
-            <artifactId>commons-io</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.sun.mail</groupId>
-            <artifactId>jakarta.mail</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.codehaus.janino</groupId>
-            <artifactId>janino</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -113,134 +76,20 @@
         </dependency>
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-core</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-profile-api</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-filter-spring</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-core</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-attribute-resolver-spring</artifactId>
-            <type>test-jar</type>
+            <artifactId>idp-profile-impl</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
             <artifactId>idp-authn-impl</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-authn-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-authn-api</artifactId>
-            <type>test-jar</type>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
             <artifactId>idp-conf</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-conf</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-profile-impl</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-profile-api</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-profile-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-storage-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-storage-api</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.opensaml</groupId>
-            <artifactId>opensaml-messaging-impl</artifactId>
-            <optional>true</optional>
             <scope>test</scope>
         </dependency>
 
-        <dependency>
-            <groupId>com.unboundid</groupId>
-            <artifactId>unboundid-ldapsdk</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>${spring.groupId}</groupId>
-            <artifactId>spring-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-session-impl</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-cas-impl</artifactId>
-            <scope>test</scope>
-            <optional>true</optional>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-admin-impl</artifactId>
-            <scope>test</scope>
-            <optional>true</optional>
-        </dependency>
-        <dependency>
-            <groupId>net.shibboleth.ext</groupId>
-            <artifactId>spring-extensions</artifactId>
-            <type>test-jar</type>
-            <scope>test</scope>
-        </dependency>
-
-
-
-
-
     </dependencies>
 
     <build>
@@ -249,7 +98,7 @@
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-dependency-plugin</artifactId>
                 <executions>
-
+                    <!-- Not sure we need this if using webflow unit tests? -->
                     <!-- <execution> <id>copy-dependencies-test</id> <phase>prepare-package</phase> <goals> <goal>copy-dependencies</goal> 
                         </goals> <configuration> <outputDirectory>${project.target.directory}</outputDirectory> </configuration> </execution> -->
                     <execution>
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
new file mode 100644
index 0000000..ce1673a
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CheckDuoState.java
@@ -0,0 +1,70 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import javax.annotation.Nonnull;
+
+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.idp.profile.ActionSupport;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * Validate the state sent in the Duo 2FA request matches that in the 2FA response.
+ */
+//TODO: so we require state? must check this is required with Duo flow to.
+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();
+        
+        if (requestState == null || responseState == null) {
+            log.error("State was not present in either the request or response, state is mandatory for Duo 2FA requests");
+            //TODO which error?
+            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!");
+            //TODO which error?
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            return;
+        } else {
+            log.trace("Duo 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 8b1efe8..76e01cd 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
@@ -27,10 +27,13 @@ import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
 /**
  * Default DuoClient registry.
+ * 
+ * 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}.
  */
 @ThreadSafe
+//TODO: this is like a factory and a registry. See PrincipalEvalPredicateFactoryRegistry.
 public class DefaultDuoOIDCClientRegistry extends AbstractIdentifiableInitializableComponent implements DuoOIDCClientRegistry{
     
     /** Class logger. */
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 ea9ff1b..5c8c864 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
@@ -18,12 +18,18 @@
 
 package net.shibboleth.idp.plugin.authn.duo.impl;
 
+import java.io.IOException;
+import java.security.SecureRandom;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
+import org.apache.commons.lang3.StringUtils;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -31,17 +37,22 @@ import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.servlet.view.RedirectView;
 
+import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
 import net.shibboleth.idp.authn.ExternalAuthentication;
 import net.shibboleth.idp.authn.ExternalAuthenticationException;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+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.AbstractInitializableComponent;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 
 /**
@@ -56,31 +67,42 @@ import net.shibboleth.utilities.java.support.logic.FunctionSupport;
 @RequestMapping("%{idp.authn.duo.OIDC.externalAuthnPath:/Authn/Duo}")
 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";
+    
+    /** The name of the Http parameter that stores the state value.*/
+    @Nonnull @NotEmpty public final static 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";
+    
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DuoOIDCAuthnController.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.*/
-    @Nonnull private DuoOIDCClientRegistry clientRegistry;
+    @NonnullAfterInit private DuoOIDCClientRegistry clientRegistry;
     
-    /** Constructor.*/
+    /** Constructor. */
     public DuoOIDCAuthnController() {
-        duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+        // PRC -> AC -> DuoAuthenticationContext
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoAuthenticationContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
     /**
-     * Set DuoIntegration lookup strategy to use.
+     * Set the Duo client registry.
      * 
-     * @param strategy lookup strategy
+     * @param duoRegistry the registry
      */
-    public void setDuoIntegrationLookupStrategy(
-            @Nonnull final Function<ProfileRequestContext,DuoOIDCIntegration> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
-        duoIntegrationLookupStrategy = Constraint.isNotNull(strategy, "DuoIntegration lookup strategy cannot be null");
+    public void setClientRegistry(@Nonnull final DuoOIDCClientRegistry duoRegistry) {        
+        clientRegistry = Constraint.isNotNull(duoRegistry,"DuoCient registry can not be null");
     }
+   
     
     /** {@inheritDoc} */
     @Override protected void doInitialize() throws ComponentInitializationException {
@@ -91,24 +113,103 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         }
     }
     
-    @GetMapping("/2FA/start")
-    public RedirectView start2FA(@Nonnull final HttpServletRequest httpRequest,
-            @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException {
+    @GetMapping("/2FA/authorize")
+    public void authorizationRequest(@Nonnull final HttpServletRequest httpRequest,
+            @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException, IOException {
         
         final String key = ExternalAuthentication.startExternalAuthentication(httpRequest);
+        
+        //put the key in the session, do not create a session, should be one.
+        //TODO: where to best stash the key so we can get it back easily.
+        final HttpSession session = httpRequest.getSession(false);
+        if (session == null) {
+            throw new ExternalAuthenticationException("Request is not associated with an existing Http Session!");
+        }
+        session.setAttribute(KEY_SESSION_ATTRIBUTE, key);
+        
         final ProfileRequestContext prc = ExternalAuthentication.getProfileRequestContext(key, httpRequest);
         
+        final DuoAuthenticationContext 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);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+            
+        }
+        final DuoOIDCIntegration integration = duoContext.getIntegration();
+        log.trace("Starting Duo 2FA for integration '{}' and user '{}'",integration.getIntegrationKey(),
+                duoContext.getUsername());
+        try {
+            final DuoOIDCClient client = clientRegistry.getIntegrationClientOrCreate(integration);            
+            //generate state, stash in the context for checking on return.
+            final String state = generateState(32);
+            duoContext.setRequestState(state);            
+            final String authURL = client.createAuthUrl(duoContext.getUsername(), state);
+            httpResponse.sendRedirect(authURL);
+
+        } catch (final DuoRegistryException | DuoClientException e) {
+            httpRequest.setAttribute(ExternalAuthentication.AUTHENTICATION_EXCEPTION_KEY, e);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+        }
+        //what happens here?
         
-        return new RedirectView("DUO");
     }
-    
-    @GetMapping("/2FA/end")
-    public void end2FA(@Nonnull final HttpServletRequest httpRequest,
-            @Nonnull final HttpServletResponse httpResponse,
-            @RequestParam("code") String code, @RequestParam("state") String state) {
+
+    @GetMapping("/2FA/authorize-callback")
+    public void authorizationCallback(@Nonnull final HttpServletRequest httpRequest,
+            @Nonnull final HttpServletResponse httpResponse) throws ExternalAuthenticationException, IOException {
         
-       // ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+        final String code = httpRequest.getParameter(CODE_PARAMETER);
+        final String state = httpRequest.getParameter(STATE_PARAMETER);
+        if (state == null || code == null) {
+            throw new ExternalAuthenticationException("Duo response must contain a 'code' and 'state' parameter");
+        }
+        final HttpSession session = httpRequest.getSession(false);
+        if (session == null) {
+            throw new ExternalAuthenticationException("Request is not associated with an existing Http Session, unable"
+                    + " to resume the flow execution");
+        }
+        
+        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");
+        }
+        //for 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);
+        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);
+            ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
+            return;
+            
+        }
+        duoContext.setAuthorizationCode(code);
+        duoContext.setResponseState(state);
+        ExternalAuthentication.finishExternalAuthentication(key, httpRequest, httpResponse);
         
     }
+    
+    /**
+     * 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.
+     * 
+     * @return the randomly generated state value.
+     */
+    @Nonnull private String generateState(@Nonnull final Integer length) {
+        Constraint.isGreaterThan(31, length, "State must be at least 32 characters");
+        final SecureRandom secureRandom = new SecureRandom();
+        final StringBuilder sb = new StringBuilder();
+        while(sb.length() < length){
+            sb.append(Integer.toHexString(secureRandom.nextInt()));
+        }
+        return sb.toString().substring(0, length);
+    }
 
 }
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoContext.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoContext.java
new file mode 100644
index 0000000..2830a06
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoContext.java
@@ -0,0 +1,96 @@
+
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+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.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+/**
+ * Similar to (but without the extraction) ExtractDuoAuthenticationFromHeaders.
+ */
+public class PopulateDuoContext extends AbstractAuthenticationAction {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateDuoContext.class);
+
+    /** Lookup strategy for username to match against Duo identity. */
+    @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
+
+    /** Lookup strategy for Duo integration. */
+    @Nonnull private Function<ProfileRequestContext, DuoOIDCIntegration> duoIntegrationLookupStrategy;
+
+    public PopulateDuoContext() {
+        usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+        duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+    }
+
+    /**
+     * Set the lookup strategy to use for the username to match against Duo identity.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setUsernameLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup 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 doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+
+        DuoAuthenticationContext context = new DuoAuthenticationContext();
+        
+        DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+        if (duoIntegration == null) {
+            log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
+        context.setIntegration(duoIntegration);
+        
+        String username = usernameLookupStrategy.apply(profileRequestContext);
+        if (username == null) {
+            log.warn("{} No principal name available to initiate a Duo 2FA request", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+            return;
+        }
+        context.setUsername(username);
+        
+        authenticationContext.addSubcontext(context, true);
+        
+        log.debug("Created Duo authentication context for '{}'",username);
+    }
+
+}
diff --git a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
index 5f9390a..343e47e 100644
--- a/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
+++ b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-flow.xml
@@ -8,11 +8,11 @@
     <action-state id="CheckDuoOIDCAuthAPI">
         <evaluate expression="HealthCheckDuoOIDCAuthAPI" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="StartDuo2FA" />
+        <transition on="proceed" to="Duo2FAAuthorizationRequest" />
     </action-state>
     
 
-    <view-state id="StartDuo2FA"
+    <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())}">
         <attribute name="csrf_excluded" value="true" type="boolean" />
        <!--  <on-render>
@@ -24,12 +24,24 @@
             <evaluate expression="flowScope.samlContext.setOutboundMessageHandler(PreEncodeMessageHandler)" />
         </on-render> -->
 
-        <transition on="proceed" to="ProcessResponse" />
+        <transition on="proceed" to="ValidateDuoResponse" />
     </view-state>
 
-    <view-state id="DisplayDuoUsernamePasswordPage" view="duo-oidc">
-
-    </view-state>
+    <!-- match the response state to the request state, fail if error  -->
+    <action-state id="ValidateDuoResponse">
+        <evaluate expression="CheckDuoState"/>
+        <!-- switch here if no match-->   
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="ExchangeCodeForDuoToken" />
+    </action-state>
+    
+    <action-state id="ExchangeCodeForDuoToken">
+        <evaluate expression="ExchangeCodeForDuoToken"/>
+        <!-- validate the response, no decide yet -->
+        
+        <evaluate expression="'proceed'" />
+        <transition on="proceed" to="SetPrincipal" />
+    </action-state>
 
     <!-- We only need the username -->
     <!-- <action-state id="ExtractUsernameFromFormRequest"> </action-state> -->
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 79b6c24..af1185f 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
@@ -12,11 +12,18 @@ import java.util.function.Supplier;
 import javax.annotation.Nonnull;
 
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 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.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 junit.framework.Assert;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
@@ -24,7 +31,7 @@ import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
-import net.shibboleth.idp.plugin.authn.util.mock.MockFlowBuilder;
+import net.shibboleth.idp.plugin.authn.mock.MockFlowBuilder;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 
@@ -74,7 +81,7 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
                 "idp.duo.oidc.apiHost","https://api.duosecurity.com/oauth/v1/token",
                 "idp.duo.oidc.integrationKey","DIU6GEFWG5LIUBVV2M3P",
                 "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
-                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.duo.impl.MockDuoOIDCClient_FAIL");
+                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_FAIL");
         
         setMockProperties(mockProperties);
         
@@ -90,7 +97,7 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
 
     }
     
-    @Test public void testDuoAuthnFlow() {
+    @Test public void testDuoAuthnFlowToAuthorizationRequest() {
         
         setFlowPath(flowToTest);
         setFlowModelResources(flowResources);
@@ -98,11 +105,11 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         addHttpBasicAuthHeader(USERNAME,PASSWORD);
         
        final Map<String,String> mockProperties = Map.of(
-                "idp.duo.oidc.redirectUri","http://localhost/callback",
+                "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.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
-                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.duo.impl.MockDuoOIDCClient_OK");
+                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
         
         setMockProperties(mockProperties);
         
@@ -115,109 +122,44 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         updateFlowExecution(flowExecution);
         flowExecution.start(inputMap, externalContext);         
         assertFlowExecutionActive();
-        assertCurrentStateEquals("StartDuo2FA");
+        assertCurrentStateEquals("Duo2FAAuthorizationRequest");
 
     }
-
-   
-}
-
-/**
- * Mock a Duo client which is available and returns a valid response.
- */
-class MockDuoOIDCClient_OK implements DuoOIDCClient{    
-    
-    @Nonnull private static final String STATUS = "OK";
-    
-    @Nonnull private static final String ISS = "https://api.duosecurity.com/oauth/v1/token";
     
-    @Nonnull private static final String SUB = "jdoe"; 
-    
-    @Nonnull private static final String AUD = "INT_KEY";
-    
-    @Nonnull private static final Integer EXP = 1590070939;
-    
-    @Nonnull private static final Double IAT = 1590070939d;
-    
-    @Nonnull private static final String REDIRECT_URI = "http://localhost:8443/callback";
-    
-    //TODO: finish values for the token.
-
-    public MockDuoOIDCClient_OK() {
-    }
-
-    @Override
-    public void initialize(DuoOIDCIntegration integration) throws DuoClientException {
-       //no-op method, nothing to initalise
+    @Test public void testDuoAuthnFlowFromAuthorizationCallback() {
         
-    }
+        setFlowPath(flowToTest);
+        setFlowModelResources(flowResources);
+        setSubflows(subflows);
+        //addHttpBasicAuthHeader(USERNAME,PASSWORD);
+        
+       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.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
+                "idp.duo.oidc.clientType","net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
+        
+        setMockProperties(mockProperties);
+        
+        LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
+        inputMap.put("calledAsSubflow", true);
 
-    @Override
-    public DuoHealthCheck healthCheck() throws DuoClientException {
-        return DuoHealthCheck.builder().withStatus(STATUS).
-                withTimestamp(Long.toString(System.currentTimeMillis())).build();
-    }
+        FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
+        FlowExecutionKey key = new MockFlowExecutionKey("1");
+        Mockito.when(flowExecution.getKey()).thenReturn(key);
+        flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
+        updateFlowExecution(flowExecution);
+      //  ((FlowExecutionImpl)flowExecution).getActiveSession();
+        setCurrentState("Duo2FAAuthorizationRequest");
+        resumeFlow(externalContext);
 
-    @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
-        return format("?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
-                REDIRECT_URI, AUD, "JWT");
-    }
 
-    @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
-        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).build();
     }
 
+   
 }
 
-/**
- * Mock a Duo client which is not available (unhealthy).
- */
-class MockDuoOIDCClient_FAIL implements DuoOIDCClient{    
-    
-    @Nonnull private static final String STATUS = "FAIL";
-    
-    @Nonnull private static final String ISS = "https://api.duosecurity.com/oauth/v1/token";
-    
-    @Nonnull private static final String SUB = "jdoe"; 
-    
-    @Nonnull private static final String AUD = "INT_KEY";
-    
-    @Nonnull private static final Integer EXP = 1590070939;
-    
-    @Nonnull private static final Double IAT = 1590070939d;
-    
-    @Nonnull private static final String REDIRECT_URI = "http://localhost:8443/callback";
-    
-    //TODO: finish values for the token.
-
-    public MockDuoOIDCClient_FAIL() {
-    }
 
-    @Override
-    public void initialize(DuoOIDCIntegration integration) throws DuoClientException {
-       //no-op method, nothing to initalise
-        
-    }
 
-    @Override
-    public DuoHealthCheck healthCheck() throws DuoClientException {
-        return DuoHealthCheck.builder().withStatus(STATUS).
-                withTimestamp(Long.toString(System.currentTimeMillis())).build();
-    }
-
-    @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
-        return format("?scope=openid&response_type=code&redirect_uri=%s&client_id=%s&request=%s",
-                REDIRECT_URI, AUD, "JWT");
-    }
 
-    @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
-        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).build();
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnControllerTest.java
index e380ea8..192199b 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
@@ -14,85 +14,298 @@
  * 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 static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
-import java.util.Properties;
-
 import javax.annotation.Nonnull;
+import javax.servlet.ServletContext;
 
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
-import org.springframework.mock.web.MockServletContext;
+import org.springframework.mock.web.MockHttpSession;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.TestPropertySource;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.web.WebAppConfiguration;
 import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.context.WebApplicationContext;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.testng.annotations.BeforeClass;
+import org.springframework.web.context.support.ServletContextAttributeExporter;
+import org.springframework.web.util.NestedServletException;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.core.collection.MutableAttributeMap;
+import org.springframework.webflow.execution.FlowExecution;
+import org.springframework.webflow.execution.repository.FlowExecutionRepository;
+import org.springframework.webflow.execution.repository.support.CompositeFlowExecutionKey;
+import org.springframework.webflow.executor.FlowExecutorImpl;
 
 import junit.framework.Assert;
-import net.shibboleth.idp.plugin.authn.util.mock.ShibbolethPropertyConfigurer;
+import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
+import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
+import net.shibboleth.idp.authn.ExternalAuthentication;
+import net.shibboleth.idp.authn.ExternalAuthenticationException;
+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.util.mock.IdPPropertyConfigurer;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
 
 /**
- *
+ * Tests for the {@link DuoOIDCAuthnController}.
  */
 @ExtendWith(SpringExtension.class)
- at ContextConfiguration(classes = {DuoOIDCAuthnController.class,ShibbolethPropertyConfigurer.class})
+ at 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",})
 public class DuoOIDCAuthnControllerTest {
-    
+
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(DuoOIDCAuthnControllerTest.class);
-    
-    /** The mock MVC entry point for testing. */ 
+
+    /** The mock MVC entry point for testing. */
     @Nonnull private MockMvc mockMvc;
-    
-    /** The web application context loaded by the test framework.*/
-    @Autowired private WebApplicationContext webApplicationContext;
 
+    /** The mock servlet context.*/
+    @Nonnull @Autowired private ServletContext servletContext;
+
+    /** The web application context loaded by the test framework. */
+    @Nonnull @Autowired private WebApplicationContext webApplicationContext;
+    
+    /** The mock http session.*/
+    @Nonnull private MockHttpSession session;
 
     @BeforeEach
-    public void setUp() throws Exception {           
-        DuoOIDCAuthnController controller = webApplicationContext.getBean(DuoOIDCAuthnController.class);
-        //check controller is instantiated.
+    public void setUp() throws Exception {
+        
+        // check controller is instantiated.
+        DuoOIDCAuthnController controller = webApplicationContext.getBean(DuoOIDCAuthnController.class);        
         Assert.assertNotNull(controller);
-        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();       
+
+        // add a Duo client registry
+        DefaultDuoOIDCClientRegistry registry = new DefaultDuoOIDCClientRegistry();
+        registry.setClientType("net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK");
+        registry.setId("TestDefaultClientRegistry");
+        registry.initialize();
+        controller.setClientRegistry(registry);
+
+        session = new MockHttpSession();
+        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
+        
+        //add and mock attributes of the servlet context.
+        exportServletContextAttributes();
+    }
+
+    /**
+     * Start a Duo 2FA request. Make sure the webflow execution key is in the HttpSession.
+     * 
+     * @throws Exception on exception.
+     */
+    @Test
+    public void testSuccessfulAuthorizeRequest() throws Exception {
+       
+        mockMvc.perform(get("/Authn/Duo/2FA/authorize").session(session).param("conversation", "e1s1")).andDo(print())
+                .andExpect(status().is3xxRedirection());
+        //assert the key is in the session
+        Assertions.assertNotNull(session.getAttribute(DuoOIDCAuthnController.KEY_SESSION_ATTRIBUTE));
+
+    }
+
+    /**
+     * Ensure the 2FA end controller adds the correct Duo response information into the 
+     * Duo authentication context.
+     * 
+     * @throws Exception on exception.
+     */
+    @Test
+    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());
+        
+        //check the duo context is populated correctly.
+       
+        DuoAuthenticationContext duoContext = extractDuoContext();
+        Assertions.assertEquals("db87f45083db5aea16eb45b7ace685c8",duoContext.getAuthorizationCode());
+        Assertions.assertEquals("8821febf7b3237fc3974f76a75f37e8c", duoContext.getResponseState());
+
     }
     
+    /**
+     * Remove the Duo authentication context from the authentication context and check the controller
+     * encodes an error as a http request attribute when ending a 2FA request.
+     * 
+     * @throws Exception on exception.
+     */
     @Test
-    public void webAppContextTest() throws Exception {
-        assertTrue(webApplicationContext.getServletContext() instanceof MockServletContext);
-    }   
+    public void testCallbackNoDuoAuthenticationContext() throws Exception {
+
+        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);
+        Assertions.assertNotNull(extAuthErrorObject);
+        Assertions.assertTrue(extAuthErrorObject instanceof String);
+        Assertions.assertEquals(EventIds.INVALID_PROFILE_CTX, (String)extAuthErrorObject);
+        
+        
+       
+    }
 
+    /**
+     * Test an exception is thrown if the callback does not contain a auth_code parameter.
+     * 
+     * @throws Exception on exception.
+     */
     @Test
-    public void test() throws Exception {
-        mockMvc.perform(get("/Authn/Duo/2FA/start").param("conversation", "e1s1")).andExpect(status().is3xxRedirection());
+    public void testCallbackNoCode() throws Exception {
+        Exception exception = Assertions.assertThrows(NestedServletException.class, () -> {
+            mockMvc.perform(get("/Authn/Duo/2FA/authorize-callback").param("state",
+                    "8821febf7b3237fc3974f76a75f37e8c")).andDo(print());
+        });
+        Assertions.assertTrue(exception.getCause() instanceof ExternalAuthenticationException); 
+    }
+    
+    /**
+     * Test an exception is thrown if the callback does not contain a state parameter.
+     * 
+     * @throws Exception on exception.
+     */
+    @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());
+        });
+        Assertions.assertTrue(exception.getCause() instanceof ExternalAuthenticationException); 
+    }
+
+    /**
+     * Export the FlowExecutor to the servlet context with the correct set of configured contexts. Mimicking the
+     * IdP's configuration of the {@link ServletContextAttributeExporter}.
+     */
+    private void exportServletContextAttributes() {
+
+        FlowExecutorImpl mockFlowExecutor = Mockito.mock(FlowExecutorImpl.class);
+        FlowExecutionRepository mockFlowExecutionRepo = Mockito.mock(FlowExecutionRepository.class);
+        FlowExecution mockFlowExecution = Mockito.mock(FlowExecution.class);
+
+        MutableAttributeMap<Object> map = new LocalAttributeMap<Object>();
+        map.put(ProfileRequestContext.BINDING_KEY, buildProfileRequestContext());
+
+        Mockito.when(mockFlowExecutor.getExecutionRepository()).thenReturn(mockFlowExecutionRepo);
+        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);
+
+        // overwrites previous if set from previous method executions.
+        servletContext.setAttribute(ExternalAuthentication.SWF_KEY, mockFlowExecutor);
+    }
+
+    /**
+     * Build a {@link ProfileRequestContext} by configuring a suitable context tree for external authentication e.g. a
+     * {@link AuthenticationContext} and {@link ExternalAuthenticationContext}.
+     * 
+     * @return a profile request context.
+     */
+    @Nonnull private ProfileRequestContext buildProfileRequestContext() {
+
+        ProfileRequestContext prc = new ProfileRequestContext();
+        AuthenticationContext ac = new AuthenticationContext();
+        ExternalAuthenticationContext ec = new ExternalAuthenticationContext(new ExternalAuthenticationImpl());
+        DuoAuthenticationContext dc = new DuoAuthenticationContext();
+
+        // will redirect here once finished or in error.
+        ec.setFlowExecutionUrl("http://localhost/idp/profile/SSO&_eventId_proceed=1");
+
+        DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
+        integ.setAPIHost("host.com");
+        integ.setApplicationKey("SDFGHJKLPOIUYTREWQZXCVBNMLKJHGFAQWERTYU");
+        integ.setIntegrationKey("DIU6GEFWG5LIUBVV2M3P");
+        integ.setRedirectURI("http://localhost/");
+        integ.setSecretKey("rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh");
+        dc.setUsername("jdoe");
+        dc.setIntegration(integ);
+
+        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();
+        scc.setPrincipalName("jdoe");
+        SessionContext sc = new SessionContext();
+        IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getPrincipalName()).thenReturn("jdoe");
+        sc.setIdPSession(session);
+
+        ac.addSubcontext(dc);
+        ac.addSubcontext(ec);
+        prc.addSubcontext(scc);
+        prc.addSubcontext(ac);
+        prc.addSubcontext(sc);
+
+        return prc;
+    }
+    
+    /**
+     * Remove the Duo authentication context from the profile request context.
+     */
+    private void removeDuoContext() {
+        final Object flowExecutorObject = servletContext.getAttribute(ExternalAuthentication.SWF_KEY);
+        Assertions.assertTrue(flowExecutorObject instanceof FlowExecutorImpl);
+        
+        Object prcObject = ((FlowExecutorImpl)flowExecutorObject).getExecutionRepository().
+                getFlowExecution(new CompositeFlowExecutionKey("1", "1")).
+                getConversationScope().get(ProfileRequestContext.BINDING_KEY);
+        
+        ((ProfileRequestContext)prcObject).getSubcontext(AuthenticationContext.class).
+        removeSubcontext(DuoAuthenticationContext.class);
+    
+    }
+    
+    
+    /**
+     * Extract the {@link DuoAuthenticationContext} 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() {
+        final Object flowExecutorObject = servletContext.getAttribute(ExternalAuthentication.SWF_KEY);
+        Assertions.assertTrue(flowExecutorObject instanceof FlowExecutorImpl);
+        
+        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);
+        Assertions.assertNotNull(duoContext);
         
+        return duoContext;
     }
 
 }
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
index b2df852..79a424a 100644
--- 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
@@ -14,7 +14,7 @@ import org.springframework.webflow.core.collection.LocalAttributeMap;
 import org.springframework.webflow.engine.Flow;
 import org.springframework.webflow.execution.FlowExecution;
 
-import net.shibboleth.idp.plugin.authn.util.mock.MockFlowBuilder;
+import net.shibboleth.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;
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
new file mode 100644
index 0000000..af70474
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
@@ -0,0 +1,62 @@
+package net.shibboleth.idp.plugin.authn.mock;
+
+import static java.lang.String.format;
+
+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.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+
+/**
+ * Mock a Duo client which is not available (unhealthy).
+ */
+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;
+        
+    }
+
+    @Override
+    public DuoHealthCheck healthCheck() throws DuoClientException {
+        return DuoHealthCheck.builder().withStatus(STATUS).
+                withTimestamp(Long.toString(System.currentTimeMillis())).build();
+    }
+
+    @Override
+    public String createAuthUrl(String username, String state) throws DuoClientException {
+        return format("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");
+    }
+
+    @Override
+    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
+                withExp(EXP).withIat(IAT).build();
+    }
+
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..68ee894
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
@@ -0,0 +1,62 @@
+package net.shibboleth.idp.plugin.authn.mock;
+
+import static java.lang.String.format;
+
+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.DuoAuthToken;
+import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+
+/**
+ * Mock a Duo client which is available and returns a valid response.
+ */
+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;
+    
+    @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;
+        
+    }
+
+    @Override
+    public DuoHealthCheck healthCheck() throws DuoClientException {
+        return DuoHealthCheck.builder().withStatus(STATUS).
+                withTimestamp(Long.toString(System.currentTimeMillis())).build();
+    }
+
+    @Override
+    public String createAuthUrl(String username, String state) throws DuoClientException {
+        return format("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");
+    }
+
+    @Override
+    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+        return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
+                withExp(EXP).withIat(IAT).build();
+    }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
similarity index 97%
rename from idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java
rename to idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
index ac1c5e9..870b8b5 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/MockFlowBuilder.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockFlowBuilder.java
@@ -1,4 +1,4 @@
-package net.shibboleth.idp.plugin.authn.util.mock;
+package net.shibboleth.idp.plugin.authn.mock;
 
 import javax.annotation.Nonnull;
 
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/ShibbolethPropertyConfigurer.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
similarity index 93%
rename from idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/ShibbolethPropertyConfigurer.java
rename to idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
index 85cc7a8..6131b2e 100644
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/ShibbolethPropertyConfigurer.java
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/util/mock/IdPPropertyConfigurer.java
@@ -9,7 +9,7 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
  * Configure the property sources placeholder to use the correct placeholder prefix of {@literal %}
  */
 @Configuration
-public class ShibbolethPropertyConfigurer {
+public class IdPPropertyConfigurer {
     
     @Bean
     public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
diff --git a/idp-duo-native-client-impl/pom.xml b/idp-duo-native-client-impl/pom.xml
index 3da5cca..f22b8e1 100644
--- a/idp-duo-native-client-impl/pom.xml
+++ b/idp-duo-native-client-impl/pom.xml
@@ -9,15 +9,21 @@
     <artifactId>idp-duo-native-client-impl</artifactId>
     <description>Native Duo SDK implementation for the Duo OIDC 2FA service</description>
 
+    <properties>
+        <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.sdk.impl</automatic.module.name>
+    </properties>
+
 
     <dependencies>
         <dependency>
             <groupId>com.duosecurity</groupId>
             <artifactId>duo-client</artifactId>
+            <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>net.shibboleth.plugin.authn</groupId>
             <artifactId>idp-duo-api</artifactId>
+            <scope>provided</scope>
         </dependency>
     </dependencies>
 
diff --git a/pom.xml b/pom.xml
index 15b7af9..75c2aeb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,9 +13,12 @@
         <shib.idp.version>4.0.0</shib.idp.version>
         <opensaml.version>4.0.0</opensaml.version>
         <duo.client.version>1.0-SNAPSHOT</duo.client.version>
+        <junit.jupitar.version>5.6.2</junit.jupitar.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>
@@ -72,97 +75,21 @@
                 <version>${project.version}</version>
             </dependency>
 
-            <!-- Test Dependencies -->
+            <!-- Test Dependencies different than those in the IdP -->
 
-            <!-- Do we need these here? -->
             <dependency>
-                <groupId>commons-io</groupId>
-                <artifactId>commons-io</artifactId>
-                <version>2.6</version>
+                <groupId>org.junit.jupiter</groupId>
+                <artifactId>junit-jupiter-api</artifactId>
+                <version>${junit.jupitar.version}</version>
                 <scope>test</scope>
             </dependency>
             <dependency>
-                <groupId>org.opensaml</groupId>
-                <artifactId>opensaml-core</artifactId>
-                <version>${opensaml.version}</version>
-                <type>test-jar</type>
+                <groupId>org.junit.jupiter</groupId>
+                <artifactId>junit-jupiter-engine</artifactId>
+                <version>${junit.jupitar.version}</version>
                 <scope>test</scope>
             </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-attribute-filter-spring</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-profile-api</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-attribute-resolver-spring</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-authn-impl</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-authn-impl</artifactId>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-authn-api</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-conf</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>net.shibboleth.idp</groupId>
-                <version>${shib.idp.version}</version>
-                <artifactId>idp-conf</artifactId>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>org.opensaml</groupId>
-                <version>${opensaml.version}</version>
-                <artifactId>opensaml-profile-impl</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>org.opensaml</groupId>
-                <version>${opensaml.version}</version>
-                <artifactId>opensaml-profile-api</artifactId>
-                <type>test-jar</type>
-                <scope>test</scope>
-            </dependency>
-            <dependency>
-                <groupId>org.opensaml</groupId>
-                <version>${opensaml.version}</version>
-                <artifactId>opensaml-storage-impl</artifactId>
-                <scope>test</scope>
-            </dependency>
-
+           
 
         </dependencies>
     </dependencyManagement>

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


More information about the commits mailing list