[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