[java-idp-plugin-duo] branch main updated: JDUO-13 - Registration of Duo Client Factory
Phil Smart
philip.smart at jisc.ac.uk
Tue Sep 1 10:32:08 UTC 2020
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=356a68ac2d233d931d4ba254327714a9ef49d49b
The following commit(s) were added to refs/heads/main by this push:
new 356a68a JDUO-13 - Registration of Duo Client Factory
356a68a is described below
commit 356a68ac2d233d931d4ba254327714a9ef49d49b
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Tue Sep 1 11:32:01 2020 +0100
JDUO-13 - Registration of Duo Client Factory
- Ensure Duo SDK impl is a strict runtime dependency of the core API
and impl modules.
- Add custom XML flow execution test classes to allow the correct IdP
property placeholder prefix (see JDUO-12).
https://issues.shibboleth.net/jira/browse/JDUO-13
---
.../duo/context/DuoOIDCAuthenticationContext.java | 3 +-
.../src/main/assembly/duo-bare-assembly.xml | 5 +-
.../src/main/assembly/duo-native-sdk-assembly.xml | 5 +-
idp-duo-impl/pom.xml | 8 -
.../idp/plugin/authn/duo/DuoOIDCDescription.java | 5 -
.../idp/plugin/authn/duo/impl/DuoSupport.java | 4 +-
.../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml | 36 +-
.../impl/AbstractAuthnXmlFlowExecutionTests.java | 69 +-
.../plugin/authn/duo/impl/DuoAuthnFlowTest.java | 41 +-
.../idp/plugin/authn/duo/package-info.java | 19 +
.../idp/plugin/authn/mock/package-info.java | 19 +
.../CustomAbstractXmlFlowExecutionTests.java | 103 ++
.../authn/spring/CustomFlowModelFlowBuilder.java | 1006 ++++++++++++++++++++
.../spring/CustomFlowRelativeResourceLoader.java | 56 ++
.../spring/CustomLocalFlowBuilderContext.java | 110 +++
.../authn/spring/CustomSubflowExpression.java | 51 +
.../idp/plugin/authn/spring/package-info.java | 19 +
.../authn/duo-oidc-authn-config-custom-factory.xml | 2 +-
idp-duo-impl/src/test/resources/logback-test.xml | 3 +
.../authn/duo/sdk/impl/DuoSDKClientFactory.java | 12 +-
.../src/main/resources/duo-client-factory-bean.xml | 34 +
21 files changed, 1516 insertions(+), 94 deletions(-)
diff --git a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
index 4ad329f..12967fb 100644
--- a/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
+++ b/idp-duo-api/src/main/java/net/shbboleth/idp/plugin/authn/duo/context/DuoOIDCAuthenticationContext.java
@@ -25,6 +25,7 @@ import org.opensaml.messaging.context.BaseContext;
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.shibboleth.idp.authn.context.AuthenticationContext;
/**
* <p>Context that carries the Duo integration and Duo authentication result token.</p>
@@ -52,7 +53,7 @@ public final class DuoOIDCAuthenticationContext extends BaseContext {
/** The token received from Duo as a result of 2FA.*/
@Nullable private DuoAuthToken authToken;
- /** The Duo OIDC client .*/
+ /** The Duo OIDC client to use for the lifetime of this request.*/
@Nullable private DuoOIDCClient client;
/** Constructor. */
diff --git a/idp-duo-distribution/src/main/assembly/duo-bare-assembly.xml b/idp-duo-distribution/src/main/assembly/duo-bare-assembly.xml
index 9c05ca3..6b7dba2 100644
--- a/idp-duo-distribution/src/main/assembly/duo-bare-assembly.xml
+++ b/idp-duo-distribution/src/main/assembly/duo-bare-assembly.xml
@@ -41,10 +41,7 @@
</fileSet>
<fileSet>
<directory>src/main/resources/</directory>
- <outputDirectory></outputDirectory>
- <includes>
- <include>conf/*</include>
- </includes>
+ <outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>
diff --git a/idp-duo-distribution/src/main/assembly/duo-native-sdk-assembly.xml b/idp-duo-distribution/src/main/assembly/duo-native-sdk-assembly.xml
index 0fbed3f..1d14c4d 100644
--- a/idp-duo-distribution/src/main/assembly/duo-native-sdk-assembly.xml
+++ b/idp-duo-distribution/src/main/assembly/duo-native-sdk-assembly.xml
@@ -67,10 +67,7 @@
</fileSet>
<fileSet>
<directory>src/main/resources/</directory>
- <outputDirectory></outputDirectory>
- <includes>
- <include>conf/*</include>
- </includes>
+ <outputDirectory></outputDirectory>
</fileSet>
</fileSets>
</assembly>
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index ef154d0..f87fb11 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -142,14 +142,6 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
- <!-- Duo client implementation is a runtime dependency that can be swapped out.
- Add as a test dependencies for SWF testing -->
- <dependency>
- <groupId>net.shibboleth.idp.plugin</groupId>
- <artifactId>idp-plugin-duo-native-client-impl</artifactId>
- <scope>test</scope>
- </dependency>
-
</dependencies>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCDescription.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCDescription.java
index 6bcdb2f..3566bfd 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCDescription.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoOIDCDescription.java
@@ -40,11 +40,6 @@ public class DuoOIDCDescription extends AbstractPluginDescription{
return "net.shibboleth.idp.plugin.authn.duo";
}
- @Override
- @Nonnull @NonnullElements public List<String> getAdditionalPropertyFiles(){
- return List.of("/conf/authn/idp-duo-oidc.properties");
- }
-
/** {@inheritDoc} */
@Override
@Nonnull @NonnullElements public List<URL> getUpdateURLs() throws IOException {
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
index 14292ed..05814a4 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoSupport.java
@@ -62,10 +62,10 @@ public final class DuoSupport {
*
* <p>The nonce is separated from the key by a dot e.g. {@literal <nonce>.<keyHex>}.</p>
*
- * <p>The nonce is assumed to be already encoded in its transmission format. The key is
+ * <p>The nonce is assumed to be already encoded in its transmission format e.g. Hex. The key is
* hex encoded before it is combined with the nonce.</p>
*
- * @param nonceHex the nonce component.
+ * @param nonce the nonce component.
* @param key the key component. The key is hex encoded before it is added to the generated state.
*
* @return the combined state component.
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index 540c4e5..733b3d4 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -9,6 +9,7 @@
default-init-method="initialize" default-destroy-method="destroy">
+ <!-- For testing, this properties placeholder does not have access to the same set of mock properties defined in SWF tests -->
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer" p:placeholderPrefix="%{"
p:placeholderSuffix="}" />
@@ -37,35 +38,17 @@
<!-- Duo Client factory and bean registry -->
<bean id="shibboleth.authn.duo.OIDC.clientRegistry" scope="singleton"
class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoOIDCClientRegistry"
- p:clientFactory="#{getObject('shibboleth.authn.duo.OIDC.clientFactory') ?: getObject('shibboleth.authn.duo.OIDC.internal.clientFactory')}" />
+ p:clientFactory-ref="%{idp.duo.oidc.clientFactoryBean:shibboleth.authn.duo.OIDC.native.sdk.clientFactory}" />
+ <!-- Load all (or none) factory bean definitions from the classpath. The actual factory bean to use is defined in the
+ client registry and overridable by the property idp.duo.oidc.clientFactoryBean -->
+ <import resource="classpath*:duo-client-factory-bean.xml" />
- <!-- FIXME: aim here is to allow this to be switched out, but as it stands you always need this dep as it is explicit
- here! -->
- <bean id="shibboleth.authn.duo.OIDC.internal.clientFactory"
- class="net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientFactory" scope="singleton">
- <!-- certficate pins. probably need managing external to the flow/plugin or overwritable -->
- <property name="caCerts">
- <list>
- <value>sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=</value>
- <value>sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</value>
- <value>sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=</value>
- <value>sha256/dykHF2FLJfEpZOvbOLX4PKrcD2w2sHd/iA/G3uHTOcw=</value>
- <value>sha256/JZaQTcTWma4gws703OR/KFk313RkrDcHRvUt6na6DCg=</value>
- <value>sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=</value>
- <value>sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE=</value>
- <value>sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k=</value>
- <value>sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U=</value>
- <value>sha256/j9ESw8g3DxR9XM06fYZeuN1UB4O6xp/GAIjjdD/zM3g=</value>
- </list>
- </property>
- </bean>
-
-
- <!-- Can override one or more of the beans above. -->
- <!-- TODO this needs fixing, idp.home will not be substituted inside this expression. -->
- <import resource="${idp.duo.oidc.config.user:%{idp.home}/conf/authn/duo-authn-config.xml}" />
+ <!-- Can override one or more of the beans above. Note, the property override is mostly to allow tests
+ to change the location of the user config file. -->
+ <import resource="%{idp.home}%{idp.duo.oidc.user.config.home:/conf/authn/duo-authn-config.xml}" />
+
<!-- Prototype per conversation beans -->
<bean id="PopulateDuoAuthenticationContext" scope="prototype"
@@ -117,5 +100,4 @@
</entry>
</util:map>
-
</beans>
\ No newline at end of file
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractAuthnXmlFlowExecutionTests.java
index 63f24be..8e2c0d3 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
@@ -26,8 +26,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.mockito.Mockito;
-import org.opensaml.core.config.ConfigurationService;
-import org.opensaml.core.metrics.FilteredMetricRegistry;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,28 +35,28 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
+
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
-import org.springframework.test.context.ContextConfiguration;
import org.springframework.webflow.config.FlowDefinitionResource;
import org.springframework.webflow.config.FlowDefinitionResourceFactory;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.test.MockExternalContext;
import org.springframework.webflow.test.MockFlowBuilderContext;
import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecutionTests;
-import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
-import com.codahale.metrics.MetricRegistry;
import com.google.common.net.HttpHeaders;
import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
import net.shibboleth.idp.authn.context.AuthenticationContext;
import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.plugin.authn.spring.CustomAbstractXmlFlowExecutionTests;
+import net.shibboleth.idp.plugin.authn.spring.CustomFlowModelFlowBuilder;
import net.shibboleth.idp.session.IdPSession;
import net.shibboleth.idp.session.context.SessionContext;
import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
@@ -69,21 +67,38 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
/**
- * <p>Base test class for testing webflows in isolation from the rest of the IdP.</p>
+ * <p>
+ * Base test class for testing webflows in isolation from the rest of the IdP.
+ * </p>
+ *
+ * <p>
+ * Each flow test reinitialises the mock beans. See {@link #setUp()} and
+ * {@link #registerMockBeanDefinitions(MockFlowBuilderContext)}.
+ * </p>
*
- * <p>Each flow test reinitialises the mock beans. See {@link #setUp()} and
- * {@link #registerMockBeanDefinitions(MockFlowBuilderContext)}.</p>
+ * <p>
+ * Extends the {@link CustomAbstractXmlFlowExecutionTests} over the standard
+ * {@link AbstractExternalizedFlowExecutionTests} for the sole reason of being able to set a
+ * {@link CustomFlowModelFlowBuilder} with a placeholder prefix of {@literal %{} to be compatible with those used by the
+ * IdP. We also set this prefix in various other locations, but those are either ignored by the inbuilt SWF flow builder
+ * when it substitutes the placeholder inside the bean import, or do not share the same Mock properties source as is set
+ * in the {@link #registerMockPropertySource(MockFlowBuilderContext)} method. If a better way is found to support that,
+ * switch back to the {@link AbstractExternalizedFlowExecutionTests}.
+ * </p>
*
- * <p>Note, you can not use test context creation annotations e.g. @SpringJUnitWebConfig to help
- * build the application context, because the {@link AbstractExternalizedFlowExecutionTests} creates a
- * new {@link StaticApplicationContext} when building the flow (see buildFlow). This is different from
- * the {@link ApplicationContext} created by {@link ContextConfiguration} annotation processors. As a
- * result, those bean configurations and environment variables need to be injected
- * inside {@link #configureFlowBuilderContext(MockFlowBuilderContext)}</p>
+ * <p>
+ * Note, you can not use test context creation annotations e.g. @SpringJUnitWebConfig to help build the application
+ * context because the {@link AbstractExternalizedFlowExecutionTests} creates a new {@link StaticApplicationContext}
+ * when building the flow (see buildFlow). This is different from the {@link ApplicationContext} created by {@link
+ * ContextConfiguration} annotation processors. As a result, those bean configurations and environment variables need to
+ * be injected inside {@link #configureFlowBuilderContext(MockFlowBuilderContext)}
+ * </p>
*
- * <p>Note, these test use JUnit, and work with the TestNG to JUnit bridge</p>
+ * <p>
+ * These test use JUnit (for SWF test compatibility), and work with the TestNG to JUnit bridge
+ * </p>
*/
-public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlowExecutionTests{
+public abstract class AbstractAuthnXmlFlowExecutionTests extends CustomAbstractXmlFlowExecutionTests{
/** Mock request. */
@Nonnull protected MockHttpServletRequest mockRequest;
@@ -190,14 +205,18 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
/**
* Setup the mock objects before each method.
+ * @throws Exception on error
+ * @throws SecurityException on error
+ * @throws NoSuchFieldException on error
*/
- @Override public void setUp() {
+ @Override public void setUp() throws NoSuchFieldException, SecurityException, Exception {
mockRequest = new MockHttpServletRequest();
mockResponse = new MockHttpServletResponse();
externalContext = new MockExternalContext();
externalContext.setNativeRequest(mockRequest);
externalContext.setNativeResponse(mockResponse);
}
+
@Override
protected FlowDefinitionResource getResource(final FlowDefinitionResourceFactory resourceFactory) {
@@ -263,9 +282,9 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
if (clientFactory != null) {
//register the client factory
- builderContext.registerBean("shibboleth.authn.duo.OIDC.clientFactory", clientFactory);
+ builderContext.registerBean("shibboleth.authn.duo.OIDC.test.clientFactory", clientFactory);
}
-
+
}
@@ -309,15 +328,22 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
final MockPropertySource mock = new MockPropertySource();
- //add the standard mocks to enable IdP function.
+ //add the standard mocks to enable basic IdP function if needed.
mock.setProperty("idp.home", "classpath:");
mock.setProperty("idp.webflows", "classpath*:/flows");
//add test specific mocks
mockProperties.forEach((key,value) -> mock.setProperty(key, value));
+ //Tell the root context to use the Shib style property prefix over ${. The PropertySourcesPlaceholderConfigurer
+ //defined in the beans file does not have access to the MockProperties set by the test, this root context resolver does
+ //so we change this here. This will not work for bean import definitions, they are run with a standard resolver ${.
+ //hence why we use a custom flow builder (see super class).
((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).
- getPropertySources().addFirst(mock);
+ getPropertySources().addFirst(mock);
+ ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).setPlaceholderPrefix("%{");
+ ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).setPlaceholderSuffix("}");
+
}
@@ -326,6 +352,7 @@ public abstract class AbstractAuthnXmlFlowExecutionTests extends AbstractXmlFlow
* {@link AuthenticationContext}.
*
* @param forceAuthn force authentication
+ * @param addC14Context add the c14 context, yes or no.
* @return a profile request context.
*/
@Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final boolean forceAuthn,
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 d116228..6e2c8d6 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
@@ -113,17 +113,20 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
"idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
"idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
"idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config.xml",
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory");
setMockProperties(mockProperties);
final LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>();
inputMap.put("calledAsSubflow", true);
-
+
final FlowExecution flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
flowExecution.getConversationScope().put("opensamlProfileRequestContext", buildProfileRequestContext(false,true));
updateFlowExecution(flowExecution);
- flowExecution.start(inputMap, externalContext);
+ flowExecution.start(inputMap, externalContext);
+ //TODO: only check it has ended, should check correct state - but it does now allow this
+ //find a way to check last state of an ended flow.
assertFlowExecutionEnded();
}
@@ -142,7 +145,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
"idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
"idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
"idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config.xml",
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory");
setMockProperties(mockProperties);
@@ -168,12 +172,9 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
setSubflows(subflows);
setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
- final Map<String,String> mockProperties = Map.of(
- "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
- "idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
- "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
- "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
+ final Map<String,String> mockProperties = Map.of(
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory",
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
setMockProperties(mockProperties);
@@ -212,11 +213,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
setClientFactory(new MockDuoOIDCClientFactory_OK_Client());
final Map<String,String> mockProperties = Map.of(
- "idp.duo.oidc.redirectUri","http://localhost/authorization-callback",
- "idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
- "idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
- "idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory",
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config-multiple-integrations.xml");
setMockProperties(mockProperties);
@@ -260,7 +258,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
"idp.duo.oidc.apiHost","api-c9f24c5a.duosecurity.com",
"idp.duo.oidc.clientId","DIU6GEFWG5LIUBVV2M3P",
"idp.duo.oidc.secretKey","rFvDfPul27v3Wew2zb6xRPzAJewJ34MP2w8UitPh",
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config-custom-factory.xml");
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory",
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config-custom-factory.xml");
setMockProperties(mockProperties);
@@ -290,7 +289,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final Map<String,String> mockProperties = Map.of(
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config.xml",
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory");
setMockProperties(mockProperties);
@@ -342,6 +342,7 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
* principal set.
*
* @throws DuoClientException if the client can not be created.*/
+ //TODO: finish this, AFD is not checked in the dummy flow, and we are not checking auth result principals.
@Test
public void testDuoAuthnFlowFromAuthorizationCallbackWithRPC() throws DuoClientException {
@@ -352,7 +353,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final Map<String,String> mockProperties = Map.of(
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory",
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config.xml");
setMockProperties(mockProperties);
@@ -430,7 +432,8 @@ public class DuoAuthnFlowTest extends AbstractAuthnXmlFlowExecutionTests {
final Map<String,String> mockProperties = Map.of(
- "idp.duo.oidc.config.user","classpath:/conf/authn/duo-oidc-authn-config.xml");
+ "idp.duo.oidc.clientFactoryBean","shibboleth.authn.duo.OIDC.test.clientFactory",
+ "idp.duo.oidc.user.config.home","/conf/authn/duo-oidc-authn-config.xml");
setMockProperties(mockProperties);
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/package-info.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/package-info.java
new file mode 100644
index 0000000..8315aff
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/duo/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Duo OIDC 2FA login flow test classes. */
+package net.shibboleth.idp.plugin.authn.duo;
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/package-info.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/package-info.java
new file mode 100644
index 0000000..541280b
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/mock/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Duo OIDC 2FA login flow mocks. */
+package net.shibboleth.idp.plugin.authn.mock;
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomAbstractXmlFlowExecutionTests.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomAbstractXmlFlowExecutionTests.java
new file mode 100644
index 0000000..41a39e9
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomAbstractXmlFlowExecutionTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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.spring;
+
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.webflow.config.FlowDefinitionResource;
+import org.springframework.webflow.config.FlowDefinitionResourceFactory;
+import org.springframework.webflow.engine.builder.FlowBuilder;
+import org.springframework.webflow.engine.model.builder.DefaultFlowModelHolder;
+import org.springframework.webflow.engine.model.builder.FlowModelBuilder;
+import org.springframework.webflow.engine.model.builder.xml.XmlFlowModelBuilder;
+import org.springframework.webflow.engine.model.registry.FlowModelHolder;
+import org.springframework.webflow.engine.model.registry.FlowModelRegistry;
+import org.springframework.webflow.engine.model.registry.FlowModelRegistryImpl;
+import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecutionTests;
+import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
+
+/**
+ * Copy of the {@link AbstractXmlFlowExecutionTests} class, but using a custom {@link CustomFlowModelBuilder}
+ * to set the property placeholder prefix of {@literal %{}.
+ */
+public abstract class CustomAbstractXmlFlowExecutionTests extends AbstractExternalizedFlowExecutionTests {
+
+ private FlowModelRegistry flowModelRegistry = new FlowModelRegistryImpl();
+
+ /**
+ * Constructs a default XML flow execution test.
+ * @see #setName(String)
+ */
+ public CustomAbstractXmlFlowExecutionTests() {
+ super();
+ }
+
+ /**
+ * Constructs an XML flow execution test with given name.
+ * @param name the name of the test
+ */
+ public CustomAbstractXmlFlowExecutionTests(String name) {
+ super(name);
+ }
+
+ protected final FlowBuilder createFlowBuilder(FlowDefinitionResource resource) {
+ registerDependentFlowModels();
+ FlowModelBuilder modelBuilder = new XmlFlowModelBuilder(resource.getPath(), flowModelRegistry);
+ FlowModelHolder modelHolder = new DefaultFlowModelHolder(modelBuilder);
+ flowModelRegistry.registerFlowModel(resource.getId(), modelHolder);
+ return new CustomFlowModelFlowBuilder(modelHolder) {
+
+ protected void registerFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
+ registerMockFlowBeans(flowBeanFactory);
+ }
+
+ };
+ }
+
+ /**
+ * Template method subclasses may override to return pointers to "flow model resources" needed to build the
+ * definition of the flow being tested. Typically overridden when the flow being tested extends from another flow.
+ * Default returns null, assuming no inheritance.
+ * @param resourceFactory the resource factory
+ * @return the flow definition model resources
+ */
+ protected FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory) {
+ return null;
+ }
+
+ /**
+ * Template method subclasses may override to register mock implementations of services used locally by the flow
+ * being tested. By default, this method does nothing.
+ * @param flowBeanFactory the local flow bean factory, you may register mock services with it using
+ * {@link ConfigurableBeanFactory#registerSingleton(String, Object)}
+ */
+ protected void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
+ }
+
+ // internal helpers
+
+ private void registerDependentFlowModels() {
+ FlowDefinitionResource[] modelResources = getModelResources(getResourceFactory());
+ if (modelResources != null) {
+ for (FlowDefinitionResource modelResource : modelResources) {
+ FlowModelBuilder modelBuilder = new XmlFlowModelBuilder(modelResource.getPath(), flowModelRegistry);
+ flowModelRegistry.registerFlowModel(modelResource.getId(), new DefaultFlowModelHolder(modelBuilder));
+ }
+ }
+ }
+
+}
+
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowModelFlowBuilder.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowModelFlowBuilder.java
new file mode 100644
index 0000000..8371ba7
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowModelFlowBuilder.java
@@ -0,0 +1,1006 @@
+/*
+ * 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.spring;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.binding.convert.ConversionExecutionException;
+import org.springframework.binding.convert.ConversionExecutor;
+import org.springframework.binding.convert.service.RuntimeBindingConversionExecutor;
+import org.springframework.binding.expression.Expression;
+import org.springframework.binding.expression.ExpressionParser;
+import org.springframework.binding.expression.ParserContext;
+import org.springframework.binding.expression.support.FluentParserContext;
+import org.springframework.binding.mapping.Mapper;
+import org.springframework.binding.mapping.impl.DefaultMapper;
+import org.springframework.binding.mapping.impl.DefaultMapping;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.request.RequestScope;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+import org.springframework.webflow.action.EvaluateAction;
+import org.springframework.webflow.action.ExternalRedirectAction;
+import org.springframework.webflow.action.FlowDefinitionRedirectAction;
+import org.springframework.webflow.action.RenderAction;
+import org.springframework.webflow.action.SetAction;
+import org.springframework.webflow.action.ViewFactoryActionAdapter;
+import org.springframework.webflow.core.collection.AttributeMap;
+import org.springframework.webflow.core.collection.LocalAttributeMap;
+import org.springframework.webflow.core.collection.MutableAttributeMap;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.engine.FlowExecutionExceptionHandler;
+import org.springframework.webflow.engine.FlowVariable;
+import org.springframework.webflow.engine.History;
+import org.springframework.webflow.engine.SubflowAttributeMapper;
+import org.springframework.webflow.engine.TargetStateResolver;
+import org.springframework.webflow.engine.Transition;
+import org.springframework.webflow.engine.TransitionCriteria;
+import org.springframework.webflow.engine.VariableValueFactory;
+import org.springframework.webflow.engine.ViewVariable;
+import org.springframework.webflow.engine.builder.BinderConfiguration;
+import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
+import org.springframework.webflow.engine.builder.FlowBuilderContext;
+import org.springframework.webflow.engine.builder.FlowBuilderException;
+import org.springframework.webflow.engine.builder.model.FlowModelFlowBuilder;
+import org.springframework.webflow.engine.builder.support.AbstractFlowBuilder;
+import org.springframework.webflow.engine.model.AbstractActionModel;
+import org.springframework.webflow.engine.model.AbstractMappingModel;
+import org.springframework.webflow.engine.model.AbstractStateModel;
+import org.springframework.webflow.engine.model.ActionStateModel;
+import org.springframework.webflow.engine.model.AttributeModel;
+import org.springframework.webflow.engine.model.BeanImportModel;
+import org.springframework.webflow.engine.model.BinderModel;
+import org.springframework.webflow.engine.model.BindingModel;
+import org.springframework.webflow.engine.model.DecisionStateModel;
+import org.springframework.webflow.engine.model.EndStateModel;
+import org.springframework.webflow.engine.model.EvaluateModel;
+import org.springframework.webflow.engine.model.ExceptionHandlerModel;
+import org.springframework.webflow.engine.model.FlowModel;
+import org.springframework.webflow.engine.model.IfModel;
+import org.springframework.webflow.engine.model.InputModel;
+import org.springframework.webflow.engine.model.OutputModel;
+import org.springframework.webflow.engine.model.PersistenceContextModel;
+import org.springframework.webflow.engine.model.RenderModel;
+import org.springframework.webflow.engine.model.SecuredModel;
+import org.springframework.webflow.engine.model.SetModel;
+import org.springframework.webflow.engine.model.SubflowStateModel;
+import org.springframework.webflow.engine.model.TransitionModel;
+import org.springframework.webflow.engine.model.VarModel;
+import org.springframework.webflow.engine.model.ViewStateModel;
+import org.springframework.webflow.engine.model.builder.FlowModelBuilderException;
+import org.springframework.webflow.engine.model.registry.FlowModelHolder;
+import org.springframework.webflow.engine.support.ActionExecutingViewFactory;
+import org.springframework.webflow.engine.support.BeanFactoryVariableValueFactory;
+import org.springframework.webflow.engine.support.DefaultTransitionCriteria;
+import org.springframework.webflow.engine.support.GenericSubflowAttributeMapper;
+import org.springframework.webflow.engine.support.TransitionCriteriaChain;
+import org.springframework.webflow.engine.support.TransitionExecutingFlowExecutionExceptionHandler;
+import org.springframework.webflow.execution.Action;
+import org.springframework.webflow.execution.AnnotatedAction;
+import org.springframework.webflow.execution.RequestContext;
+import org.springframework.webflow.execution.ViewFactory;
+import org.springframework.webflow.scope.ConversationScope;
+import org.springframework.webflow.scope.FlashScope;
+import org.springframework.webflow.scope.FlowScope;
+import org.springframework.webflow.scope.ViewScope;
+import org.springframework.webflow.security.SecurityRule;
+
+/**
+ * Copy of {@link FlowModelFlowBuilder} for setting the property placeholder prefix of {@literal %{ }.
+ */
+public class CustomFlowModelFlowBuilder extends AbstractFlowBuilder {
+
+ private static final boolean IS_SPRING_FACES_PRESENT = ClassUtils.isPresent(
+ "org.springframework.faces.webflow.FlowActionListener", FlowModelFlowBuilder.class.getClassLoader());
+
+ public static final String VALIDATOR_FLOW_ATTR = FlowModelFlowBuilder.class.getSimpleName() + ".validator";
+
+ public static final String VALIDATION_HINT_RESOLVER_FLOW_ATTR =
+ FlowModelFlowBuilder.class.getSimpleName() + ".validationHintResolver";
+
+ private FlowModelHolder flowModelHolder;
+
+ private FlowModel flowModel;
+
+ private CustomLocalFlowBuilderContext localFlowBuilderContext;
+
+
+ /**
+ * Creates a flow builder that can build a {@link Flow} from a {@link FlowModel}.
+ * @param flowModelHolder the flow model holder
+ */
+ public CustomFlowModelFlowBuilder(FlowModelHolder flowModelHolder) {
+ Assert.notNull(flowModelHolder, "The FlowModelHolder is required");
+ this.flowModelHolder = flowModelHolder;
+ }
+
+ /**
+ * Initialize this builder. This could cause the builder to open a stream to an externalized resource representing
+ * the flow definition, for example.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ protected void doInit() throws FlowBuilderException {
+ try {
+ flowModel = flowModelHolder.getFlowModel();
+ initLocalFlowContext();
+ } catch (FlowModelBuilderException e) {
+ throw new FlowBuilderException("Unable to get the model for this flow", e);
+ }
+ if ("true".equals(flowModel.getAbstract())) {
+ throw new FlowBuilderException("Abstract flow models cannot be instantiated.");
+ }
+ }
+
+ protected Flow createFlow() {
+ String flowId = getContext().getFlowId();
+ AttributeMap<Object> flowAttributes = parseFlowMetaAttributes(flowModel);
+ flowAttributes = getContext().getFlowAttributes().union(flowAttributes);
+ if (IS_SPRING_FACES_PRESENT) {
+ flowAttributes.asMap().put(VALIDATOR_FLOW_ATTR, getLocalContext().getValidator());
+ flowAttributes.asMap().put(VALIDATION_HINT_RESOLVER_FLOW_ATTR,
+ getLocalContext().getValidationHintResolver());
+ }
+ Flow flow = getLocalContext().getFlowArtifactFactory().createFlow(flowId, flowAttributes);
+ flow.setApplicationContext(getLocalContext().getApplicationContext());
+ return flow;
+ }
+
+ /**
+ * Builds any variables initialized by the flow when it starts.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildVariables() throws FlowBuilderException {
+ if (flowModel.getVars() != null) {
+ for (VarModel varModel : flowModel.getVars()) {
+ getFlow().addVariable(parseFlowVariable(varModel));
+ }
+ }
+ }
+
+ /**
+ * Builds the input mapper responsible for mapping flow input on start.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildInputMapper() throws FlowBuilderException {
+ getFlow().setInputMapper(parseFlowInputMapper(flowModel.getInputs()));
+ }
+
+ /**
+ * Builds any start actions to execute when the flow starts.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildStartActions() throws FlowBuilderException {
+ getFlow().getStartActionList().addAll(parseActions(flowModel.getOnStartActions()));
+ }
+
+ /**
+ * Builds the states of the flow.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildStates() throws FlowBuilderException {
+ if (flowModel.getStates() == null) {
+ throw new FlowBuilderException("At least one state is required to build a Flow");
+ }
+ for (AbstractStateModel state : flowModel.getStates()) {
+ if (state instanceof ActionStateModel) {
+ parseAndAddActionState((ActionStateModel) state, getFlow());
+ } else if (state instanceof ViewStateModel) {
+ parseAndAddViewState((ViewStateModel) state, getFlow());
+ } else if (state instanceof DecisionStateModel) {
+ parseAndAddDecisionState((DecisionStateModel) state, getFlow());
+ } else if (state instanceof SubflowStateModel) {
+ parseAndAddSubflowState((SubflowStateModel) state, getFlow());
+ } else if (state instanceof EndStateModel) {
+ parseAndAddEndState((EndStateModel) state, getFlow());
+ }
+ }
+ if (flowModel.getStartStateId() != null) {
+ getFlow().setStartState(flowModel.getStartStateId());
+ }
+ }
+
+ /**
+ * Builds any transitions shared by all states of the flow.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildGlobalTransitions() throws FlowBuilderException {
+ getFlow().getGlobalTransitionSet().addAll(parseTransitions(flowModel.getGlobalTransitions()));
+ }
+
+ /**
+ * Builds any end actions to execute when the flow ends.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildEndActions() throws FlowBuilderException {
+ getFlow().getEndActionList().addAll(parseActions(flowModel.getOnEndActions()));
+ }
+
+ /**
+ * Builds the output mapper responsible for mapping flow output on end.
+ *
+ * @throws FlowBuilderException an exception occurred building the flow
+ */
+ public void buildOutputMapper() throws FlowBuilderException {
+ if (flowModel.getOutputs() != null) {
+ getFlow().setOutputMapper(parseFlowOutputMapper(flowModel.getOutputs()));
+ }
+ }
+
+ /**
+ * Creates and adds all exception handlers to the flow built by this builder.
+ *
+ * @throws FlowBuilderException an exception occurred building this flow
+ */
+ public void buildExceptionHandlers() throws FlowBuilderException {
+ getFlow().getExceptionHandlerSet()
+ .addAll(parseExceptionHandlers(flowModel.getExceptionHandlers(), flowModel.getGlobalTransitions()));
+ }
+
+ public boolean hasFlowChanged() {
+ return flowModelHolder.hasFlowModelChanged();
+ }
+
+ public String getFlowResourceString() {
+ return flowModelHolder.getFlowModelResource().getDescription();
+ }
+
+ /**
+ * Shutdown the builder, releasing any resources it holds. A new flow construction process should start with another
+ * call to the {@link #init(FlowBuilderContext)} method.
+ *
+ * @throws FlowBuilderException an exception occurred building this flow
+ */
+ protected void doDispose() throws FlowBuilderException {
+ flowModel = null;
+ setLocalContext(null);
+ }
+
+ // subclassing hooks
+
+ protected FlowModel getFlowModel() {
+ return flowModel;
+ }
+
+ protected CustomLocalFlowBuilderContext getLocalContext() {
+ return localFlowBuilderContext;
+ }
+
+ protected void setLocalContext(CustomLocalFlowBuilderContext localFlowBuilderContext) {
+ this.localFlowBuilderContext = localFlowBuilderContext;
+ }
+
+ /**
+ * Register beans in the bean factory local to the flow definition being built.
+ * <p>
+ * Subclasses may override this method to customize the population of the bean factory local to the flow definition
+ * being built; for example, to register mock implementations of services in a test environment.
+ *
+ * @param beanFactory the bean factory; register local beans with it using
+ * {@link ConfigurableBeanFactory#registerSingleton(String, Object)}
+ */
+ protected void registerFlowBeans(ConfigurableBeanFactory beanFactory) {
+ }
+
+ // internal helpers
+
+ private void initLocalFlowContext() {
+ Resource[] contextResources = parseContextResources(getFlowModel().getBeanImports());
+ GenericApplicationContext flowContext = createFlowApplicationContext(contextResources);
+ setLocalContext(new CustomLocalFlowBuilderContext(getContext(), flowContext));
+ }
+
+ private Resource[] parseContextResources(List<BeanImportModel> beanImports) {
+ if (beanImports != null && !beanImports.isEmpty()) {
+ Resource flowResource = flowModelHolder.getFlowModelResource();
+ List<Resource> resources = new ArrayList<>(beanImports.size());
+ for (BeanImportModel beanImport : getFlowModel().getBeanImports()) {
+ try {
+ resources.add(flowResource.createRelative(beanImport.getResource()));
+ } catch (IOException e) {
+ throw new FlowBuilderException(
+ "Could not access flow-relative artifact resource '" + beanImport.getResource() + "'", e);
+ }
+ }
+ return resources.toArray(new Resource[resources.size()]);
+ } else {
+ return new Resource[0];
+ }
+ }
+
+ private GenericApplicationContext createFlowApplicationContext(Resource[] resources) {
+ ApplicationContext parent = getContext().getApplicationContext();
+ GenericApplicationContext flowContext;
+ if (parent instanceof WebApplicationContext) {
+ GenericWebApplicationContext webContext = new GenericWebApplicationContext();
+ webContext.setServletContext(((WebApplicationContext) parent).getServletContext());
+ flowContext = webContext;
+ } else {
+ flowContext = new GenericApplicationContext();
+ }
+ //HERE is the setting of the property placeholder prefix.
+ flowContext.getEnvironment().setPlaceholderPrefix("%{");
+ flowContext.getEnvironment().setPlaceholderSuffix("}");
+ flowContext.setDisplayName("Flow ApplicationContext [" + getContext().getFlowId() + "]");
+ flowContext.setParent(parent);
+ flowContext.getBeanFactory().registerScope("request", new RequestScope());
+ flowContext.getBeanFactory().registerScope("flash", new FlashScope());
+ flowContext.getBeanFactory().registerScope("view", new ViewScope());
+ flowContext.getBeanFactory().registerScope("flow", new FlowScope());
+ flowContext.getBeanFactory().registerScope("conversation", new ConversationScope());
+
+ // Ensure the current ClassLoader is used, or otherwise setting the ResourceLoader would suppress it
+ ClassLoader classLoaderToUse = flowContext.getClassLoader();
+ flowContext.setClassLoader(classLoaderToUse);
+
+ Resource flowResource = flowModelHolder.getFlowModelResource();
+ flowContext.setResourceLoader(new CustomFlowRelativeResourceLoader(flowResource));
+
+
+ AnnotationConfigUtils.registerAnnotationConfigProcessors(flowContext);
+ new XmlBeanDefinitionReader(flowContext).loadBeanDefinitions(resources);
+ registerFlowBeans(flowContext.getBeanFactory());
+ registerMessageSource(flowContext, flowResource);
+
+ flowContext.refresh();
+
+ return flowContext;
+ }
+
+ private boolean isFlowInDevelopment() {
+ return getContext().getFlowAttributes().getBoolean("development", false);
+ }
+
+ private void registerMessageSource(GenericApplicationContext flowContext, Resource flowResource) {
+ boolean localMessageSourcePresent =
+ flowContext.containsLocalBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME);
+ if (!localMessageSourcePresent) {
+ Resource messageBundle;
+ try {
+ messageBundle = flowResource.createRelative("messages.properties");
+ } catch (IOException e) {
+ messageBundle = null;
+ }
+ if (messageBundle != null && messageBundle.exists()) {
+ BeanDefinitionBuilder builder =
+ BeanDefinitionBuilder.rootBeanDefinition(ReloadableResourceBundleMessageSource.class);
+ builder.addPropertyValue("basename", "messages");
+ if (isFlowInDevelopment()) {
+ builder.addPropertyValue("cacheSeconds", "0");
+ }
+ flowContext.registerBeanDefinition(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME,
+ builder.getBeanDefinition());
+ }
+ }
+ }
+
+ private AttributeMap<Object> parseFlowMetaAttributes(FlowModel flow) {
+ MutableAttributeMap<Object> flowAttributes = parseMetaAttributes(flow.getAttributes());
+ parseAndPutPersistenceContext(flow.getPersistenceContext(), flowAttributes);
+ parseAndPutSecured(flow.getSecured(), flowAttributes);
+ return flowAttributes;
+ }
+
+ private FlowVariable parseFlowVariable(VarModel var) {
+ Class<?> clazz = toClass(var.getClassName());
+ VariableValueFactory valueFactory = new BeanFactoryVariableValueFactory(clazz,
+ getFlow().getApplicationContext().getAutowireCapableBeanFactory());
+ return new FlowVariable(var.getName(), valueFactory);
+ }
+
+ private Mapper parseFlowInputMapper(List<InputModel> inputs) {
+ if (inputs != null && !inputs.isEmpty()) {
+ DefaultMapper inputMapper = new DefaultMapper();
+ for (InputModel inputModel : inputs) {
+ inputMapper.addMapping(parseFlowInputMapping(inputModel));
+ }
+ return inputMapper;
+ } else {
+ return null;
+ }
+ }
+
+ private DefaultMapping parseFlowInputMapping(InputModel input) {
+ ExpressionParser parser = getLocalContext().getExpressionParser();
+ String name = input.getName();
+ String value = null;
+ if (StringUtils.hasText(input.getValue())) {
+ value = input.getValue();
+ } else {
+ value = "flowScope." + name;
+ }
+ Expression source = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+ Expression target = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+ DefaultMapping mapping = new DefaultMapping(source, target);
+ parseAndSetMappingConversionExecutor(input, mapping);
+ parseAndSetMappingRequired(input, mapping);
+ return mapping;
+ }
+
+ private Mapper parseSubflowInputMapper(List<InputModel> inputs) {
+ if (inputs != null && !inputs.isEmpty()) {
+ DefaultMapper inputMapper = new DefaultMapper();
+ for (InputModel inputModel : inputs) {
+ inputMapper.addMapping(parseSubflowInputMapping(inputModel));
+ }
+ return inputMapper;
+ } else {
+ return null;
+ }
+ }
+
+ private DefaultMapping parseSubflowInputMapping(InputModel input) {
+ ExpressionParser parser = getLocalContext().getExpressionParser();
+ String name = input.getName();
+ String value = null;
+ if (StringUtils.hasText(input.getValue())) {
+ value = input.getValue();
+ } else {
+ value = name;
+ }
+ Expression source = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+ Expression target = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+ DefaultMapping mapping = new DefaultMapping(source, target);
+ parseAndSetMappingConversionExecutor(input, mapping);
+ parseAndSetMappingRequired(input, mapping);
+ return mapping;
+ }
+
+ private Mapper parseFlowOutputMapper(List<OutputModel> outputs) {
+ if (outputs != null && !outputs.isEmpty()) {
+ DefaultMapper outputMapper = new DefaultMapper();
+ for (OutputModel outputModel : outputs) {
+ outputMapper.addMapping(parseFlowOutputMapping(outputModel));
+ }
+ return outputMapper;
+ } else {
+ return null;
+ }
+ }
+
+ private DefaultMapping parseFlowOutputMapping(OutputModel output) {
+ ExpressionParser parser = getLocalContext().getExpressionParser();
+ String name = output.getName();
+ String value = null;
+ if (StringUtils.hasText(output.getValue())) {
+ value = output.getValue();
+ } else {
+ value = name;
+ }
+ Expression source = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+ Expression target = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+ DefaultMapping mapping = new DefaultMapping(source, target);
+ parseAndSetMappingConversionExecutor(output, mapping);
+ parseAndSetMappingRequired(output, mapping);
+ return mapping;
+ }
+
+ private Mapper parseSubflowOutputMapper(List<OutputModel> outputs) {
+ if (outputs != null && !outputs.isEmpty()) {
+ DefaultMapper outputMapper = new DefaultMapper();
+ for (OutputModel outputModel : outputs) {
+ outputMapper.addMapping(parseSubflowOutputMapping(outputModel));
+ }
+ return outputMapper;
+ } else {
+ return null;
+ }
+ }
+
+ private DefaultMapping parseSubflowOutputMapping(OutputModel output) {
+ ExpressionParser parser = getLocalContext().getExpressionParser();
+ String name = output.getName();
+ String value = null;
+ if (StringUtils.hasText(output.getValue())) {
+ value = output.getValue();
+ } else {
+ value = "flowScope." + name;
+ }
+ Expression source = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+ Expression target = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+ DefaultMapping mapping = new DefaultMapping(source, target);
+ parseAndSetMappingConversionExecutor(output, mapping);
+ parseAndSetMappingRequired(output, mapping);
+ return mapping;
+ }
+
+ private void parseAndSetMappingConversionExecutor(AbstractMappingModel mappingModel, DefaultMapping mapping) {
+ if (StringUtils.hasText(mappingModel.getType())) {
+ Class<?> type = toClass(mappingModel.getType());
+ ConversionExecutor typeConverter =
+ new RuntimeBindingConversionExecutor(type, getLocalContext().getConversionService());
+ mapping.setTypeConverter(typeConverter);
+ }
+ }
+
+ private void parseAndSetMappingRequired(AbstractMappingModel mappingModel, DefaultMapping mapping) {
+ if (StringUtils.hasText(mappingModel.getRequired())) {
+ boolean required = ((Boolean) fromStringTo(Boolean.class).execute(mappingModel.getRequired()));
+ mapping.setRequired(required);
+ }
+ }
+
+ private void parseAndAddViewState(ViewStateModel state, Flow flow) {
+ ViewFactory viewFactory = parseViewFactory(state.getView(), state.getId(), false, state.getBinder());
+ Boolean redirect = null;
+ if (StringUtils.hasText(state.getRedirect())) {
+ redirect = (Boolean) fromStringTo(Boolean.class).execute(state.getRedirect());
+ }
+ boolean popup = false;
+ if (StringUtils.hasText(state.getPopup())) {
+ popup = ((Boolean) fromStringTo(Boolean.class).execute(state.getPopup()));
+ }
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+ if (state.getModel() != null) {
+ attributes.put("model", getLocalContext().getExpressionParser().parseExpression(state.getModel(),
+ new FluentParserContext().evaluate(RequestContext.class)));
+ }
+ if (state.getValidationHints() != null) {
+ attributes.put("validationHints", getLocalContext().getExpressionParser().parseExpression(
+ state.getValidationHints(), new FluentParserContext().evaluate(RequestContext.class)));
+ }
+ parseAndPutSecured(state.getSecured(), attributes);
+ getLocalContext().getFlowArtifactFactory().createViewState(state.getId(), flow,
+ parseViewVariables(state.getVars()), parseActions(state.getOnEntryActions()), viewFactory, redirect,
+ popup, parseActions(state.getOnRenderActions()), parseTransitions(state.getTransitions()),
+ parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+ parseActions(state.getOnExitActions()), attributes);
+ }
+
+ private void parseAndAddActionState(ActionStateModel state, Flow flow) {
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+ parseAndPutSecured(state.getSecured(), attributes);
+ getLocalContext().getFlowArtifactFactory().createActionState(state.getId(), flow,
+ parseActions(state.getOnEntryActions()), parseActions(state.getActions()),
+ parseTransitions(state.getTransitions()),
+ parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+ parseActions(state.getOnExitActions()), attributes);
+ }
+
+ private void parseAndAddDecisionState(DecisionStateModel state, Flow flow) {
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+ parseAndPutSecured(state.getSecured(), attributes);
+ getLocalContext().getFlowArtifactFactory().createDecisionState(state.getId(), flow,
+ parseActions(state.getOnEntryActions()), parseIfs(state.getIfs()),
+ parseExceptionHandlers(state.getExceptionHandlers(), null), parseActions(state.getOnExitActions()),
+ attributes);
+ }
+
+ private void parseAndAddSubflowState(SubflowStateModel state, Flow flow) {
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+ parseAndPutSecured(state.getSecured(), attributes);
+ getLocalContext().getFlowArtifactFactory().createSubflowState(state.getId(), flow,
+ parseActions(state.getOnEntryActions()), parseSubflowExpression(state.getSubflow()),
+ parseSubflowAttributeMapper(state), parseTransitions(state.getTransitions()),
+ parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+ parseActions(state.getOnExitActions()), attributes);
+ }
+
+ private void parseAndAddEndState(EndStateModel state, Flow flow) {
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+ if (StringUtils.hasText(state.getCommit())) {
+ attributes.put("commit", fromStringTo(Boolean.class).execute(state.getCommit()));
+ }
+ parseAndPutSecured(state.getSecured(), attributes);
+ Action finalResponseAction;
+ ViewFactory viewFactory = parseViewFactory(state.getView(), state.getId(), true, null);
+ if (viewFactory != null) {
+ finalResponseAction = new ViewFactoryActionAdapter(viewFactory);
+ } else {
+ finalResponseAction = null;
+ }
+ getLocalContext().getFlowArtifactFactory().createEndState(state.getId(), flow,
+ parseActions(state.getOnEntryActions()), finalResponseAction, parseFlowOutputMapper(state.getOutputs()),
+ parseExceptionHandlers(state.getExceptionHandlers(), null), attributes);
+ }
+
+ private ViewFactory parseViewFactory(String view, String stateId, boolean endState, BinderModel binderModel) {
+ if (!StringUtils.hasText(view)) {
+ if (endState) {
+ return null;
+ } else {
+ view = getLocalContext().getViewFactoryCreator().getViewIdByConvention(stateId);
+ Expression viewId = getLocalContext().getExpressionParser().parseExpression(view,
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+ return createViewFactory(viewId, binderModel);
+ }
+ } else if (view.startsWith("externalRedirect:")) {
+ String encodedUrl = view.substring("externalRedirect:".length());
+ Expression externalUrl = getLocalContext().getExpressionParser().parseExpression(encodedUrl,
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+ return new ActionExecutingViewFactory(new ExternalRedirectAction(externalUrl));
+ } else if (view.startsWith("flowRedirect:")) {
+ String flowRedirect = view.substring("flowRedirect:".length());
+ Expression expression = getLocalContext().getExpressionParser().parseExpression(flowRedirect,
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+ return new ActionExecutingViewFactory(new FlowDefinitionRedirectAction(expression));
+ } else {
+ Expression viewId = getLocalContext().getExpressionParser().parseExpression(view,
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+ return createViewFactory(viewId, binderModel);
+ }
+ }
+
+ private ViewFactory createViewFactory(Expression viewId, BinderModel binderModel) {
+ BinderConfiguration binderConfiguration = createBinderConfiguration(binderModel);
+ return getLocalContext().getViewFactoryCreator().createViewFactory(viewId,
+ getLocalContext().getExpressionParser(), getLocalContext().getConversionService(), binderConfiguration,
+ getLocalContext().getValidator(), getLocalContext().getValidationHintResolver());
+ }
+
+ private BinderConfiguration createBinderConfiguration(BinderModel binderModel) {
+ if (binderModel != null && binderModel.getBindings() != null) {
+ BinderConfiguration binderConfiguration = new BinderConfiguration();
+ List<BindingModel> bindings = binderModel.getBindings();
+ for (BindingModel bindingModel : bindings) {
+ boolean required = false;
+ if (StringUtils.hasText(bindingModel.getRequired())) {
+ required = ((Boolean) fromStringTo(Boolean.class).execute(bindingModel.getRequired()));
+ }
+ Binding binding = new Binding(bindingModel.getProperty(), bindingModel.getConverter(), required);
+ binderConfiguration.addBinding(binding);
+ }
+ return binderConfiguration;
+ } else {
+ return null;
+ }
+ }
+
+ private ViewVariable[] parseViewVariables(List<VarModel> vars) {
+ if (vars != null && !vars.isEmpty()) {
+ List<ViewVariable> variables = new ArrayList<>(vars.size());
+ for (VarModel varModel : vars) {
+ variables.add(parseViewVariable(varModel));
+ }
+ return variables.toArray(new ViewVariable[variables.size()]);
+ } else {
+ return new ViewVariable[0];
+ }
+ }
+
+ private ViewVariable parseViewVariable(VarModel var) {
+ Class<?> clazz = toClass(var.getClassName());
+ VariableValueFactory valueFactory = new BeanFactoryVariableValueFactory(clazz,
+ getFlow().getApplicationContext().getAutowireCapableBeanFactory());
+ return new ViewVariable(var.getName(), valueFactory);
+ }
+
+ private Transition[] parseIfs(List<IfModel> ifModels) {
+ if (ifModels != null && !ifModels.isEmpty()) {
+ List<Transition> transitions = new ArrayList<>(ifModels.size());
+ for (IfModel ifModel : ifModels) {
+ transitions.addAll(Arrays.asList(parseIf(ifModel)));
+ }
+ return transitions.toArray(new Transition[transitions.size()]);
+ } else {
+ return new Transition[0];
+ }
+ }
+
+ private Transition[] parseIf(IfModel ifModel) {
+ Transition thenTransition = parseThen(ifModel);
+ if (StringUtils.hasText(ifModel.getElse())) {
+ Transition elseTransition = parseElse(ifModel);
+ return new Transition[] {thenTransition, elseTransition};
+ } else {
+ return new Transition[] {thenTransition};
+ }
+ }
+
+ private Transition parseThen(IfModel ifModel) {
+ Expression test = getLocalContext().getExpressionParser().parseExpression(ifModel.getTest(),
+ new FluentParserContext().evaluate(RequestContext.class).expectResult(Boolean.class));
+ TransitionCriteria matchingCriteria = new DefaultTransitionCriteria(test);
+ TargetStateResolver targetStateResolver =
+ (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(ifModel.getThen());
+ return getLocalContext().getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, null,
+ null);
+ }
+
+ private Transition parseElse(IfModel ifModel) {
+ TargetStateResolver stateResolver =
+ (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(ifModel.getElse());
+ return getLocalContext().getFlowArtifactFactory().createTransition(stateResolver, null, null, null);
+ }
+
+ private Expression parseSubflowExpression(String subflow) {
+ Expression subflowId = getLocalContext().getExpressionParser().parseExpression(subflow,
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+ return new CustomSubflowExpression(subflowId, getLocalContext().getFlowDefinitionLocator());
+ }
+
+ private SubflowAttributeMapper parseSubflowAttributeMapper(SubflowStateModel state) {
+ if (StringUtils.hasText(state.getSubflowAttributeMapper())) {
+ String beanId = state.getSubflowAttributeMapper();
+ return getLocalContext().getApplicationContext().getBean(beanId, SubflowAttributeMapper.class);
+ } else {
+ Mapper inputMapper = parseSubflowInputMapper(state.getInputs());
+ Mapper outputMapper = parseSubflowOutputMapper(state.getOutputs());
+ return new GenericSubflowAttributeMapper(inputMapper, outputMapper);
+ }
+ }
+
+ private FlowExecutionExceptionHandler[] parseExceptionHandlers(List<ExceptionHandlerModel> modelExceptionHandlers,
+ List<TransitionModel> modelTransitions) {
+ FlowExecutionExceptionHandler[] transitionExecutingHandlers =
+ parseTransitionExecutingExceptionHandlers(modelTransitions);
+ FlowExecutionExceptionHandler[] customHandlers = parseCustomExceptionHandlers(modelExceptionHandlers);
+ FlowExecutionExceptionHandler[] exceptionHandlers =
+ new FlowExecutionExceptionHandler[transitionExecutingHandlers.length + customHandlers.length];
+ System.arraycopy(transitionExecutingHandlers, 0, exceptionHandlers, 0, transitionExecutingHandlers.length);
+ System.arraycopy(customHandlers, 0, exceptionHandlers, transitionExecutingHandlers.length,
+ customHandlers.length);
+ return exceptionHandlers;
+ }
+
+ private FlowExecutionExceptionHandler[]
+ parseTransitionExecutingExceptionHandlers(List<TransitionModel> transitionModels) {
+ if (transitionModels != null && !transitionModels.isEmpty()) {
+ List<FlowExecutionExceptionHandler> exceptionHandlers = new ArrayList<>(transitionModels.size());
+ for (TransitionModel model : transitionModels) {
+ if (StringUtils.hasText(model.getOnException())) {
+ if (model.getSecured() != null) {
+ throw new FlowBuilderException("Exception based transitions cannot be secured");
+ }
+ exceptionHandlers.add(parseTransitionExecutingExceptionHandler(model));
+ }
+ }
+ return exceptionHandlers.toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]);
+ } else {
+ return new FlowExecutionExceptionHandler[0];
+ }
+ }
+
+ private FlowExecutionExceptionHandler parseTransitionExecutingExceptionHandler(TransitionModel transition) {
+ TransitionExecutingFlowExecutionExceptionHandler handler =
+ new TransitionExecutingFlowExecutionExceptionHandler();
+ Class<Throwable> exceptionClass = toClass(transition.getOnException(), Throwable.class);
+ TargetStateResolver targetStateResolver =
+ (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(transition.getTo());
+ handler.add(exceptionClass, targetStateResolver);
+ handler.getActionList().addAll(parseActions(transition.getActions()));
+ return handler;
+ }
+
+ private FlowExecutionExceptionHandler[]
+ parseCustomExceptionHandlers(List<ExceptionHandlerModel> exceptionHandlerModels) {
+ if (exceptionHandlerModels != null && !exceptionHandlerModels.isEmpty()) {
+ List<FlowExecutionExceptionHandler> exceptionHandlers = new ArrayList<>(exceptionHandlerModels.size());
+ for (ExceptionHandlerModel exceptionHandlerModel : exceptionHandlerModels) {
+ exceptionHandlers.add(parseCustomExceptionHandler(exceptionHandlerModel));
+ }
+ return exceptionHandlers.toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]);
+ } else {
+ return new FlowExecutionExceptionHandler[0];
+ }
+ }
+
+ private FlowExecutionExceptionHandler parseCustomExceptionHandler(ExceptionHandlerModel exceptionHandler) {
+ return getLocalContext().getApplicationContext().getBean(exceptionHandler.getBean(),
+ FlowExecutionExceptionHandler.class);
+ }
+
+ private Transition[] parseTransitions(List<TransitionModel> transitionModels) {
+ if (transitionModels != null && !transitionModels.isEmpty()) {
+ List<Transition> transitions = new ArrayList<>(transitionModels.size());
+ if (transitionModels != null) {
+ for (TransitionModel transition : transitionModels) {
+ if (!StringUtils.hasText(transition.getOnException())) {
+ transitions.add(parseTransition(transition));
+ }
+ }
+ }
+ return transitions.toArray(new Transition[transitions.size()]);
+ } else {
+ return new Transition[0];
+ }
+ }
+
+ private Transition parseTransition(TransitionModel transition) {
+ TransitionCriteria matchingCriteria =
+ (TransitionCriteria) fromStringTo(TransitionCriteria.class).execute(transition.getOn());
+ TargetStateResolver stateResolver =
+ (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(transition.getTo());
+ TransitionCriteria executionCriteria =
+ TransitionCriteriaChain.criteriaChainFor(parseActions(transition.getActions()));
+ MutableAttributeMap<Object> attributes = parseMetaAttributes(transition.getAttributes());
+ if (StringUtils.hasText(transition.getBind())) {
+ attributes.put("bind", fromStringTo(Boolean.class).execute(transition.getBind()));
+ }
+ if (StringUtils.hasText(transition.getValidate())) {
+ attributes.put("validate", fromStringTo(Boolean.class).execute(transition.getValidate()));
+ }
+ if (StringUtils.hasText(transition.getValidationHints())) {
+ attributes.put("validationHints", getLocalContext().getExpressionParser().parseExpression(
+ transition.getValidationHints(), new FluentParserContext().evaluate(RequestContext.class)));
+ }
+ if (StringUtils.hasText(transition.getHistory())) {
+ attributes.put("history", fromStringTo(History.class).execute(transition.getHistory().toUpperCase()));
+ }
+ parseAndPutSecured(transition.getSecured(), attributes);
+ return getLocalContext().getFlowArtifactFactory().createTransition(stateResolver, matchingCriteria,
+ executionCriteria, attributes);
+ }
+
+ private Action[] parseActions(List<AbstractActionModel> actionModels) {
+ if (actionModels != null && !actionModels.isEmpty()) {
+ List<AnnotatedAction> actions = new ArrayList<>(actionModels.size());
+ for (AbstractActionModel actionModel : actionModels) {
+ Action action;
+ if (actionModel instanceof EvaluateModel) {
+ action = parseEvaluateAction((EvaluateModel) actionModel);
+ } else if (actionModel instanceof RenderModel) {
+ action = parseRenderAction((RenderModel) actionModel);
+ } else if (actionModel instanceof SetModel) {
+ action = parseSetAction((SetModel) actionModel);
+ } else {
+ action = null;
+ }
+ if (action != null) {
+ AnnotatedAction annotatedAction = new AnnotatedAction(action);
+ annotatedAction.getAttributes().putAll(parseMetaAttributes(actionModel.getAttributes()));
+ actions.add(annotatedAction);
+ }
+ }
+ return actions.toArray(new Action[actions.size()]);
+ } else {
+ return new Action[0];
+ }
+ }
+
+ private Action parseEvaluateAction(EvaluateModel evaluate) {
+ FluentParserContext evaluateExpressionParserContext = new FluentParserContext().evaluate(RequestContext.class);
+ if (StringUtils.hasText(evaluate.getResultType())) {
+ evaluateExpressionParserContext.expectResult(toClass(evaluate.getResultType()));
+ }
+ Expression evaluateExpression = getLocalContext().getExpressionParser()
+ .parseExpression(evaluate.getExpression(), evaluateExpressionParserContext);
+ Expression resultExpression = null;
+ if (StringUtils.hasText(evaluate.getResult())) {
+ resultExpression = getLocalContext().getExpressionParser().parseExpression(evaluate.getResult(),
+ new FluentParserContext().evaluate(RequestContext.class));
+ }
+ return new EvaluateAction(evaluateExpression, resultExpression);
+ }
+
+ private Action parseRenderAction(RenderModel render) {
+ String[] fragmentExpressionStrings = StringUtils.commaDelimitedListToStringArray(render.getFragments());
+ fragmentExpressionStrings = StringUtils.trimArrayElements(fragmentExpressionStrings);
+ ParserContext context =
+ new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class);
+ Expression[] fragments = new Expression[fragmentExpressionStrings.length];
+ for (int i = 0; i < fragmentExpressionStrings.length; i++) {
+ String fragment = fragmentExpressionStrings[i];
+ fragments[i] = getLocalContext().getExpressionParser().parseExpression(fragment, context);
+ }
+ return new RenderAction(fragments);
+ }
+
+ private Action parseSetAction(SetModel set) {
+ Expression nameExpression = getLocalContext().getExpressionParser().parseExpression(set.getName(),
+ new FluentParserContext().evaluate(RequestContext.class));
+ FluentParserContext valueParserContext = new FluentParserContext().evaluate(RequestContext.class);
+ if (StringUtils.hasText(set.getType())) {
+ valueParserContext.expectResult(toClass(set.getType()));
+ }
+ Expression valueExpression =
+ getLocalContext().getExpressionParser().parseExpression(set.getValue(), valueParserContext);
+ return new SetAction(nameExpression, valueExpression);
+ }
+
+ private MutableAttributeMap<Object> parseMetaAttributes(List<AttributeModel> attributeModels) {
+ if (attributeModels != null && !attributeModels.isEmpty()) {
+ LocalAttributeMap<Object> attributes = new LocalAttributeMap<>();
+ for (AttributeModel attributeModel : attributeModels) {
+ parseAndPutMetaAttribute(attributeModel, attributes);
+ }
+ return attributes;
+ } else {
+ return new LocalAttributeMap<>();
+ }
+ }
+
+ private void parseAndPutMetaAttribute(AttributeModel attribute, MutableAttributeMap<Object> attributes) {
+ String name = attribute.getName();
+ String value = attribute.getValue();
+ attributes.put(name, parseAttributeValueIfNecessary(attribute, value));
+ }
+
+ private Object parseAttributeValueIfNecessary(AttributeModel attribute, String stringValue) {
+ if (StringUtils.hasText(attribute.getType())) {
+ Class<?> targetClass = toClass(attribute.getType());
+ return fromStringTo(targetClass).execute(stringValue);
+ } else {
+ return stringValue;
+ }
+ }
+
+ private void parseAndPutPersistenceContext(PersistenceContextModel persistenceContext,
+ MutableAttributeMap<Object> attributes) {
+ if (persistenceContext != null) {
+ attributes.put("persistenceContext", true);
+ }
+ }
+
+ private void parseAndPutSecured(SecuredModel secured, MutableAttributeMap<Object> attributes) {
+ if (secured != null) {
+ SecurityRule rule = new SecurityRule();
+ rule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(secured.getAttributes()));
+ String comparisonType = secured.getMatch();
+ if ("any".equals(comparisonType)) {
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ } else if ("all".equals(comparisonType)) {
+ rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+ } else {
+ // default to any
+ rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+ }
+ attributes.put(SecurityRule.SECURITY_ATTRIBUTE_NAME, rule);
+ }
+ }
+
+ private ConversionExecutor fromStringTo(Class<?> targetType) throws ConversionExecutionException {
+ return getLocalContext().getConversionService().getConversionExecutor(String.class, targetType);
+ }
+
+ private Class<?> toClass(String name) {
+ Class<?> clazz = getLocalContext().getConversionService().getClassForAlias(name);
+ if (clazz != null) {
+ return clazz;
+ } else {
+ try {
+ ClassLoader classLoader = getLocalContext().getApplicationContext().getClassLoader();
+ return ClassUtils.forName(name, classLoader);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Unable to load class '" + name + "'");
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> Class<T> toClass(String name, Class<T> superType) {
+ Class<?> clazz = toClass(name);
+ Assert.isAssignable(superType, clazz);
+ return (Class<T>) clazz;
+ }
+
+ public String toString() {
+ return new ToStringCreator(this).append("flowModelResource", flowModelHolder.getFlowModelResource()).toString();
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowRelativeResourceLoader.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowRelativeResourceLoader.java
new file mode 100644
index 0000000..46eb616
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomFlowRelativeResourceLoader.java
@@ -0,0 +1,56 @@
+/*
+ * 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.spring;
+
+import java.io.IOException;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+
+public class CustomFlowRelativeResourceLoader implements ResourceLoader {
+
+ private Resource flowResource;
+
+ public CustomFlowRelativeResourceLoader(Resource resource) {
+ this.flowResource = resource;
+ }
+
+ public ClassLoader getClassLoader() {
+ return flowResource.getClass().getClassLoader();
+ }
+
+ public Resource getResource(String location) {
+ if (location.startsWith(CLASSPATH_URL_PREFIX)) {
+ return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
+ } else {
+ return createFlowRelativeResource(location);
+ }
+ }
+
+ private Resource createFlowRelativeResource(String location) {
+ try {
+ return flowResource.createRelative(location);
+ } catch (IOException e) {
+ IllegalArgumentException iae = new IllegalArgumentException(
+ "Unable to access a flow relative resource at location '" + location + "'");
+ iae.initCause(e);
+ throw iae;
+ }
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomLocalFlowBuilderContext.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomLocalFlowBuilderContext.java
new file mode 100644
index 0000000..c98b784
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomLocalFlowBuilderContext.java
@@ -0,0 +1,110 @@
+/*
+ * 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.spring;
+
+import org.springframework.binding.convert.ConversionService;
+import org.springframework.binding.expression.ExpressionParser;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.validation.Validator;
+import org.springframework.webflow.core.collection.AttributeMap;
+import org.springframework.webflow.definition.registry.FlowDefinitionLocator;
+import org.springframework.webflow.engine.builder.FlowArtifactFactory;
+import org.springframework.webflow.engine.builder.FlowBuilderContext;
+import org.springframework.webflow.engine.builder.ViewFactoryCreator;
+import org.springframework.webflow.validation.ValidationHintResolver;
+
+public class CustomLocalFlowBuilderContext {
+
+ private FlowBuilderContext parent;
+
+ private ApplicationContext localFlowContext;
+
+ public CustomLocalFlowBuilderContext(FlowBuilderContext parent, GenericApplicationContext localFlowContext) {
+ this.parent = parent;
+ this.localFlowContext = localFlowContext;
+ }
+
+ public ApplicationContext getApplicationContext() {
+ return localFlowContext;
+ }
+
+ public String getFlowId() {
+ return parent.getFlowId();
+ }
+
+ public AttributeMap<Object> getFlowAttributes() {
+ return parent.getFlowAttributes();
+ }
+
+ public FlowDefinitionLocator getFlowDefinitionLocator() {
+ if (localFlowContext.containsLocalBean("flowRegistry")) {
+ return localFlowContext.getBean("flowRegistry", FlowDefinitionLocator.class);
+ } else {
+ return parent.getFlowDefinitionLocator();
+ }
+ }
+
+ public FlowArtifactFactory getFlowArtifactFactory() {
+ if (localFlowContext.containsLocalBean("flowArtifactFactory")) {
+ return localFlowContext.getBean("flowArtifactFactory", FlowArtifactFactory.class);
+ } else {
+ return parent.getFlowArtifactFactory();
+ }
+ }
+
+ public ConversionService getConversionService() {
+ if (localFlowContext.containsLocalBean("conversionService")) {
+ return localFlowContext.getBean("conversionService", ConversionService.class);
+ } else {
+ return parent.getConversionService();
+ }
+ }
+
+ public ViewFactoryCreator getViewFactoryCreator() {
+ if (localFlowContext.containsLocalBean("viewFactoryCreator")) {
+ return localFlowContext.getBean("viewFactoryCreator", ViewFactoryCreator.class);
+ } else {
+ return parent.getViewFactoryCreator();
+ }
+ }
+
+ public ExpressionParser getExpressionParser() {
+ if (localFlowContext.containsLocalBean("expressionParser")) {
+ return localFlowContext.getBean("expressionParser", ExpressionParser.class);
+ } else {
+ return parent.getExpressionParser();
+ }
+ }
+
+ public Validator getValidator() {
+ if (localFlowContext.containsLocalBean("validator")) {
+ return localFlowContext.getBean("validator", Validator.class);
+ } else {
+ return parent.getValidator();
+ }
+ }
+
+ public ValidationHintResolver getValidationHintResolver() {
+ if (localFlowContext.containsLocalBean("validationHintResolver")) {
+ return localFlowContext.getBean("validationHintResolver", ValidationHintResolver.class);
+ } else {
+ return parent.getValidationHintResolver();
+ }
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomSubflowExpression.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomSubflowExpression.java
new file mode 100644
index 0000000..98f8513
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/CustomSubflowExpression.java
@@ -0,0 +1,51 @@
+/*
+ * 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.spring;
+
+import org.springframework.binding.expression.EvaluationException;
+import org.springframework.binding.expression.Expression;
+import org.springframework.webflow.definition.registry.FlowDefinitionLocator;
+
+public class CustomSubflowExpression implements Expression {
+
+ private Expression subflowId;
+
+ private FlowDefinitionLocator flowDefinitionLocator;
+
+ public CustomSubflowExpression(Expression subflowId, FlowDefinitionLocator flowDefinitionLocator) {
+ this.subflowId = subflowId;
+ this.flowDefinitionLocator = flowDefinitionLocator;
+ }
+
+ public Object getValue(Object context) throws EvaluationException {
+ String subflowId = (String) this.subflowId.getValue(context);
+ return flowDefinitionLocator.getFlowDefinition(subflowId);
+ }
+
+ public void setValue(Object context, Object value) throws EvaluationException {
+ throw new UnsupportedOperationException("Cannot set a subflow expression");
+ }
+
+ public Class<?> getValueType(Object context) {
+ return null;
+ }
+
+ public String getExpressionString() {
+ return null;
+ }
+
+}
diff --git a/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/package-info.java b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/package-info.java
new file mode 100644
index 0000000..ce7e8ae
--- /dev/null
+++ b/idp-duo-impl/src/test/java/net/shibboleth/idp/plugin/authn/spring/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Duo OIDC 2FA login flow custom spring test classes. */
+package net.shibboleth.idp.plugin.authn.spring;
diff --git a/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-custom-factory.xml b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-custom-factory.xml
index 9654f72..4a2969f 100644
--- a/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-custom-factory.xml
+++ b/idp-duo-impl/src/test/resources/conf/authn/duo-oidc-authn-config-custom-factory.xml
@@ -10,7 +10,7 @@
<!-- Add a custom Duo client factory in the same way a user would-->
- <bean id="shibboleth.authn.duo.OIDC.clientFactory"
+ <bean id="shibboleth.authn.duo.OIDC.test.clientFactory"
class="net.shibboleth.idp.plugin.authn.mock.MockDuoOIDCClientFactory_OK_Client" scope="singleton">
</bean>
diff --git a/idp-duo-impl/src/test/resources/logback-test.xml b/idp-duo-impl/src/test/resources/logback-test.xml
index 62dbda8..6ddfb8a 100644
--- a/idp-duo-impl/src/test/resources/logback-test.xml
+++ b/idp-duo-impl/src/test/resources/logback-test.xml
@@ -25,6 +25,9 @@
<appender-ref ref="STDOUT" />
</logger>
+ <logger name="org.springframework.beans.factory.xml.XmlBeanDefinitionReader" level="TRACE" additivity="false">
+ <appender-ref ref="STDOUT" />
+ </logger>
<logger name="org.springframework.binding.mapping.impl" level="DEBUG" additivity="false">
<appender-ref ref="STDOUT" />
diff --git a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
index 26bbcaf..ef0872f 100644
--- a/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
+++ b/idp-duo-native-client-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/sdk/impl/DuoSDKClientFactory.java
@@ -12,6 +12,7 @@ import net.shbboleth.idp.plugin.authn.duo.DuoClientException;
import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClient;
import net.shbboleth.idp.plugin.authn.duo.DuoOIDCClientFactory;
import net.shbboleth.idp.plugin.authn.duo.DuoOIDCIntegration;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
@@ -28,12 +29,19 @@ public class DuoSDKClientFactory implements DuoOIDCClientFactory{
/**
* Sets the list of CA certificate pins used to verify the Duo client connection
- * to the API host.
+ * to the API host. If null, the client will use the internal defaults.
*
* @param certs the list of certificate pins.
*/
public void setCaCerts(@Nullable final List<String> certs) {
- caCerts = certs;
+ //check if null, as the native duo client uses the internal defaults if null
+ //but would respect an empty list.
+ if (certs != null) {
+ caCerts = List.copyOf(StringSupport.normalizeStringCollection(certs));
+ } else {
+ caCerts = null;
+ }
+
}
@Override
diff --git a/idp-duo-native-client-impl/src/main/resources/duo-client-factory-bean.xml b/idp-duo-native-client-impl/src/main/resources/duo-client-factory-bean.xml
new file mode 100644
index 0000000..6aa4760
--- /dev/null
+++ b/idp-duo-native-client-impl/src/main/resources/duo-client-factory-bean.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+ default-init-method="initialize" default-destroy-method="destroy">
+
+
+ <bean id="shibboleth.authn.duo.OIDC.native.sdk.clientFactory"
+ class="net.shibboleth.idp.plugin.authn.duo.sdk.impl.DuoSDKClientFactory" scope="singleton"
+ p:caCerts="#{getObject('shibboleth.authn.duo.OIDC.native.sdk.caCerts') ?: getObject('shibboleth.authn.duo.OIDC.native.sdk.defaultCaCerts')}">
+
+ </bean>
+
+ <!-- The Default set of CA Certificate Pins used by the Duo SDK -->
+ <util:list id="shibboleth.authn.duo.OIDC.native.sdk.defaultCaCerts" value-type="java.lang.String"
+ list-class="java.util.ArrayList">
+ <value>sha256/I/Lt/z7ekCWanjD0Cvj5EqXls2lOaThEA0H2Bg4BT/o=</value>
+ <value>sha256/r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</value>
+ <value>sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=</value>
+ <value>sha256/dykHF2FLJfEpZOvbOLX4PKrcD2w2sHd/iA/G3uHTOcw=</value>
+ <value>sha256/JZaQTcTWma4gws703OR/KFk313RkrDcHRvUt6na6DCg=</value>
+ <value>sha256/++MBgDH5WGvL9Bcn5Be30cRcL0f5O+NyoXuWtQdX1aI=</value>
+ <value>sha256/f0KW/FtqTjs108NpYj42SrGvOB2PpxIVM8nWxjPqJGE=</value>
+ <value>sha256/NqvDJlas/GRcYbcWE8S/IceH9cq77kg0jVhZeAPXq8k=</value>
+ <value>sha256/9+ze1cZgR9KO1kZrVDxA4HQ6voHRCSVNz4RdTCx4U8U=</value>
+ <value>sha256/j9ESw8g3DxR9XM06fYZeuN1UB4O6xp/GAIjjdD/zM3g=</value>
+ </util:list>
+
+</beans>
\ 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