From 20bfbe6054208b973d3ab2a3915719b89b8a9e65 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Thu, 5 Oct 2023 17:09:32 -0400 Subject: [PATCH] GUACAMOLE-1289: Update the Duo extension to the v4 API --- extensions/guacamole-auth-duo/pom.xml | 100 +---- .../duo/DuoAuthenticationProviderModule.java | 3 +- .../auth/duo/DuoAuthenticationSession.java | 74 ++++ .../duo/DuoAuthenticationSessionManager.java} | 20 +- .../auth/duo/UserVerificationService.java | 110 ++++-- .../guacamole/auth/duo/api/DuoCookie.java | 245 ------------ .../guacamole/auth/duo/api/DuoService.java | 205 ---------- .../auth/duo/api/SignedDuoCookie.java | 332 ---------------- .../auth/duo/conf/ConfigurationService.java | 107 +++-- .../auth/duo/form/DuoSignedResponseField.java | 98 ----- .../src/main/resources/config/duoConfig.js | 33 -- .../duoSignedResponseController.js | 86 ---- .../src/main/resources/guac-manifest.json | 14 +- .../main/resources/lib/DuoWeb/Duo-Web-v2.js | 366 ------------------ .../src/main/resources/lib/DuoWeb/LICENSE.js | 27 -- .../src/main/resources/styles/duo.css | 62 --- .../templates/duoSignedResponseField.html | 6 - 17 files changed, 254 insertions(+), 1634 deletions(-) create mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/DuoAuthenticationSession.java rename extensions/guacamole-auth-duo/src/main/{resources/duoModule.js => java/org/apache/guacamole/auth/duo/DuoAuthenticationSessionManager.java} (64%) delete mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java delete mode 100644 extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/form/DuoSignedResponseField.java delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/config/duoConfig.js delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/controllers/duoSignedResponseController.js delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/Duo-Web-v2.js delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/lib/DuoWeb/LICENSE.js delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/styles/duo.css delete mode 100644 extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html diff --git a/extensions/guacamole-auth-duo/pom.xml b/extensions/guacamole-auth-duo/pom.xml index 7d6ab3b23f..0614cb47d0 100644 --- a/extensions/guacamole-auth-duo/pom.xml +++ b/extensions/guacamole-auth-duo/pom.xml @@ -39,93 +39,9 @@ UTF-8 + true - - - - - - com.keithbranton.mojo - angular-maven-plugin - 0.3.4 - - - generate-resources - - html2js - - - - - ${basedir}/src/main/resources - **/*.html - ${basedir}/src/main/resources/generated/templates-main/templates.js - app/ext/duo - - - - - - com.github.buckelieg - minify-maven-plugin - - - default-cli - - UTF-8 - - ${basedir}/src/main/resources - ${project.build.directory}/classes - - / - / - duo.css - - - license.txt - - - - **/*.css - - - / - / - duo.js - - - license.txt - lib/DuoWeb/LICENSE.js - - - - **/*.js - - - - - **/*.test.js - - CLOSURE - - - - OFF - OFF - - - - - minify - - - - - - - - @@ -155,6 +71,20 @@ 2.5 provided + + + + com.duosecurity + duo-universal-sdk + 1.1.3 + + + + + org.jetbrains.kotlin + kotlin-stdlib-common + 1.4.10 + 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 a60523bf87..c50e1039de 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 @@ protected void configure() { // 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 0000000000..8dd3e8db1e --- /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 49a342f843..9bfbcc4668 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. */ +package org.apache.guacamole.auth.duo; + +import com.google.inject.Singleton; +import org.apache.guacamole.net.auth.AuthenticationSessionManager; + /** - * Module which provides handling for Duo multi-factor authentication. + * An AuthenticationSessionManager implementation that temporarily stores + * authentication attempts for Duo MFA while they are underway. */ -angular.module('guacDuo', [ - 'form' -]); - -// Ensure the guacDuo module is loaded along with the rest of the app -angular.module('index').requires.push('guacDuo'); +@Singleton +public class DuoAuthenticationSessionManager extends AuthenticationSessionManager { + + // 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 abcb486057..7ac16d51a0 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; @@ -38,6 +43,24 @@ */ 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. */ @@ -45,10 +68,11 @@ public class UserVerificationService { 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 @@ public void verifyAuthenticatedUser(AuthenticatedUser authenticatedUser) // Ignore anonymous users if (authenticatedUser.getIdentifier().equals(AuthenticatedUser.ANONYMOUS_IDENTIFIER)) return; - - // Retrieve signed Duo response from request - String signedResponse = request.getParameter(DuoSignedResponseField.PARAMETER_NAME); - - // If no signed response, request one - if (signedResponse == null) { - - // 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)); - - // Create an overall description of the additional credentials - // required to verify identity - CredentialsInfo expectedCredentials = new CredentialsInfo( - Collections.singletonList(signedResponseField)); + + String username = authenticatedUser.getIdentifier(); + + try { + + // 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); + + // If no code or state is received, assume Duo MFA redirect has not occured and do it. + if (duoCode == null || duoState == null) { + + // 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 6fa2a88c4c..0000000000 --- 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 11cca13c55..0000000000 --- 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 c959acdd16..0000000000 --- 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 40ccde9e00..212b4a6182 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 @@ public class ConfigurationService { * 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 @@ public class ConfigurationService { * 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 @@ public String getAPIHostname() throws GuacamoleException { } /** - * 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 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 String getApplicationKey() throws GuacamoleException { - return environment.getRequiredProperty(DUO_APPLICATION_KEY); + 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 df46a31220..0000000000 --- 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 43c37dc0cf..0000000000 --- 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 b4ca4f360a..0000000000 --- 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 5d98adca4f..2a9d727a91 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 a02a95756d..0000000000 --- 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 ' + - '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: - * ``` - * - * ``` - * - * @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 58ead21752..0000000000 --- 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 6d01a85cdd..0000000000 --- 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 e51e1900b1..0000000000 --- a/extensions/guacamole-auth-duo/src/main/resources/templates/duoSignedResponseField.html +++ /dev/null @@ -1,6 +0,0 @@ -
-
- - -
-