[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