You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by jm...@apache.org on 2016/12/06 06:27:32 UTC

[04/12] incubator-guacamole-client git commit: GUACAMOLE-136: Implement basic support for verifying user identity using Duo.

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..40ccde9
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/conf/ConfigurationService.java
@@ -0,0 +1,160 @@
+/*
+ * 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.conf;
+
+import com.google.inject.Inject;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.properties.StringGuacamoleProperty;
+
+/**
+ * Service for retrieving configuration information regarding the Duo
+ * authentication extension.
+ */
+public class ConfigurationService {
+
+    /**
+     * The Guacamole server environment.
+     */
+    @Inject
+    private Environment environment;
+
+    /**
+     * The property within guacamole.properties which defines the hostname
+     * of the Duo API endpoint to be used to verify user identities. This will
+     * usually be in the form "api-XXXXXXXX.duosecurity.com", where "XXXXXXXX"
+     * is some arbitrary alphanumeric value assigned by Duo and specific to
+     * your organization.
+     */
+    private static final StringGuacamoleProperty DUO_API_HOSTNAME =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "duo-api-hostname"; }
+
+    };
+
+    /**
+     * The property within guacamole.properties which defines the integration
+     * key received from Duo for verifying Guacamole users. This value MUST be
+     * exactly 20 characters.
+     */
+    private static final StringGuacamoleProperty DUO_INTEGRATION_KEY =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "duo-integration-key"; }
+
+    };
+
+    /**
+     * The property within guacamole.properties which defines the secret key
+     * received from Duo for verifying Guacamole users. This value MUST be
+     * exactly 40 characters.
+     */
+    private static final StringGuacamoleProperty DUO_SECRET_KEY =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "duo-secret-key"; }
+
+    };
+
+    /**
+     * 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.
+     */
+    private static final StringGuacamoleProperty DUO_APPLICATION_KEY =
+            new StringGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "duo-application-key"; }
+
+    };
+
+    /**
+     * Returns the hostname of the Duo API endpoint to be used to verify user
+     * identities, as defined in guacamole.properties by the "duo-api-hostname"
+     * property. This will usually be in the form
+     * "api-XXXXXXXX.duosecurity.com", where "XXXXXXXX" is some arbitrary
+     * alphanumeric value assigned by Duo and specific to your organization.
+     *
+     * @return
+     *     The hostname of the Duo API endpoint to be used to verify user
+     *     identities.
+     *
+     * @throws GuacamoleException
+     *     If the associated property within guacamole.properties is missing.
+     */
+    public String getAPIHostname() throws GuacamoleException {
+        return environment.getRequiredProperty(DUO_API_HOSTNAME);
+    }
+
+    /**
+     * Returns the integration key received from Duo for verifying Guacamole
+     * users, as defined in guacamole.properties by the "duo-integration-key"
+     * property. This value MUST be exactly 20 characters.
+     *
+     * @return
+     *     The integration key 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);
+    }
+
+    /**
+     * 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.
+     *
+     * @return
+     *     The secret key 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);
+    }
+
+    /**
+     * 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 arbitrary random key which was generated for Guacamole.
+     *
+     * @throws GuacamoleException
+     *     If the associated property within guacamole.properties is missing.
+     */
+    public String getApplicationKey() throws GuacamoleException {
+        return environment.getRequiredProperty(DUO_APPLICATION_KEY);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..192cbf8
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java
@@ -0,0 +1,100 @@
+/*
+ * 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;
+import org.codehaus.jackson.annotate.JsonProperty;
+
+/**
+ * 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.
+     */
+    @JsonProperty("apiHost")
+    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;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js b/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js
new file mode 100644
index 0000000..43c37dc
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js
@@ -0,0 +1,33 @@
+/*
+ * 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'
+    });
+
+}]);

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js b/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js
new file mode 100644
index 0000000..0d10f8e
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js
@@ -0,0 +1,78 @@
+/*
+ * 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',
+    function duoSignedResponseController($scope) {
+
+    /**
+     * The iframe which contains the Duo authentication interface.
+     *
+     * @type HTMLIFrameElement
+     */
+    var iframe = $('.duo-signature-response-field iframe')[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
+        $(iframe).parents('form').submit();
+
+    };
+
+    // 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
+    });
+
+}]);

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/duoModule.js
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/duoModule.js b/extensions/guacamole-auth-duo/src/main/resources/duoModule.js
new file mode 100644
index 0000000..49a342f
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/duoModule.js
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+/**
+ * Module which provides handling for Duo multi-factor authentication.
+ */
+angular.module('guacDuo', [
+    'form'
+]);
+
+// Ensure the guacDuo module is loaded along with the rest of the app
+angular.module('index').requires.push('guacDuo');

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
new file mode 100644
index 0000000..ff8fab2
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -0,0 +1,35 @@
+{
+
+    "guacamoleVersion" : "0.9.10-incubating",
+
+    "name"      : "Duo TFA Authentication Backend",
+    "namespace" : "duo",
+
+    "authProviders" : [
+        "org.apache.guacamole.auth.duo.DuoAuthenticationProvider"
+    ],
+
+    "translations" : [
+        "translations/en.json"
+    ],
+
+    "js" : [
+
+        "duoModule.js",
+        "controllers/duoSignedResponseController.js",
+        "config/duoConfig.js",
+
+        "lib/DuoWeb/LICENSE.js",
+        "lib/DuoWeb/Duo-Web-v2.js"
+
+    ],
+
+    "css" : [
+        "styles/duo.css"
+    ],
+
+    "resources" : {
+        "templates/duoSignedResponseField.html" : "text/html"
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..a02a957
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js
@@ -0,0 +1,366 @@
+/**
+ * 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));

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..58ead21
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js
@@ -0,0 +1,27 @@
+/*
+ * 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

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css b/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css
new file mode 100644
index 0000000..36d6031
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/styles/duo.css
@@ -0,0 +1,38 @@
+/*
+ * 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 iframe {
+    width: 100%;
+    max-width: 620px;
+    height: 330px;
+    border: none;
+}
+
+.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;
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html b/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html
new file mode 100644
index 0000000..4658ed0
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html
@@ -0,0 +1,3 @@
+<div class="duo-signature-response-field" ng-class="{ loading : !duoInterfaceLoaded }">
+    <iframe></iframe>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/extensions/guacamole-auth-duo/src/main/resources/translations/en.json
----------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/en.json b/extensions/guacamole-auth-duo/src/main/resources/translations/en.json
new file mode 100644
index 0000000..8682cba
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/translations/en.json
@@ -0,0 +1,13 @@
+{
+
+    "DATA_SOURCE_DUO" : {
+        "NAME" : "Duo TFA Backend"
+    },
+
+    "LOGIN" : {
+        "FIELD_HEADER_GUAC_DUO_SIGNED_RESPONSE" : "",
+        "INFO_DUO_VALIDATION_CODE_INCORRECT"    : "Duo validation code incorrect.",
+        "INFO_DUO_AUTH_REQUIRED"                : "Please authenticate with Duo to continue."
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/48af3ef4/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 99aed8b..adad175 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,7 @@
         <module>guacamole-common-js</module>
 
         <!-- Authentication extensions -->
+        <module>extensions/guacamole-auth-duo</module>
         <module>extensions/guacamole-auth-jdbc</module>
         <module>extensions/guacamole-auth-ldap</module>
         <module>extensions/guacamole-auth-noauth</module>