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>