[java-idp-plugin-duo] branch main updated: JDUO-70 - Copy Duo AuthAPI classes into plugin
Phil Smart
philip.smart at jisc.ac.uk
Fri Jul 7 08:50:06 UTC 2023
This is an automated email from the git hooks/post-receive script.
philsmart pushed a commit to branch main
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=96e5120bd37cfb46e7e74af4ace03f7813784010
The following commit(s) were added to refs/heads/main by this push:
new 96e5120 JDUO-70 - Copy Duo AuthAPI classes into plugin
96e5120 is described below
commit 96e5120bd37cfb46e7e74af4ace03f7813784010
Author: philipsmart <philipsmart at 192.168.1.38>
AuthorDate: Fri Jul 7 09:50:02 2023 +0100
JDUO-70 - Copy Duo AuthAPI classes into plugin
- Transplanted the Duo API packages directly into the plugin.
- Copied the impl classes into the impl package of the plugin. Renaming
their package.
- Updated the flow beans to reference the new impl classes. The API
classes have the same FQCNs so no need to change them — unless we get a
package sealing issue once deployed.
https://shibboleth.atlassian.net/browse/JDUO-70
---
.../idp/authn/duo/BasicDuoIntegration.java | 165 ++++++++++
.../net/shibboleth/idp/authn/duo/DuoAuthAPI.java | 104 ++++++
.../shibboleth/idp/authn/duo/DuoIntegration.java | 61 ++++
.../net/shibboleth/idp/authn/duo/DuoPrincipal.java | 86 +++++
.../duo/context/DuoAuthenticationContext.java | 179 ++++++++++
.../idp/authn/duo/context/package-info.java | 24 ++
.../net/shibboleth/idp/authn/duo/package-info.java | 24 ++
.../authn/duo/impl/AbstractDuoAuthenticator.java | 161 +++++++++
.../plugin/authn/duo/impl/DuoAuthAPIResponse.java | 62 ++++
.../authn/duo/impl/DuoAuthAuthenticator.java | 112 +++++++
.../idp/plugin/authn/duo/impl/DuoAuthResponse.java | 59 ++++
.../idp/plugin/authn/duo/impl/DuoDevice.java | 107 ++++++
.../plugin/authn/duo/impl/DuoFailureResponse.java | 79 +++++
.../duo/impl/DuoNonceClaimLookupStrategy.java | 3 +-
.../authn/duo/impl/DuoPreauthAuthenticator.java | 86 +++++
.../plugin/authn/duo/impl/DuoPreauthResponse.java | 61 ++++
.../plugin/authn/duo/impl/DuoResponseWrapper.java | 62 ++++
.../idp/plugin/authn/duo/impl/DuoSupport.java | 123 +++++++
.../duo/impl/DuoUsernameClaimLookupStrategy.java | 3 +-
.../impl/ExtractDuoAuthenticationFromHeaders.java | 262 +++++++++++++++
.../plugin/authn/duo/impl/ValidateDuoAuthAPI.java | 366 +++++++++++++++++++++
.../impl/ValidateDuoTokenAuthenticationResult.java | 10 +-
.../META-INF/net.shibboleth.idp/postconfig.xml | 8 +
.../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml | 8 +-
24 files changed, 2206 insertions(+), 9 deletions(-)
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/BasicDuoIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/BasicDuoIntegration.java
new file mode 100644
index 0000000..5d2fdc1
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/BasicDuoIntegration.java
@@ -0,0 +1,165 @@
+/*
+ * 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.authn.duo;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Wrapper for use of Duo.
+ *
+ * @since 3.3.0
+ */
+public class BasicDuoIntegration extends AbstractInitializableComponent implements DuoIntegration {
+
+ /** API host. */
+ @NonnullAfterInit @NotEmpty private String apiHost;
+
+ /** Application key. */
+ @Nullable @NotEmpty private String applicationKey;
+
+ /** Integration key. */
+ @NonnullAfterInit @NotEmpty private String integrationKey;
+
+ /** Secret key. */
+ @NonnullAfterInit @NotEmpty private String secretKey;
+
+ /** Container for supported principals. */
+ @Nonnull private final Subject supportedPrincipals;
+
+ /** Constructor. */
+ public BasicDuoIntegration() {
+ supportedPrincipals = new Subject();
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getAPIHost() {
+ checkComponentActive();
+ assert apiHost != null;
+ return apiHost;
+ }
+
+ /**
+ * Set the API host to use.
+ *
+ * @param host API host
+ */
+ public void setAPIHost(@Nonnull @NotEmpty final String host) {
+ checkSetterPreconditions();
+ apiHost = Constraint.isNotNull(StringSupport.trimOrNull(host), "API host cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nullable @NotEmpty public String getApplicationKey() {
+ return applicationKey;
+ }
+
+ /**
+ * Set the application key to use.
+ *
+ * @param key application key
+ */
+ public void setApplicationKey(@Nullable @NotEmpty final String key) {
+ checkSetterPreconditions();
+ applicationKey = StringSupport.trimOrNull(key);
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getIntegrationKey() {
+ checkComponentActive();
+ assert integrationKey != null;
+ return integrationKey;
+ }
+
+ /**
+ * Set the integration key to use.
+ *
+ * @param key integration key
+ */
+ public void setIntegrationKey(@Nonnull @NotEmpty final String key) {
+ checkSetterPreconditions();
+ integrationKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Integration key cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getSecretKey() {
+ checkComponentActive();
+ assert secretKey != null;
+ return secretKey;
+ }
+
+ /**
+ * Set the secret key to use.
+ *
+ * @param key secret key
+ */
+ public void setSecretKey(@Nonnull @NotEmpty final String key) {
+ checkSetterPreconditions();
+ secretKey = Constraint.isNotNull(StringSupport.trimOrNull(key), "Secret key cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @Unmodifiable @NotLive public <T extends Principal> Set<T> getSupportedPrincipals(
+ @Nonnull final Class<T> c) {
+ final Set<T> result = supportedPrincipals.getPrincipals(c);
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Set supported non-user-specific principals that the action will include in the subjects
+ * it generates, in place of any default principals from the flow.
+ *
+ * <p>Setting to a null or empty collection will maintain the default behavior of relying on the flow.</p>
+ *
+ * @param <T> a type of principal to add, if not generic
+ * @param principals supported principals to include
+ */
+ public <T extends Principal> void setSupportedPrincipals(@Nullable final Collection<T> principals) {
+ checkSetterPreconditions();
+
+ supportedPrincipals.getPrincipals().clear();
+
+ if (principals != null && !principals.isEmpty()) {
+ supportedPrincipals.getPrincipals().addAll(Set.copyOf(principals));
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ if (apiHost == null || integrationKey == null || secretKey == null) {
+ throw new ComponentInitializationException("API host and integration keys must be set");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.java
new file mode 100644
index 0000000..07055ff
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.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.authn.duo;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * Constants defined in the Duo AuthAPI.
+ *
+ * @since 3.4.0
+ */
+public final class DuoAuthAPI {
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_USERNAME = "username";
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_IPADDR = "ipaddr";
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR = "factor";
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_DEVICE = "device";
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_PASSCODE = "passcode";
+
+ /** Duo AuthAPI parameter name. */
+ @Nonnull @NotEmpty public static final String DUO_PUSHINFO = "pushinfo";
+
+ /** Duo AuthAPI factor "auto" value. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_AUTO = "auto";
+
+ /** Duo AuthAPI factor "push" value. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_PUSH = "push";
+
+ /** Duo AuthAPI factor "passcode" value. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_PASSCODE = "passcode";
+
+ /** Duo AuthAPI factor "sms" value. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_SMS = "sms";
+
+ /** Duo AuthAPI factor "enum" value. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_PHONE = "phone";
+
+ /** Duo AuthAPI device "auto" value. */
+ @Nonnull @NotEmpty public static final String DUO_DEVICE_AUTO = "auto";
+
+ /** Duo AuthAPI preauth "allow" result value. */
+ @Nonnull @NotEmpty public static final String DUO_PREAUTH_RESULT_ALLOW = "allow";
+
+ /** Duo AuthAPI preauth "auth" result value. */
+ @Nonnull @NotEmpty public static final String DUO_PREAUTH_RESULT_AUTH = "auth";
+
+ /** Duo AuthAPI preauth "deny" result value. */
+ @Nonnull @NotEmpty public static final String DUO_PREAUTH_RESULT_DENY = "deny";
+
+ /** Duo AuthAPI preauth "enroll" result value. */
+ @Nonnull @NotEmpty public static final String DUO_PREAUTH_RESULT_ENROLL = "enroll";
+
+ /** 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 flow default header name for factor. */
+ @Nonnull @NotEmpty public static final String DUO_FACTOR_HEADER_NAME = "X-Shibboleth-Duo-Factor";
+
+ /** Duo flow default header name for device ID. */
+ @Nonnull @NotEmpty public static final String DUO_DEVICE_HEADER_NAME = "X-Shibboleth-Duo-Device";
+
+ /** Duo flow default header name for passcode. */
+ @Nonnull @NotEmpty public static final String DUO_PASSCODE_HEADER_NAME = "X-Shibboleth-Duo-Passcode";
+
+ /** Constructor. */
+ private DuoAuthAPI() {
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoIntegration.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoIntegration.java
new file mode 100644
index 0000000..fd05dca
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoIntegration.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.authn.duo;
+
+import net.shibboleth.idp.authn.principal.PrincipalSupportingComponent;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Interface to a particular Duo integration point.
+ *
+ * @since 3.3.0
+ */
+public interface DuoIntegration extends PrincipalSupportingComponent {
+
+ /**
+ * Get the name of the API host to contact.
+ *
+ * @return name of API host
+ */
+ @Nonnull @NotEmpty String getAPIHost();
+
+ /**
+ * Get the application key.
+ *
+ * @return the application key
+ */
+ @Nullable @NotEmpty String getApplicationKey();
+
+ /**
+ * Get the integration key.
+ *
+ * @return the integration key
+ */
+ @Nonnull @NotEmpty String getIntegrationKey();
+
+ /**
+ * Get the secret key.
+ *
+ * @return the secret key
+ */
+ @Nonnull @NotEmpty String getSecretKey();
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoPrincipal.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoPrincipal.java
new file mode 100644
index 0000000..a4ff6a0
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/DuoPrincipal.java
@@ -0,0 +1,86 @@
+/*
+ * 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.authn.duo;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.idp.authn.principal.CloneablePrincipal;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+import com.google.common.base.MoreObjects;
+
+/** Principal based on a Duo authentication. */
+public class DuoPrincipal implements CloneablePrincipal {
+
+ /** The username. */
+ @Nonnull @NotEmpty private String username;
+
+ /**
+ * Constructor.
+ *
+ * @param name the username
+ */
+ public DuoPrincipal(@Nonnull @NotEmpty @ParameterName(name="name") final String name) {
+ username = Constraint.isNotNull(StringSupport.trimOrNull(name), "Username cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull @NotEmpty public String getName() {
+ return username;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode() {
+ return username.hashCode();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals(final Object other) {
+ if (other == null) {
+ return false;
+ }
+
+ if (this == other) {
+ return true;
+ }
+
+ if (other instanceof DuoPrincipal) {
+ return username.equals(((DuoPrincipal) other).getName());
+ }
+
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("username", username).toString();
+ }
+
+ /** {@inheritDoc} */
+ @Nonnull public DuoPrincipal clone() throws CloneNotSupportedException {
+ final DuoPrincipal copy = (DuoPrincipal) super.clone();
+ copy.username = username;
+ return copy;
+ }
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java
new file mode 100644
index 0000000..a43d2cb
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java
@@ -0,0 +1,179 @@
+/*
+ * 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.authn.duo.context;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.shared.annotation.constraint.Live;
+
+/**
+ * Context that carries Duo factor and device or passcode to be used in validation.
+ *
+ * <p>This is used for AuthAPI-based use of Duo rather than the usual delegation
+ * of the process to their Web SDK.</p>
+ *
+ * @parent {@link AuthenticationContext}
+ * @added After extracting the Duo factor and device or passcode during authentication
+ */
+public final class DuoAuthenticationContext 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. */
+ @Nonnull private Map<String,String> pushInfo;
+
+ /** Constructor. */
+ public DuoAuthenticationContext() {
+ pushInfo = new HashMap<>();
+ }
+
+ /**
+ * Get the username.
+ *
+ * @return username
+ */
+ @Nullable public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Set the username.
+ *
+ * @param name username
+ *
+ * @return this context
+ */
+ @Nonnull public DuoAuthenticationContext setUsername(@Nullable final String name) {
+ username = name;
+ return this;
+ }
+
+ /**
+ * 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 @Live public Map<String,String> getPushInfo() {
+ return pushInfo;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java
new file mode 100644
index 0000000..1d6939c
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Context classes related to Duo v2 AuthAPI authentication. Copied in from idp-authn-api.
+ */
+ at NonnullElements
+package net.shibboleth.idp.authn.duo.context;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/package-info.java b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/package-info.java
new file mode 100644
index 0000000..e43b801
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/authn/duo/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Public APIs related to Duo v2 AuthAPI authentication. Copied in from idp-authn-api.
+ */
+ at NonnullElements
+package net.shibboleth.idp.authn.duo;
+
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoAuthenticator.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoAuthenticator.java
new file mode 100644
index 0000000..988fcc5
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/AbstractDuoAuthenticator.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.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpStatus;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+
+import com.duosecurity.duoweb.DuoWebException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A base class for authentication actions which call a Duo AuthAPI endpont.
+ */
+ at ThreadSafe
+public abstract class AbstractDuoAuthenticator extends AbstractInitializableComponent {
+
+ /** HttpClient for contacting Duo. */
+ @NonnullAfterInit private HttpClient httpClient;
+
+ /** HTTP client security parameters. */
+ @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /**
+ * Set the {@link HttpClient} to use for contacting Duo.
+ *
+ * @param client HttpClient
+ */
+ public void setHttpClient(@Nonnull final HttpClient client) {
+ checkSetterPreconditions();
+ httpClient = Constraint.isNotNull(client, "HTTP client cannot be null");
+ }
+
+ /**
+ * Set the optional client security parameters.
+ *
+ * @param params the new client security parameters
+ */
+ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+ checkSetterPreconditions();
+ httpClientSecurityParameters = params;
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper}.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+
+ if (httpClient == null) {
+ throw new ComponentInitializationException("HttpClient cannot be null");
+ }
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ }
+
+ /**
+ * Performs a call to the Duo AuthAPI. Upon a successful call, the JSON response is mapped into the appropriate type
+ * of {@link DuoResponseWrapper}.
+ *
+ * @param request the prepared HTTP request
+ * @param wrapperTypeRef the type of {@link DuoResponseWrapper} to use
+ * @param <T> the DuoResponse type being wrapped
+ *
+ * @return a {@link DuoResponseWrapper}
+ *
+ * @throws IOException on an I/O error
+ * @throws DuoWebException on a Duo-related error
+ */
+ protected <T extends DuoResponseWrapper<?>> T doAPIRequest(@Nonnull final ClassicHttpRequest request,
+ @Nonnull final TypeReference<T> wrapperTypeRef)
+ throws DuoWebException, IOException {
+
+ // Make the request.
+ final HttpClientContext clientContext = HttpClientContext.create();
+ assert clientContext != null;
+ HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, true);
+ HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+ final ClassicHttpResponse httpResponse = httpClient.executeOpen(null, request, clientContext);
+ final String scheme = request.getScheme();
+ assert scheme != null;
+ HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, scheme);
+
+ // Check the HTTP response code.
+ final int httpStatusCode = httpResponse.getCode();
+ if (httpStatusCode == HttpStatus.SC_BAD_REQUEST) {
+ final InputStream httpContent = httpResponse.getEntity().getContent();
+ final DuoFailureResponse msg = objectMapper.readValue(httpContent, DuoFailureResponse.class);
+ final StringBuilder builder = new StringBuilder();
+ builder.append(msg.getMessage() != null ? msg.getMessage() : "no message")
+ .append(" (")
+ .append(msg.getMessageDetail() != null ? msg.getMessageDetail() : "no detail")
+ .append(")");
+ throw new DuoWebException(builder.toString());
+ }
+ if (httpStatusCode != HttpStatus.SC_OK) {
+ throw new IOException("Non-ok status code (" + httpStatusCode + ") returned from Duo: "
+ + httpResponse.getReasonPhrase());
+ } else if (httpResponse.getEntity() == null) {
+ throw new IOException("No response body returned from Duo");
+ }
+
+ // Parse the JSON response.
+ final T duoResponse = objectMapper.readValue(httpResponse.getEntity().getContent(), wrapperTypeRef);
+
+ if (duoResponse == null) {
+ throw new DuoWebException("Unable to parse JSON response");
+ } else if (!"OK".equals(duoResponse.getStat())) {
+ throw new DuoWebException("Unexpected status value in JSON response: " + duoResponse.getStat());
+ }
+
+ return duoResponse;
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAPIResponse.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAPIResponse.java
new file mode 100644
index 0000000..ce361fd
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAPIResponse.java
@@ -0,0 +1,62 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Describes the results of a Duo AuthAPI call, intended for use with a jackson
+ * {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ *
+ * @since 2.0.0
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class DuoAuthAPIResponse {
+
+ /** the result. */
+ @JsonProperty("result")
+ private String result;
+
+ /** the status message. */
+ @JsonProperty("status_msg")
+ private String statusMessage;
+
+ /**
+ * Get the Duo result string.
+ *
+ * @return the result string
+ */
+ @Nonnull public String getResult() {
+ assert result != null;
+ return result;
+ }
+
+ /**
+ * Get the Duo status message.
+ *
+ * @return the Duo status message
+ */
+ @Nonnull public String getStatusMessage() {
+ assert statusMessage != null;
+ return statusMessage;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAuthenticator.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAuthenticator.java
new file mode 100644
index 0000000..b1cc280
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthAuthenticator.java
@@ -0,0 +1,112 @@
+/*
+ * 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.URI;
+import java.net.URISyntaxException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.net.URIBuilder;
+
+import com.duosecurity.duoweb.DuoWebException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Implementation of the the Duo AuthApi /v2/auth endpoint.
+ */
+public class DuoAuthAuthenticator extends AbstractDuoAuthenticator {
+
+ /** pushinfo escaper. */
+ @Nonnull private final Escaper paramEscaper;
+
+ /** a TypeReference for the repsonse generated by the endpoint. */
+ @Nonnull private final TypeReference<DuoResponseWrapper<DuoAuthResponse>> wrapperTypeRef;
+
+ /** Constructor. */
+ @SuppressWarnings("null")
+ public DuoAuthAuthenticator() {
+ wrapperTypeRef = new TypeReference<>() {};
+ paramEscaper = UrlEscapers.urlFormParameterEscaper();
+ }
+
+ /**
+ * Perform an authentication action via the Duo AuthApi /auth endpoint.
+ *
+ * @param duoContext Duo authentication context to use
+ * @param duoIntegration Duo integration to use
+ *
+ * @return a {@link DuoAuthResponse}
+ *
+ * @throws DuoWebException if an error occurs
+ */
+ public DuoAuthResponse authenticate(@Nonnull final DuoAuthenticationContext duoContext,
+ @Nonnull final DuoIntegration duoIntegration) throws DuoWebException {
+
+ try {
+ // prepare the request
+ final URI uri = new URIBuilder().setScheme("https").setHost(duoIntegration.getAPIHost())
+ .setPath("/auth/v2/auth").build();
+ final ClassicRequestBuilder rb =
+ ClassicRequestBuilder.post().setUri(uri).addParameter(DuoAuthAPI.DUO_USERNAME, duoContext.getUsername());
+ assert rb != null;
+ if (duoContext.getClientAddress() != null) {
+ rb.addParameter(DuoAuthAPI.DUO_IPADDR, duoContext.getClientAddress());
+ }
+ if (duoContext.getFactor() != null) {
+ rb.addParameter(DuoAuthAPI.DUO_FACTOR, duoContext.getFactor());
+ }
+ if (duoContext.getDeviceID() != null) {
+ rb.addParameter(DuoAuthAPI.DUO_DEVICE, duoContext.getDeviceID());
+ }
+ if (duoContext.getPasscode() != null) {
+ rb.addParameter(DuoAuthAPI.DUO_PASSCODE, duoContext.getPasscode());
+ }
+ if (!duoContext.getPushInfo().isEmpty()) {
+ final ArrayList<String> pushinfo = new ArrayList<>(duoContext.getPushInfo().size());
+ for (final Map.Entry<String,String> entry : duoContext.getPushInfo().entrySet()) {
+ pushinfo.add(paramEscaper.escape(entry.getKey()) + "=" + paramEscaper.escape(entry.getValue()));
+ }
+ rb.addParameter(DuoAuthAPI.DUO_PUSHINFO, StringSupport.listToStringValue(pushinfo, "&"));
+ }
+ DuoSupport.signRequest(rb, duoIntegration);
+ final ClassicHttpRequest request = rb.build();
+ assert request != null;
+
+ // do it
+ return doAPIRequest(request, wrapperTypeRef).getResponse();
+ } catch (final IOException | URISyntaxException | InvalidKeyException | NoSuchAlgorithmException ex) {
+ throw new DuoWebException("Duo AuthAPI auth request failed: " + ex.getMessage());
+ }
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthResponse.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthResponse.java
new file mode 100644
index 0000000..810ae35
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoAuthResponse.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Describes the results of an authentication attempt via the Duo AuthAPI, intended for use with a jackson
+ * {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoAuthResponse extends DuoAuthAPIResponse {
+
+ /** the status string. */
+ @JsonProperty("status") private String status;
+
+ /** the trusted device token string. */
+ @JsonProperty("trusted_device_token") private String trustedDeviceToken;
+
+ /**
+ * Get the Duo status string.
+ *
+ * @return Duo status string
+ */
+ @Nonnull public String getStatus() {
+ assert status != null;
+ return status;
+ }
+
+ /**
+ * Get the Duo trusted device token string.
+ *
+ * @return Duo trusted device token string
+ */
+ @Nullable public String getTrustedDeviceToken() {
+ assert trustedDeviceToken != null;
+ return trustedDeviceToken;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoDevice.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoDevice.java
new file mode 100644
index 0000000..fa3ab67
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoDevice.java
@@ -0,0 +1,107 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Represents a Duo device, intended for use with a jackson
+ * {@link com.fasterxml.jackson.databind.ObjectMapper}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoDevice {
+
+ /** the Duo device identifier. */
+ @JsonProperty("device") @Nullable private String device;
+
+ /** the Duo device type. */
+ @JsonProperty("type") @Nullable private String type;
+
+ /** the Duo device number. */
+ @JsonProperty("number") @Nullable private String number;
+
+ /** the Duo device name. */
+ @JsonProperty("name") @Nullable private String name;
+
+ /** a {@link List} of Duo device capabilities. */
+ @JsonProperty("capabilities") @Nullable private List<String> capabilities;
+
+ /** Constructor. */
+ public DuoDevice() {
+ capabilities = new ArrayList<>();
+ }
+
+ /**
+ * Get the device identifier.
+ *
+ * @return the device identifier
+ */
+ @Nonnull public String getDevice() {
+ assert device != null;
+ return device;
+ }
+
+ /**
+ * Get the device type.
+ *
+ * @return the device type
+ */
+ @Nonnull public String getType() {
+ assert type != null;
+ return type;
+ }
+
+ /**
+ * Get the device number.
+ *
+ * @return the device number
+ */
+ @Nonnull public String getNumber() {
+ assert number != null;
+ return number;
+ }
+
+ /**
+ * Get the device name.
+ *
+ * @return the device name
+ */
+ @Nonnull public String getName() {
+ assert name != null;
+ return name;
+ }
+
+ /**
+ * Get the device capabilities.
+ *
+ * @return the device capabilities
+ */
+ @Nonnull public Collection<String> getCapabilities() {
+ assert capabilities != null;
+ return capabilities;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoFailureResponse.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoFailureResponse.java
new file mode 100644
index 0000000..34fde44
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoFailureResponse.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+/**
+ * Describes the failure of a Duo AuthAPI call.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoFailureResponse {
+
+ /** the failure status. */
+ @JsonProperty("stat") @Nullable private String stat;
+
+ /** the failure code. */
+ @JsonProperty("code") @Nullable private String code;
+
+ /** the failure message. */
+ @JsonProperty("message") @Nullable private String message;
+
+ /** the failure message detail. */
+ @JsonProperty("message_detail") @Nullable private String messageDetail;
+
+ /**
+ * Get the failure status.
+ *
+ * @return failure status
+ */
+ @Nullable public String getStat() {
+ return stat;
+ }
+
+ /**
+ * Get the failure code.
+ *
+ * @return failure code
+ */
+ @Nullable public String getCode() {
+ return code;
+ }
+
+ /**
+ * Get the failure message.
+ *
+ * @return failure message
+ */
+ @Nullable public String getMessage() {
+ return message;
+ }
+
+ /**
+ * Get the failure message details.
+ *
+ * @return failure message details
+ */
+ @Nullable public String getMessageDetail() {
+ return messageDetail;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategy.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategy.java
index ee889c0..58e172e 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategy.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoNonceClaimLookupStrategy.java
@@ -28,11 +28,10 @@ import org.opensaml.profile.context.ProfileRequestContext;
import com.nimbusds.jwt.JWTClaimsSet;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
/**
- * Find the nonce from the {@link DuoAuthenticationContext}. Returns null if not found.
+ * Find the nonce from the {@link DuoOIDCAuthenticationContext}. Returns null if not found.
*/
@ThreadSafe
public final class DuoNonceClaimLookupStrategy implements BiFunction<ProfileRequestContext,JWTClaimsSet, String> {
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthAuthenticator.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthAuthenticator.java
new file mode 100644
index 0000000..f31f790
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthAuthenticator.java
@@ -0,0 +1,86 @@
+/*
+ * 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.URI;
+import java.net.URISyntaxException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.annotation.Nonnull;
+
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.net.URIBuilder;
+
+import com.duosecurity.duoweb.DuoWebException;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
+
+
+
+/**
+ * Implementation of the the Duo AuthAPI /v2/preauth endpoint.
+ */
+public class DuoPreauthAuthenticator extends AbstractDuoAuthenticator {
+
+ /** TypeReference for the response generated by the endpoint. */
+ @Nonnull private final TypeReference<DuoResponseWrapper<DuoPreauthResponse>> wrapperTypeRef;
+
+ /** Constructor. */
+ public DuoPreauthAuthenticator() {
+ wrapperTypeRef = new TypeReference<>() {};
+ }
+
+ /**
+ * Perform an authentication action via the Duo AuthAPI /preauth endpoint.
+ *
+ * @param duoContext Duo authentication context to use
+ * @param duoIntegration Duo integration to use
+ *
+ * @return a {@link DuoPreauthResponse}
+ *
+ * @throws DuoWebException if an error occurs
+ */
+ public DuoPreauthResponse authenticate(@Nonnull final DuoAuthenticationContext duoContext,
+ @Nonnull final DuoIntegration duoIntegration) throws DuoWebException {
+ try {
+ // Prepare the request
+ final URI uri = new URIBuilder().setScheme("https").setHost(duoIntegration.getAPIHost())
+ .setPath("/auth/v2/preauth").build();
+ final ClassicRequestBuilder rb =
+ ClassicRequestBuilder.post().setUri(uri).addParameter(DuoAuthAPI.DUO_USERNAME, duoContext.getUsername());
+
+ if (duoContext.getClientAddress() != null) {
+ rb.addParameter(DuoAuthAPI.DUO_IPADDR, duoContext.getClientAddress());
+ }
+
+ DuoSupport.signRequest(rb, duoIntegration);
+ final ClassicHttpRequest request = rb.build();
+
+ return doAPIRequest(request, wrapperTypeRef).getResponse();
+ } catch (final IOException | URISyntaxException | InvalidKeyException | NoSuchAlgorithmException ex) {
+ throw new DuoWebException("Duo AuthAPI preauth request failed: " + ex.getMessage());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthResponse.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthResponse.java
new file mode 100644
index 0000000..1d24c95
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoPreauthResponse.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.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Describes the results of an pre-authentication attempt via the Duo AuthAPI.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoPreauthResponse extends DuoAuthAPIResponse {
+
+ /** the {@link List} of {@link DuoDevice}s registered. */
+ @JsonProperty("devices") @Nonnull private List<DuoDevice> devices = new ArrayList<>();
+
+ /** the {@link URL} for the self-enrollment portal. */
+ @JsonProperty("enroll_portal_url") @Nullable private URL enrollPortalURL;
+
+ /**
+ * Get the Duo devices.
+ *
+ * @return Duo devices
+ */
+ @Nonnull public Collection<DuoDevice> getDevices() {
+ return devices;
+ }
+
+ /**
+ * Get the Duo enrollment portal URL.
+ *
+ * @return Duo enrollment portal URL
+ */
+ @Nullable public URL getEnrollPortalURL() {
+ return enrollPortalURL;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoResponseWrapper.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoResponseWrapper.java
new file mode 100644
index 0000000..e38048c
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoResponseWrapper.java
@@ -0,0 +1,62 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Handle a generic object returned from the response that will come from the Duo
+ * AuthAPI.
+ *
+ * @param <T> the subclass of {@link DuoAuthAPIResponse} being wrapped
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoResponseWrapper<T extends DuoAuthAPIResponse> {
+
+ /** the inner response. */
+ @JsonProperty("response")
+ private T response;
+
+ /** the response status. */
+ @JsonProperty("stat")
+ private String stat;
+
+ /**
+ * Get the inner response.
+ *
+ * @return inner response
+ */
+ @Nonnull public T getResponse() {
+ assert response != null;
+ return response;
+ }
+
+ /**
+ * Get the response status.
+ *
+ * @return response status
+ */
+ @Nonnull public String getStat() {
+ assert stat != null;
+ return stat;
+ }
+
+}
\ No newline at end of file
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 df2c515..1bde4cf 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
@@ -17,29 +17,152 @@
package net.shibboleth.idp.plugin.authn.duo.impl;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import com.duosecurity.duoweb.Base64;
+import com.duosecurity.duoweb.Util;
+import com.google.common.escape.Escaper;
+import com.google.common.net.UrlEscapers;
+
+import net.shibboleth.idp.authn.duo.DuoIntegration;
import net.shibboleth.idp.plugin.authn.duo.DuoException;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
/**
* Helper methods for Duo 2FA.
*/
@ThreadSafe
public final class DuoSupport {
+
+ /** RFC 2822 formatter for date/time. */
+ public static final DateTimeFormatter RFC_2822_DATE_FORMAT;
+ static {
+ RFC_2822_DATE_FORMAT = DateTimeFormatter.ofPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z");
+ }
/** Private Constructor. */
private DuoSupport() {
}
+ /**
+ * The signature requires that the request parameters being in a particular order as specified in the API.
+ *
+ * @param request the request
+ * @param date the date
+ * @param sigVersion the signature version
+ *
+ * @return the parameters to be signed in their canonical order
+ *
+ * @throws UnsupportedEncodingException failure from {@link java.net.URLEncoder}
+ *
+ * @since 2.0.0
+ */
+ private static String canonRequest(@Nonnull final ClassicRequestBuilder request, @Nonnull final String date,
+ final int sigVersion) throws UnsupportedEncodingException {
+ final URI uri = request.getUri();
+ String canon = "";
+ if (sigVersion == 2) {
+ canon += date + "\n";
+ }
+ canon += request.getMethod().toUpperCase() + "\n";
+ canon += uri.getHost().toLowerCase() + "\n";
+ canon += uri.getPath() + "\n";
+ final List<NameValuePair> parms = request.getParameters();
+ assert parms != null;
+ canon += createQueryString(parms);
+
+ return canon;
+ }
+
+ /**
+ * Builds a string representation of the query string with the parameter names is alphabetical order. The names and
+ * values are URL encoded and then they are concatenated with '&' in between.
+ *
+ * @param params the name/value pairs to be joined
+ *
+ * @return the canonical query string
+ *
+ * @throws UnsupportedEncodingException failure from {@link java.net.URLEncoder}
+ *
+ * @since 2.0.0
+ */
+ private static String createQueryString(@Nonnull final List<NameValuePair> params)
+ throws UnsupportedEncodingException {
+
+ final ArrayList<String> args = new ArrayList<>();
+
+ // sort by name
+ Collections.sort(params, new Comparator<NameValuePair>() {
+ public int compare(final NameValuePair nvp1, final NameValuePair nvp2) {
+ return nvp1.getName().compareTo(nvp2.getName());
+ }
+ });
+
+ // URL encode and join the name/values with '='
+ final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
+ for (final NameValuePair nvp : params) {
+ final String name = escaper.escape(nvp.getName()).replace("+", "%20").replace("*", "%2A")
+ .replace("%7E", "~");
+ final String value = escaper.escape(nvp.getValue()).replace("+", "%20").replace("*", "%2A")
+ .replace("%7E", "~");
+ args.add(name + "=" + value);
+ }
+
+ // Concatenate everything togther with '&'
+ return StringSupport.listToStringValue(args, "&");
+ }
+
+ /**
+ * Sign a Duo AuthAPI request.
+ *
+ * @param request the request to be signed
+ * @param duo integration parameters to use
+ *
+ * @throws InvalidKeyException bad skey value
+ * @throws NoSuchAlgorithmException unknown encryption algorithm
+ * @throws UnsupportedEncodingException failure from {@link java.net.URLEncoder}
+ *
+ * @since 2.0.0
+ */
+ @NotEmpty public static void signRequest(@Nonnull final ClassicRequestBuilder request,
+ @Nonnull final DuoIntegration duo)
+ throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+ final String ikey = duo.getIntegrationKey();
+ final String skey = duo.getSecretKey();
+ final int sigVersion = 2;
+ final String date = RFC_2822_DATE_FORMAT.format(ZonedDateTime.now());
+ assert date != null;
+ final String canon = canonRequest(request, date, sigVersion);
+ final String sig = Util.hmacSign(skey, canon);
+
+ final String auth = ikey + ":" + sig;
+ final String header = "Basic " + Base64.encodeBytes(auth.getBytes());
+ request.addHeader("Authorization", header);
+ request.addHeader("Date", date);
+ }
+
/**
* Generates a random identifier to be used as a nonce.
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategy.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategy.java
index 0034c54..7664837 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategy.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DuoUsernameClaimLookupStrategy.java
@@ -28,11 +28,10 @@ import org.opensaml.profile.context.ProfileRequestContext;
import com.nimbusds.jwt.JWTClaimsSet;
import net.shibboleth.idp.authn.context.AuthenticationContext;
-import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
import net.shibboleth.idp.plugin.authn.duo.context.DuoOIDCAuthenticationContext;
/**
- * Find the authenticating principals username from the {@link DuoAuthenticationContext}. Returns null if not found.
+ * Find the authenticating principals username from the {@link DuoOIDCAuthenticationContext}. Returns null if not found.
*/
@ThreadSafe
public final class DuoUsernameClaimLookupStrategy implements BiFunction<ProfileRequestContext,JWTClaimsSet, String> {
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java
new file mode 100644
index 0000000..31db120
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java
@@ -0,0 +1,262 @@
+/*
+ * 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.Map;
+import java.util.function.Function;
+
+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 jakarta.servlet.http.HttpServletRequest;
+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.DuoAuthAPI;
+import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
+import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.servlet.HttpServletSupport;
+
+/**
+ * An action that extracts the Duo factor and device or passcode from HTTP request headers into a
+ * {@link DuoAuthenticationContext}, and attaches it to the {@link AuthenticationContext}.
+
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @pre
+ * <pre>
+ * ProfileRequestContext.getSubcontext(AuthenticationContext.class) != null
+ * </pre>
+ *
+ * @post If getHttpServletRequest() != null, the content of the headers are checked.
+ * The information found will be attached via a {@link DuoAuthenticationContext}.
+ */
+public class ExtractDuoAuthenticationFromHeaders extends AbstractAuthenticationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractDuoAuthenticationFromHeaders.class);
+
+ /** Whether "auto" should be the default for factor and device. */
+ private boolean autoAuthenticationSupported;
+
+ /** Whether to trust, and extract, the client address. */
+ private boolean clientAddressTrusted;
+
+ /** Header name for factor. */
+ @Nonnull @NotEmpty private String factorHeaderName;
+
+ /** Header name for device. */
+ @Nonnull @NotEmpty private String deviceHeaderName;
+
+ /** Header name for passcode. */
+ @Nonnull @NotEmpty private String passcodeHeaderName;
+
+ /** Strategy function for populating pushinfo AuthAPI parameter. */
+ @Nullable private Function<ProfileRequestContext,Map<String,String>> pushInfoLookupStrategy;
+
+ /** Constructor. */
+ ExtractDuoAuthenticationFromHeaders() {
+ autoAuthenticationSupported = true;
+ clientAddressTrusted = true;
+
+ factorHeaderName = DuoAuthAPI.DUO_FACTOR_HEADER_NAME;
+ deviceHeaderName = DuoAuthAPI.DUO_DEVICE_HEADER_NAME;
+ passcodeHeaderName = DuoAuthAPI.DUO_PASSCODE_HEADER_NAME;
+ }
+
+ /**
+ * Set the factor header name.
+ *
+ * @param headerName the factor header name
+ */
+ public void setFactorHeader(@Nonnull @NotEmpty final String headerName) {
+ checkSetterPreconditions();
+ factorHeaderName = Constraint.isNotNull(StringSupport.trimOrNull(headerName),
+ "Factor header name cannot be null or empty.");
+ }
+
+ /**
+ * Set the device header name.
+ *
+ * @param headerName the factor header name
+ */
+ public void setDeviceHeader(@Nonnull @NotEmpty final String headerName) {
+ checkSetterPreconditions();
+ deviceHeaderName = Constraint.isNotNull(StringSupport.trimOrNull(headerName),
+ "Device header name cannot be null or empty.");
+ }
+
+ /**
+ * Set the passcode header name.
+ *
+ * @param headerName the factor header name
+ */
+ public void setPasscodeHeader(@Nonnull @NotEmpty final String headerName) {
+ checkSetterPreconditions();
+ passcodeHeaderName = Constraint.isNotNull(StringSupport.trimOrNull(headerName),
+ "Passcode header name cannot be null or empty.");
+ }
+
+ /**
+ * Get whether the client address should be trusted for use in API calls.
+ *
+ * @return whether client address should be trusted
+ */
+ public boolean isClientAddressTrusted() {
+ return clientAddressTrusted;
+ }
+
+ /**
+ * Set whether the client address should be trusted for use in API calls.
+ *
+ * @param flag flag to set
+ */
+ public void setClientAdddressTrusted(final boolean flag) {
+ checkSetterPreconditions();
+ clientAddressTrusted = flag;
+ }
+
+ /**
+ * Get whether "auto" is the default setting.
+ *
+ * @return whether "auto" is the default setting
+ */
+ public boolean isAutoAuthenticationSupported() {
+ return autoAuthenticationSupported;
+ }
+
+ /**
+ * Set whether "auto" is the default setting.
+ *
+ * @param flag flag to set
+ */
+ public void setAutoAuthenticationSupported(final boolean flag) {
+ checkSetterPreconditions();
+ autoAuthenticationSupported = flag;
+ }
+
+ /**
+ * Set lookup strategy for AuthAPI pushinfo parameter.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setPushInfoLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Map<String,String>> strategy) {
+ checkSetterPreconditions();
+ pushInfoLookupStrategy = strategy;
+ }
+
+// Checkstyle: CyclomaticComplexity OFF
+ /** {@inheritDoc} */
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ log.debug("{} Checking for Duo authentication headers", getLogPrefix());
+
+ final DuoAuthenticationContext duoCtx = new DuoAuthenticationContext();
+
+ extractHeaders(duoCtx);
+
+ if (duoCtx.getFactor() == null) {
+ if (autoAuthenticationSupported && !profileRequestContext.isBrowserProfile()) {
+ log.debug("{} Non-browser request with no Duo factor specified, enabling auto method", getLogPrefix());
+ duoCtx.setFactor(DuoAuthAPI.DUO_FACTOR_AUTO);
+ } else {
+ log.debug("{} No Duo factor specified, auto method will not be attempted", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ }
+
+ // Check for missing passcode.
+ if (DuoAuthAPI.DUO_FACTOR_PASSCODE.equals(duoCtx.getFactor())) {
+ if (duoCtx.getPasscode() == null) {
+ log.warn("{} Request for passcode-based Duo login with no password supplied", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ return;
+ }
+ } else if (autoAuthenticationSupported && duoCtx.getDeviceID() == null) {
+ // Set auto device if needed.
+ duoCtx.setDeviceID(DuoAuthAPI.DUO_DEVICE_AUTO);
+ }
+
+ // Populate pushinfo either customized or just with service name.
+ if (pushInfoLookupStrategy != null) {
+ final Map<String,String> pushinfo = pushInfoLookupStrategy.apply(profileRequestContext);
+ if (pushinfo != null) {
+ duoCtx.getPushInfo().putAll(pushinfo);
+ }
+ } else {
+ final RelyingPartyUIContext uiCtx = authenticationContext.getSubcontext(RelyingPartyUIContext.class);
+ if (uiCtx != null) {
+ final String name = uiCtx.getServiceName();
+ if (name != null) {
+ duoCtx.getPushInfo().put("service", uiCtx.getServiceName());
+ }
+ }
+ }
+
+ authenticationContext.addSubcontext(duoCtx, true);
+
+ log.debug("{} Duo AuthAPI parameters extracted from request (Factor: {}, Device: {}, Passcode: {})",
+ getLogPrefix(), duoCtx.getFactor(), duoCtx.getDeviceID(),
+ duoCtx.getPasscode() != null ? "set" : "not set");
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Extracts the Duo API arguments passed in via the request headers.
+ *
+ * @param context the DuoApiAuthContext to store the parameters in
+ */
+ protected void extractHeaders(@Nonnull final DuoAuthenticationContext context) {
+
+ final HttpServletRequest httpRequest = getHttpServletRequest();
+ if (httpRequest == null) {
+ return;
+ }
+
+ if (clientAddressTrusted) {
+ context.setClientAddress(HttpServletSupport.getRemoteAddr(httpRequest));
+ }
+
+ final String factor = httpRequest.getHeader(factorHeaderName);
+ if (factor != null && !factor.isEmpty()) {
+ context.setFactor(factor);
+ }
+
+ final String device = httpRequest.getHeader(deviceHeaderName);
+ if (device != null && !device.isEmpty()) {
+ context.setDeviceID(device);
+ }
+
+ final String passcode = httpRequest.getHeader(passcodeHeaderName);
+ if (passcode != null && !passcode.isEmpty()) {
+ context.setPasscode(passcode);
+ }
+ }
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoAuthAPI.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoAuthAPI.java
new file mode 100644
index 0000000..3e13c14
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ValidateDuoAuthAPI.java
@@ -0,0 +1,366 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.duosecurity.duoweb.DuoWebException;
+
+import net.shibboleth.idp.authn.AbstractValidationAction;
+import net.shibboleth.idp.authn.AuthnAuditFields;
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.authn.duo.DuoPrincipal;
+import net.shibboleth.idp.authn.duo.context.DuoAuthenticationContext;
+import net.shibboleth.idp.authn.impl.AbstractAuditingValidationAction;
+import net.shibboleth.idp.profile.IdPAuditFields;
+import net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * An action that checks for a {@link DuoAuthenticationContext} and directly produces an
+ * {@link net.shibboleth.idp.authn.AuthenticationResult} based on that identity by authenticating against the Duo
+ * AuthAPI.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @event {@link AuthnEventIds#ACCOUNT_LOCKED}
+ * @event {@link AuthnEventIds#ACCOUNT_WARNING}
+ * @event {@link AuthnEventIds#ACCOUNT_ERROR}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link AuthnEventIds#INVALID_CREDENTIALS}
+ * @pre
+ *
+ * <pre>
+ * ProfileRequestContext.getSubcontext(AuthenticationContext.class).getAttemptedFlow() != null
+ * </pre>
+ *
+ * @post If AuthenticationContext.getSubcontext(DuoAuthenticationContext.class) != null, then an
+ * {@link net.shibboleth.idp.authn.AuthenticationResult} is saved to the {@link AuthenticationContext} on a
+ * successful login. On a failed login, the
+ * {@link AbstractValidationAction#handleError(ProfileRequestContext, AuthenticationContext, String, String)}
+ * method is called.
+ */
+public class ValidateDuoAuthAPI extends AbstractAuditingValidationAction {
+
+ /** Default prefix for metrics. */
+ @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.authn.duo";
+
+ /** Class logger. */
+ @Nonnull @NotEmpty private final Logger log = LoggerFactory.getLogger(ValidateDuoAuthAPI.class);
+
+ /** Lookp strategy for Duo integration. */
+ @Nonnull private Function<ProfileRequestContext,DuoIntegration> duoIntegrationLookupStrategy;
+
+ /** Lookup strategy for username to match against Duo identity. */
+ @Nonnull private Function<ProfileRequestContext,String> usernameLookupStrategy;
+
+ /** Implementation of Duo AuthApi /auth endpoint. */
+ @NonnullAfterInit private DuoAuthAuthenticator authAuthenticator;
+
+ /** Implementation of Duo AuthApi /preauth enpoint. */
+ @NonnullAfterInit private DuoPreauthAuthenticator preauthAuthenticator;
+
+ /** DuoApi context for tokens. Non-Null after preExecute **/
+ @NonnullBeforeExec @NotEmpty private DuoAuthenticationContext duoContext;
+
+ /** Duo integration to use. */
+ @NonnullBeforeExec private DuoIntegration duoIntegration;
+
+ /** Attempted username. */
+ @NonnullBeforeExec @NotEmpty private String username;
+
+ /** Constructor. */
+ public ValidateDuoAuthAPI() {
+ duoIntegrationLookupStrategy = FunctionSupport.constant(null);
+ usernameLookupStrategy = new CanonicalUsernameLookupStrategy();
+ setMetricName(DEFAULT_METRIC_NAME);
+ }
+
+ /**
+ * Set DuoIntegration lookup strategy to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDuoIntegrationLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,DuoIntegration> strategy) {
+ checkSetterPreconditions();
+ duoIntegrationLookupStrategy = Constraint.isNotNull(strategy, "DuoIntegration lookup strategy cannot be null");
+ }
+
+ /**
+ * Set DuoIntegration details to use directly.
+ *
+ * @param duo Duo integration details
+ */
+ public void setDuoIntegration(@Nonnull final DuoIntegration duo) {
+ checkSetterPreconditions();
+ Constraint.isNotNull(duo, "DuoIntegration cannot be null");
+ duoIntegrationLookupStrategy = FunctionSupport.constant(duo);
+ }
+
+ /**
+ * Set the lookup strategy to use for the username to match against Duo identity.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUsernameLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ usernameLookupStrategy = Constraint.isNotNull(strategy, "Username lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the {@link DuoAuthAuthenticator}.
+ *
+ * @param authenticator a Duo AuthAPI /auth endpoint implementation
+ */
+ public void setAuthAuthenticator(@Nonnull final DuoAuthAuthenticator authenticator) {
+ checkSetterPreconditions();
+ authAuthenticator = Constraint.isNotNull(authenticator, "DuoAuthAuthenticator cannot be null");
+ }
+
+ /**
+ * Set the {@link DuoPreauthAuthenticator}.
+ *
+ * @param authenticator a Duo AuthAPI /preauth endpoint implementation
+ */
+ public void setPreauthAuthenticator(@Nonnull final DuoPreauthAuthenticator authenticator) {
+ checkSetterPreconditions();
+ preauthAuthenticator = Constraint.isNotNull(authenticator, "DuoPreauthAuthenticator cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (authAuthenticator == null) {
+ throw new ComponentInitializationException("DuoAuthAuthenticator cannot be null");
+ }
+
+ if (preauthAuthenticator == null) {
+ throw new ComponentInitializationException("DuoPreauthAuthenticator cannot be null");
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+ return false;
+ }
+
+ duoIntegration = duoIntegrationLookupStrategy.apply(profileRequestContext);
+ if (duoIntegration == null) {
+ log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ username = usernameLookupStrategy.apply(profileRequestContext);
+ if (username == null) {
+ log.warn("{} No principal name available to cross-check Duo result", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
+ 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(profileRequestContext);
+ return false;
+ } else if (duoContext.getFactor() == null) {
+ log.info("{} No factor set in DuoAuthenticationContext", getLogPrefix());
+ handleError(profileRequestContext, authenticationContext, "No Duo factor set in DuoAuthenticationContext",
+ AuthnEventIds.REQUEST_UNSUPPORTED);
+ recordFailure(profileRequestContext);
+ return false;
+ }
+
+ duoContext.setUsername(username);
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ // CheckStyle: CyclomaticComplexity|MethodLength|ReturnCount OFF
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+
+ log.trace("{} Attempting Duo AuthAPI authentication", getLogPrefix());
+
+ try {
+ // Duo AuthAPI pre-authentication
+ final DuoPreauthResponse preAuthResponse = preauthAuthenticator.authenticate(duoContext, duoIntegration);
+ if (preAuthResponse == null) {
+ log.info("{} No Duo AuthAPI preauthentication response", getLogPrefix());
+ throw new DuoWebException("No preauthentication response");
+ }
+
+ final String preAuthResult = preAuthResponse.getResult();
+
+ if (DuoAuthAPI.DUO_PREAUTH_RESULT_ALLOW.equals(preAuthResult)) {
+ // User in bypass mode; treat as authenticated.
+ log.info("{} Duo pre-authentication (bypass) succeeded for '{}'", getLogPrefix(), username);
+ recordSuccess(profileRequestContext);
+ buildAuthenticationResult(profileRequestContext, authenticationContext);
+ return;
+ }
+
+ if (!DuoAuthAPI.DUO_PREAUTH_RESULT_AUTH.equals(preAuthResult)) {
+ // Either deny or enroll.
+ log.info("{} Duo pre-authentication failed for '{}': {}", getLogPrefix(), username,
+ preAuthResponse.getStatusMessage());
+ handleError(profileRequestContext, authenticationContext,
+ String.format("%s:%s:%s", preAuthResult, username, preAuthResponse.getStatusMessage()),
+ AuthnEventIds.ACCOUNT_ERROR);
+ recordFailure(profileRequestContext);
+ return;
+ }
+
+ // Validate device ID specified against the enrolled set.
+ if (duoContext.getDeviceID() != null && !DuoAuthAPI.DUO_DEVICE_AUTO.equals(duoContext.getDeviceID())) {
+ boolean found = false;
+ for (final DuoDevice device : preAuthResponse.getDevices()) {
+ final String deviceId = duoContext.getDeviceID();
+ assert deviceId != null;
+ if (deviceId.equals(device.getDevice())) {
+ found = true;
+ break;
+ } else if (deviceId.equals(device.getName())) {
+ log.debug("{} Remapped device ID based on device name ({}) for '{}'", getLogPrefix(),
+ device.getName(), username);
+ duoContext.setDeviceID(device.getDevice());
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ log.info("{} Duo authentication failed for '{}': non-existent device ID ({})", getLogPrefix(),
+ username, duoContext.getDeviceID());
+ handleError(profileRequestContext, authenticationContext, AuthnEventIds.INVALID_CREDENTIALS,
+ AuthnEventIds.INVALID_CREDENTIALS);
+ recordFailure(profileRequestContext);
+ return;
+ }
+ }
+
+ // Duo AuthAPI authentication
+ final DuoAuthResponse authenticationResponse = authAuthenticator.authenticate(duoContext, duoIntegration);
+ if (authenticationResponse == null) {
+ log.info("{} No Duo AuthAPI authentication response", getLogPrefix());
+ throw new DuoWebException("No authentication response");
+ }
+
+ final String authResult = authenticationResponse.getResult();
+ if (DuoAuthAPI.DUO_AUTH_RESULT_ALLOW.equals(authResult)) {
+ log.info("{} Duo authentication succeeded for '{}' (Factor: {}, Device: {})", getLogPrefix(), username,
+ duoContext.getFactor(), duoContext.getDeviceID());
+ recordSuccess(profileRequestContext);
+ buildAuthenticationResult(profileRequestContext, authenticationContext);
+ } else if (DuoAuthAPI.DUO_AUTH_RESULT_DENY.equals(authResult)) {
+ log.info("{} Duo authentication failed for '{}'", getLogPrefix(), username);
+ handleError(profileRequestContext, authenticationContext, authenticationResponse.getStatus(),
+ AuthnEventIds.INVALID_CREDENTIALS);
+ recordFailure(profileRequestContext);
+ } else {
+ throw new DuoWebException("Unexpected authentication response");
+ }
+ } catch (final DuoWebException e) {
+ log.error("{} Duo AuthAPI access failed for '{}'", getLogPrefix(), username, e);
+ handleError(profileRequestContext, authenticationContext, e, AuthnEventIds.AUTHN_EXCEPTION);
+ recordFailure(profileRequestContext);
+ }
+ }
+ // CheckStyle: CyclomaticComplexity|MethodLength|ReturnCount OFF
+
+ /** {@inheritDoc} */
+ @Override protected @Nonnull Subject populateSubject(@Nonnull final Subject subject) {
+ assert isPreExecuteCalled();
+ final DuoPrincipal princ = new DuoPrincipal(username);
+ subject.getPrincipals().add(princ);
+ final Set<Principal> princs = duoIntegration.getSupportedPrincipals(Principal.class);
+ subject.getPrincipals().addAll(princs);
+ return subject;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final AuthenticationContext authenticationContext) {
+ super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+
+ // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
+ profileRequestContext.ensureSubcontext(SubjectCanonicalizationContext.class).setPrincipalName(username);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable @Unmodifiable @NotLive protected Map<String,String> getAuditFields(
+ @Nonnull final ProfileRequestContext profileRequestContext) {
+ final Map<String,String> fields = new HashMap<>();
+
+ if (username != null) {
+ fields.put(IdPAuditFields.USERNAME, username);
+ }
+
+ if (duoIntegration != null) {
+ fields.put(AuthnAuditFields.DUO_CLIENT_ID, duoIntegration.getIntegrationKey());
+ }
+ final DuoAuthenticationContext duoCtx = duoContext;
+ if (duoCtx != null) {
+ if (duoCtx.getDeviceID() != null) {
+ fields.put(AuthnAuditFields.DUO_DEVICE_ID, duoCtx.getDeviceID());
+ }
+ if (duoCtx.getFactor() != null) {
+ fields.put(AuthnAuditFields.DUO_FACTOR, duoCtx.getFactor());
+ }
+ }
+
+ return CollectionSupport.copyToMap(fields);
+ }
+
+}
\ No newline at end of file
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
index 309b623..d9aa31a 100644
--- 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
@@ -64,6 +64,9 @@ import net.shibboleth.shared.annotation.constraint.NotEmpty;
* </pre>
*/
public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValidationAction {
+
+ /** Default prefix for metrics. */
+ @Nonnull @NotEmpty private static final String DEFAULT_METRIC_NAME = "net.shibboleth.idp.plugin.authn.duo";
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(ValidateDuoTokenAuthenticationResult.class);
@@ -81,7 +84,12 @@ public class ValidateDuoTokenAuthenticationResult extends AbstractAuditingValida
@Nullable @NotEmpty private String username;
/** Hook to map context information (often Duo factors in the Duo token) to principal collections.*/
- @Nullable private Function<ProfileRequestContext,Collection<Principal>> contextToPrincipalMappingStrategy;
+ @Nullable private Function<ProfileRequestContext,Collection<Principal>> contextToPrincipalMappingStrategy;
+
+ /** Constructor.*/
+ public ValidateDuoTokenAuthenticationResult() {
+ setMetricName(DEFAULT_METRIC_NAME);
+ }
/**
* Get the context to principal mapping strategy for mapping context information into
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
index cbcf8cd..2c16b5d 100644
--- 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
@@ -36,5 +36,13 @@
<bean id="shibboleth.DuoOIDCAuthnController"
class="net.shibboleth.idp.plugin.authn.duo.impl.DuoOIDCAuthnController" />
+ <bean p:id="duo" class="net.shibboleth.idp.authn.principal.GenericPrincipalService"
+ c:claz="net.shibboleth.idp.authn.duo.DuoPrincipal">
+ <constructor-arg name="serializer">
+ <bean class="net.shibboleth.idp.authn.principal.SimplePrincipalSerializer"
+ c:claz="net.shibboleth.idp.authn.duo.DuoPrincipal" c:name="DUO" />
+ </constructor-arg>
+ </bean>
+
</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
index 3d2f72d..aa2e28d 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -74,7 +74,7 @@
Non-Browser actions and beans. Code lives in idp-authn-impl for now, may migrate here later.
-->
<bean id="ExtractDuoAuthenticationFromHeaders" scope="prototype"
- class="net.shibboleth.idp.authn.duo.impl.ExtractDuoAuthenticationFromHeaders"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ExtractDuoAuthenticationFromHeaders"
p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
p:autoAuthenticationSupported="%{idp.duo.oidc.nonbrowser.auto:true}"
p:clientAdddressTrusted="%{idp.duo.oidc.nonbrowser.clientAddressTrusted:true}"
@@ -84,19 +84,19 @@
p:pushInfoLookupStrategy="#{getObject('shibboleth.authn.DuoOIDC.PushInfoLookupStrategy')}" />
<bean id="DuoPreauthAuthenticator" lazy-init="true"
- class="net.shibboleth.idp.authn.duo.impl.DuoPreauthAuthenticator"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.DuoPreauthAuthenticator"
p:objectMapper-ref="shibboleth.JSONObjectMapper"
p:httpClient="#{getObject('shibboleth.authn.DuoOIDC.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
p:httpClientSecurityParameters="#{getObject('shibboleth.authn.DuoOIDC.NonBrowser.HttpClientSecurityParameters')}" />
<bean id="DuoAuthAuthenticator" lazy-init="true"
- class="net.shibboleth.idp.authn.duo.impl.DuoAuthAuthenticator"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.DuoAuthAuthenticator"
p:objectMapper-ref="shibboleth.JSONObjectMapper"
p:httpClient="#{getObject('shibboleth.authn.DuoOIDC.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
p:httpClientSecurityParameters="#{getObject('shibboleth.authn.DuoOIDC.NonBrowser.HttpClientSecurityParameters')}" />
<bean id="ValidateDuoAuthAPI" scope="prototype"
- class="net.shibboleth.idp.authn.duo.impl.ValidateDuoAuthAPI"
+ class="net.shibboleth.idp.plugin.authn.duo.impl.ValidateDuoAuthAPI"
p:usernameLookupStrategy-ref="shibboleth.authn.DuoOIDC.UsernameLookupStrategy"
p:duoIntegrationLookupStrategy-ref="shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegrationStrategy"
p:addDefaultPrincipals="%{idp.authn.DuoOIDC.addDefaultPrincipals:true}"
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list