[java-identity-provider] branch master updated: IDP-1239 - Non-browser support for Duo authentication

Scott Cantor cantor.2 at osu.edu
Wed Aug 8 16:32:37 EDT 2018


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch master
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=f443ca245fc7084871cc236d949349b1f471bc71

The following commit(s) were added to refs/heads/master by this push:
       new  f443ca2   IDP-1239 - Non-browser support for Duo authentication
f443ca2 is described below

commit f443ca245fc7084871cc236d949349b1f471bc71
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Aug 8 16:32:22 2018 -0400

    IDP-1239 - Non-browser support for Duo authentication
    
    https://issues.shibboleth.net/jira/browse/IDP-1239
    
    Initial code import and refactoring.
---
 .../net/shibboleth/idp/authn/duo/DuoAuthAPI.java   |  92 +++++++
 .../duo/context/DuoAuthenticationContext.java      | 134 +++++++++
 .../idp/authn/duo/context/package-info.java        |  21 ++
 idp-authn-impl/pom.xml                             |   5 +
 .../authn/duo/impl/AbstractDuoAuthenticator.java   | 156 +++++++++++
 .../idp/authn/duo/impl/DuoAuthAPIResponse.java     |  80 ++++++
 .../idp/authn/duo/impl/DuoAuthAuthenticator.java   |  90 ++++++
 .../idp/authn/duo/impl/DuoAuthResponse.java        |  77 ++++++
 .../shibboleth/idp/authn/duo/impl/DuoDevice.java   | 101 +++++++
 .../idp/authn/duo/impl/DuoFailureResponse.java     |  79 ++++++
 .../authn/duo/impl/DuoPreauthAuthenticator.java    |  79 ++++++
 .../idp/authn/duo/impl/DuoPreauthResponse.java     |  80 ++++++
 .../idp/authn/duo/impl/DuoResponseWrapper.java     |  60 ++++
 .../shibboleth/idp/authn/duo/impl/DuoSupport.java  | 120 +++++++-
 .../impl/ExtractDuoAuthenticationFromHeaders.java  | 217 +++++++++++++++
 .../idp/authn/duo/impl/ValidateDuoAuthAPI.java     | 301 +++++++++++++++++++++
 .../idp/authn/duo/impl/ValidateDuoWebResponse.java |   2 -
 .../ExtractDuoAuthenticationFromHeadersTest.java   | 143 ++++++++++
 18 files changed, 1832 insertions(+), 5 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.java
new file mode 100644
index 0000000..73ba8b6
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/DuoAuthAPI.java
@@ -0,0 +1,92 @@
+/*
+ * 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.utilities.java.support.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_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 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 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-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java
new file mode 100644
index 0000000..d968b58
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/DuoAuthenticationContext.java
@@ -0,0 +1,134 @@
+/*
+ * 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 javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+
+/**
+ * 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 class DuoAuthenticationContext extends BaseContext {
+
+    /** Username. */
+    @Nullable private String username;
+    
+    /** Factor. */
+    @Nullable private String duoFactor;
+
+    /** Device ID. */
+    @Nullable private String duoDevice;
+
+    /** Passcode. */
+    @Nullable private String duoPasscode;
+
+    /**
+     * 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 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;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java
new file mode 100644
index 0000000..3ebfebd
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/duo/context/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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 authentication.
+ */
+package net.shibboleth.idp.authn.duo.context;
\ No newline at end of file
diff --git a/idp-authn-impl/pom.xml b/idp-authn-impl/pom.xml
index e9dac8a..c684074 100644
--- a/idp-authn-impl/pom.xml
+++ b/idp-authn-impl/pom.xml
@@ -66,6 +66,11 @@
         </dependency>
 
         <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
           <groupId>com.github.jasminb</groupId>
           <artifactId>jsonapi-converter</artifactId>
         </dependency>
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/AbstractDuoAuthenticator.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/AbstractDuoAuthenticator.java
new file mode 100644
index 0000000..49ea3ed
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/AbstractDuoAuthenticator.java
@@ -0,0 +1,156 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpUriRequest;
+
+import com.duosecurity.duoweb.DuoWebException;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * 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;
+
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /**
+     * Get the {@link HttpClient} to use for contacting Duo.
+     * 
+     * @return HttpClient
+     */
+    @NonnullAfterInit public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    /**
+     * Set the {@link HttpClient} to use for contacting Duo.
+     * 
+     * @param client HttpClient
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        httpClient = Constraint.isNotNull(client, "HTTP client cannot be null");
+    }
+
+    /**
+     * Get the JSON {@link ObjectMapper}.
+     * 
+     * @return ObjectMapper
+     */
+    @NonnullAfterInit public ObjectMapper getObjectMapper() {
+        return objectMapper;
+    }
+
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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 ClientProtocolException on an HTTP error 
+     * @throws DuoWebException on a Duo-related error
+     */
+    protected <T extends DuoResponseWrapper<?>> T doAPIRequest(@Nonnull final HttpUriRequest request,
+            @Nonnull final TypeReference<T> wrapperTypeRef)
+                    throws DuoWebException, ClientProtocolException, IOException {
+
+        // Make the request.
+        final HttpResponse httpResponse = getHttpClient().execute(request);
+
+        // Check the HTTP response code.
+        final int httpStatusCode = httpResponse.getStatusLine().getStatusCode();
+        if (httpStatusCode == HttpStatus.SC_BAD_REQUEST) {
+            final InputStream httpContent = httpResponse.getEntity().getContent();
+            final DuoFailureResponse msg = objectMapper.readValue(httpContent, DuoFailureResponse.class);
+            throw new DuoWebException(msg.getMessage() != null ? msg.getMessage() : "no message" + "("
+                    + msg.getMessageDetail() != null ? msg.getMessageDetail() : "no detail" + ")");
+        }
+        if (httpStatusCode != HttpStatus.SC_OK) {
+            throw new IOException("Non-ok status code (" + httpStatusCode + ") returned from Duo: "
+                    + httpResponse.getStatusLine().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;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAPIResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAPIResponse.java
new file mode 100644
index 0000000..a053e5c
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAPIResponse.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.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 ObjectMapper}.
+ * 
+ * @since 3.4.0
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public abstract class DuoAuthAPIResponse {
+
+    /** the result. */
+    @JsonProperty("result")
+    @Nonnull private String result;
+    
+    /** the status message. */
+    @JsonProperty("status_msg")
+    @Nonnull private String statusMessage;
+    
+    /**
+     * Get the Duo result string.
+     * 
+     * @return the result string
+     */
+    @Nonnull public String getResult() {
+        return result;
+    }
+    
+    /**
+     * Get the Duo status message.
+     * 
+     * @return the Duo status message
+     */
+    @Nonnull public String getStatusMessage() {
+        return statusMessage;
+    }
+
+    /**
+     * Check if the result of the response is allow state.
+     * 
+     * @return true if result of the response is allow state
+     */
+    public abstract boolean isAllow();
+    
+    /**
+     * Check if the result of the response is deny state.
+     * 
+     * @return true if result of the response is deny state
+     */
+    public abstract boolean isDeny();
+    
+    /**
+     * Check if the status of the response is bypass state.
+     * 
+     * @return true if status of the response is bypass state
+     */
+    public abstract boolean isBypass();
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAuthenticator.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAuthenticator.java
new file mode 100644
index 0000000..95c5f86
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthAuthenticator.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.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.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.utils.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/auth endpoint.
+ */
+public class DuoAuthAuthenticator extends AbstractDuoAuthenticator {
+
+    /** a TypeReference for the repsonse generated by the endpoint. */
+    @Nonnull private final TypeReference<DuoResponseWrapper<DuoAuthResponse>> wrapperTypeRef;
+
+    /** Constructor. */
+    public DuoAuthAuthenticator() {
+        wrapperTypeRef = new TypeReference<DuoResponseWrapper<DuoAuthResponse>>() {};
+    }
+
+    /**
+     * 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 RequestBuilder rb =
+                    RequestBuilder.post().setUri(uri).addParameter("username", duoContext.getUsername());
+            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());
+            }
+            DuoSupport.signRequest(rb, duoIntegration);
+            final HttpUriRequest request = rb.build();
+
+            // do it
+            return doAPIRequest(request, wrapperTypeRef).getResponse();
+        } catch (final IOException | URISyntaxException | InvalidKeyException | NoSuchAlgorithmException ex) {
+            throw new DuoWebException("Duo AuthAPI auth request failed: " + ex.getMessage());
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthResponse.java
new file mode 100644
index 0000000..d7a4510
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoAuthResponse.java
@@ -0,0 +1,77 @@
+/*
+ * 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.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+
+/**
+ * Describes the results of an authentication attempt via the Duo AuthAPI, intended for use with a jackson
+ * {@link ObjectMapper}.
+ */
+ at JsonIgnoreProperties(ignoreUnknown = true)
+public class DuoAuthResponse extends DuoAuthAPIResponse {
+
+    /** the status string. */
+    @JsonProperty("status") @Nonnull private String status;
+
+    /** the trusted device token string. */
+    @JsonProperty("trusted_device_token") @Nullable private String trustedDeviceToken;
+
+    /**
+     * Get the Duo status string.
+     * 
+     * @return Duo status string
+     */
+    @Nonnull public String getStatus() {
+        return status;
+    }
+
+    /**
+     * Get the Duo trusted device token string.
+     * 
+     * @return Duo trusted device token string
+     */
+    @Nullable public String getTrustedDeviceToken() {
+        return trustedDeviceToken;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isAllow() {
+        return getResult().equals(DuoAuthAPI.DUO_AUTH_RESULT_ALLOW);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isDeny() {
+        return getResult().equals(DuoAuthAPI.DUO_AUTH_RESULT_DENY);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isBypass() {
+        return getStatus().equals(DuoAuthAPI.DUO_AUTH_STATUS_BYPASS);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoDevice.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoDevice.java
new file mode 100644
index 0000000..8ad3e5a
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoDevice.java
@@ -0,0 +1,101 @@
+/*
+ * 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.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 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<String>();
+    }
+
+    /**
+     * Get the device identifier.
+     * 
+     * @return the device identifier
+     */
+    @Nullable public String getDevice() {
+        return device;
+    }
+
+    /**
+     * Get the device type.
+     * 
+     * @return the device type
+     */
+    @Nullable public String getType() {
+        return type;
+    }
+
+    /**
+     * Get the device number.
+     * 
+     * @return the device number
+     */
+    @Nullable public String getNumber() {
+        return number;
+    }
+
+    /**
+     * Get the device name.
+     * 
+     * @return the device name
+     */
+    @Nullable public String getName() {
+        return name;
+    }
+
+    /**
+     * Get the device capabilities.
+     * 
+     * @return the device capabilities
+     */
+    @Nonnull public Collection<String> getCapabilities() {
+        return capabilities;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoFailureResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoFailureResponse.java
new file mode 100644
index 0000000..5dd7d35
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/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.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-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthAuthenticator.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthAuthenticator.java
new file mode 100644
index 0000000..29982b6
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthAuthenticator.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.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 com.duosecurity.duoweb.DuoWebException;
+//import javax.json.JsonObject;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.utils.URIBuilder;
+
+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<DuoResponseWrapper<DuoPreauthResponse>>() {};
+    }
+
+    /**
+     * 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 RequestBuilder rb =
+                    RequestBuilder.post().setUri(uri).addParameter("username", duoContext.getUsername());
+            DuoSupport.signRequest(rb, duoIntegration);
+            final HttpUriRequest 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-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthResponse.java
new file mode 100644
index 0000000..3c349f9
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoPreauthResponse.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.duo.impl;
+
+import java.net.URL;
+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;
+
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+
+/**
+ * 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 DuoDeivces} registered. */
+    @JsonProperty("devices") @Nonnull private List<DuoDevice> devices;
+
+    /** 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;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isAllow() {
+        return getResult().equals(DuoAuthAPI.DUO_AUTH_RESULT_ALLOW);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isDeny() {
+        return getResult().equals(DuoAuthAPI.DUO_AUTH_RESULT_DENY);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isBypass() {
+        return getResult().equals(DuoAuthAPI.DUO_PREAUTH_RESULT_ALLOW);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoResponseWrapper.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoResponseWrapper.java
new file mode 100644
index 0000000..40de6c7
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoResponseWrapper.java
@@ -0,0 +1,60 @@
+/*
+ * 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.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")
+    @Nonnull private T response;
+    
+    /** the response status. */
+    @JsonProperty("stat")
+    @Nonnull private String stat;
+    
+    /**
+     * Get the inner response.
+     * 
+     * @return inner response
+     */
+    @Nonnull public T getResponse() {
+        return response;
+    }
+
+    /**
+     * Get the response status.
+     * 
+     * @return response status
+     */
+    @Nonnull public String getStat() {
+        return stat;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
index e450f72..68a7952 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/DuoSupport.java
@@ -17,29 +17,49 @@
 
 package net.shibboleth.idp.authn.duo.impl;
 
+import com.duosecurity.duoweb.Base64;
 import com.duosecurity.duoweb.DuoWeb;
 import com.duosecurity.duoweb.DuoWebException;
+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.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 
 import javax.annotation.Nonnull;
 
+import org.apache.http.NameValuePair;
+import org.apache.http.client.methods.RequestBuilder;
+import org.joda.time.DateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
 /**
- * Simple wrapper for DuoWeb operations.
+ * Helpers for DuoWeb and Duo AuthAPI operations.
  * 
  * @since 3.3.0
  */
 public final class DuoSupport {
-    
+
+    /** RFC 2822 formatter for date/time. */
+    public static final DateTimeFormatter RFC_2822_DATE_FORMAT;
+
     /** Constructor. */
     private DuoSupport() {
     }
-
+    
     /**
      * Created a signed request to Duo for a user.
      * 
@@ -92,5 +112,99 @@ public final class DuoSupport {
         }
     }
 // Checkstyle: ThrowsCount ON
+ 
+    /**
+     * 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 URLEncoder}
+     * 
+     * @since 3.4.0
+     */
+    @Nonnull @NotEmpty public static void signRequest(@Nonnull final RequestBuilder 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 = new DateTime().toString(RFC_2822_DATE_FORMAT);
+        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);
+    }
+
+    /**
+     * 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 URLEncoder}
+     */
+    private static String canonRequest(@Nonnull final RequestBuilder 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";
+        canon += createQueryString(request.getParameters());
+
+        return canon;
+    }
+
+    /**
+     * Builds a string representaion 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 URLEncoder}
+     */
+    private static String createQueryString(@Nonnull final List<NameValuePair> params)
+            throws UnsupportedEncodingException {
+
+        final ArrayList<String> args = new ArrayList<String>();
+
+        // 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, "&");
+    }
+    
+    static {
+        RFC_2822_DATE_FORMAT = DateTimeFormat.forPattern("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z");
+    }
     
 }
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java
new file mode 100644
index 0000000..7a91ec7
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeaders.java
@@ -0,0 +1,217 @@
+/*
+ * 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.impl;
+
+import javax.annotation.Nonnull;
+import javax.servlet.http.HttpServletRequest;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * 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}.
+ * 
+ * @param <InboundMessageType> type of in-bound message
+ * @param <OutboundMessageType> type of out-bound message
+
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link AuthnEventIds#NO_CREDENTIALS}
+ * @event {@link AuthnEventIds#INVALID_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<InboundMessageType,OutboundMessageType>
+        extends AbstractAuthenticationAction<InboundMessageType,OutboundMessageType> {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ExtractDuoAuthenticationFromHeaders.class);
+
+    /** Whether "auto" should be the default for factor and device. */
+    private boolean autoAuthenticationSupported;
+
+    /** 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;
+
+    /** Constructor. */
+    ExtractDuoAuthenticationFromHeaders() {
+        autoAuthenticationSupported = 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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        passcodeHeaderName = Constraint.isNotNull(StringSupport.trimOrNull(headerName),
+                "Passcode header name cannot be null or empty.");
+    }
+
+    /**
+     * 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) {
+        autoAuthenticationSupported = flag;
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(
+            @Nonnull final ProfileRequestContext<InboundMessageType,OutboundMessageType> profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        if (!super.doPreExecute(profileRequestContext, authenticationContext)) {
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(
+            @Nonnull final ProfileRequestContext<InboundMessageType,OutboundMessageType> 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);
+        }
+
+        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");
+    }
+
+    /**
+     * 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;
+        }
+        
+        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);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java
new file mode 100644
index 0000000..6aac899
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoAuthAPI.java
@@ -0,0 +1,301 @@
+/*
+ * 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.impl;
+
+import java.security.Principal;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Function;
+
+import net.shibboleth.idp.authn.AbstractValidationAction;
+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.session.context.navigate.CanonicalUsernameLookupStrategy;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+
+import com.duosecurity.duoweb.DuoWebException;
+
+/**
+ * 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_WARNING}
+ * @event {@link AuthnEventIds#ACCOUNT_ERROR}
+ * @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 AbstractValidationAction {
+
+    /** 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. */
+    @Nonnull private DuoAuthAuthenticator authAuthenticator;
+
+    /** Implementation of Duo AuthApi /preauth enpoint. */
+    @Nonnull private DuoPreauthAuthenticator preauthAuthenticator;
+
+    /** DuoApi context for tokens. **/
+    @Nonnull @NotEmpty private DuoAuthenticationContext duoContext;
+
+    /** Duo integration to use. */
+    @Nullable private DuoIntegration duoIntegration;
+
+    /** Attempted username. */
+    @Nullable @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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        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) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        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();
+            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();
+            return false;
+        }
+
+        duoContext.setUsername(username);
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    // CheckStyle: 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();
+                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();
+                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 '{}'", getLogPrefix(), username);
+                recordSuccess();
+                buildAuthenticationResult(profileRequestContext, authenticationContext);
+            } else if (DuoAuthAPI.DUO_AUTH_RESULT_DENY.equals(authResult)) {
+                log.info("{} Duo authentication failed for '{}'", getLogPrefix(), username);
+                handleError(profileRequestContext, authenticationContext, AuthnEventIds.INVALID_CREDENTIALS,
+                        AuthnEventIds.INVALID_CREDENTIALS);
+                recordFailure();
+            } 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();
+        }
+    }
+    // CheckStyle: ReturnCount OFF
+
+    /** {@inheritDoc} */
+    @Override protected Subject populateSubject(@Nonnull final Subject subject) {
+        subject.getPrincipals().add(new DuoPrincipal(username));
+        subject.getPrincipals().addAll(duoIntegration.getSupportedPrincipals(Principal.class));
+        return subject;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void buildAuthenticationResult(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull final AuthenticationContext authenticationContext) {
+        super.buildAuthenticationResult(profileRequestContext, authenticationContext);
+
+        // Bypass c14n. We already operate on a canonical name, so just re-confirm it.
+        profileRequestContext.getSubcontext(SubjectCanonicalizationContext.class, true).setPrincipalName(username);
+    }
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
index d5397d7..b680545 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/duo/impl/ValidateDuoWebResponse.java
@@ -145,7 +145,6 @@ public class ValidateDuoWebResponse extends AbstractValidationAction {
         if (duoIntegration == null) {
             log.warn("{} No DuoIntegration returned by lookup strategy", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            recordFailure();
             return false;
         }
 
@@ -160,7 +159,6 @@ public class ValidateDuoWebResponse extends AbstractValidationAction {
         if (servletRequest == null) {
             log.error("{} No ServletRequest available", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
-            recordFailure();
             return false;
         }
         
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeadersTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeadersTest.java
new file mode 100644
index 0000000..07cc56e
--- /dev/null
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/duo/impl/ExtractDuoAuthenticationFromHeadersTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.impl;
+
+
+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.authn.impl.BaseAuthenticationContextTest;
+import net.shibboleth.idp.profile.ActionTestingSupport;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.webflow.execution.Event;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/** {@link ExtractDuoAuthenticationFromHeaders} unit test. */
+public class ExtractDuoAuthenticationFromHeadersTest extends BaseAuthenticationContextTest {
+    
+    private ExtractDuoAuthenticationFromHeaders action; 
+    
+    @BeforeMethod public void setUp() throws Exception {
+        super.setUp();
+        
+        action = new ExtractDuoAuthenticationFromHeaders();
+        action.setHttpServletRequest(new MockHttpServletRequest());
+        action.initialize();
+    }
+    
+    @Test public void testNoServletNoAuto() throws Exception {
+        action = new ExtractDuoAuthenticationFromHeaders();
+        action.setAutoAuthenticationSupported(false);
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+        
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNull(duoCtx);
+    }
+
+    @Test public void testNoAuto() throws Exception {
+        action = new ExtractDuoAuthenticationFromHeaders();
+        action.setHttpServletRequest(new MockHttpServletRequest());
+        action.setAutoAuthenticationSupported(false);
+        action.initialize();
+        final Event event = action.execute(src);
+        
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNull(duoCtx);
+    }
+    
+    @Test public void testNoServletAuto() throws Exception {
+        action = new ExtractDuoAuthenticationFromHeaders();
+        action.initialize();
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+        
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNotNull(duoCtx);
+        Assert.assertEquals(duoCtx.getFactor(), DuoAuthAPI.DUO_FACTOR_AUTO);
+        Assert.assertEquals(duoCtx.getDeviceID(), DuoAuthAPI.DUO_DEVICE_AUTO);
+        Assert.assertNull(duoCtx.getPasscode());
+    }
+    
+    @Test public void testFactorAutoDevice() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader(DuoAuthAPI.DUO_FACTOR_HEADER_NAME, DuoAuthAPI.DUO_FACTOR_PUSH);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNotNull(duoCtx);
+        Assert.assertEquals(duoCtx.getFactor(), DuoAuthAPI.DUO_FACTOR_PUSH);
+        Assert.assertEquals(duoCtx.getDeviceID(), DuoAuthAPI.DUO_DEVICE_AUTO);
+        Assert.assertNull(duoCtx.getPasscode());
+    }
+    
+    @Test public void testDeviceAutoFactor() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader(DuoAuthAPI.DUO_DEVICE_HEADER_NAME, "foo");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNotNull(duoCtx);
+        Assert.assertEquals(duoCtx.getFactor(), DuoAuthAPI.DUO_FACTOR_AUTO);
+        Assert.assertEquals(duoCtx.getDeviceID(), "foo");
+        Assert.assertNull(duoCtx.getPasscode());
+    }
+    
+    @Test public void testNoPasscode() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader(DuoAuthAPI.DUO_FACTOR_HEADER_NAME, DuoAuthAPI.DUO_FACTOR_PASSCODE);
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertEvent(event, AuthnEventIds.NO_CREDENTIALS);
+
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNull(duoCtx);
+    }
+
+    @Test public void testPasscode() throws Exception {
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader(DuoAuthAPI.DUO_FACTOR_HEADER_NAME, DuoAuthAPI.DUO_FACTOR_PASSCODE);
+        ((MockHttpServletRequest) action.getHttpServletRequest()).addHeader(DuoAuthAPI.DUO_PASSCODE_HEADER_NAME, "foo");
+        
+        final Event event = action.execute(src);
+        ActionTestingSupport.assertProceedEvent(event);
+
+        final AuthenticationContext authCtx = prc.getSubcontext(AuthenticationContext.class, false);
+        final DuoAuthenticationContext duoCtx = authCtx.getSubcontext(DuoAuthenticationContext.class, false);
+        Assert.assertNotNull(duoCtx);
+        Assert.assertEquals(duoCtx.getFactor(), DuoAuthAPI.DUO_FACTOR_PASSCODE);
+        Assert.assertNull(duoCtx.getDeviceID());
+        Assert.assertEquals(duoCtx.getPasscode(), "foo");
+    }
+
+}
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list