[java-idp-plugin-duo] 06/15: Add Population of duo context early in flow

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

commit a2e682beed4d2dc1bdf2eb88a239255cf8ecf507
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Jun 16 10:35:09 2020 +0100

    Add Population of duo context early in flow
---
 idp-duo-api/pom.xml                                |  11 +-
 .../authn/duo/AbstractDuoAuthenticationAction.java |  67 ++++-
 .../idp/plugin/authn/duo/DuoOIDCClient.java        |   9 +-
 .../idp/plugin/authn/duo/DuoOIDCPrincipal.java     |  94 +++++++
 .../duo/context/DuoAuthenticationContext.java      |  26 ++
 .../plugin/authn/duo/model/DuoAccessDevice.java    | 160 +++++++++++
 .../idp/plugin/authn/duo/model/DuoApplication.java |  98 +++++++
 .../idp/plugin/authn/duo/model/DuoAuthContext.java | 310 +++++++++++++++++++++
 .../idp/plugin/authn/duo/model/DuoAuthDevice.java  | 162 +++++++++++
 .../idp/plugin/authn/duo/model/DuoAuthToken.java   | 168 ++++++++---
 .../idp/plugin/authn/duo/model/DuoHealthCheck.java |  20 +-
 .../idp/plugin/authn/duo/DuoOIDCPrincipalTest.java |  27 ++
 idp-duo-impl/.checkstyle                           |   7 +
 idp-duo-impl/pom.xml                               |  83 +++---
 .../idp/plugin/authn/duo/impl/CheckDuoState.java   |  14 +-
 .../authn/duo/impl/DuoOIDCAuthnController.java     |  32 +--
 .../idp/plugin/authn/duo/impl/DuoSupport.java      |  38 +++
 .../authn/duo/impl/ExchangeCodeForDuoToken.java    |  55 ++++
 .../authn/duo/impl/HealthCheckDuoOIDCAuthAPI.java  |   9 +-
 ....java => PopulateDuoAuthenticationContext.java} |  52 +++-
 .../plugin/authn/duo/impl/ValidateDuo2FAToken.java | 144 ++++++++++
 .../flows/authn/duo/duo-oidc-authn-beans.xml       |  21 +-
 .../flows/authn/duo/duo-oidc-authn-flow.xml        |  10 +-
 .../impl/AbstractAuthnXmlFlowExecutionTests.java   |  36 +--
 .../duo/impl/AbstractDuoSAML2SSOFlowTest.java      |  42 ---
 .../duo/impl/DefaultDuoOIDCClientRegistryTest.java |  10 +-
 .../plugin/authn/duo/impl/DuoAuthnFlowTest.java    |  66 +++--
 .../authn/duo/impl/DuoOIDCAuthnControllerTest.java |   3 +-
 .../plugin/authn/duo/impl/DuoSAML2FlowTest.java    | 128 ---------
 .../authn/duo/impl/PasswordAuthnFlowTest.java      |   9 +-
 ...rtySourcesPlaceholderConfigurerInitializer.java |  41 ---
 .../plugin/authn/mock/MockDuoOIDCClient_FAIL.java  |  21 +-
 .../plugin/authn/mock/MockDuoOIDCClient_OK.java    |  17 +-
 pom.xml                                            | 201 +++++++++++--
 34 files changed, 1747 insertions(+), 444 deletions(-)

diff --git a/idp-duo-api/pom.xml b/idp-duo-api/pom.xml
index 9a6c0f4..8138e05 100644
--- a/idp-duo-api/pom.xml
+++ b/idp-duo-api/pom.xml
@@ -13,18 +13,9 @@
     </properties>
 
 
+    <!-- TODO: API probs does not need to know about spring webflow -->
     <dependencies>
-        <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-authn-api</artifactId>
-        </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 e56ffaf..0de0c2f 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
@@ -5,17 +5,20 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import 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.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;
@@ -26,7 +29,8 @@ 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 a lookup from the in-flight {@link DuoIntegration}.
+ * and makes available the {@link DuoOIDCClient} from a lookup from the in-flight {@link DuoIntegration}
+ * attached to the {@link DuoAuthenticationContext}.
  * 
  * Duo authentication action implementations should override
  * {@link #doExecute(ProfileRequestContext, AuthenticationContext, DuoOIDCClient)} method.
@@ -42,16 +46,24 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
     /** 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;
     
     /** The Duo OIDC client .*/
     @Nonnull private DuoOIDCClient duoClient;
+    
+    /** The Duo authentication Context.*/
+    @Nullable private DuoAuthenticationContext duoContext;
         
     
     /** Constructor.*/
     public AbstractDuoAuthenticationAction() {
         duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+        duoContextLookupStrategy = new ChildContextLookup<>(DuoAuthenticationContext.class).
+                compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
     /**
@@ -60,7 +72,7 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      * @param duoRegistry the registry
      */
     public void setClientRegistry(@Nonnull final DuoOIDCClientRegistry duoRegistry) {        
-        clientRegistry = Constraint.isNotNull(duoRegistry,"DuoCient registry can not be null");
+        clientRegistry = Constraint.isNotNull(duoRegistry,"DuoClient registry can not be null");
     }
     
     /**
@@ -76,6 +88,18 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
         duoIntegrationLookupStrategy = FunctionSupport.constant(duo);
     }
     
+    /**
+     * Set Duo authentication context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDuoContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookuplookup strategy cannot be null");
+    }
+    
     /**
      * Set DuoIntegration lookup strategy to use.
      * 
@@ -105,24 +129,33 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
         if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
             return false;
         }
-        
-        DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+        //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, EventIds.INVALID_PROFILE_CTX);
+            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 (DuoRegistryException e) {
+        } catch (final DuoRegistryException e) {
             log.warn("{} No DuoClient established (located or created) for "
                     + "this integration", getLogPrefix(),e);
-            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
+            ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
             return false;
         }
         
-        return doPreExecute(profileRequestContext, authenticationContext, duoClient);
+        
+        return doPreExecute(profileRequestContext, authenticationContext, duoClient, duoContext);
     }
     
     /**
@@ -135,7 +168,7 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
     @Override
     protected final void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
-        doExecute(profileRequestContext,authenticationContext,duoClient);
+        doExecute(profileRequestContext,authenticationContext,duoClient,duoContext);
     }
     
     /**
@@ -143,16 +176,18 @@ public class AbstractDuoAuthenticationAction extends AbstractAuthenticationActio
      * 
      * @param profileRequestContext the current IdP profile request context
      * @param authenticationContext the current authentication context
-     * @param DuoOIDCClient the Duo OIDC client appropriate for the located Duo integration.
+     * @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 AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
+            @Nonnull final DuoAuthenticationContext context) {
         return true;
     }
     
-    
+    //TODO: failry overloaded here?
     /**
      * Performs this Duo authentication action using the supplied Duo client. Implementations
      * should override this method.
@@ -160,9 +195,11 @@ 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
      */
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client) {
+            @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
+            @Nonnull final DuoAuthenticationContext context) {
         
     }
 
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 bc4d738..af0f7ac 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,5 +1,7 @@
 package net.shbboleth.idp.plugin.authn.duo;
 
+import javax.annotation.Nonnull;
+
 import net.shbboleth.idp.plugin.authn.duo.model.DuoAuthToken;
 import net.shbboleth.idp.plugin.authn.duo.model.DuoHealthCheck;
 
@@ -7,6 +9,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 {
     
     /**
@@ -16,7 +19,7 @@ public interface DuoOIDCClient extends InitializingDuoClient {
      * 
      * @throws DuoClientException if there is an error returning the health check response
      */
-    DuoHealthCheck healthCheck() throws DuoClientException;    
+    @Nonnull DuoHealthCheck healthCheck() throws DuoClientException;    
     
     /**
      * Constructs a redirection URL string with query parameters required to initiate a Duo 2FA request.
@@ -28,7 +31,7 @@ public interface DuoOIDCClient extends InitializingDuoClient {
      * 
      * @throws DuoClientException
      */
-    String createAuthUrl(String username, String state) throws DuoClientException;
+    @Nonnull String createAuthUrl(String username, String state) throws DuoClientException;
     
     /**
      * Verifies the code returned by Duo and exchanges it for a token which contains information pertaining to
@@ -40,6 +43,6 @@ public interface DuoOIDCClient extends InitializingDuoClient {
      *
      * @return the token
      */
-    DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException;
+    @Nonnull DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException;
 
 }
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/DuoOIDCPrincipal.java
new file mode 100644
index 0000000..3c3af2b
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipal.java
@@ -0,0 +1,94 @@
+/*
+ * 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 javax.annotation.concurrent.Immutable;
+
+import net.shibboleth.idp.authn.principal.CloneablePrincipal;
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+
+/** Principal based on a Duo OIDC based authentication. */
+//TODO: test the immutability and cloning.
+ at Immutable
+public final class DuoOIDCPrincipal implements CloneablePrincipal {
+
+    /** The username. */
+    @Nonnull @NotEmpty private final String username;
+
+    /**
+     * Constructor.
+     * 
+     * @param name the username
+     */
+    public DuoOIDCPrincipal(@Nonnull @NotEmpty @ParameterName(name="name") final String name) {
+        username = Constraint.isNotNull(StringSupport.trimOrNull(name), "Username cannot be null or empty");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull @NotEmpty public String getName() {
+        return username;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return username.hashCode();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(final Object other) {
+        if (other == null) {
+            return false;
+        }
+
+        if (this == other) {
+            return true;
+        }
+
+        if (other instanceof DuoOIDCPrincipal) {
+            return username.equals(((DuoOIDCPrincipal) other).getName());
+        }
+
+        return false;
+    }
+
+    
+    
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("DuoOIDCPrincipal [username=");
+        builder.append(username);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public DuoOIDCPrincipal clone() throws CloneNotSupportedException {
+        return new DuoOIDCPrincipal(username);
+    }
+}
\ No newline at end of file
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 414bc96..c0f8a33 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
@@ -26,6 +26,7 @@ import javax.annotation.Nullable;
 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;
@@ -71,6 +72,9 @@ public final class DuoAuthenticationContext extends BaseContext {
     
     /** The authorization code return from the Duo authorization request.*/
     @Nullable private String authCode;
+    
+    /** The token received from Duo as a result of 2FA.*/
+    @Nullable private DuoAuthToken authToken;
 
     /** Constructor. */
     public DuoAuthenticationContext() {
@@ -93,6 +97,27 @@ public final class DuoAuthenticationContext extends BaseContext {
         return this;
     }
     
+    /**
+     * Set the Duo authentication token.
+     * 
+     * @param token the duo authentication token
+     * 
+     * @return this context
+     */
+    @Nonnull public DuoAuthenticationContext setAuthToken(@Nullable final DuoAuthToken token) {
+        authToken = token;
+        return this;
+    }
+    
+    /**
+     * Get the Duo authentication token.
+     * 
+     * @return the token
+     */
+    @Nullable public DuoAuthToken getAuthToken() {
+        return authToken;
+    }
+    
     /**
      * Get the request state.
      * 
@@ -102,6 +127,7 @@ public final class DuoAuthenticationContext extends BaseContext {
         return requestState;
     }
     
+    
     /**
      * Set the request state.
      * 
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
new file mode 100644
index 0000000..108c5cf
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAccessDevice.java
@@ -0,0 +1,160 @@
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+ at 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 state where the device is located.*/
+    @Nullable private final String state;
+
+    /** The city where the device is located.*/
+    @Nullable private final String city;
+
+    /** The country where the device is located.*/
+    @Nullable private final String country;
+    
+
+    /**
+     * @return Returns the ip.
+     */
+    public final String getIp() {
+        return ip;
+    }
+
+    /**
+     * @return Returns the name.
+     */
+    public final String getName() {
+        return name;
+    }
+
+    /**
+     * @return Returns the state.
+     */
+    public final String getState() {
+        return state;
+    }
+
+    /**
+     * @return Returns the city.
+     */
+    public final String getCity() {
+        return city;
+    }
+
+    /**
+     * @return Returns the country.
+     */
+    public final String getCountry() {
+        return country;
+    }
+
+    /**
+     * Private constructor, can only be called by this builder.
+     *
+     * @param builder the builder to build the instance with
+     */
+    private DuoAccessDevice(Builder builder) {
+        this.ip = builder.ip;
+        this.name = builder.name;
+        this.state = builder.state;
+        this.city = builder.city;
+        this.country = builder.country;
+    }
+
+    /**
+     * Creates builder to build {@link DuoAccessDevice}.
+     * @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);
+
+        public IBuildStage withCity(String city);
+
+        public IBuildStage withCountry(String country);
+
+        public DuoAccessDevice build();
+    }
+
+    /**
+     * Builder to build {@link DuoAccessDevice}.
+     */
+    
+    public static final class Builder implements IIpStage, INameStage, IBuildStage {
+        private String ip;
+
+        private String name;
+
+        private String state;
+
+        private String city;
+
+        private String country;
+
+        private Builder() {
+        }
+
+        @Override
+        public INameStage withIp(String ip) {
+            this.ip = ip;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withState(String state) {
+            this.state = state;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withCity(String city) {
+            this.city = city;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withCountry(String country) {
+            this.country = country;
+            return this;
+        }
+
+        @Override
+        public DuoAccessDevice build() {
+            return new DuoAccessDevice(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
new file mode 100644
index 0000000..66487ca
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoApplication.java
@@ -0,0 +1,98 @@
+
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.Immutable;
+
+
+ at Immutable
+public final class DuoApplication {
+
+    /** Application key.*/
+    @Nonnull private final String key;
+
+    /** Application name.*/
+    @Nonnull private final String name;
+    
+    
+    /**
+     * @return Returns the key.
+     */
+    public final String getKey() {
+        return key;
+    }
+
+    /**
+     * @return Returns the name.
+     */
+    public final String getName() {
+        return name;
+    }
+
+    /**
+     * Private constructor, can only be called by this builder.
+     *
+     * @param builder the builder to build the instance with
+     */
+    private DuoApplication(Builder builder) {
+        this.key = builder.key;
+        this.name = builder.name;
+    }
+
+    /**
+     * 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();
+    }
+
+    /**
+     * Builder to build {@link DuoApplication}.
+     */
+    
+    public static final class Builder implements IKeyStage, INameStage, IBuildStage {
+        private String key;
+
+        private String name;
+
+        private Builder() {
+        }
+
+        @Override
+        public INameStage withKey(String key) {
+            this.key = key;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public DuoApplication build() {
+            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
new file mode 100644
index 0000000..bfbedaa
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthContext.java
@@ -0,0 +1,310 @@
+
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+
+/** Duo authentication context.*/
+//FIXME: need some spec, what are these fields.
+public final class DuoAuthContext {
+
+    /** Authentication result e.g. {@literal 'success'}.*/
+    @Nonnull private String result;
+
+    /** Authentication timestamp.*/
+    @Nonnull private Integer timestamp;
+
+    /** Device used during 2FA.*/
+    @Nonnull private DuoAuthDevice authDevice;
+
+    /** Authentication id.*/
+    @Nonnull private String txid;
+
+    /** Type of event.*/
+    @Nonnull private String eventType;
+
+    /** Reason for context e.g. 'user_approved'.*/
+    @Nonnull private String reason;
+
+    //TODO: difference between this and authdevice?
+    @Nonnull private DuoAccessDevice accessDevice;
+
+    /** Duo 2FA application integration information.*/
+    @Nonnull private DuoApplication application;
+
+    /** The factor used in the 2FA request.*/
+    @Nonnull private String factor;
+
+    /** Username of the user for which 2FA was performed.*/
+    @Nonnull private String username;
+    
+    /** Key of the user for which 2FA was preformed.*/
+    @Nonnull private String userKey;
+
+    
+    /**
+     * @return Returns the result.
+     */
+    public final String getResult() {
+        return result;
+    }
+
+    /**
+     * @return Returns the timestamp.
+     */
+    public final Integer getTimestamp() {
+        return timestamp;
+    }
+
+    /**
+     * @return Returns the authDevice.
+     */
+    public final DuoAuthDevice getAuthDevice() {
+        return authDevice;
+    }
+
+    /**
+     * @return Returns the txid.
+     */
+    public final String getTxid() {
+        return txid;
+    }
+
+    /**
+     * @return Returns the eventType.
+     */
+    public final String getEventType() {
+        return eventType;
+    }
+
+    /**
+     * @return Returns the reason.
+     */
+    public final String getReason() {
+        return reason;
+    }
+
+    /**
+     * @return Returns the accessDevice.
+     */
+    public final DuoAccessDevice getAccessDevice() {
+        return accessDevice;
+    }
+
+    /**
+     * @return Returns the application.
+     */
+    public final DuoApplication getApplication() {
+        return application;
+    }
+
+    /**
+     * @return Returns the factor.
+     */
+    public final String getFactor() {
+        return factor;
+    }
+
+    /**
+     * @return Returns the username.
+     */
+    public final String getUsername() {
+        return username;
+    }
+
+    /**
+     * @return Returns the userKey.
+     */
+    public final String getUserKey() {
+        return userKey;
+    }
+
+    private DuoAuthContext(Builder builder) {
+        this.result = builder.result;
+        this.timestamp = builder.timestamp;
+        this.authDevice = builder.authDevice;
+        this.txid = builder.txid;
+        this.eventType = builder.eventType;
+        this.reason = builder.reason;
+        this.accessDevice = builder.accessDevice;
+        this.application = builder.application;
+        this.factor = builder.factor;
+        this.username = builder.username;
+        this.userKey = builder.userKey;
+    }
+
+    /**
+     * 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();
+    }
+
+    /**
+     * Builder to build {@link DuoAuthContext}.
+     */
+    
+    public static final class Builder
+            implements IResultStage, ITimestampStage, IAuthDeviceStage, ITxidStage, IEventTypeStage, IReasonStage,
+            IAccessDeviceStage, IApplicationStage, IFactorStage, IUsernameStage, IUserKeyStage, IBuildStage {
+        private String result;
+
+        private Integer timestamp;
+
+        private DuoAuthDevice authDevice;
+
+        private String txid;
+
+        private String eventType;
+
+        private String reason;
+
+        private DuoAccessDevice accessDevice;
+
+        private DuoApplication application;
+
+        private String factor;
+
+        private String username;
+
+        private String userKey;
+
+        private Builder() {
+        }
+
+        @Override
+        public ITimestampStage withResult(String result) {
+            this.result = result;
+            return this;
+        }
+
+        @Override
+        public IAuthDeviceStage withTimestamp(Integer timestamp) {
+            this.timestamp = timestamp;
+            return this;
+        }
+
+        @Override
+        public ITxidStage withAuthDevice(DuoAuthDevice authDevice) {
+            this.authDevice = authDevice;
+            return this;
+        }
+
+        @Override
+        public IEventTypeStage withTxid(String txid) {
+            this.txid = txid;
+            return this;
+        }
+
+        @Override
+        public IReasonStage withEventType(String eventType) {
+            this.eventType = eventType;
+            return this;
+        }
+
+        @Override
+        public IAccessDeviceStage withReason(String reason) {
+            this.reason = reason;
+            return this;
+        }
+
+        @Override
+        public IApplicationStage withAccessDevice(DuoAccessDevice accessDevice) {
+            this.accessDevice = accessDevice;
+            return this;
+        }
+
+        @Override
+        public IFactorStage withApplication(DuoApplication application) {
+            this.application = application;
+            return this;
+        }
+
+        @Override
+        public IUsernameStage withFactor(String factor) {
+            this.factor = factor;
+            return this;
+        }
+
+        @Override
+        public IUserKeyStage withUsername(String username) {
+            this.username = username;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withUserKey(String userKey) {
+            this.userKey = userKey;
+            return this;
+        }
+
+        @Override
+        public DuoAuthContext build() {
+            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
new file mode 100644
index 0000000..f176f7f
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoAuthDevice.java
@@ -0,0 +1,162 @@
+
+package net.shbboleth.idp.plugin.authn.duo.model;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/** Device used during 2FA. */
+ at Immutable
+public final class DuoAuthDevice {
+
+    /** 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 state where the device is located.*/
+    @Nullable private final String state;
+
+    /** The city where the device is located.*/
+    @Nullable private final String city;
+
+    /** The country where the device is located.*/
+    @Nullable private final String country;
+
+    
+    /**
+     * @return Returns the ip.
+     */
+    public final String getIp() {
+        return ip;
+    }
+
+    /**
+     * @return Returns the name.
+     */
+    public final String getName() {
+        return name;
+    }
+
+    /**
+     * @return Returns the state.
+     */
+    public final String getState() {
+        return state;
+    }
+
+    /**
+     * @return Returns the city.
+     */
+    public final String getCity() {
+        return city;
+    }
+
+    /**
+     * @return Returns the country.
+     */
+    public final String getCountry() {
+        return country;
+    }
+
+    /**
+     * Private constructor, can only be called by this builder.
+     *
+     * @param builder the builder to build the instance with
+     */
+    private DuoAuthDevice(Builder builder) {
+        this.ip = builder.ip;
+        this.name = builder.name;
+        this.state = builder.state;
+        this.city = builder.city;
+        this.country = builder.country;
+    }
+
+    /**
+     * 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);
+
+        public IBuildStage withCity(String city);
+
+        public IBuildStage withCountry(String country);
+
+        public DuoAuthDevice build();
+    }
+
+    /**
+     * Builder to build {@link DuoAuthDevice}.
+     */
+    
+    public static final class Builder implements IIpStage, INameStage, IBuildStage {
+        private String ip;
+
+        private String name;
+
+        private String state;
+
+        private String city;
+
+        private String country;
+
+        private Builder() {
+        }
+
+        @Override
+        public INameStage withIp(String ip) {
+            this.ip = ip;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withName(String name) {
+            this.name = name;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withState(String state) {
+            this.state = state;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withCity(String city) {
+            this.city = city;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withCountry(String country) {
+            this.country = country;
+            return this;
+        }
+
+        @Override
+        public DuoAuthDevice build() {
+            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 d6909a5..d1a4a8d 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
@@ -2,103 +2,152 @@ package net.shbboleth.idp.plugin.authn.duo.model;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
 /**
  * <p>A Duo authentication token which describes the result of 2FA.</p>
  * 
- * <p>Analogous to an OIDC token. Descriptions are taken from the 
- * OpenID connect core 1.0 spec.</p>
+ * <p>Analogous in part to an OIDC token. Some descriptions are taken from the 
+ * OpenID connect core 1.0 specification.</p>
  * 
  * <p>Includes a staged builder for fluent generation.</p>
  */
-public class DuoAuthToken {
+//TODO: make these immutable
+ at Immutable
+public final class DuoAuthToken {
     
     /** The issuer identifier for the issuer of the response.*/
-    @Nonnull @NotEmpty private String iss;
+    @Nonnull @NotEmpty final private String iss;
     
     /** the subject identifier.*/ 
-    @Nonnull @NotEmpty private String sub;
+    @Nonnull @NotEmpty final private String sub;
     
     /** Shorthand name by which the End-User wishes to be referred to as.*/
-    @Nullable private String preferred_username;
+    @Nullable private final String preferred_username;
     
     /** The audience(s) that this token is indented for.*/
-    @Nonnull @NotEmpty private String aud;
+    @Nonnull @NotEmpty private final String aud;
     
     /** Expiration time on or after which the ID Token MUST NOT be accepted for processing.*/ 
-    @Nonnull @NotEmpty private Integer exp;
+    @Nonnull @NotEmpty private final Integer exp;
     
     /**  Time at which the JWT was issued. In seconds since Unix EPOCH.*/
-    @Nonnull @NotEmpty private Double iat;
+    @Nonnull @NotEmpty private final Double iat;
     
     /** Time when the End-User authentication occurred.  In seconds since Unix EPOCH.*/
-    @Nullable private Integer auth_time;
+    @Nullable private final Integer auth_time;
     
-//  private AuthResult auth_result;
-//  private AuthContext auth_context;
+    /** Status message after 2FA e.g. 'Login Successful'.*/
+    @Nonnull private final String authResultStatusMessage;
+    
+    /** Status of 2FA 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;
     
-    private DuoAuthToken(Builder builder) {
-        this.iss = builder.iss;
-        this.sub = builder.sub;
-        this.aud = builder.aud;
-        this.exp = builder.exp;
-        this.iat = builder.iat;
-        this.preferred_username = builder.preferred_username;
-        this.auth_time = builder.auth_time;
-    }
 
     /**
      * @return Returns the iss.
      */
-    public String getIss() {
+    public final String getIss() {
         return iss;
     }
 
     /**
      * @return Returns the sub.
      */
-    public String getSub() {
+    public final String getSub() {
         return sub;
     }
 
     /**
      * @return Returns the preferred_username.
      */
-    public String getPreferred_username() {
+    public final String getPreferred_username() {
         return preferred_username;
     }
 
     /**
      * @return Returns the aud.
      */
-    public String getAud() {
+    public final String getAud() {
         return aud;
     }
 
     /**
      * @return Returns the exp.
      */
-    public Integer getExp() {
+    public final Integer getExp() {
         return exp;
     }
 
     /**
      * @return Returns the iat.
      */
-    public Double getIat() {
+    public final Double getIat() {
         return iat;
     }
 
     /**
      * @return Returns the auth_time.
      */
-    public Integer getAuth_time() {
+    public final Integer getAuth_time() {
         return auth_time;
     }
 
+    /**
+     * @return Returns the authResultStatusMessage.
+     */
+    public final String getAuthResultStatusMessage() {
+        return authResultStatusMessage;
+    }
+
+    /**
+     * @return Returns the authResultStatus.
+     */
+    public final String getAuthResultStatus() {
+        return authResultStatus;
+    }
+
+    /**
+     * @return Returns the authResult.
+     */
+    public final String getAuthResult() {
+        return authResult;
+    }
+
+    /**
+     * @return Returns the authContext.
+     */
+    public final DuoAuthContext getAuthContext() {
+        return authContext;
+    }
+
+    /**
+     * Private constructor, can only be called by this builder.
+     *
+     * @param builder the builder to build the instance with
+     */
+    private DuoAuthToken(Builder builder) {
+        this.iss = builder.iss;
+        this.sub = builder.sub;
+        this.aud = builder.aud;
+        this.exp = builder.exp;
+        this.iat = builder.iat;
+        this.authResultStatusMessage = builder.authResultStatusMessage;
+        this.authResultStatus = builder.authResultStatus;
+        this.authResult = builder.authResult;
+        this.authContext = builder.authContext;
+        this.preferred_username = builder.preferred_username;
+        this.auth_time = builder.auth_time;
+    }
+
     /**
      * Creates builder to build {@link DuoAuthToken}.
      * @return created builder
@@ -130,7 +179,27 @@ public class DuoAuthToken {
 
     
     public interface IIatStage {
-        public IBuildStage withIat(Double iat);
+        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);
     }
 
     
@@ -146,7 +215,8 @@ public class DuoAuthToken {
      * Builder to build {@link DuoAuthToken}.
      */
     
-    public static final class Builder implements IIssStage, ISubStage, IAudStage, IExpStage, IIatStage, IBuildStage {
+    public static final class Builder implements IIssStage, ISubStage, IAudStage, IExpStage, IIatStage,
+            IAuthResultStatusMessageStage, IAuthResultStatusStage, IAuthResultStage, IAuthContextStage, IBuildStage {
         private String iss;
 
         private String sub;
@@ -157,6 +227,14 @@ public class DuoAuthToken {
 
         private Double iat;
 
+        private String authResultStatusMessage;
+
+        private String authResultStatus;
+
+        private String authResult;
+
+        private DuoAuthContext authContext;
+
         private String preferred_username;
 
         private Integer auth_time;
@@ -189,11 +267,35 @@ public class DuoAuthToken {
         }
 
         @Override
-        public IBuildStage withIat(Double iat) {
+        public IAuthResultStatusMessageStage withIat(Double iat) {
             this.iat = iat;
             return this;
         }
 
+        @Override
+        public IAuthResultStatusStage withAuthResultStatusMessage(String authResultStatusMessage) {
+            this.authResultStatusMessage = authResultStatusMessage;
+            return this;
+        }
+
+        @Override
+        public IAuthResultStage withAuthResultStatus(String authResultStatus) {
+            this.authResultStatus = authResultStatus;
+            return this;
+        }
+
+        @Override
+        public IAuthContextStage withAuthResult(String authResult) {
+            this.authResult = authResult;
+            return this;
+        }
+
+        @Override
+        public IBuildStage withAuthContext(DuoAuthContext authContext) {
+            this.authContext = authContext;
+            return this;
+        }
+
         @Override
         public IBuildStage withPreferred_username(String preferred_username) {
             this.preferred_username = preferred_username;
@@ -211,10 +313,10 @@ public class DuoAuthToken {
             return new DuoAuthToken(this);
         }
     }
-    
-    
 
     
+   
+    
     
 
 }
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/model/DuoHealthCheck.java
index 39ff993..5833c3c 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
@@ -12,27 +12,29 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
  * <p>Represents a health check response from Duo's 2FA endpoint.</p>
  * 
  * <p>Includes a staged builder for fluent generation.</p>
+ * 
+ * <p>Is immutable, can only be built using the builder.</p>
  */
 @Immutable
 public class DuoHealthCheck {
     
     /** The health status of the 2FA endpoint.*/
-    @Nonnull @NotEmpty final private String status;
+    @Nonnull @NotEmpty private final String status;
 
     /** When the response was issued, as ms since Unix EPOCH.*/
-    @Nullable final private Integer responseTimestamp;
+    @Nullable private final Integer responseTimestamp;
 
     /** The response code.*/
-    @Nullable @NotEmpty final private String code;
+    @Nullable @NotEmpty private final String code;
 
     /** The timestamp ....!!!*/
-    @Nullable final private String timestamp;
+    @Nullable private final String timestamp;
 
     /** The response message.*/
-    @Nullable final private String message;
+    @Nullable private final String message;
 
     /** The detailed response message.*/ 
-    @Nullable final private String messageDetail;
+    @Nullable private final String messageDetail;
 
     
     /**
@@ -99,7 +101,11 @@ public class DuoHealthCheck {
 
     
 
-    //TODO: Add Javadoc below.
+    /**
+     * Private constructor, can only be called by this builder.
+     *
+     * @param builder the builder to build the instance with
+     */
     private DuoHealthCheck(Builder builder) {
         this.status = builder.status;
         this.responseTimestamp = builder.responseTimestamp;
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
new file mode 100644
index 0000000..d5b5963
--- /dev/null
+++ b/idp-duo-api/src/test/java/net/shbboleth/idp/plugin/authn/duo/DuoOIDCPrincipalTest.java
@@ -0,0 +1,27 @@
+package net.shbboleth.idp.plugin.authn.duo;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+
+public class DuoOIDCPrincipalTest {
+
+    @Test
+    public void testClone() throws CloneNotSupportedException {
+        final String name = "jdoe";
+        DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
+        DuoOIDCPrincipal principalTwo = principal.clone();
+        assertEquals(principal, principalTwo);       
+    }
+    
+    @Test
+    public void testImmutable() throws CloneNotSupportedException {
+        final String name = "jdoe";
+        DuoOIDCPrincipal principal = new DuoOIDCPrincipal(name);
+        //should be obvious as string is not mutable. But dbl check.
+        String nameInPrincipal = principal.getName();
+        nameInPrincipal = "jdoe-modified";
+        assertEquals(principal.getName(), name);       
+    }
+
+}
diff --git a/idp-duo-impl/.checkstyle b/idp-duo-impl/.checkstyle
new file mode 100644
index 0000000..162e469
--- /dev/null
+++ b/idp-duo-impl/.checkstyle
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false">
+  <fileset name="all" enabled="true" check-config-name="shibboleth" local="false">
+    <file-match-pattern match-pattern="." include-pattern="true"/>
+  </fileset>
+</fileset-config>
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index 07efa35..e9d8856 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -9,34 +9,44 @@
     </parent>
 
     <artifactId>idp-duo-impl</artifactId>
-    
+
     <properties>
         <automatic.module.name>net.shibboleth.idp.plugin.authn.duo.impl</automatic.module.name>
+        <apache.commons.lang.version>3.10</apache.commons.lang.version>
     </properties>
 
 
-    <!--  TODO: make sure any classes we are using explicitly in our code are directly imported, and not transitively imported for best
-    practice. -->
+    <!-- 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 -->
+        <!-- compile time depedencies -->
         <dependency>
-            <groupId>net.shibboleth.idp</groupId>
-            <artifactId>idp-authn-api</artifactId>
-            <scope>Provided</scope>
+            <groupId>net.shibboleth.plugin.authn</groupId>
+            <artifactId>idp-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 -->
+
         <dependency> <!-- Provides some lookup strategies -->
-            <groupId>net.shibboleth.idp</groupId>
+            <groupId>${idp.groupId}</groupId>
             <artifactId>idp-session-api</artifactId>
-             <scope>Provided</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <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>
@@ -45,35 +55,10 @@
             <scope>runtime</scope>
         </dependency>
 
-        <!-- Provided/Test Dependencies -->
-        <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>javax.servlet-api</artifactId>
-            <scope>provided</scope>
-        </dependency>
-
-        <dependency>
-            <groupId>${slf4j.groupId}</groupId>
-            <artifactId>jcl-over-slf4j</artifactId>
-            <scope>provided</scope>
-        </dependency>
 
         <!-- Test Dependencies -->
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-api</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-engine</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <scope>test</scope>
-        </dependency>
+
+
         <dependency>
             <groupId>net.shibboleth.idp</groupId>
             <artifactId>idp-profile-impl</artifactId>
@@ -90,6 +75,22 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.webflow</groupId>
+            <artifactId>spring-webflow</artifactId>
+            <version>2.5.1.RELEASE</version>
+
+        </dependency>
+
+        <!-- Spring webflow tests require Junit4 / will junit-vintage-engine help? -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+
+
     </dependencies>
 
     <build>
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
index ce1673a..d170c14 100644
--- 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
@@ -2,6 +2,7 @@ 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;
@@ -10,13 +11,15 @@ 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.
+ * 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>
  */
-//TODO: so we require state? must check this is required with Duo flow to.
 public class CheckDuoState extends AbstractAuthenticationAction {    
     
     /** Class logger. */
@@ -48,20 +51,19 @@ public class CheckDuoState extends AbstractAuthenticationAction {
         
         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");
-            //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");
+            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/DuoOIDCAuthnController.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoOIDCAuthnController.java
index 5c8c864..84b207f 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
@@ -36,8 +36,6 @@ import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientRegistry;
@@ -52,7 +50,6 @@ 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.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 
 /**
@@ -94,6 +91,16 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
                 compose(new ChildContextLookup<>(AuthenticationContext.class));
     }
     
+    /**
+     * Set Duo authentication context lookup strategy to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDuoContextLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+        duoContextLookupStrategy = Constraint.isNotNull(strategy, "DuoContextLookuplookup strategy cannot be null");
+    }
+    
     /**
      * Set the Duo client registry.
      * 
@@ -143,7 +150,7 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         try {
             final DuoOIDCClient client = clientRegistry.getIntegrationClientOrCreate(integration);            
             //generate state, stash in the context for checking on return.
-            final String state = generateState(32);
+            final String state = DuoSupport.generateState(32);
             duoContext.setRequestState(state);            
             final String authURL = client.createAuthUrl(duoContext.getUsername(), state);
             httpResponse.sendRedirect(authURL);
@@ -195,21 +202,6 @@ public class DuoOIDCAuthnController extends AbstractInitializableComponent{
         
     }
     
-    /**
-     * 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/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
new file mode 100644
index 0000000..c30e768
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -0,0 +1,38 @@
+package net.shibboleth.idp.plugin.authn.duo.impl;
+
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Helper methods for Duo 2FA. 
+ */
+public final class DuoSupport {
+    
+    
+    /** Private Constructor. */
+    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.
+     * 
+     * @return the randomly generated state value.
+     */
+    @Nonnull public static 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/ExchangeCodeForDuoToken.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
new file mode 100644
index 0000000..b3a83dd
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExchangeCodeForDuoToken.java
@@ -0,0 +1,55 @@
+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.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.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
+ * of 2FA. Adds the token to the Duo context.
+ */
+public class ExchangeCodeForDuoToken extends AbstractDuoAuthenticationAction{
+    
+    /** Class logger.*/
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExchangeCodeForDuoToken.class);
+    
+    
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
+            @Nonnull final DuoAuthenticationContext duoContext) {    
+        
+        final String code = duoContext.getAuthorizationCode();
+        
+        if (code == null) {
+            log.error("{} Duo 2FA 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);
+        }
+        
+        try {
+            final DuoAuthToken token = client.exchangeAuthorizationCodeFor2FAResult(code);
+            log.trace("{} Duo 2FA token received '{}'",getLogPrefix(),token);
+            duoContext.setAuthToken(token);
+            
+        } 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);
+        }
+        
+    }
+
+}
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 005a286..6da837b 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
@@ -11,8 +11,8 @@ import org.slf4j.LoggerFactory;
 import net.shbboleth.idp.plugin.authn.duo.AbstractDuoAuthenticationAction;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
 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;
 
@@ -20,7 +20,6 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
  * An action that checks the health of the Duo 2FA endpoint for the established Duo integration. 
  */
 //TODO: Support multiple integrations (multiplexing) e.g. the strategy.
-//TODO: Not a validation action - this is just checking the health of the endpoint change.
 //TODO: abstract the duo action integration and client lookup? done?
 public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoAuthenticationAction{
     
@@ -36,7 +35,8 @@ public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoAuthenticationAction{
     //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 AuthenticationContext authenticationContext, @Nonnull final DuoOIDCClient client,
+            @Nonnull final DuoAuthenticationContext duoContext) {        
         
         try {
             final DuoHealthCheck healthCheckResponse = client.healthCheck();            
@@ -49,7 +49,8 @@ public class HealthCheckDuoOIDCAuthAPI extends AbstractDuoAuthenticationAction{
             }
             if (DUO_HEALTH_CHECK_FAIL.equalsIgnoreCase(healthCheckResponse.getStatus())) {
                 //2FA is unavailable. 
-                log.warn("{} Duo 2FA endpoints are unhealthy, current status '{}'",getLogPrefix(),healthCheckResponse.getStatus());
+                log.warn("{} Duo 2FA endpoints are unhealthy, current status '{}'",getLogPrefix(),
+                        healthCheckResponse.getStatus());
                 throw new DuoClientException("Duo 2FA endpoints are unhealthy");
             }            
             //2FA available
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/PopulateDuoAuthenticationContext.java
similarity index 60%
rename from idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoContext.java
rename to idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PopulateDuoAuthenticationContext.java
index 2830a06..6737096 100644
--- 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/PopulateDuoAuthenticationContext.java
@@ -5,32 +5,39 @@ import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
+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 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.authn.duo.DuoIntegration;
 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.
+ * An action to create (or lookup if configured) and populate the {@link DuoAuthenticationContext} 
+ * with the username and chosen {@link DuoIntegration} appropriate for this request. 
+ * 
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link org.opensaml.profile.action.EventIds#INVALID_PROFILE_CTX}
+ * @event {@link net.shibboleth.idp.authn.AuthnEventIds#NO_CREDENTIALS}
+ * @post See above.
  */
-public class PopulateDuoContext extends AbstractAuthenticationAction {
+public class PopulateDuoAuthenticationContext extends AbstractAuthenticationAction {
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(PopulateDuoContext.class);
+    @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;
 
     /** Lookup strategy for username to match against Duo identity. */
     @Nonnull private Function<ProfileRequestContext, String> usernameLookupStrategy;
@@ -38,7 +45,10 @@ public class PopulateDuoContext extends AbstractAuthenticationAction {
     /** Lookup strategy for Duo integration. */
     @Nonnull private Function<ProfileRequestContext, DuoOIDCIntegration> duoIntegrationLookupStrategy;
 
-    public PopulateDuoContext() {
+    /** Constructor.*/
+    public PopulateDuoAuthenticationContext() {
+        duoAuthContextCreationStrategy =
+                new ChildContextLookup<>(DuoAuthenticationContext.class, true);
         usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
         duoIntegrationLookupStrategy = FunctionSupport.constant(null);
     }
@@ -53,6 +63,19 @@ public class PopulateDuoContext extends AbstractAuthenticationAction {
 
         usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
     }
+    
+    /**
+     * Set the strategy used to locate the {@link DuoAuthenticationContext} to operate on.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setDuoContextCreationStrategy(
+            @Nonnull final Function<ProfileRequestContext,DuoAuthenticationContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        duoAuthContextCreationStrategy = Constraint.isNotNull(strategy, "DuoAuthenticationContext"
+                + " creation strategy cannot be null");
+    }
 
     /**
      * Set DuoIntegration lookup strategy to use.
@@ -70,9 +93,14 @@ public class PopulateDuoContext extends AbstractAuthenticationAction {
     @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
 
-        DuoAuthenticationContext context = new DuoAuthenticationContext();
+        final DuoAuthenticationContext context = duoAuthContextCreationStrategy.apply(profileRequestContext);
+        if (context == null) {
+            log.error("{} Error creating DuoAuthenticationContext", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return;
+        }
         
-        DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+        final DuoOIDCIntegration duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
         if (duoIntegration == null) {
             log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
@@ -80,7 +108,7 @@ public class PopulateDuoContext extends AbstractAuthenticationAction {
         }
         context.setIntegration(duoIntegration);
         
-        String username = usernameLookupStrategy.apply(profileRequestContext);
+        final String username = usernameLookupStrategy.apply(profileRequestContext);
         if (username == null) {
             log.warn("{} No principal name available to initiate a Duo 2FA request", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
@@ -88,8 +116,6 @@ public class PopulateDuoContext extends AbstractAuthenticationAction {
         }
         context.setUsername(username);
         
-        authenticationContext.addSubcontext(context, true);
-        
         log.debug("Created Duo authentication context for '{}'",username);
     }
 
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
new file mode 100644
index 0000000..6b661c9
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuo2FAToken.java
@@ -0,0 +1,144 @@
+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/resources/flows/authn/duo/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/flows/authn/duo/duo-oidc-authn-beans.xml
index f62375b..f459057 100644
--- 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
@@ -22,7 +22,8 @@
     <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"
+    <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}" 
@@ -36,9 +37,25 @@
         class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
         p:clientType="%{idp.duo.oidc.clientType:net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientAdaptor}"/>
     
-    <bean id="HealthCheckDuoOIDCAuthAPI" scope="prototype" class="net.shibboleth.idp.plugin.authn.duo.impl.HealthCheckDuoOIDCAuthAPI"                 
+    
+    <!--  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" 
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 343e47e..745061c 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
@@ -6,6 +6,7 @@
 
     <!-- TODO: throws an AuthnException if the endpoint is not healthy, no backoff etc. -->
     <action-state id="CheckDuoOIDCAuthAPI">
+        <evaluate expression="PopulateDuoAuthenticationContext" />
         <evaluate expression="HealthCheckDuoOIDCAuthAPI" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="Duo2FAAuthorizationRequest" />
@@ -37,14 +38,13 @@
     
     <action-state id="ExchangeCodeForDuoToken">
         <evaluate expression="ExchangeCodeForDuoToken"/>
-        <!-- validate the response, no decide yet -->
-        
+        <!-- validate the response and set principal -->
+        <evaluate expression="ValidateDuo2FAToken"/>       
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="SetPrincipal" />
+        <transition on="proceed" to="proceed" />
     </action-state>
 
-    <!-- We only need the username -->
-    <!-- <action-state id="ExtractUsernameFromFormRequest"> </action-state> -->
+    
 
     <bean-import resource="duo-oidc-authn-beans.xml" />
 </flow>
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
index 82fae20..4570dd1 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
@@ -8,6 +8,7 @@ import java.util.Map.Entry;
 
 import javax.annotation.Nonnull;
 
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.mockito.Mockito;
 import org.opensaml.core.config.ConfigurationService;
@@ -39,7 +40,6 @@ import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
 import com.codahale.metrics.MetricRegistry;
 import com.google.common.net.HttpHeaders;
 
-import junit.framework.Assert;
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
@@ -107,7 +107,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * @param testFlowModelResources the parent flows.
      */
     public void setFlowModelResources(@Nonnull final Map<String,String> testFlowModelResources) {
-        Assert.assertNotNull(testFlowModelResources);        
+        Assertions.assertNotNull(testFlowModelResources);        
         flowModelResources = testFlowModelResources;
     }
     
@@ -117,12 +117,12 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * @param testFlowPath the path to the file to test.
      */
     public void setFlowPath(@Nonnull @NotEmpty final String testFlowPath) {
-        Assert.assertNotNull(testFlowPath);
+        Assertions.assertNotNull(testFlowPath);
         flowPath = testFlowPath;
     }
     
     public void setMockProperties(@Nonnull final Map<String,String> properties) {
-        Assert.assertNotNull(properties);
+        Assertions.assertNotNull(properties);
         mockProperties = properties;
     }
     
@@ -132,14 +132,14 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * @param mockSubflows the mock subflows.
      */
     public void setSubflows(@Nonnull @NonnullElements List<Flow> mockSubflows) {
-        Assert.assertNotNull(mockSubflows);
+        Assertions.assertNotNull(mockSubflows);
         subflows = mockSubflows;
     }
     
    
     public void addHttpBasicAuthHeader(@Nonnull final String username, @Nonnull final String password) {
-        Assert.assertNotNull(username);
-        Assert.assertNotNull(password);
+        Assertions.assertNotNull(username);
+        Assertions.assertNotNull(password);
         mockRequest.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+ 
                 Base64.getEncoder().encodeToString((username+":"+password).getBytes()));        
     }
@@ -190,7 +190,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      * @param builderContext the builder context to register the subflows with.
      */
     private void registerMockSubflows(@Nonnull final MockFlowBuilderContext builderContext) {        
-        Assert.assertNotNull(builderContext);       
+        Assertions.assertNotNull(builderContext);       
         
         subflows.forEach(flow -> builderContext.registerSubflow(flow));        
     }
@@ -254,15 +254,15 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
     private void addBeanDefinition(@Nonnull final MockFlowBuilderContext builderContext, 
             @Nonnull final String beanName, @Nonnull final BeanDefinition bean) {
         
-        Assert.assertNotNull(builderContext);
-        Assert.assertNotNull(beanName);
-        Assert.assertNotNull(bean);
-        Assert.assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
+        Assertions.assertNotNull(builderContext);
+        Assertions.assertNotNull(beanName);
+        Assertions.assertNotNull(bean);
+        Assertions.assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
         
         BeanFactory factory = ((ConfigurableApplicationContext) builderContext.getApplicationContext()).getBeanFactory();
         
-        Assert.assertNotNull(factory);
-        Assert.assertTrue(factory instanceof DefaultListableBeanFactory);
+        Assertions.assertNotNull(factory);
+        Assertions.assertTrue(factory instanceof DefaultListableBeanFactory);
         
         ((DefaultListableBeanFactory)factory).registerBeanDefinition(beanName, bean);;
         
@@ -276,10 +276,10 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
      */
     private void registerMockPropertySource(@Nonnull final MockFlowBuilderContext builderContext) {
         
-        Assert.assertNotNull(builderContext);
-        Assert.assertNotNull(builderContext.getApplicationContext());
-        Assert.assertNotNull(builderContext.getApplicationContext().getEnvironment());
-        Assert.assertTrue(builderContext.getApplicationContext().getEnvironment() instanceof StandardEnvironment);
+        Assertions.assertNotNull(builderContext);
+        Assertions.assertNotNull(builderContext.getApplicationContext());
+        Assertions.assertNotNull(builderContext.getApplicationContext().getEnvironment());
+        Assertions.assertTrue(builderContext.getApplicationContext().getEnvironment() instanceof StandardEnvironment);
         
         final MockPropertySource mock = new MockPropertySource();
         
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java
deleted file mode 100644
index b515a58..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoSAML2SSOFlowTest.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import java.net.URL;
-
-import org.slf4j.LoggerFactory;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.joran.JoranConfigurator;
-import ch.qos.logback.classic.util.ContextInitializer;
-import ch.qos.logback.core.joran.spi.JoranException;
-import ch.qos.logback.core.util.StatusPrinter;
-import net.shibboleth.idp.test.flows.saml2.AbstractSAML2SSOFlowTest;
-
-public class AbstractDuoSAML2SSOFlowTest extends AbstractSAML2SSOFlowTest{
-    
-    
-    /**
-     * Reload the configuration of the active logger context. This is useful if a logback-text.xml
-     * has been provided for the plugin which would otherwise be reset by the <code>shibboleth.LoggingService</code>
-     * on initialisation of the IdP. 
-     */
-    protected void reloadLogger() {
-        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
-
-        ContextInitializer ci = new ContextInitializer(loggerContext);
-        URL url = ci.findURLOfDefaultConfigurationFile(true);
-
-        try {
-            JoranConfigurator configurator = new JoranConfigurator();
-            configurator.setContext(loggerContext);
-            loggerContext.reset();
-            configurator.doConfigure(url);
-            loggerContext.start();
-        } catch (JoranException je) {
-            // StatusPrinter will handle this
-        }
-        StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoOIDCClientRegistryTest.java
index a2ec73b..f467de6 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
@@ -4,11 +4,11 @@ package net.shibboleth.idp.plugin.authn.duo.impl;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
-import junit.framework.Assert;
 import net.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shbboleth.idp.plugin.authn.duo.DuoRegistryException;
-import net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientAdaptor;
+
+import net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClient_OK;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
 class DefaultDuoOIDCClientRegistryTest {
@@ -16,7 +16,7 @@ class DefaultDuoOIDCClientRegistryTest {
     @Test
     void test() throws DuoRegistryException {
         DefaultDuoOIDCClientRegistry factory = new DefaultDuoOIDCClientRegistry();
-        factory.setClientType(DuoSDKClientAdaptor.class.getName());
+        factory.setClientType(MockDuoOIDCClient_OK.class.getName());
         DefaultDuoOIDCIntegration integ = new DefaultDuoOIDCIntegration();
         integ.setAPIHost("host.com");
         integ.setApplicationKey("SDFGHJKLPOIUYTREWQZXCVBNMLKJHGFAQWERTYU");
@@ -27,7 +27,7 @@ class DefaultDuoOIDCClientRegistryTest {
         DuoOIDCClient client = factory.getIntegrationClientOrCreate(integ);
         DuoOIDCClient clientTwo = factory.getIntegrationClientOrCreate(integ);
         
-        Assert.assertEquals(client, clientTwo);
+        Assertions.assertEquals(client, clientTwo);
         
         //set to a different value
         integ.setIntegrationKey("DIU6GEFWG5LIUBVV2M3B");
@@ -35,7 +35,7 @@ class DefaultDuoOIDCClientRegistryTest {
         DuoOIDCClient clientThree = factory.getIntegrationClientOrCreate(integ);
         
         System.out.println("Client: "+client+" Client2: "+clientTwo+" Client3: "+clientThree);
-        Assert.assertNotSame(client, clientThree);
+        Assertions.assertNotSame(client, clientThree);
         
     }
     
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 af1185f..bb99fca 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
@@ -13,8 +13,10 @@ import javax.annotation.Nonnull;
 
 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.webflow.core.collection.LocalAttributeMap;
 import org.springframework.webflow.engine.Flow;
 import org.springframework.webflow.engine.impl.FlowExecutionImpl;
@@ -25,12 +27,14 @@ import org.springframework.webflow.execution.repository.support.CompositeFlowExe
 import org.springframework.webflow.test.MockFlowExecutionContext;
 import org.springframework.webflow.test.MockFlowExecutionKey;
 
-import junit.framework.Assert;
 import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
 import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
 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.MockFlowBuilder;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
@@ -39,11 +43,10 @@ import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 //TODO: IS THIS A ONE FLOW TEST? if so, maybe set FLOW to test in constructor and the supporting stuff in the test method?
 public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(DuoAuthnFlowTest.class);
+
     
     /** Path to the flow to be tested.*/
-    @Nonnull final String flowToTest = "/flows/authn/duo/duo-oidc-authn-flow.xml";
+    @Nonnull final private static String FLOW = "/flows/authn/duo/duo-oidc-authn-flow.xml";
     
     /** 
      * Map of flow resources that support building the flow to test.
@@ -63,18 +66,21 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     /** The password of the user to test.*/
     @Nonnull private final static String PASSWORD = "changeit";
     
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DuoAuthnFlowTest.class);
+    
     /**
-     * Runs the flow to fail:
+     * Runs the flow to fail.
      * <ol>
      * <li>Uses baisc auth, so skips username and password login view</li>
      * </ol>
      */
     @Test public void testDuoAuthnFlowUnavailable() {
         
-        setFlowPath(flowToTest);
+        setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-        addHttpBasicAuthHeader(USERNAME,PASSWORD);
+       //addHttpBasicAuthHeader(USERNAME,PASSWORD);
         
        final Map<String,String> mockProperties = Map.of(
                 "idp.duo.oidc.redirectUri","http://localhost/callback",
@@ -99,10 +105,10 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     
     @Test public void testDuoAuthnFlowToAuthorizationRequest() {
         
-        setFlowPath(flowToTest);
+        setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-        addHttpBasicAuthHeader(USERNAME,PASSWORD);
+       // addHttpBasicAuthHeader(USERNAME,PASSWORD);
         
        final Map<String,String> mockProperties = Map.of(
                 "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
@@ -128,11 +134,12 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     
     @Test public void testDuoAuthnFlowFromAuthorizationCallback() {
         
-        setFlowPath(flowToTest);
+        setFlowPath(FLOW);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
         //addHttpBasicAuthHeader(USERNAME,PASSWORD);
         
+        //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",
@@ -142,20 +149,39 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
         
         setMockProperties(mockProperties);
         
-        LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
-        inputMap.put("calledAsSubflow", true);
 
-        FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
-        FlowExecutionKey key = new MockFlowExecutionKey("1");
-        Mockito.when(flowExecution.getKey()).thenReturn(key);
-        flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false));
+        final FlowExecutionImpl flowExecution = (FlowExecutionImpl)getFlowExecutionFactory()
+                .createFlowExecution(getFlowDefinition());
+        final ProfileRequestContext prc =  buildProfileRequestContext(false);
+        //add a DuoContext
+        final DuoAuthenticationContext duoContext = new DuoAuthenticationContext();
+        final String state = DuoSupport.generateState(32);
+        duoContext.setAuthorizationCode("adummycode");
+        duoContext.setRequestState(state);
+        duoContext.setResponseState(state);
+        duoContext.setUsername("jdoe");
+        prc.getSubcontext(AuthenticationContext.class).addSubcontext(duoContext);
+        flowExecution.getConversationScope().put("opensamlProfileRequestContext",prc);
         updateFlowExecution(flowExecution);
-      //  ((FlowExecutionImpl)flowExecution).getActiveSession();
-        setCurrentState("Duo2FAAuthorizationRequest");
+        
+        //set start view and ending event to transition on.
+        externalContext.setEventId("proceed");
+        setCurrentState("Duo2FAAuthorizationRequest");       
         resumeFlow(externalContext);
-
-
+        
+        //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");
+        
     }
+    
+    //TODO: test each action, make sure the Invalid end states are tested.
 
    
 }
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 192199b..ac12807 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
@@ -52,7 +52,6 @@ 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.shbboleth.idp.plugin.authn.duo.DefaultDuoOIDCIntegration;
 import net.shbboleth.idp.plugin.authn.duo.context.DuoAuthenticationContext;
 import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
@@ -96,7 +95,7 @@ public class DuoOIDCAuthnControllerTest {
         
         // check controller is instantiated.
         DuoOIDCAuthnController controller = webApplicationContext.getBean(DuoOIDCAuthnController.class);        
-        Assert.assertNotNull(controller);
+        Assertions.assertNotNull(controller);
 
         // add a Duo client registry
         DefaultDuoOIDCClientRegistry registry = new DefaultDuoOIDCClientRegistry();
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java
deleted file mode 100644
index 313066e..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSAML2FlowTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development, 
- * Inc. (UCAID) under one or more contributor license agreements.  See the 
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache 
- * License, Version 2.0 (the "License"); you may not use this file except in 
- * compliance with the License.  You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.core.xml.io.MarshallingException;
-import org.opensaml.core.xml.util.XMLObjectSupport;
-import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.saml.common.SAMLObject;
-import org.opensaml.saml.common.xml.SAMLConstants;
-import org.opensaml.saml.saml2.core.AuthnRequest;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.web.context.WebApplicationContext;
-import org.springframework.webflow.executor.FlowExecutionResult;
-import org.testng.annotations.Test;
-import org.w3c.dom.Element;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.joran.JoranConfigurator;
-import ch.qos.logback.classic.util.ContextInitializer;
-import ch.qos.logback.core.joran.spi.JoranException;
-import ch.qos.logback.core.util.StatusPrinter;
-import net.shibboleth.idp.log.LogbackLoggingService;
-import net.shibboleth.idp.test.flows.saml2.AbstractSAML2SSOFlowTest;
-import net.shibboleth.utilities.java.support.codec.Base64Support;
-import net.shibboleth.utilities.java.support.codec.EncodingException;
-import net.shibboleth.utilities.java.support.xml.SerializeSupport;
-
-
-/**
- * Temporary class to see if a flow can complete.
- */
- at ContextConfiguration(initializers = PropertySourcesPlaceholderConfigurerInitializer.class)
-public class DuoSAML2FlowTest extends AbstractDuoSAML2SSOFlowTest{
-    
-    /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(DuoSAML2FlowTest.class);
-    
-    /** Flow id. */
-    @Nonnull public final static String FLOW_ID = "SAML2/POST/SSO";
-
-    /**
-     * 
-     * End to end SAML2 SSO POST flow test.
-     * 
-     * @throws Exception if an exception occurs.
-     */
-    @Test public void testSAML2SSOFlow() throws Exception {
-        
-        //TODO: do this automatically?
-        reloadLogger();  
-        
-        buildRequest();
-
-        overrideEndStateOutput(FLOW_ID);
-        
-        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
-        
-        validateResult(result, FLOW_ID);
-       
-    }
-    
-    /**
-     * Build the {@link MockHttpServletRequest}.
-     * 
-     * @throws Exception if an error occurs
-     */
-    //TODO: this is in the SAML2POSTSSOFlowTest class as well, should we extend here etc.    
-    public void buildRequest() throws Exception {
-
-        request.setMethod("POST");
-        request.setRequestURI("/idp/profile/" + FLOW_ID);
-
-        final AuthnRequest authnRequest = buildAuthnRequest(request);
-        authnRequest.setDestination(getDestinationPost(request));
-
-        final MessageContext messageContext =
-                buildOutboundMessageContext(authnRequest, SAMLConstants.SAML2_POST_BINDING_URI);
-        final SAMLObject message = (SAMLObject) messageContext.getMessage();
-        final String encodedMessage = encodeMessage(message);
-        request.addParameter("SAMLRequest", encodedMessage);
-    }
-    
-    
-    
-    /**
-     * Base64 the given SAML message.
-     * 
-     * @param message the SAML message
-     * @return Base64 encoded message
-     * @throws MarshallingException if there is a problem marshalling the XMLObject
-     * @throws UnsupportedEncodingException If the named charset is not supported
-     * @throws EncodingException if the message can not be base64 encoded
-     */
-    @Nonnull public String encodeMessage(@Nonnull final SAMLObject message) throws MarshallingException,
-            UnsupportedEncodingException, EncodingException {
-        final Element domMessage = XMLObjectSupport.marshall(message);
-        final String messageXML = SerializeSupport.nodeToString(domMessage);
-
-        return Base64Support.encode(messageXML.getBytes("UTF-8"), Base64Support.UNCHUNKED);
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordAuthnFlowTest.java
index 79a424a..92ea6ec 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
@@ -47,6 +47,10 @@ public class PasswordAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     /** The password of the user to test.*/
     @Nonnull final static String PASSWORD = "changeit";
     
+    
+    @Test public void nothing() {
+        
+    }
 
     /**
      * Runs the flow:
@@ -54,12 +58,13 @@ public class PasswordAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
      * <li>Uses baisc auth, so skips username and password login view</li>
      * </ol>
      */
-    @Test public void testPasswordAuthnFlow() {
+    //@Test 
+    public void testPasswordAuthnFlow() {
         
         setFlowPath(flowToTest);
         setFlowModelResources(flowResources);
         setSubflows(subflows);
-        addHttpBasicAuthHeader(USERNAME,PASSWORD);
+       // addHttpBasicAuthHeader(USERNAME,PASSWORD);
           
         LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
         inputMap.put("calledAsSubflow", true);
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java
deleted file mode 100644
index fd008fa..0000000
--- a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/PropertySourcesPlaceholderConfigurerInitializer.java
+++ /dev/null
@@ -1,41 +0,0 @@
-
-package net.shibboleth.idp.plugin.authn.duo.impl;
-
-
-
-import javax.annotation.Nonnull;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.context.ApplicationContextInitializer;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.mock.env.MockPropertySource;
-import org.springframework.stereotype.Component;
-
-import net.shibboleth.idp.test.TestEnvironmentApplicationContextInitializer;
-
-//TODO: remove this if not used.
- at Deprecated
-public class PropertySourcesPlaceholderConfigurerInitializer
-        implements ApplicationContextInitializer<ConfigurableApplicationContext> {
-
-    /** Class logger. */
-    @Nonnull
-    private final Logger log = LoggerFactory.getLogger(TestEnvironmentApplicationContextInitializer.class);
-
-    /** {@inheritDoc} */
-    @Override
-    public void initialize(@Nonnull final ConfigurableApplicationContext applicationContext) {
-
-        final MockPropertySource mock = new MockPropertySource();
-        mock.setProperty("idp.home", "classpath:");
-        mock.setProperty("idp.webflows", "classpath*:/flows");
-//        mock.setProperty("idp.storage.htmlLocalStorage", "false");
-//        mock.setProperty("idp.session.trackSPSessions", "false");
-//        mock.setProperty("idp.session.secondaryServiceIndex", "false");
-//        mock.setProperty("idp.service.metadata.resources", "testbed.MetadataResolverResources");
-        applicationContext.getEnvironment().getPropertySources().addFirst(mock);
-        log.info("Prepending properties '{}'", mock.getSource());
-    }
-
-}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_FAIL.java
index af70474..02246cf 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
@@ -7,12 +7,20 @@ 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;
 
 /**
  * Mock a Duo client which is not available (unhealthy).
+ * 
+ * Of note, this is used to test failure branches of action classes, but in reality
+ * the Duo 2FA endpoint will not return failure to the IdP??!! so these cases may never
+ * happen in practice!!??
  */
 public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{    
     
@@ -48,15 +56,22 @@ public class MockDuoOIDCClient_FAIL implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
+    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");
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code) throws DuoClientException {
         return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).build();
+                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()).
+                        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/MockDuoOIDCClient_OK.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/MockDuoOIDCClient_OK.java
index 68ee894..e5671d5 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
@@ -7,6 +7,10 @@ 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;
@@ -48,15 +52,22 @@ public class MockDuoOIDCClient_OK implements DuoOIDCClient{
     }
 
     @Override
-    public String createAuthUrl(String username, String state) throws DuoClientException {
+    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");
     }
 
     @Override
-    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(String code) throws DuoClientException {
+    public DuoAuthToken exchangeAuthorizationCodeFor2FAResult(final String code) throws DuoClientException {
         return DuoAuthToken.builder().withIss(ISS).withSub(SUB).withAud(AUD).
-                withExp(EXP).withIat(IAT).build();
+                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();
     }
 
 }
diff --git a/pom.xml b/pom.xml
index 75c2aeb..e61a335 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,21 +10,28 @@
     <packaging>pom</packaging>
 
     <properties>
+        <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>
         <opensaml.version>4.0.0</opensaml.version>
+        <opensaml.groupId>org.opensaml</opensaml.groupId>
+        <idp.groupId>net.shibboleth.idp</idp.groupId>
         <duo.client.version>1.0-SNAPSHOT</duo.client.version>
         <junit.jupitar.version>5.6.2</junit.jupitar.version>
+        <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>
+        <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? -->
+    <!-- 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.0.0</version> </parent> 
 
     <modules>
         <module>idp-duo-api</module>
@@ -49,8 +56,138 @@
         </repository>
     </repositories>
 
+
+    <dependencies>
+
+        <!-- Compile Dependencies provided by the IdP -->
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-profile-api</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-authn-api</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${opensaml.groupId}</groupId>
+            <artifactId>opensaml-messaging-api</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>net.shibboleth.utilities</groupId>
+            <artifactId>java-support</artifactId>
+            <version>${java-support.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${idp.groupId}</groupId>
+            <artifactId>idp-profile-api</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>*</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${slf4j.groupId}</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${slf4j.groupId}</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>${spring-webflow.groupId}</groupId>
+            <artifactId>spring-webflow</artifactId>
+            <version>${spring-webflow.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+        </dependency>
+
+
+        <!-- Test Dependencies -->
+
+        <dependency>
+            <groupId>${spring.groupId}</groupId>
+            <artifactId>spring-test</artifactId>
+        </dependency>
+
+        <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>
+            <version>${junit.jupitar.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
     <dependencyManagement>
         <dependencies>
+
+            <!-- IdP BOM when importing IdP deps TODO: this inherists properties from java-parent as well -->
             <dependency>
                 <groupId>net.shibboleth.idp</groupId>
                 <artifactId>idp-bom</artifactId>
@@ -58,6 +195,18 @@
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
+
+            <!-- OpenSAML BOM when importing opensaml deps -->
+            <dependency>
+                <groupId>org.opensaml</groupId>
+                <artifactId>opensaml-bom</artifactId>
+                <version>${opensaml.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+
+            <!-- Duo dependencies -->
             <dependency>
                 <groupId>com.duosecurity</groupId>
                 <artifactId>duo-client</artifactId>
@@ -75,23 +224,33 @@
                 <version>${project.version}</version>
             </dependency>
 
-            <!-- Test Dependencies different than those in the IdP -->
 
-            <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>
-                <version>${junit.jupitar.version}</version>
-                <scope>test</scope>
-            </dependency>
-           
+
 
         </dependencies>
     </dependencyManagement>
 
+    <build>
+        <plugins>
+            <plugin>
+                <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>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>license-maven-plugin</artifactId>
+                <version>2.0.0</version>
+            </plugin>
+
+        </plugins>
+    </build>
+
 </project>
\ No newline at end of file

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


More information about the commits mailing list