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