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