Repository: incubator-guacamole-client Updated Branches: refs/heads/master cf6a2b84a -> 653b7f58a
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java ---------------------------------------------------------------------- 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 new file mode 100644 index 0000000..11cca13 --- /dev/null +++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoService.java @@ -0,0 +1,205 @@ +/* + * 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; + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9056bb0f/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java ---------------------------------------------------------------------- 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 new file mode 100644 index 0000000..49fb34b --- /dev/null +++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java @@ -0,0 +1,332 @@ +/* + * 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 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 javax.xml.bind.DatatypeConverter; +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 DatatypeConverter.printHexBinary(mac.doFinal(data.getBytes("UTF-8"))).toLowerCase(); + + } + + // 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; + } + +}
