GUACAMOLE-1289: Update the Duo extension to the v4 API
diff --git a/extensions/guacamole-auth-duo/pom.xml b/extensions/guacamole-auth-duo/pom.xml
index 7d6ab3b..0614cb4 100644
--- a/extensions/guacamole-auth-duo/pom.xml
+++ b/extensions/guacamole-auth-duo/pom.xml
@@ -39,93 +39,9 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <enforcer.skip>true</enforcer.skip>
</properties>
- <build>
- <plugins>
-
- <!-- Pre-cache Angular templates with maven-angular-plugin -->
- <plugin>
- <groupId>com.keithbranton.mojo</groupId>
- <artifactId>angular-maven-plugin</artifactId>
- <version>0.3.4</version>
- <executions>
- <execution>
- <phase>generate-resources</phase>
- <goals>
- <goal>html2js</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <sourceDir>${basedir}/src/main/resources</sourceDir>
- <include>**/*.html</include>
- <target>${basedir}/src/main/resources/generated/templates-main/templates.js</target>
- <prefix>app/ext/duo</prefix>
- </configuration>
- </plugin>
-
- <!-- JS/CSS Minification Plugin -->
- <plugin>
- <groupId>com.github.buckelieg</groupId>
- <artifactId>minify-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>default-cli</id>
- <configuration>
- <charset>UTF-8</charset>
-
- <webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
- <webappTargetDir>${project.build.directory}/classes</webappTargetDir>
-
- <cssSourceDir>/</cssSourceDir>
- <cssTargetDir>/</cssTargetDir>
- <cssFinalFile>duo.css</cssFinalFile>
-
- <cssSourceFiles>
- <cssSourceFile>license.txt</cssSourceFile>
- </cssSourceFiles>
-
- <cssSourceIncludes>
- <cssSourceInclude>**/*.css</cssSourceInclude>
- </cssSourceIncludes>
-
- <jsSourceDir>/</jsSourceDir>
- <jsTargetDir>/</jsTargetDir>
- <jsFinalFile>duo.js</jsFinalFile>
-
- <jsSourceFiles>
- <jsSourceFile>license.txt</jsSourceFile>
- <jsSourceFile>lib/DuoWeb/LICENSE.js</jsSourceFile>
- </jsSourceFiles>
-
- <jsSourceIncludes>
- <jsSourceInclude>**/*.js</jsSourceInclude>
- </jsSourceIncludes>
-
- <!-- Do not minify and include tests -->
- <jsSourceExcludes>
- <jsSourceExclude>**/*.test.js</jsSourceExclude>
- </jsSourceExcludes>
- <jsEngine>CLOSURE</jsEngine>
-
- <!-- Disable warnings for JSDoc annotations -->
- <closureWarningLevels>
- <misplacedTypeAnnotation>OFF</misplacedTypeAnnotation>
- <nonStandardJsDocs>OFF</nonStandardJsDocs>
- </closureWarningLevels>
-
- </configuration>
- <goals>
- <goal>minify</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
-
- </plugins>
- </build>
-
<dependencies>
<!-- Guacamole Extension API -->
@@ -155,6 +71,20 @@
<version>2.5</version>
<scope>provided</scope>
</dependency>
+
+ <!-- Duo SDK -->
+ <dependency>
+ <groupId>com.duosecurity</groupId>
+ <artifactId>duo-universal-sdk</artifactId>
+ <version>1.1.3</version>
+ </dependency>
+
+ <!-- kotlin-stdlib-common -->
+ <dependency>
+ <groupId>org.jetbrains.kotlin</groupId>
+ <artifactId>kotlin-stdlib-common</artifactId>
+ <version>1.4.10</version>
+ </dependency>
</dependencies>
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
index a60523b..c50e103 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationProviderModule.java
@@ -21,7 +21,6 @@
import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.duo.api.DuoService;
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
@@ -74,8 +73,8 @@
// Bind Duo-specific services
bind(ConfigurationService.class);
- bind(DuoService.class);
bind(UserVerificationService.class);
+ bind(DuoAuthenticationSessionManager.class);
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
new file mode 100644
index 0000000..8dd3e8d
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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 org.apache.guacamole.auth.duo;
+
+import org.apache.guacamole.net.auth.AuthenticationSession;
+
+/**
+ * An AuthenticationSession that stores the information required for an
+ * in-progress Duo authentication attempt.
+ */
+public class DuoAuthenticationSession extends AuthenticationSession {
+
+ /**
+ * The session state generated by the Duo client, which is used to track
+ * the session through the redirect and return process.
+ */
+ private final String state;
+
+ /**
+ * The username of the user who is authenticating with this session.
+ */
+ private final String username;
+
+ /**
+ * Create a new instance of this authenticaiton session, having the given length of time
+ * for expriation and the state generated by the Duo Client.
+ *
+ * @param expires
+ * The number of milliseconds before this session is invalid.
+ *
+ * @param state
+ * The session state, as generated by the Duo Client.
+ *
+ * @param username
+ * The username of the user who is attempting authentication with Duo.
+ */
+ public DuoAuthenticationSession(long expires, String state, String username) {
+ super(expires);
+ this.state = state;
+ this.username = username;
+ }
+
+ /**
+ * Return the stored session state.
+ *
+ * @return
+ * The stored session state.
+ */
+ public String getState() {
+ return state;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/resources/duoModule.js b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
similarity index 64%
rename from extensions/guacamole-auth-duo/src/main/resources/duoModule.js
rename to extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
index 49a342f..9bfbcc4 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/duoModule.js
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java
@@ -17,12 +17,18 @@
* under the License.
*/
-/**
- * Module which provides handling for Duo multi-factor authentication.
- */
-angular.module('guacDuo', [
- 'form'
-]);
+package org.apache.guacamole.auth.duo;
-// Ensure the guacDuo module is loaded along with the rest of the app
-angular.module('index').requires.push('guacDuo');
+import com.google.inject.Singleton;
+import org.apache.guacamole.net.auth.AuthenticationSessionManager;
+
+/**
+ * An AuthenticationSessionManager implementation that temporarily stores
+ * authentication attempts for Duo MFA while they are underway.
+ */
+@Singleton
+public class DuoAuthenticationSessionManager extends AuthenticationSessionManager<DuoAuthenticationSession> {
+
+ // Nothing to see here.
+
+}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
index abcb486..7ac16d5 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/UserVerificationService.java
@@ -19,16 +19,21 @@
package org.apache.guacamole.auth.duo;
+import com.duosecurity.Client;
+import com.duosecurity.exception.DuoException;
+import com.duosecurity.model.Token;
import com.google.inject.Inject;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.duo.api.DuoService;
+import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.duo.conf.ConfigurationService;
-import org.apache.guacamole.auth.duo.form.DuoSignedResponseField;
-import org.apache.guacamole.form.Field;
+import org.apache.guacamole.form.RedirectField;
import org.apache.guacamole.language.TranslatableGuacamoleClientException;
import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
+import org.apache.guacamole.language.TranslatableMessage;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
@@ -39,16 +44,35 @@
public class UserVerificationService {
/**
+ * The name of the parameter which Duo will return in it's GET call-back
+ * that contains the code that the client will use to generate a token.
+ */
+ private static final String DUO_CODE_PARAMETER_NAME = "duo_code";
+
+ /**
+ * The name of the parameter that will be used in the GET call-back that
+ * contains the session state.
+ */
+ private static final String DUO_STATE_PARAMETER_NAME = "state";
+
+ /**
+ * The value that will be returned in the token if Duo authentication
+ * was successful.
+ */
+ private static final String DUO_TOKEN_SUCCESS_VALUE = "ALLOW";
+
+ /**
* Service for retrieving Duo configuration information.
*/
@Inject
private ConfigurationService confService;
/**
- * Service for verifying users against Duo.
+ * The authentication session manager that temporarily stores in-progress
+ * authentication attempts.
*/
@Inject
- private DuoService duoService;
+ private DuoAuthenticationSessionManager duoSessionManager;
/**
* Verifies the identity of the given user via the Duo multi-factor
@@ -75,39 +99,69 @@
// Ignore anonymous users
if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER))
return;
+
+ String username = authenticatedUser.getIdentifier();
- // Retrieve signed Duo response from request
- String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME);
+ try {
- // If no signed response, request one
- if (signedResponse == null) {
+ // Set up the Duo Client
+ Client duoClient = new Client.Builder(
+ confService.getClientId(),
+ confService.getClientSecret(),
+ confService.getAPIHostname(),
+ confService.getRedirectUrl().toString())
+ .build();
+
+ duoClient.healthCheck();
+
+
+ // Retrieve signed Duo Code and State from the request
+ String duoCode = request.getParameter(DUO_CODE_PARAMETER_NAME);
+ String duoState = request.getParameter(DUO_STATE_PARAMETER_NAME);
- // Create field which requests a signed response from Duo that
- // verifies the identity of the given user via the configured
- // Duo API endpoint
- Field signedResponseField = new DuoSignedResponseField(
- confService.getAPIHostname(),
- duoService.createSignedRequest(authenticatedUser));
+ // If no code or state is received, assume Duo MFA redirect has not occured and do it.
+ if (duoCode == null || duoState == null) {
- // Create an overall description of the additional credentials
- // required to verify identity
- CredentialsInfo expectedCredentials = new CredentialsInfo(
- Collections.singletonList(signedResponseField));
+ // Get a new session state from the Duo client
+ duoState = duoClient.generateState();
+
+ // Add this session
+ duoSessionManager.defer(new DuoAuthenticationSession(confService.getAuthTimeout(), duoState, username), duoState);
// Request additional credentials
throw new TranslatableGuacamoleInsufficientCredentialsException(
- "Verification using Duo is required before authentication "
- + "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
- expectedCredentials);
+ "Verification using Duo is required before authentication "
+ + "can continue.", "LOGIN.INFO_DUO_AUTH_REQUIRED",
+ new CredentialsInfo(Collections.singletonList(
+ new RedirectField(
+ DUO_CODE_PARAMETER_NAME,
+ new URI(duoClient.createAuthUrl(username, duoState)),
+ new TranslatableMessage("LOGIN.INFO_DUO_REDIRECT_PENDING")
+ )
+ ))
+ );
}
- // If signed response does not verify this user's identity, abort auth
- if (!duoService.isValidSignedResponse(authenticatedUser, signedResponse))
+ // Retrieve the deferred authenticaiton attempt
+ DuoAuthenticationSession duoSession = duoSessionManager.resume(duoState);
+
+ // Get the token from the DuoClient using the code and username, and check status
+ Token token = duoClient.exchangeAuthorizationCodeFor2FAResult(duoCode, duoSession.getUsername());
+ if (token == null
+ || token.getAuth_result() == null
+ || !DUO_TOKEN_SUCCESS_VALUE.equals(token.getAuth_result().getStatus()))
throw new TranslatableGuacamoleClientException("Provided Duo "
+ "validation code is incorrect.",
"LOGIN.INFO_DUO_VALIDATION_CODE_INCORRECT");
+ }
+ catch (DuoException e) {
+ throw new GuacamoleServerException("Duo Client error.", e);
+ }
+ catch (URISyntaxException e) {
+ throw new GuacamoleServerException("Error creating URI from Duo Authentication URL.", e);
+ }
}
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
deleted file mode 100644
index 6fa2a88..0000000
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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 org.apache.guacamole.auth.duo.api;
-
-import com.google.common.io.BaseEncoding;
-import java.io.UnsupportedEncodingException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleException;
-
-/**
- * Data which describes the identity of the user being verified by Duo.
- */
-public class DuoCookie {
-
- /**
- * Pattern which matches valid cookies. Each cookie is made up of three
- * sections, separated from each other by pipe symbols ("|").
- */
- private static final Pattern COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9]+)");
-
- /**
- * The index of the capturing group within COOKIE_FORMAT which contains the
- * username.
- */
- private static final int USERNAME_GROUP = 1;
-
- /**
- * The index of the capturing group within COOKIE_FORMAT which contains the
- * integration key.
- */
- private static final int INTEGRATION_KEY_GROUP = 2;
-
- /**
- * The index of the capturing group within COOKIE_FORMAT which contains the
- * expiration timestamp.
- */
- private static final int EXPIRATION_TIMESTAMP_GROUP = 3;
-
- /**
- * The username of the user being verified.
- */
- private final String username;
-
- /**
- * The integration key provided by Duo and specific to this deployment of
- * Guacamole.
- */
- private final String integrationKey;
-
- /**
- * The time that this cookie expires, in seconds since midnight of
- * 1970-01-01 (UTC).
- */
- private final long expires;
-
- /**
- * Creates a new DuoCookie which describes the identity of a user being
- * verified.
- *
- * @param username
- * The username of the user being verified.
- *
- * @param integrationKey
- * The integration key provided by Duo and specific to this deployment
- * of Guacamole.
- *
- * @param expires
- * The time that this cookie expires, in seconds since midnight of
- * 1970-01-01 (UTC).
- */
- public DuoCookie(String username, String integrationKey, long expires) {
- this.username = username;
- this.integrationKey = integrationKey;
- this.expires = expires;
- }
-
- /**
- * Returns the username of the user being verified.
- *
- * @return
- * The username of the user being verified.
- */
- public String getUsername() {
- return username;
- }
-
- /**
- * Returns the integration key provided by Duo and specific to this
- * deployment of Guacamole.
- *
- * @return
- * The integration key provided by Duo and specific to this deployment
- * of Guacamole.
- */
- public String getIntegrationKey() {
- return integrationKey;
- }
-
- /**
- * Returns the time that this cookie expires. The expiration time is
- * represented in seconds since midnight of 1970-01-01 (UTC).
- *
- * @return
- * The time that this cookie expires, in seconds since midnight of
- * 1970-01-01 (UTC).
- */
- public long getExpirationTimestamp(){
- return expires;
- }
-
- /**
- * Returns the current time as the number of seconds elapsed since
- * midnight of 1970-01-01 (UTC).
- *
- * @return
- * The current time as the number of seconds elapsed since midnight of
- * 1970-01-01 (UTC).
- */
- public static long currentTimestamp() {
- return System.currentTimeMillis() / 1000;
- }
-
- /**
- * Returns whether this cookie has expired (the current time has met or
- * exceeded the expiration timestamp).
- *
- * @return
- * true if this cookie has expired, false otherwise.
- */
- public boolean isExpired() {
- return currentTimestamp() >= expires;
- }
-
- /**
- * Parses a base64-encoded Duo cookie, producing a new DuoCookie object
- * containing the data therein. If the given string is not a valid Duo
- * cookie, an exception is thrown. Note that the cookie may be expired, and
- * must be checked for expiration prior to actual use.
- *
- * @param str
- * The base64-encoded Duo cookie to parse.
- *
- * @return
- * A new DuoCookie object containing the same data as the given
- * base64-encoded Duo cookie string.
- *
- * @throws GuacamoleException
- * If the given string is not a valid base64-encoded Duo cookie.
- */
- public static DuoCookie parseDuoCookie(String str) throws GuacamoleException {
-
- // Attempt to decode data as base64
- String data;
- try {
- data = new String(BaseEncoding.base64().decode(str), "UTF-8");
- }
-
- // Bail if invalid base64 is provided
- catch (IllegalArgumentException e) {
- throw new GuacamoleClientException("Username is not correctly "
- + "encoded as base64.", e);
- }
-
- // Throw hard errors if standard pieces of Java are missing
- catch (UnsupportedEncodingException e) {
- throw new UnsupportedOperationException("Unexpected lack of "
- + "UTF-8 support.", e);
- }
-
- // Verify format of provided data
- Matcher matcher = COOKIE_FORMAT.matcher(data);
- if (!matcher.matches())
- throw new GuacamoleClientException("Format of base64-encoded "
- + "username is invalid.");
-
- // Get username and key (simple strings)
- String username = matcher.group(USERNAME_GROUP);
- String key = matcher.group(INTEGRATION_KEY_GROUP);
-
- // Parse expiration time
- long expires;
- try {
- expires = Long.parseLong(matcher.group(EXPIRATION_TIMESTAMP_GROUP));
- }
-
- // Bail if expiration timestamp is not a valid long
- catch (NumberFormatException e) {
- throw new GuacamoleClientException("Expiration timestamp is "
- + "not valid.", e);
- }
-
- // Return parsed cookie
- return new DuoCookie(username, key, expires);
-
- }
-
- /**
- * Returns the base64-encoded string representation of this DuoCookie. The
- * format used is identical to that required by the Duo service: the
- * username, integration key, and expiration timestamp separated by pipe
- * symbols ("|") and encoded with base64.
- *
- * @return
- * The base64-encoded string representation of this DuoCookie.
- */
- @Override
- public String toString() {
-
- try {
-
- // Separate each cookie field with pipe symbols
- String data = username + "|" + integrationKey + "|" + expires;
-
- // Encode resulting cookie string with base64
- return BaseEncoding.base64().encode(data.getBytes("UTF-8"));
-
- }
-
- // Throw hard errors if standard pieces of Java are missing
- catch (UnsupportedEncodingException e) {
- throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
- }
-
- }
-
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java
deleted file mode 100644
index 11cca13..0000000
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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 org.apache.guacamole.auth.duo.api;
-
-import com.google.inject.Inject;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.duo.conf.ConfigurationService;
-import org.apache.guacamole.net.auth.AuthenticatedUser;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Service which produces signed requests and parses/verifies signed responses
- * as required by Duo's API.
- */
-public class DuoService {
-
- /**
- * Logger for this class.
- */
- private static final Logger logger = LoggerFactory.getLogger(DuoService.class);
-
- /**
- * Pattern which matches valid Duo responses. Each response is made up of
- * two sections, separated from each other by a colon, where each section
- * is a signed Duo cookie.
- */
- private static final Pattern RESPONSE_FORMAT = Pattern.compile("([^:]+):([^:]+)");
-
- /**
- * The index of the capturing group within RESPONSE_FORMAT which
- * contains the DUO_RESPONSE cookie signed by the secret key.
- */
- private static final int DUO_COOKIE_GROUP = 1;
-
- /**
- * The index of the capturing group within RESPONSE_FORMAT which
- * contains the APPLICATION cookie signed by the application key.
- */
- private static final int APP_COOKIE_GROUP = 2;
-
- /**
- * The amount of time that each generated cookie remains valid, in seconds.
- */
- private static final int COOKIE_EXPIRATION_TIME = 300;
-
- /**
- * Service for retrieving Duo configuration information.
- */
- @Inject
- private ConfigurationService confService;
-
- /**
- * Creates and signs a new request to verify the identity of the given
- * user. This request may ultimately be sent to Duo, resulting in a signed
- * response from Duo if that verification succeeds.
- *
- * @param authenticatedUser
- * The user whose identity should be verified.
- *
- * @return
- * A signed user verification request which can be sent to Duo.
- *
- * @throws GuacamoleException
- * If required Duo-specific configuration options are missing or
- * invalid, or if an error prevents generation of the signature.
- */
- public String createSignedRequest(AuthenticatedUser authenticatedUser)
- throws GuacamoleException {
-
- // Generate a cookie associating the username with the integration key
- DuoCookie cookie = new DuoCookie(authenticatedUser.getIdentifier(),
- confService.getIntegrationKey(),
- DuoCookie.currentTimestamp() + COOKIE_EXPIRATION_TIME);
-
- // Sign cookie with secret key
- SignedDuoCookie duoCookie = new SignedDuoCookie(cookie,
- SignedDuoCookie.Type.DUO_REQUEST,
- confService.getSecretKey());
-
- // Sign cookie with application key
- SignedDuoCookie appCookie = new SignedDuoCookie(cookie,
- SignedDuoCookie.Type.APPLICATION,
- confService.getApplicationKey());
-
- // Return signed request containing both signed cookies, separated by
- // a colon (as required by Duo)
- return duoCookie + ":" + appCookie;
-
- }
-
- /**
- * Returns whether the given signed response is a valid response from Duo
- * which verifies the identity of the given user. If the given response is
- * invalid or does not verify the identity of the given user (including if
- * it is a valid response which verifies the identity of a DIFFERENT user),
- * false is returned.
- *
- * @param authenticatedUser
- * The user that the given signed response should verify.
- *
- * @param signedResponse
- * The signed response received from Duo in response to a signed
- * request.
- *
- * @return
- * true if the signed response is a valid response from Duo AND verifies
- * the identity of the given user, false otherwise.
- *
- * @throws GuacamoleException
- * If required Duo-specific configuration options are missing or
- * invalid, or if an error occurs prevents validation of the signature.
- */
- public boolean isValidSignedResponse(AuthenticatedUser authenticatedUser,
- String signedResponse) throws GuacamoleException {
-
- SignedDuoCookie duoCookie;
- SignedDuoCookie appCookie;
-
- // Retrieve username from externally-authenticated user
- String username = authenticatedUser.getIdentifier();
-
- // Retrieve Duo-specific keys from configuration
- String applicationKey = confService.getApplicationKey();
- String integrationKey = confService.getIntegrationKey();
- String secretKey = confService.getSecretKey();
-
- try {
-
- // Verify format of response
- Matcher matcher = RESPONSE_FORMAT.matcher(signedResponse);
- if (!matcher.matches()) {
- logger.debug("Duo response is not in correct format.");
- return false;
- }
-
- // Parse signed cookie defining the user verified by Duo
- duoCookie = SignedDuoCookie.parseSignedDuoCookie(secretKey,
- matcher.group(DUO_COOKIE_GROUP));
-
- // Parse signed cookie defining the user this application
- // originally requested
- appCookie = SignedDuoCookie.parseSignedDuoCookie(applicationKey,
- matcher.group(APP_COOKIE_GROUP));
-
- }
-
- // Simply return false if signature fails to verify
- catch (GuacamoleException e) {
- logger.debug("Duo signature verification failed.", e);
- return false;
- }
-
- // Verify neither cookie is expired
- if (duoCookie.isExpired() || appCookie.isExpired()) {
- logger.debug("Duo response contained expired cookie(s).");
- return false;
- }
-
- // Verify the cookies in the response have the correct types
- if (duoCookie.getType() != SignedDuoCookie.Type.DUO_RESPONSE
- || appCookie.getType() != SignedDuoCookie.Type.APPLICATION) {
- logger.debug("Duo response did not contain correct cookie type(s).");
- return false;
- }
-
- // Verify integration key matches both cookies
- if (!duoCookie.getIntegrationKey().equals(integrationKey)
- || !appCookie.getIntegrationKey().equals(integrationKey)) {
- logger.debug("Integration key of Duo response is incorrect.");
- return false;
- }
-
- // Verify both cookies are for the current user
- if (!duoCookie.getUsername().equals(username)
- || !appCookie.getUsername().equals(username)) {
- logger.debug("Username of Duo response is incorrect.");
- return false;
- }
-
- // All verifications tests pass
- return true;
-
- }
-
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
deleted file mode 100644
index c959acd..0000000
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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 org.apache.guacamole.auth.duo.api;
-
-import com.google.common.io.BaseEncoding;
-import java.io.UnsupportedEncodingException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.GuacamoleServerException;
-
-/**
- * A DuoCookie which is cryptographically signed with a provided key using
- * HMAC-SHA1.
- */
-public class SignedDuoCookie extends DuoCookie {
-
- /**
- * Pattern which matches valid signed cookies. Like unsigned cookies, each
- * signed cookie is made up of three sections, separated from each other by
- * pipe symbols ("|").
- */
- private static final Pattern SIGNED_COOKIE_FORMAT = Pattern.compile("([^|]+)\\|([^|]+)\\|([0-9a-f]+)");
-
- /**
- * The index of the capturing group within SIGNED_COOKIE_FORMAT which
- * contains the cookie type prefix.
- */
- private static final int PREFIX_GROUP = 1;
-
- /**
- * The index of the capturing group within SIGNED_COOKIE_FORMAT which
- * contains the cookie's base64-encoded data.
- */
- private static final int DATA_GROUP = 2;
-
- /**
- * The index of the capturing group within SIGNED_COOKIE_FORMAT which
- * contains the signature.
- */
- private static final int SIGNATURE_GROUP = 3;
-
- /**
- * The signature algorithm that should be used to sign the cookie, as
- * defined by:
- * http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Mac
- */
- private static final String SIGNATURE_ALGORITHM = "HmacSHA1";
-
- /**
- * The type of a signed Duo cookie. Each signed Duo cookie has an
- * associated type which determines the prefix included in the string
- * representation of that cookie. As that type is included in the data
- * that is signed, different types will result in different signatures,
- * even if the data portion of the cookie is otherwise identical.
- */
- public enum Type {
-
- /**
- * A Duo cookie which has been signed with the secret key for inclusion
- * in a Duo request.
- */
- DUO_REQUEST("TX"),
-
- /**
- * A Duo cookie which has been signed with the secret key by Duo and
- * was included in a Duo response.
- */
- DUO_RESPONSE("AUTH"),
-
- /**
- * A Duo cookie which has been signed with the application key for
- * inclusion in a Duo request. Such cookies are also included in Duo
- * responses, for verification by the application.
- */
- APPLICATION("APP");
-
- /**
- * The prefix associated with the Duo cookie type. This prefix will
- * be included in the string representation of the cookie.
- */
- private final String prefix;
-
- /**
- * Creates a new Duo cookie type associated with the given string
- * prefix. This prefix will be included in the string representation of
- * the cookie.
- *
- * @param prefix
- * The prefix to associated with the Duo cookie type.
- */
- Type(String prefix) {
- this.prefix = prefix;
- }
-
- /**
- * Returns the prefix associated with the Duo cookie type.
- *
- * @return
- * The prefix to associated with this Duo cookie type.
- */
- public String getPrefix() {
- return prefix;
- }
-
- /**
- * Returns the cookie type associated with the given prefix. If no such
- * cookie type exists, null is returned.
- *
- * @param prefix
- * The prefix of the cookie type to search for.
- *
- * @return
- * The cookie type associated with the given prefix, or null if no
- * such cookie type exists.
- */
- public static Type fromPrefix(String prefix) {
-
- // Search through all defined cookie types for the given prefix
- for (Type type : Type.values()) {
- if (type.getPrefix().equals(prefix))
- return type;
- }
-
- // No such cookie type exists
- return null;
-
- }
-
- }
-
- /**
- * The type of this Duo cookie.
- */
- private final Type type;
-
- /**
- * The signature produced when the cookie was signed with HMAC-SHA1. The
- * signature covers the prefix of the type and the cookie's base64-encoded
- * data, separated by a pipe symbol.
- */
- private final String signature;
-
- /**
- * Creates a new SignedDuoCookie which describes the identity of a user
- * being verified and is cryptographically signed with HMAC-SHA1 by a given
- * key.
- *
- * @param cookie
- * The cookie defining the identity being verified.
- *
- * @param type
- * The type of the cookie being created.
- *
- * @param key
- * The key to use to generate the cryptographic signature. This key
- * will not be stored within the cookie.
- *
- * @throws GuacamoleException
- * If the given signing key is invalid.
- */
- public SignedDuoCookie(DuoCookie cookie, Type type, String key)
- throws GuacamoleException {
-
- // Init underlying cookie
- super(cookie.getUsername(), cookie.getIntegrationKey(),
- cookie.getExpirationTimestamp());
-
- // Store cookie type and signature
- this.type = type;
- this.signature = sign(key, type.getPrefix() + "|" + cookie.toString());
-
- }
-
- /**
- * Signs the given arbitrary string data with the given key using the
- * algorithm defined by SIGNATURE_ALGORITHM. Both the data and the key will
- * be interpreted as UTF-8 bytes.
- *
- * @param key
- * The key which should be used to sign the given data.
- *
- * @param data
- * The data being signed.
- *
- * @return
- * The signature produced by signing the given data with the given key,
- * encoded as lowercase hexadecimal.
- *
- * @throws GuacamoleException
- * If the given signing key is invalid.
- */
- private static String sign(String key, String data) throws GuacamoleException {
-
- try {
-
- // Attempt to sign UTF-8 bytes of provided data
- Mac mac = Mac.getInstance(SIGNATURE_ALGORITHM);
- mac.init(new SecretKeySpec(key.getBytes("UTF-8"), SIGNATURE_ALGORITHM));
-
- // Return signature as hex
- return BaseEncoding.base16().lowerCase().encode(mac.doFinal(data.getBytes("UTF-8")));
-
- }
-
- // Re-throw any errors which prevent signature
- catch (InvalidKeyException e){
- throw new GuacamoleServerException("Signing key is invalid.", e);
- }
-
- // Throw hard errors if standard pieces of Java are missing
- catch (UnsupportedEncodingException e) {
- throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
- }
- catch (NoSuchAlgorithmException e) {
- throw new UnsupportedOperationException("Unexpected lack of support "
- + "for required signature algorithm "
- + "\"" + SIGNATURE_ALGORITHM + "\".", e);
- }
-
- }
-
- /**
- * Returns the type of this Duo cookie. The Duo cookie type is dictated
- * by the context of the cookie's use, and is included with the cookie's
- * underlying data when generating the signature.
- *
- * @return
- * The type of this Duo cookie.
- */
- public Type getType() {
- return type;
- }
-
- /**
- * Returns the signature produced when the cookie was signed with HMAC-SHA1.
- * The signature covers the prefix of the cookie's type and the cookie's
- * base64-encoded data, separated by a pipe symbol.
- *
- * @return
- * The signature produced when the cookie was signed with HMAC-SHA1.
- */
- public String getSignature() {
- return signature;
- }
-
- /**
- * Parses a signed Duo cookie string, such as that produced by the
- * toString() function or received from the Duo service, producing a new
- * SignedDuoCookie object containing the associated cookie data and
- * signature. If the given string is not a valid Duo cookie, or if the
- * signature is incorrect, an exception is thrown. Note that the cookie may
- * be expired, and must be checked for expiration prior to actual use.
- *
- * @param key
- * The key that was used to sign the Duo cookie.
- *
- * @param str
- * The Duo cookie string to parse.
- *
- * @return
- * A new SignedDuoCookie object containing the same data and signature
- * as the given Duo cookie string.
- *
- * @throws GuacamoleException
- * If the given string is not a valid Duo cookie string, or if the
- * signature of the cookie is invalid.
- */
- public static SignedDuoCookie parseSignedDuoCookie(String key, String str)
- throws GuacamoleException {
-
- // Verify format of provided data
- Matcher matcher = SIGNED_COOKIE_FORMAT.matcher(str);
- if (!matcher.matches())
- throw new GuacamoleClientException("Format of signed Duo cookie "
- + "is invalid.");
-
- // Parse type from prefix
- Type type = Type.fromPrefix(matcher.group(PREFIX_GROUP));
- if (type == null)
- throw new GuacamoleClientException("Invalid Duo cookie prefix.");
-
- // Parse cookie from base64-encoded data
- DuoCookie cookie = DuoCookie.parseDuoCookie(matcher.group(DATA_GROUP));
-
- // Verify signature of cookie
- SignedDuoCookie signedCookie = new SignedDuoCookie(cookie, type, key);
- if (!signedCookie.getSignature().equals(matcher.group(SIGNATURE_GROUP)))
- throw new GuacamoleClientException("Duo cookie has incorrect signature.");
-
- // Cookie has valid signature and has parsed successfully
- return signedCookie;
-
- }
-
- /**
- * Returns the string representation of this SignedDuoCookie. The format
- * used is identical to that required by the Duo service: the type prefix,
- * base64-encoded cookie data, and HMAC-SHA1 signature separated by pipe
- * symbols ("|").
- *
- * @return
- * The string representation of this SignedDuoCookie.
- */
- @Override
- public String toString() {
- return type.getPrefix() + "|" + super.toString() + "|" + signature;
- }
-
-}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java
index 40ccde9..212b4a6 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java
@@ -20,9 +20,12 @@
package org.apache.guacamole.auth.duo.conf;
import com.google.inject.Inject;
+import java.net.URI;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.properties.IntegerGuacamoleProperty;
import org.apache.guacamole.properties.StringGuacamoleProperty;
+import org.apache.guacamole.properties.URIGuacamoleProperty;
/**
* Service for retrieving configuration information regarding the Duo
@@ -56,11 +59,11 @@
* key received from Duo for verifying Guacamole users. This value MUST be
* exactly 20 characters.
*/
- private static final StringGuacamoleProperty DUO_INTEGRATION_KEY =
+ private static final StringGuacamoleProperty DUO_CLIENT_ID =
new StringGuacamoleProperty() {
@Override
- public String getName() { return "duo-integration-key"; }
+ public String getName() { return "duo-client-id"; }
};
@@ -69,26 +72,38 @@
* received from Duo for verifying Guacamole users. This value MUST be
* exactly 40 characters.
*/
- private static final StringGuacamoleProperty DUO_SECRET_KEY =
+ private static final StringGuacamoleProperty DUO_CLIENT_SECRET =
new StringGuacamoleProperty() {
@Override
- public String getName() { return "duo-secret-key"; }
+ public String getName() { return "duo-client-secret"; }
};
-
+
/**
- * The property within guacamole.properties which defines the arbitrary
- * random key which was generated for Guacamole. Note that this value is not
- * provided by Duo, but is expected to be generated by the administrator of
- * the system hosting Guacamole. This value MUST be at least 40 characters.
+ * The property within guacamole.properties which defines the redirect URL
+ * that Duo will call after the second factor has been completed. This
+ * should be the URL used to access Guacamole.
*/
- private static final StringGuacamoleProperty DUO_APPLICATION_KEY =
- new StringGuacamoleProperty() {
-
+ private static final URIGuacamoleProperty DUO_REDIRECT_URL =
+ new URIGuacamoleProperty() {
+
@Override
- public String getName() { return "duo-application-key"; }
-
+ public String getName() { return "duo-redirect-url"; }
+
+ };
+
+ /**
+ * The property that configures the timeout, in seconds, of in-progress
+ * Duo authentication attempts. Authentication attempts that take longer
+ * than this period of time will be invalidated.
+ */
+ private static final IntegerGuacamoleProperty DUO_AUTH_TIMEOUT =
+ new IntegerGuacamoleProperty() {
+
+ @Override
+ public String getName() { return "duo-auth-timeout"; }
+
};
/**
@@ -110,51 +125,65 @@
}
/**
- * Returns the integration key received from Duo for verifying Guacamole
- * users, as defined in guacamole.properties by the "duo-integration-key"
+ * Returns the Duo client id received from Duo for verifying Guacamole
+ * users, as defined in guacamole.properties by the "duo-client-id"
* property. This value MUST be exactly 20 characters.
*
* @return
- * The integration key received from Duo for verifying Guacamole
- * users.
+ * The client id received from Duo for verifying Guacamole users.
*
* @throws GuacamoleException
* If the associated property within guacamole.properties is missing.
*/
- public String getIntegrationKey() throws GuacamoleException {
- return environment.getRequiredProperty(DUO_INTEGRATION_KEY);
+ public String getClientId() throws GuacamoleException {
+ return environment.getRequiredProperty(DUO_CLIENT_ID);
}
/**
- * Returns the secret key received from Duo for verifying Guacamole users,
- * as defined in guacamole.properties by the "duo-secret-key" property. This
- * value MUST be exactly 20 characters.
+ * Returns the client secert received from Duo for verifying Guacamole users,
+ * as defined in guacamole.properties by the "duo-client-secert" property.
+ * This value MUST be exactly 20 characters.
*
* @return
- * The secret key received from Duo for verifying Guacamole users.
+ * The client secret received from Duo for verifying Guacamole users.
*
* @throws GuacamoleException
* If the associated property within guacamole.properties is missing.
*/
- public String getSecretKey() throws GuacamoleException {
- return environment.getRequiredProperty(DUO_SECRET_KEY);
+ public String getClientSecret() throws GuacamoleException {
+ return environment.getRequiredProperty(DUO_CLIENT_SECRET);
}
-
+
/**
- * Returns the arbitrary random key which was generated for Guacamole, as
- * defined in guacamole.properties by the "duo-application-key" property.
- * Note that this value is not provided by Duo, but is expected to be
- * generated by the administrator of the system hosting Guacamole. This
- * value MUST be at least 40 characters.
- *
+ * Return the callback URL that will be called by Duo after authentication
+ * with Duo has been completed. This should be the URL to return the user
+ * to the Guacamole interface, and will be a full URL.
+ *
* @return
- * The arbitrary random key which was generated for Guacamole.
- *
- * @throws GuacamoleException
- * If the associated property within guacamole.properties is missing.
+ * The URL for Duo to use to callback to the Guacamole interface after
+ * authentication has been completed.
+ *
+ * @throws GuacamoleException
+ * If guacamole.properties cannot be read, or if the property is not
+ * defined.
*/
- public String getApplicationKey() throws GuacamoleException {
- return environment.getRequiredProperty(DUO_APPLICATION_KEY);
+ public URI getRedirectUrl() throws GuacamoleException {
+ return environment.getRequiredProperty(DUO_REDIRECT_URL);
+ }
+
+ /**
+ * Return the number of seconds after which in-progress authentication attempts with
+ * Duo should be invalidated. The default is 30 seconds.
+ *
+ * @return
+ * The number of seconds after which in-progress Duo MFA attempts should
+ * be invalidated.
+ *
+ * @throws GuacamoleException
+ * If guacamole.properties cannot be parsed.
+ */
+ public int getAuthTimeout() throws GuacamoleException {
+ return environment.getProperty(DUO_AUTH_TIMEOUT, 30);
}
}
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java
deleted file mode 100644
index df46a31..0000000
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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 org.apache.guacamole.auth.duo.form;
-
-import org.apache.guacamole.form.Field;
-
-/**
- * A custom field type which uses the DuoWeb API to produce a signed response
- * for a particular user. The signed response serves as an additional
- * authentication factor, as it cryptographically verifies possession of the
- * physical device associated with that user's Duo account.
- */
-public class DuoSignedResponseField extends Field {
-
- /**
- * The name of the HTTP parameter which an instance of this field will
- * populate within a user's credentials.
- */
- public static final String PARAMETER_NAME = "guac-duo-signed-response";
-
- /**
- * The unique name associated with this field type.
- */
- private static final String FIELD_TYPE_NAME = "GUAC_DUO_SIGNED_RESPONSE";
-
- /**
- * The hostname of the DuoWeb API endpoint.
- */
- private final String apiHost;
-
- /**
- * The signed request generated by a call to DuoWeb.signRequest().
- */
- private final String signedRequest;
-
- /**
- * Creates a new field which uses the DuoWeb API to prompt the user for
- * additional credentials. The provided credentials, if valid, will
- * ultimately be verified by Duo's service, resulting in a signed response
- * which can be cryptographically verified.
- *
- * @param apiHost
- * The hostname of the DuoWeb API endpoint.
- *
- * @param signedRequest
- * A signed request generated for the user in question by a call to
- * DuoWeb.signRequest().
- */
- public DuoSignedResponseField(String apiHost, String signedRequest) {
-
- // Init base field type properties
- super(PARAMETER_NAME, FIELD_TYPE_NAME);
-
- // Init Duo-specific properties
- this.apiHost = apiHost;
- this.signedRequest = signedRequest;
-
- }
-
- /**
- * Returns the hostname of the DuoWeb API endpoint.
- *
- * @return
- * The hostname of the DuoWeb API endpoint.
- */
- public String getApiHost() {
- return apiHost;
- }
-
- /**
- * Returns the signed request string, which must have been generated by a
- * call to DuoWeb.signRequest().
- *
- * @return
- * The signed request generated by a call to DuoWeb.signRequest().
- */
- public String getSignedRequest() {
- return signedRequest;
- }
-
-}
diff --git a/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js b/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js
deleted file mode 100644
index 43c37dc..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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.
- */
-
-/**
- * Config block which registers Duo-specific field types.
- */
-angular.module('guacDuo').config(['formServiceProvider',
- function guacDuoConfig(formServiceProvider) {
-
- // Define field for the signed response from the Duo service
- formServiceProvider.registerFieldType('GUAC_DUO_SIGNED_RESPONSE', {
- module : 'guacDuo',
- controller : 'duoSignedResponseController',
- templateUrl : 'app/ext/duo/templates/duoSignedResponseField.html'
- });
-
-}]);
diff --git a/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js b/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js
deleted file mode 100644
index b4ca4f3..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF 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.
- */
-
-/**
- * Controller for the "GUAC_DUO_SIGNED_RESPONSE" field which uses the DuoWeb
- * API to prompt the user for additional credentials, ultimately receiving a
- * signed response from the Duo service.
- */
-angular.module('guacDuo').controller('duoSignedResponseController', ['$scope', '$element',
- function duoSignedResponseController($scope, $element) {
-
- /**
- * The iframe which contains the Duo authentication interface.
- *
- * @type HTMLIFrameElement
- */
- var iframe = $element.find('iframe')[0];
-
- /**
- * The submit button which should be used to submit the login form once
- * the Duo response has been received.
- *
- * @type HTMLInputElement
- */
- var submit = $element.find('input[type="submit"]')[0];
-
- /**
- * Whether the Duo interface has finished loading within the iframe.
- *
- * @type Boolean
- */
- $scope.duoInterfaceLoaded = false;
-
- /**
- * Submits the signed response from Duo once the user has authenticated.
- * This is a callback invoked by the DuoWeb API after the user has been
- * verified and the signed response has been received.
- *
- * @param {HTMLFormElement} form
- * The form element provided by the DuoWeb API containing the signed
- * response as the value of an input field named "sig_response".
- */
- var submitSignedResponse = function submitSignedResponse(form) {
-
- // Update model to match received response
- $scope.$apply(function updateModel() {
- $scope.model = form.elements['sig_response'].value;
- });
-
- // Submit updated credentials
- submit.click();
-
- };
-
- // Update Duo loaded state when iframe finishes loading
- iframe.onload = function duoLoaded() {
- $scope.$apply(function updateLoadedState() {
- $scope.duoInterfaceLoaded = true;
- });
- };
-
- // Initialize Duo interface within iframe
- Duo.init({
- iframe : iframe,
- host : $scope.field.apiHost,
- sig_request : $scope.field.signedRequest,
- submit_callback : submitSignedResponse
- });
-
-}]);
diff --git a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
index 5d98adc..2a9d727 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -20,18 +20,6 @@
"translations/pt.json",
"translations/ru.json",
"translations/zh.json"
- ],
-
- "js" : [
- "duo.min.js"
- ],
-
- "css" : [
- "duo.min.css"
- ],
-
- "resources" : {
- "templates/duoSignedResponseField.html" : "text/html"
- }
+ ]
}
diff --git a/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js b/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js
deleted file mode 100644
index a02a957..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js
+++ /dev/null
@@ -1,366 +0,0 @@
-/**
- * Duo Web SDK v2
- * Copyright 2015, Duo Security
- */
-window.Duo = (function(document, window) {
- var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
- var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
-
- var iframeId = 'duo_iframe',
- postAction = '',
- postArgument = 'sig_response',
- host,
- sigRequest,
- duoSig,
- appSig,
- iframe,
- submitCallback;
-
- function throwError(message, url) {
- throw new Error(
- 'Duo Web SDK error: ' + message +
- (url ? ('\n' + 'See ' + url + ' for more information') : '')
- );
- }
-
- function hyphenize(str) {
- return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
- }
-
- // cross-browser data attributes
- function getDataAttribute(element, name) {
- if ('dataset' in element) {
- return element.dataset[name];
- } else {
- return element.getAttribute('data-' + hyphenize(name));
- }
- }
-
- // cross-browser event binding/unbinding
- function on(context, event, fallbackEvent, callback) {
- if ('addEventListener' in window) {
- context.addEventListener(event, callback, false);
- } else {
- context.attachEvent(fallbackEvent, callback);
- }
- }
-
- function off(context, event, fallbackEvent, callback) {
- if ('removeEventListener' in window) {
- context.removeEventListener(event, callback, false);
- } else {
- context.detachEvent(fallbackEvent, callback);
- }
- }
-
- function onReady(callback) {
- on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
- }
-
- function offReady(callback) {
- off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
- }
-
- function onMessage(callback) {
- on(window, 'message', 'onmessage', callback);
- }
-
- function offMessage(callback) {
- off(window, 'message', 'onmessage', callback);
- }
-
- /**
- * Parse the sig_request parameter, throwing errors if the token contains
- * a server error or if the token is invalid.
- *
- * @param {String} sig Request token
- */
- function parseSigRequest(sig) {
- if (!sig) {
- // nothing to do
- return;
- }
-
- // see if the token contains an error, throwing it if it does
- if (sig.indexOf('ERR|') === 0) {
- throwError(sig.split('|')[1]);
- }
-
- // validate the token
- if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
- throwError(
- 'Duo was given a bad token. This might indicate a configuration ' +
- 'problem with one of Duo\'s client libraries.',
- 'https://www.duosecurity.com/docs/duoweb#first-steps'
- );
- }
-
- var sigParts = sig.split(':');
-
- // hang on to the token, and the parsed duo and app sigs
- sigRequest = sig;
- duoSig = sigParts[0];
- appSig = sigParts[1];
-
- return {
- sigRequest: sig,
- duoSig: sigParts[0],
- appSig: sigParts[1]
- };
- }
-
- /**
- * This function is set up to run when the DOM is ready, if the iframe was
- * not available during `init`.
- */
- function onDOMReady() {
- iframe = document.getElementById(iframeId);
-
- if (!iframe) {
- throw new Error(
- 'This page does not contain an iframe for Duo to use.' +
- 'Add an element like <iframe id="duo_iframe"></iframe> ' +
- 'to this page. ' +
- 'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
- 'for more information.'
- );
- }
-
- // we've got an iframe, away we go!
- ready();
-
- // always clean up after yourself
- offReady(onDOMReady);
- }
-
- /**
- * Validate that a MessageEvent came from the Duo service, and that it
- * is a properly formatted payload.
- *
- * The Google Chrome sign-in page injects some JS into pages that also
- * make use of postMessage, so we need to do additional validation above
- * and beyond the origin.
- *
- * @param {MessageEvent} event Message received via postMessage
- */
- function isDuoMessage(event) {
- return Boolean(
- event.origin === ('https://' + host) &&
- typeof event.data === 'string' &&
- (
- event.data.match(DUO_MESSAGE_FORMAT) ||
- event.data.match(DUO_ERROR_FORMAT)
- )
- );
- }
-
- /**
- * Validate the request token and prepare for the iframe to become ready.
- *
- * All options below can be passed into an options hash to `Duo.init`, or
- * specified on the iframe using `data-` attributes.
- *
- * Options specified using the options hash will take precedence over
- * `data-` attributes.
- *
- * Example using options hash:
- * ```javascript
- * Duo.init({
- * iframe: "some_other_id",
- * host: "api-main.duo.test",
- * sig_request: "...",
- * post_action: "/auth",
- * post_argument: "resp"
- * });
- * ```
- *
- * Example using `data-` attributes:
- * ```
- * <iframe id="duo_iframe"
- * data-host="api-main.duo.test"
- * data-sig-request="..."
- * data-post-action="/auth"
- * data-post-argument="resp"
- * >
- * </iframe>
- * ```
- *
- * @param {Object} options
- * @param {String} options.iframe The iframe, or id of an iframe to set up
- * @param {String} options.host Hostname
- * @param {String} options.sig_request Request token
- * @param {String} [options.post_action=''] URL to POST back to after successful auth
- * @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
- * @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
- * the callback function with reference to the "duo_form" form object
- * submit_callback can be used to prevent the webpage from reloading.
- */
- function init(options) {
- if (options) {
- if (options.host) {
- host = options.host;
- }
-
- if (options.sig_request) {
- parseSigRequest(options.sig_request);
- }
-
- if (options.post_action) {
- postAction = options.post_action;
- }
-
- if (options.post_argument) {
- postArgument = options.post_argument;
- }
-
- if (options.iframe) {
- if ('tagName' in options.iframe) {
- iframe = options.iframe;
- } else if (typeof options.iframe === 'string') {
- iframeId = options.iframe;
- }
- }
-
- if (typeof options.submit_callback === 'function') {
- submitCallback = options.submit_callback;
- }
- }
-
- // if we were given an iframe, no need to wait for the rest of the DOM
- if (iframe) {
- ready();
- } else {
- // try to find the iframe in the DOM
- iframe = document.getElementById(iframeId);
-
- // iframe is in the DOM, away we go!
- if (iframe) {
- ready();
- } else {
- // wait until the DOM is ready, then try again
- onReady(onDOMReady);
- }
- }
-
- // always clean up after yourself!
- offReady(init);
- }
-
- /**
- * This function is called when a message was received from another domain
- * using the `postMessage` API. Check that the event came from the Duo
- * service domain, and that the message is a properly formatted payload,
- * then perform the post back to the primary service.
- *
- * @param event Event object (contains origin and data)
- */
- function onReceivedMessage(event) {
- if (isDuoMessage(event)) {
- // the event came from duo, do the post back
- doPostBack(event.data);
-
- // always clean up after yourself!
- offMessage(onReceivedMessage);
- }
- }
-
- /**
- * Point the iframe at Duo, then wait for it to postMessage back to us.
- */
- function ready() {
- if (!host) {
- host = getDataAttribute(iframe, 'host');
-
- if (!host) {
- throwError(
- 'No API hostname is given for Duo to use. Be sure to pass ' +
- 'a `host` parameter to Duo.init, or through the `data-host` ' +
- 'attribute on the iframe element.',
- 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
- );
- }
- }
-
- if (!duoSig || !appSig) {
- parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
-
- if (!duoSig || !appSig) {
- throwError(
- 'No valid signed request is given. Be sure to give the ' +
- '`sig_request` parameter to Duo.init, or use the ' +
- '`data-sig-request` attribute on the iframe element.',
- 'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
- );
- }
- }
-
- // if postAction/Argument are defaults, see if they are specified
- // as data attributes on the iframe
- if (postAction === '') {
- postAction = getDataAttribute(iframe, 'postAction') || postAction;
- }
-
- if (postArgument === 'sig_response') {
- postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
- }
-
- // point the iframe at Duo
- iframe.src = [
- 'https://', host, '/frame/web/v1/auth?tx=', duoSig,
- '&parent=', encodeURIComponent(document.location.href),
- '&v=2.3'
- ].join('');
-
- // listen for the 'message' event
- onMessage(onReceivedMessage);
- }
-
- /**
- * We received a postMessage from Duo. POST back to the primary service
- * with the response token, and any additional user-supplied parameters
- * given in form#duo_form.
- */
- function doPostBack(response) {
- // create a hidden input to contain the response token
- var input = document.createElement('input');
- input.type = 'hidden';
- input.name = postArgument;
- input.value = response + ':' + appSig;
-
- // user may supply their own form with additional inputs
- var form = document.getElementById('duo_form');
-
- // if the form doesn't exist, create one
- if (!form) {
- form = document.createElement('form');
-
- // insert the new form after the iframe
- iframe.parentElement.insertBefore(form, iframe.nextSibling);
- }
-
- // make sure we are actually posting to the right place
- form.method = 'POST';
- form.action = postAction;
-
- // add the response token input to the form
- form.appendChild(input);
-
- // away we go!
- if (typeof submitCallback === "function") {
- submitCallback.call(null, form);
- } else {
- form.submit();
- }
- }
-
- // when the DOM is ready, initialize
- // note that this will get cleaned up if the user calls init directly!
- onReady(init);
-
- return {
- init: init,
- _parseSigRequest: parseSigRequest,
- _isDuoMessage: isDuoMessage,
- _doPostBack: doPostBack
- };
-}(document, window));
diff --git a/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js b/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js
deleted file mode 100644
index 58ead21..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) 2011, Duo Security, Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * 1. Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * 3. The name of the author may not be used to endorse or promote products
- * derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
\ No newline at end of file
diff --git a/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css b/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css
deleted file mode 100644
index 6d01a85..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-.duo-signature-response-field-container {
- height: 100%;
- width: 100%;
- position: fixed;
- left: 0;
- top: 0;
- display: table;
- background: white;
-}
-
-.duo-signature-response-field {
- width: 100%;
- display: table-cell;
- vertical-align: middle;
-}
-
-.duo-signature-response-field input[type="submit"] {
- display: none !important;
-}
-
-.duo-signature-response-field iframe {
- width: 100%;
- max-width: 620px;
- height: 330px;
- border: none;
- box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
- display: block;
- margin: 1.5em auto;
-}
-
-.duo-signature-response-field iframe {
- opacity: 1;
- -webkit-transition: opacity 0.125s;
- -moz-transition: opacity 0.125s;
- -ms-transition: opacity 0.125s;
- -o-transition: opacity 0.125s;
- transition: opacity 0.125s;
-}
-
-.duo-signature-response-field.loading iframe {
- opacity: 0;
-}
diff --git a/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html b/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html
deleted file mode 100644
index e51e190..0000000
--- a/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<div class="duo-signature-response-field-container">
- <div class="duo-signature-response-field" ng-class="{ loading : !duoInterfaceLoaded }">
- <iframe></iframe>
- <input type="submit">
- </div>
-</div>