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>