GUACAMOLE-514: Merge parameter definition and translations for VNC "username" field.

diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
index 1d3344d..c21e9c3 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
@@ -91,15 +91,16 @@
         Collection<TrackedActiveConnection> activeConnections = new ArrayList<TrackedActiveConnection>(identifiers.size());
         for (ActiveConnectionRecord record : records) {
 
-            // Sensitive information should be included if the connection was
-            // started by the current user OR the user is an admin
-            boolean includeSensitiveInformation =
+            // The current user should have access to sensitive information and
+            // be able to connect to (join) the active connection if they are
+            // the user that started the connection OR the user is an admin
+            boolean hasPrivilegedAccess =
                     isAdmin || username.equals(record.getUsername());
 
             // Add connection if within requested identifiers
             if (identifierSet.contains(record.getUUID().toString())) {
                 TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get();
-                activeConnection.init(user, record, includeSensitiveInformation);
+                activeConnection.init(user, record, hasPrivilegedAccess, hasPrivilegedAccess);
                 activeConnections.add(activeConnection);
             }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
index 5424550..cdbcc07 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
@@ -21,15 +21,20 @@
 
 import com.google.inject.Inject;
 import java.util.Date;
+import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
 import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
 import org.apache.guacamole.auth.jdbc.sharing.ConnectionSharingService;
+import org.apache.guacamole.auth.jdbc.sharing.connection.SharedConnectionDefinition;
 import org.apache.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord;
+import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
 import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.ActiveConnection;
 import org.apache.guacamole.net.auth.credentials.UserCredentials;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
 
 /**
  * An implementation of the ActiveConnection object which has an associated
@@ -44,6 +49,12 @@
     private ConnectionSharingService sharingService;
 
     /**
+     * Service for creating and tracking tunnels.
+     */
+    @Inject
+    private GuacamoleTunnelService tunnelService;
+
+    /**
      * The identifier of this active connection.
      */
     private String identifier;
@@ -85,6 +96,11 @@
     private GuacamoleTunnel tunnel;
 
     /**
+     * Whether connections to this TrackedActiveConnection are allowed.
+     */
+    private boolean connectable;
+
+    /**
      * Initializes this TrackedActiveConnection, copying the data associated
      * with the given active connection record. At a minimum, the identifier
      * of this active connection will be set, the start date, and the
@@ -102,13 +118,19 @@
      *     Whether sensitive data should be copied from the connection record
      *     as well. This includes the remote host, associated tunnel, and
      *     username.
+     *
+     * @param connectable
+     *     Whether the user that retrieved this object should be allowed to
+     *     join the active connection.
      */
     public void init(ModeledAuthenticatedUser currentUser,
             ActiveConnectionRecord activeConnectionRecord,
-            boolean includeSensitiveInformation) {
+            boolean includeSensitiveInformation,
+            boolean connectable) {
 
         super.init(currentUser);
         this.connectionRecord = activeConnectionRecord;
+        this.connectable      = connectable;
         
         // Copy all non-sensitive data from given record
         this.connection               = activeConnectionRecord.getConnection();
@@ -169,11 +191,32 @@
         this.sharingProfileIdentifier = sharingProfileIdentifier;
     }
 
+    /**
+     * Shares this active connection with the user that retrieved it, returning
+     * a SharedConnectionDefinition that can be used to establish a tunnel to
+     * the shared connection. If provided, access within the shared connection
+     * will be restricted by the sharing profile with the given identifier.
+     *
+     * @param identifier
+     *     The identifier of the sharing profile that defines the restrictions
+     *     applying to the shared connection, or null if no such restrictions
+     *     apply.
+     *
+     * @return
+     *     A new SharedConnectionDefinition which can be used to establish a
+     *     tunnel to the shared connection.
+     *
+     * @throws GuacamoleException
+     *     If permission to share this active connection is denied.
+     */
+    private SharedConnectionDefinition share(String identifier) throws GuacamoleException {
+        return sharingService.shareConnection(getCurrentUser(), connectionRecord, identifier);
+    }
+
     @Override
     public UserCredentials getSharingCredentials(String identifier)
             throws GuacamoleException {
-        return sharingService.generateTemporaryCredentials(getCurrentUser(),
-                connectionRecord, identifier);
+        return sharingService.getSharingCredentials(share(identifier));
     }
 
     @Override
@@ -216,4 +259,26 @@
         this.tunnel = tunnel;
     }
 
+    @Override
+    public boolean isConnectable() {
+        return connectable;
+    }
+
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Establish connection only if connecting is allowed
+        if (isConnectable())
+            return tunnelService.getGuacamoleTunnel(getCurrentUser(), share(null), info, tokens);
+
+        throw new GuacamoleSecurityException("Permission denied.");
+
+    }
+
+    @Override
+    public int getActiveConnections() {
+        return 0;
+    }
+
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
index efdecf0..fe038b6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
@@ -21,6 +21,7 @@
 
 import com.google.inject.Inject;
 import java.util.Collections;
+import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
 import org.apache.guacamole.GuacamoleException;
@@ -30,11 +31,14 @@
 import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
 import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService;
 import org.apache.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord;
+import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
+import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
 import org.apache.guacamole.form.Field;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.UserCredentials;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
 
 /**
  * Service which provides convenience methods for sharing active connections.
@@ -75,10 +79,16 @@
             ));
 
     /**
-     * Generates a set of temporary credentials which can be used to connect to
-     * the given connection using the given sharing profile. If the user does
-     * not have permission to share the connection via the given sharing
-     * profile, permission will be denied.
+     * Creates a new SharedConnectionDefinition which can be used to connect to
+     * the given connection, optionally restricting access to the shared
+     * connection using the given sharing profile. If the user does not have
+     * permission to share the connection via the given sharing profile,
+     * permission will be denied.
+     *
+     * @see GuacamoleTunnelService#getGuacamoleTunnel(RemoteAuthenticatedUser,
+     *          SharedConnectionDefinition, GuacamoleClientInformation, Map)
+     *
+     * @see #getSharingCredentials(SharedConnectionDefinition)
      *
      * @param user
      *     The user sharing the connection.
@@ -88,42 +98,67 @@
      *
      * @param sharingProfileIdentifier
      *     The identifier of the sharing profile dictating the semantics or
-     *     restrictions applying to the shared session.
+     *     restrictions applying to the shared session, or null if no such
+     *     restrictions should apply.
      *
      * @return
-     *     A newly-generated set of temporary credentials which can be used to
-     *     connect to the given connection.
+     *     A new SharedConnectionDefinition which can be used to connect to the
+     *     given connection.
      *
      * @throws GuacamoleException
      *     If permission to share the given connection is denied.
      */
-    public UserCredentials generateTemporaryCredentials(ModeledAuthenticatedUser user,
+    public SharedConnectionDefinition shareConnection(ModeledAuthenticatedUser user,
             ActiveConnectionRecord activeConnection,
             String sharingProfileIdentifier) throws GuacamoleException {
 
-        // Pull sharing profile (verifying access)
-        ModeledSharingProfile sharingProfile =
-                sharingProfileService.retrieveObject(user,
-                        sharingProfileIdentifier);
+        // If a sharing profile is provided, verify that permission to use that
+        // profile to share the given connection is actually granted
+        ModeledSharingProfile sharingProfile = null;
+        if (sharingProfileIdentifier != null) {
 
-        // Verify that this profile is indeed a sharing profile for the
-        // requested connection
-        String connectionIdentifier = activeConnection.getConnectionIdentifier();
-        if (sharingProfile == null || !sharingProfile.getPrimaryConnectionIdentifier().equals(connectionIdentifier))
-            throw new GuacamoleSecurityException("Permission denied.");
+            // Pull sharing profile (verifying access)
+            sharingProfile = sharingProfileService.retrieveObject(user, sharingProfileIdentifier);
+
+            // Verify that this profile is indeed a sharing profile for the
+            // requested connection
+            String connectionIdentifier = activeConnection.getConnectionIdentifier();
+            if (sharingProfile == null || !sharingProfile.getPrimaryConnectionIdentifier().equals(connectionIdentifier))
+                throw new GuacamoleSecurityException("Permission denied.");
+
+        }
 
         // Generate a share key for the requested connection
         String key = keyGenerator.getShareKey();
-        connectionMap.add(new SharedConnectionDefinition(activeConnection,
-                sharingProfile, key));
+        SharedConnectionDefinition definition = new SharedConnectionDefinition(activeConnection, sharingProfile, key);
+        connectionMap.add(definition);
 
         // Ensure the share key is properly invalidated when the original
         // connection is closed
         activeConnection.registerShareKey(key);
 
+        return definition;
+
+    }
+
+    /**
+     * Generates a set of temporary credentials which can be used to connect to
+     * the given connection shared by the SharedConnectionDefinition.
+     *
+     * @param definition
+     *     The SharedConnectionDefinition which defines the connection being
+     *     shared and any applicable restrictions.
+     *
+     * @return
+     *     A newly-generated set of temporary credentials which can be used to
+     *     connect to the connection shared by the given
+     *     SharedConnectionDefinition.
+     */
+    public UserCredentials getSharingCredentials(SharedConnectionDefinition definition) {
+
         // Return credentials defining a single expected parameter
         return new UserCredentials(SHARE_KEY,
-                Collections.singletonMap(SHARE_KEY_NAME, key));
+                Collections.singletonMap(SHARE_KEY_NAME, definition.getShareKey()));
 
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
index cb48013..4e7c3d5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
@@ -29,9 +29,11 @@
 
 /**
  * Defines the semantics/restrictions of a shared connection by associating an
- * active connection with a sharing profile. The sharing profile defines the
- * access provided to users of the shared active connection through its
- * connection parameters.
+ * active connection with an optional sharing profile. The sharing profile, if
+ * present, defines the access provided to users of the shared active
+ * connection through its connection parameters. If no sharing profile is
+ * present, the shared connection has the same level of access as the original
+ * connection.
  */
 public class SharedConnectionDefinition {
 
@@ -88,7 +90,8 @@
      *
      * @param sharingProfile
      *     A sharing profile whose associated parameters dictate the level of
-     *     access provided to the shared connection.
+     *     access provided to the shared connection, or null if the connection
+     *     should be given full access.
      *
      * @param shareKey
      *     The unique key with which a user may access the shared connection.
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
index 20ac299..abecf32 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
@@ -202,8 +202,10 @@
 
     /**
      * Returns a guacamole configuration containing the protocol and parameters
-     * from the given connection. If tokens are used in the connection
-     * parameter values, credentials from the given user will be substituted
+     * from the given connection. If the ID of an active connection is
+     * provided, that connection will be joined instead of starting a new
+     * primary connection. If tokens are used in the connection parameter
+     * values, credentials from the given user will be substituted
      * appropriately.
      *
      * @param user
@@ -213,19 +215,29 @@
      *     The connection whose protocol and parameters should be added to the
      *     returned configuration.
      *
+     * @param connectionID
+     *     The ID of the active connection to be joined, as returned by guacd,
+     *     or null if a new primary connection should be established.
+     *
      * @return
      *     A GuacamoleConfiguration containing the protocol and parameters from
      *     the given connection.
      */
     private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
-            ModeledConnection connection) {
+            ModeledConnection connection, String connectionID) {
 
         // Generate configuration from available data
         GuacamoleConfiguration config = new GuacamoleConfiguration();
 
-        // Set protocol from connection
-        ConnectionModel model = connection.getModel();
-        config.setProtocol(model.getProtocol());
+        // Join existing active connection, if any
+        if (connectionID != null)
+            config.setConnectionID(connectionID);
+
+        // Set protocol from connection if not joining an active connection
+        else {
+            ConnectionModel model = connection.getModel();
+            config.setProtocol(model.getProtocol());
+        }
 
         // Set parameters from associated data
         Collection<ConnectionParameterModel> parameters = connectionParameterMapper.select(connection.getIdentifier());
@@ -470,16 +482,17 @@
             // Retrieve connection information associated with given connection record
             ModeledConnection connection = activeConnection.getConnection();
 
-            // Pull configuration directly from the connection if we are not
-            // joining an active connection
+            // Pull configuration directly from the connection, additionally
+            // joining the existing active connection (without sharing profile
+            // restrictions) if such a connection exists
             if (activeConnection.isPrimaryConnection()) {
                 activeConnections.put(connection.getIdentifier(), activeConnection);
                 activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection);
-                config = getGuacamoleConfiguration(activeConnection.getUser(), connection);
+                config = getGuacamoleConfiguration(activeConnection.getUser(), connection, activeConnection.getConnectionID());
             }
 
-            // If we ARE joining an active connection, generate a configuration
-            // which does so
+            // If we ARE joining an active connection under the restrictions of
+            // a sharing profile, generate a configuration which does so
             else {
 
                 // Verify that the connection ID is known
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
index 3a4b148..a150212 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
@@ -32,7 +32,6 @@
 import org.apache.guacamole.net.GuacamoleSocket;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.ConnectionRecord;
-import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
 
 
 /**
@@ -202,8 +201,8 @@
      *
      * @param sharingProfile
      *     The sharing profile that was used to share access to the given
-     *     connection. As a record created in this way always refers to a
-     *     shared connection, this value may NOT be null.
+     *     connection, or null if no sharing profile should be used (access to
+     *     the connection is unrestricted).
      */
     public void init(RemoteAuthenticatedUser user,
             ActiveConnectionRecord activeConnection,
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 898deaf..17c8ed6 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -141,11 +141,17 @@
             <scope>provided</scope>
         </dependency>
 
-        <!-- JLDAP -->
+        <!-- Apache Directory LDAP API -->
         <dependency>
-            <groupId>com.novell.ldap</groupId>
-            <artifactId>jldap</artifactId>
-            <version>4.3</version>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-all</artifactId>
+            <version>2.0.0.AM4</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <!-- Guice -->
diff --git a/extensions/guacamole-auth-ldap/src/licenses/LICENSE b/extensions/guacamole-auth-ldap/src/licenses/LICENSE
index 9368e8f..0414ad6 100644
--- a/extensions/guacamole-auth-ldap/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-ldap/src/licenses/LICENSE
@@ -220,6 +220,15 @@
         Public Domain (bundled/aopalliance-1.0/LICENSE)
 
 
+Apache Directory LDAP API (http://directory.apache.org)
+-------------------------------------------------------
+
+    Version: 2.0.0.AM4
+    From: 'Apache Software Foundation' (http://apache.org)
+    License(s):
+        Apache v2.0 (bundled/directory-api-2.0.0/LICENSE-2.0.txt)
+
+
 Google Guice (https://github.com/google/guice)
 ----------------------------------------------
 
@@ -229,20 +238,6 @@
         Apache v2.0 (bundled/guice-3.0/COPYING)
 
 
-JLDAP (http://www.openldap.org/jldap/)
---------------------------------------
-
-    Version: 4.3
-    From: 'The OpenLDAP Foundation' (http://www.openldap.org/)
-    License(s):
-        OpenLDAP Public License v2.8 (bundled/jldap-4.3/LICENSE)
-        OpenLDAP Public License v2.0.1 (bundled/jldap-4.3/LICENSE-2.0.1)
-
-NOTE: JLDAP is *NOT* dual-licensed. Whether a particular source file is under
-version 2.8 or 2.0.1 of the OpenLDAP Public License depends on the license
-header of the file in question.
-
-
 JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
 ----------------------------------------------------------------------------
 
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt b/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE b/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE
deleted file mode 100644
index 05ad757..0000000
--- a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE
+++ /dev/null
@@ -1,47 +0,0 @@
-The OpenLDAP Public License
-  Version 2.8, 17 August 2003
-
-Redistribution and use of this software and associated documentation
-("Software"), with or without modification, are permitted provided
-that the following conditions are met:
-
-1. Redistributions in source form must retain copyright statements
-   and notices,
-
-2. Redistributions in binary form must reproduce applicable copyright
-   statements and notices, this list of conditions, and the following
-   disclaimer in the documentation and/or other materials provided
-   with the distribution, and
-
-3. Redistributions must contain a verbatim copy of this document.
-
-The OpenLDAP Foundation may revise this license from time to time.
-Each revision is distinguished by a version number.  You may use
-this Software under terms of this license revision or under the
-terms of any subsequent revision of the license.
-
-THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS
-CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED 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 OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S)
-OR OWNER(S) OF THE SOFTWARE 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.
-
-The names of the authors and copyright holders must not be used in
-advertising or otherwise to promote the sale, use or other dealing
-in this Software without specific, written prior permission.  Title
-to copyright in this Software shall at all times remain with copyright
-holders.
-
-OpenLDAP is a registered trademark of the OpenLDAP Foundation.
-
-Copyright 1999-2003 The OpenLDAP Foundation, Redwood City,
-California, USA.  All Rights Reserved.  Permission to copy and
-distribute verbatim copies of this document is granted.
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1 b/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1
deleted file mode 100644
index db93ec5..0000000
--- a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1
+++ /dev/null
@@ -1,56 +0,0 @@
-A number of files contained in OpenLDAP Software contain
-a statement:
- USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
- TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF
- WHICH IS AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR
- IN THE FILE "LICENSE" IN THE TOP-LEVEL DIRECTORY OF THE
- DISTRIBUTION.
-
-The following is a verbatim copy of version 2.0.1 of the OpenLDAP
-Public License referenced in the above statement.
-
-
-The OpenLDAP Public License
-
-  Version 2.0.1, 21 December 1999
-  Copyright 1999, The OpenLDAP Foundation, Redwood City, California, USA.
-  All Rights Reserved.
-
-Redistribution and use of this software and associated documentation
-("Software"), with or without modification, are permitted provided
-that the following conditions are met:
-
-1. Redistributions of source code must retain copyright
-statements and notices.  Redistributions must also contain a
-copy of this document.
-
-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 "OpenLDAP" must not be used to endorse or promote
-products derived from this Software without prior written
-permission of the OpenLDAP Foundation.  For written permission,
-please contact foundation@openldap.org.
-
-4. Products derived from this Software may not be called "OpenLDAP"
-nor may "OpenLDAP" appear in their names without prior written
-permission of the OpenLDAP Foundation.  OpenLDAP is a trademark
-of the OpenLDAP Foundation.
-
-5. Due credit should be given to the OpenLDAP Project
-(http://www.openldap.org/).
-
-THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND CONTRIBUTORS
-``AS IS'' AND ANY EXPRESSED 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 OPENLDAP FOUNDATION OR ITS CONTRIBUTORS 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.
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
index 949d1c8..9004c13 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
@@ -21,27 +21,29 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPAttributeSet;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.auth.ldap.group.UserGroupService;
 import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
 import org.apache.guacamole.auth.ldap.user.LDAPUserContext;
 import org.apache.guacamole.auth.ldap.user.UserService;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.Credentials;
+import org.apache.guacamole.token.TokenName;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
-import org.apache.guacamole.token.TokenName;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -113,16 +115,15 @@
      *     If required properties are missing, and thus the user DN cannot be
      *     determined.
      */
-    private String getUserBindDN(String username)
-            throws GuacamoleException {
+    private Dn getUserBindDN(String username) throws GuacamoleException {
 
         // If a search DN is provided, search the LDAP directory for the DN
         // corresponding to the given username
-        String searchBindDN = confService.getSearchBindDN();
+        Dn searchBindDN = confService.getSearchBindDN();
         if (searchBindDN != null) {
 
             // Create an LDAP connection using the search account
-            LDAPConnection searchConnection = ldapService.bindAs(
+            LdapNetworkConnection searchConnection = ldapService.bindAs(
                 searchBindDN,
                 confService.getSearchBindPassword()
             );
@@ -136,7 +137,7 @@
             try {
 
                 // Retrieve all DNs associated with the given username
-                List<String> userDNs = userService.getUserDNs(searchConnection, username);
+                List<Dn> userDNs = userService.getUserDNs(searchConnection, username);
                 if (userDNs.isEmpty())
                     return null;
 
@@ -164,53 +165,6 @@
     }
 
     /**
-     * Binds to the LDAP server using the provided Guacamole credentials. The
-     * DN of the user is derived using the LDAP configuration properties
-     * provided in guacamole.properties, as is the server hostname and port
-     * information.
-     *
-     * @param credentials
-     *     The credentials to use to bind to the LDAP server.
-     *
-     * @return
-     *     A bound LDAP connection, or null if the connection could not be
-     *     bound.
-     *
-     * @throws GuacamoleException
-     *     If an error occurs while binding to the LDAP server.
-     */
-    private LDAPConnection bindAs(Credentials credentials)
-        throws GuacamoleException {
-
-        // Get username and password from credentials
-        String username = credentials.getUsername();
-        String password = credentials.getPassword();
-
-        // Require username
-        if (username == null || username.isEmpty()) {
-            logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
-            return null;
-        }
-
-        // Require password, and do not allow anonymous binding
-        if (password == null || password.isEmpty()) {
-            logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
-            return null;
-        }
-
-        // Determine user DN
-        String userDN = getUserBindDN(username);
-        if (userDN == null) {
-            logger.debug("Unable to determine DN for user \"{}\".", username);
-            return null;
-        }
-
-        // Bind using user's DN
-        return ldapService.bindAs(userDN, password);
-
-    }
-
-    /**
      * Returns an AuthenticatedUser representing the user authenticated by the
      * given credentials. Also adds custom LDAP attributes to the
      * AuthenticatedUser.
@@ -228,39 +182,40 @@
      */
     public LDAPAuthenticatedUser authenticateUser(Credentials credentials)
             throws GuacamoleException {
-
+        
+        String username = credentials.getUsername();
+        String password = credentials.getPassword();
+        
+        // Username and password are required
+        if (username == null
+                || username.isEmpty()
+                || password == null
+                || password.isEmpty()) {
+            throw new GuacamoleInvalidCredentialsException(
+                    "Anonymous bind is not currently allowed by the LDAP"
+                    + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD);
+        }
+        
+        Dn bindDn = getUserBindDN(username);
+        if (bindDn == null || bindDn.isEmpty()) {
+            throw new GuacamoleInvalidCredentialsException("Unable to determine"
+                    + " DN of user " + username, CredentialsInfo.USERNAME_PASSWORD);
+        }
+        
         // Attempt bind
-        LDAPConnection ldapConnection;
-        try {
-            ldapConnection = bindAs(credentials);
-        }
-        catch (GuacamoleException e) {
-            logger.error("Cannot bind with LDAP server: {}", e.getMessage());
-            logger.debug("Error binding with LDAP server.", e);
-            ldapConnection = null;
-        }
+        LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, password);
+            
+        // Retrieve group membership of the user that just authenticated
+        Set<String> effectiveGroups =
+                userGroupService.getParentUserGroupIdentifiers(ldapConnection,
+                        bindDn);
 
-        // If bind fails, permission to login is denied
-        if (ldapConnection == null)
-            throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
-
-        try {
-
-            // Retrieve group membership of the user that just authenticated
-            Set<String> effectiveGroups =
-                    userGroupService.getParentUserGroupIdentifiers(ldapConnection,
-                            ldapConnection.getAuthenticationDN());
-
-            // Return AuthenticatedUser if bind succeeds
-            LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-            authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, credentials.getUsername()), effectiveGroups);
-            return authenticatedUser;
-
-        }
-        // Always disconnect
-        finally {
-            ldapService.disconnect(ldapConnection);
-        }
+        // Return AuthenticatedUser if bind succeeds
+        LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+        authenticatedUser.init(credentials, getAttributeTokens(ldapConnection,
+                bindDn), effectiveGroups, bindDn);
+        
+        return authenticatedUser;
 
     }
 
@@ -286,8 +241,8 @@
      * @throws GuacamoleException
      *     If an error occurs retrieving the user DN or the attributes.
      */
-    private Map<String, String> getAttributeTokens(LDAPConnection ldapConnection,
-            String username) throws GuacamoleException {
+    private Map<String, String> getAttributeTokens(LdapNetworkConnection ldapConnection,
+            Dn userDn) throws GuacamoleException {
 
         // Get attributes from configuration information
         List<String> attrList = confService.getAttributes();
@@ -298,29 +253,27 @@
 
         // Build LDAP query parameters
         String[] attrArray = attrList.toArray(new String[attrList.size()]);
-        String userDN = getUserBindDN(username);
 
         Map<String, String> tokens = new HashMap<>();
         try {
 
             // Get LDAP attributes by querying LDAP
-            LDAPEntry userEntry = ldapConnection.read(userDN, attrArray);
+            Entry userEntry = ldapConnection.lookup(userDn, attrArray);
             if (userEntry == null)
                 return Collections.<String, String>emptyMap();
 
-            LDAPAttributeSet attrSet = userEntry.getAttributeSet();
-            if (attrSet == null)
+            Collection<Attribute> attributes = userEntry.getAttributes();
+            if (attributes == null)
                 return Collections.<String, String>emptyMap();
 
             // Convert each retrieved attribute into a corresponding token
-            for (Object attrObj : attrSet) {
-                LDAPAttribute attr = (LDAPAttribute)attrObj;
-                tokens.put(TokenName.canonicalize(attr.getName(),
-                        LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getStringValue());
+            for (Attribute attr : attributes) {
+                tokens.put(TokenName.canonicalize(attr.getId(),
+                        LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getString());
             }
 
         }
-        catch (LDAPException e) {
+        catch (LdapException e) {
             throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
         }
 
@@ -347,23 +300,25 @@
 
         // Bind using credentials associated with AuthenticatedUser
         Credentials credentials = authenticatedUser.getCredentials();
-        LDAPConnection ldapConnection = bindAs(credentials);
-        if (ldapConnection == null)
-            return null;
+        if (authenticatedUser instanceof LDAPAuthenticatedUser) {
+            Dn bindDn = ((LDAPAuthenticatedUser) authenticatedUser).getBindDn();
+            LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, credentials.getPassword());
 
-        try {
+            try {
 
-            // Build user context by querying LDAP
-            LDAPUserContext userContext = userContextProvider.get();
-            userContext.init(authenticatedUser, ldapConnection);
-            return userContext;
+                // Build user context by querying LDAP
+                LDAPUserContext userContext = userContextProvider.get();
+                userContext.init(authenticatedUser, ldapConnection);
+                return userContext;
 
+            }
+
+            // Always disconnect
+            finally {
+                ldapService.disconnect(ldapConnection);
+            }
         }
-
-        // Always disconnect
-        finally {
-            ldapService.disconnect(ldapConnection);
-        }
+        return null;
 
     }
 
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java
deleted file mode 100644
index 1fd1bea..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java
+++ /dev/null
@@ -1,74 +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.ldap;
-
-import com.novell.ldap.LDAPSearchConstraints;
-
-/**
- * Data type that handles acceptable values for configuring
- * alias dereferencing behavior when querying LDAP servers.
- */
-public enum DereferenceAliasesMode {
-
-    /**
-     * Never dereference aliases. This is the default.
-     */
-    NEVER(LDAPSearchConstraints.DEREF_NEVER),
-
-    /**
-     * Aliases are dereferenced below the base object, but not to locate
-     * the base object itself. So, if the base object is itself an alias
-     * the search will not complete.
-     */
-    SEARCHING(LDAPSearchConstraints.DEREF_SEARCHING),
-
-    /**
-     * Aliases are only dereferenced to locate the base object, but not
-     * after that. So, a search against a base object that is an alias will
-     * find any subordinates of the real object the alias references, but
-     * further aliases in the search will not be dereferenced.
-     */
-    FINDING(LDAPSearchConstraints.DEREF_FINDING),
-
-    /**
-     * Aliases will always be dereferenced, both to locate the base object
-     * and when handling results returned by the search.
-     */
-    ALWAYS(LDAPSearchConstraints.DEREF_ALWAYS);
-
-    /**
-     * The integer constant as defined in the JLDAP library that
-     * the LDAPSearchConstraints class uses to define the
-     * dereferencing behavior during search operations.
-     */
-    public final int DEREF_VALUE;
-
-    /**
-     * Initializes the dereference aliases object with the integer
-     * value the setting maps to per the JLDAP implementation.
-     *
-     * @param derefValue
-     *     The value associated with this dereference setting
-     */
-    private DereferenceAliasesMode(int derefValue) {
-        this.DEREF_VALUE = derefValue;
-    }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java
deleted file mode 100644
index 5dce244..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java
+++ /dev/null
@@ -1,120 +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.ldap;
-
-/**
- * Service for escaping LDAP filters, distinguished names (DN's), etc.
- */
-public class EscapingService {
-
-    /**
-     * Escapes the given string for use within an LDAP search filter. This
-     * implementation is provided courtesy of OWASP:
-     * 
-     * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
-     *
-     * @param filter
-     *     The string to escape such that it has no special meaning within an
-     *     LDAP search filter.
-     *
-     * @return
-     *     The escaped string, safe for use within an LDAP search filter.
-     */
-    public String escapeLDAPSearchFilter(String filter) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < filter.length(); i++) {
-            char curChar = filter.charAt(i);
-            switch (curChar) {
-                case '\\':
-                    sb.append("\\5c");
-                    break;
-                case '*':
-                    sb.append("\\2a");
-                    break;
-                case '(':
-                    sb.append("\\28");
-                    break;
-                case ')':
-                    sb.append("\\29");
-                    break;
-                case '\u0000':
-                    sb.append("\\00");
-                    break;
-                default:
-                    sb.append(curChar);
-            }
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Escapes the given string such that it is safe for use within an LDAP
-     * distinguished name (DN). This implementation is provided courtesy of
-     * OWASP:
-     * 
-     * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
-     *
-     * @param name
-     *     The string to escape such that it has no special meaning within an
-     *     LDAP DN.
-     *
-     * @return
-     *     The escaped string, safe for use within an LDAP DN.
-     */
-    public String escapeDN(String name) {
-        StringBuilder sb = new StringBuilder();
-        if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
-            sb.append('\\'); // add the leading backslash if needed
-        }
-        for (int i = 0; i < name.length(); i++) {
-            char curChar = name.charAt(i);
-            switch (curChar) {
-                case '\\':
-                    sb.append("\\\\");
-                    break;
-                case ',':
-                    sb.append("\\,");
-                    break;
-                case '+':
-                    sb.append("\\+");
-                    break;
-                case '"':
-                    sb.append("\\\"");
-                    break;
-                case '<':
-                    sb.append("\\<");
-                    break;
-                case '>':
-                    sb.append("\\>");
-                    break;
-                case ';':
-                    sb.append("\\;");
-                    break;
-                default:
-                    sb.append(curChar);
-            }
-        }
-        if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
-            sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
-        }
-        return sb.toString();
-    }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
index 23decec..9cfaadf 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
@@ -20,6 +20,7 @@
 package org.apache.guacamole.auth.ldap;
 
 import com.google.inject.AbstractModule;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.auth.ldap.connection.ConnectionService;
 import org.apache.guacamole.auth.ldap.user.UserService;
 import org.apache.guacamole.GuacamoleException;
@@ -76,7 +77,6 @@
         // Bind LDAP-specific services
         bind(ConfigurationService.class);
         bind(ConnectionService.class);
-        bind(EscapingService.class);
         bind(LDAPConnectionService.class);
         bind(ObjectQueryService.class);
         bind(UserGroupService.class);
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
index 3aaf324..49a3f7c 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
@@ -20,14 +20,28 @@
 package org.apache.guacamole.auth.ldap;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPConstraints;
-import com.novell.ldap.LDAPException;
-import com.novell.ldap.LDAPJSSESecureSocketFactory;
-import com.novell.ldap.LDAPJSSEStartTLSFactory;
-import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.BindRequestImpl;
+import org.apache.directory.api.ldap.model.message.BindResponse;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.url.LdapUrl;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.GuacamoleUnsupportedException;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
+import org.apache.guacamole.auth.ldap.conf.EncryptionMethod;
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -39,7 +53,7 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
+    private static final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
 
     /**
      * Service for retrieving LDAP server configuration information.
@@ -48,19 +62,24 @@
     private ConfigurationService confService;
 
     /**
-     * Creates a new instance of LDAPConnection, configured as required to use
-     * whichever encryption method is requested within guacamole.properties.
+     * Creates a new instance of LdapNetworkConnection, configured as required
+     * to use whichever encryption method is requested within
+     * guacamole.properties.
      *
      * @return
-     *     A new LDAPConnection instance which has already been configured to
-     *     use the encryption method requested within guacamole.properties.
+     *     A new LdapNetworkConnection instance which has already been 
+     *     configured to use the encryption method requested within 
+     *     guacamole.properties.
      *
      * @throws GuacamoleException
      *     If an error occurs while parsing guacamole.properties, or if the
      *     requested encryption method is actually not implemented (a bug).
      */
-    private LDAPConnection createLDAPConnection() throws GuacamoleException {
+    private LdapNetworkConnection createLDAPConnection() throws GuacamoleException {
 
+        String host = confService.getServerHostname();
+        int port = confService.getServerPort();
+        
         // Map encryption method to proper connection and socket factory
         EncryptionMethod encryptionMethod = confService.getEncryptionMethod();
         switch (encryptionMethod) {
@@ -68,17 +87,17 @@
             // Unencrypted LDAP connection
             case NONE:
                 logger.debug("Connection to LDAP server without encryption.");
-                return new LDAPConnection();
+                return new LdapNetworkConnection(host, port);
 
             // LDAP over SSL (LDAPS)
             case SSL:
                 logger.debug("Connecting to LDAP server using SSL/TLS.");
-                return new LDAPConnection(new LDAPJSSESecureSocketFactory());
+                return new LdapNetworkConnection(host, port, true);
 
             // LDAP + STARTTLS
             case STARTTLS:
                 logger.debug("Connecting to LDAP server using STARTTLS.");
-                return new LDAPConnection(new LDAPJSSEStartTLSFactory());
+                return new LdapNetworkConnection(host, port);
 
             // The encryption method, though known, is not actually
             // implemented. If encountered, this would be a bug.
@@ -106,86 +125,103 @@
      * @throws GuacamoleException
      *     If an error occurs while binding to the LDAP server.
      */
-    public LDAPConnection bindAs(String userDN, String password)
+    public LdapNetworkConnection bindAs(Dn userDN, String password)
             throws GuacamoleException {
 
-        // Obtain appropriately-configured LDAPConnection instance
-        LDAPConnection ldapConnection = createLDAPConnection();
-
-        // Configure LDAP connection constraints
-        LDAPConstraints ldapConstraints = ldapConnection.getConstraints();
-        if (ldapConstraints == null)
-          ldapConstraints = new LDAPConstraints();
-
-        // Set whether or not we follow referrals
-        ldapConstraints.setReferralFollowing(confService.getFollowReferrals());
-
-        // Set referral authentication to use the provided credentials.
-        if (userDN != null && !userDN.isEmpty())
-            ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password));
-
-        // Set the maximum number of referrals we follow
-        ldapConstraints.setHopLimit(confService.getMaxReferralHops());
-
-        // Set timelimit to wait for LDAP operations, converting to ms
-        ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000);
-
-        // Apply the constraints to the connection
-        ldapConnection.setConstraints(ldapConstraints);
-
-        try {
+        // Get ldapConnection and try to connect and bind.
+        try (LdapNetworkConnection ldapConnection = createLDAPConnection()) {
 
             // Connect to LDAP server
-            ldapConnection.connect(
-                confService.getServerHostname(),
-                confService.getServerPort()
-            );
+            ldapConnection.connect();
 
             // Explicitly start TLS if requested
             if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS)
-                ldapConnection.startTLS();
+                ldapConnection.startTls();
 
-        }
-        catch (LDAPException e) {
-            logger.error("Unable to connect to LDAP server: {}", e.getMessage());
-            logger.debug("Failed to connect to LDAP server.", e);
-            return null;
-        }
-
-        // Bind using provided credentials
-        try {
-
-            byte[] passwordBytes;
-            try {
-
-                // Convert password into corresponding byte array
-                if (password != null)
-                    passwordBytes = password.getBytes("UTF-8");
-                else
-                    passwordBytes = null;
-
-            }
-            catch (UnsupportedEncodingException e) {
-                logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
-                logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
-                disconnect(ldapConnection);
-                return null;
-            }
-
-            // Bind as user
-            ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes);
+            // Bind using provided credentials
+            BindRequest bindRequest = new BindRequestImpl();
+            bindRequest.setDn(userDN);
+            bindRequest.setCredentials(password);
+            BindResponse bindResponse = ldapConnection.bind(bindRequest);
+            if (bindResponse.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS)
+                return ldapConnection;
+            
+            else
+                throw new GuacamoleInvalidCredentialsException("Error binding"
+                        + " to server: " + bindResponse.toString(),
+                        CredentialsInfo.USERNAME_PASSWORD);
 
         }
 
         // Disconnect if an error occurs during bind
-        catch (LDAPException e) {
-            logger.debug("LDAP bind failed.", e);
-            disconnect(ldapConnection);
-            return null;
+        catch (LdapException e) {
+            logger.debug("Unable to bind to LDAP server.", e);
+            throw new GuacamoleInvalidCredentialsException(
+                    "Unable to bind to the LDAP server.",
+                    CredentialsInfo.USERNAME_PASSWORD);
         }
 
-        return ldapConnection;
-
+    }
+    
+    /**
+     * Generate a new LdapNetworkConnection object for following a referral
+     * with the given LdapUrl, and copy the username and password
+     * from the original connection.
+     * 
+     * @param referralUrl
+     *     The LDAP URL to follow.
+     * 
+     * @param ldapConfig
+     *     The connection configuration to use to retrieve username and
+     *     password.
+     * 
+     * @param hop
+     *     The current hop number of this referral - once the configured
+     *     limit is reached, this method will throw an exception.
+     * 
+     * @return
+     *     A LdapNetworkConnection object that points at the location
+     *     specified in the referralUrl.
+     *     
+     * @throws GuacamoleException
+     *     If an error occurs parsing out the LdapUrl object or the
+     *     maximum number of referral hops is reached.
+     */
+    public LdapNetworkConnection getReferralConnection(LdapUrl referralUrl,
+            LdapConnectionConfig ldapConfig, int hop)
+            throws GuacamoleException {
+       
+        if (hop >= confService.getMaxReferralHops())
+            throw new GuacamoleServerException("Maximum number of referrals reached.");
+        
+        LdapConnectionConfig referralConfig = new LdapConnectionConfig();
+        
+        // Copy bind name and password from original config
+        referralConfig.setName(ldapConfig.getName());
+        referralConfig.setCredentials(ldapConfig.getCredentials());        
+        
+        // Look for host - if not there, bail out.
+        String host = referralUrl.getHost();
+        if (host == null || host.isEmpty())
+            throw new GuacamoleServerException("Referral URL contains no host.");
+       
+        referralConfig.setLdapHost(host);
+       
+        // Look for port, or assign a default.
+        int port = referralUrl.getPort();
+        if (port < 1)
+            referralConfig.setLdapPort(389);
+        else
+            referralConfig.setLdapPort(port);
+        
+        // Deal with SSL connections
+        if (referralUrl.getScheme().equals(LdapUrl.LDAPS_SCHEME))
+            referralConfig.setUseSsl(true);
+        else
+            referralConfig.setUseSsl(false);
+        
+        return new LdapNetworkConnection(referralConfig);
+        
     }
 
     /**
@@ -195,19 +231,53 @@
      * @param ldapConnection
      *     The LDAP connection to disconnect.
      */
-    public void disconnect(LDAPConnection ldapConnection) {
+    public void disconnect(LdapConnection ldapConnection) {
 
         // Attempt disconnect
         try {
-            ldapConnection.disconnect();
+            ldapConnection.close();
         }
 
         // Warn if disconnect unexpectedly fails
-        catch (LDAPException e) {
+        catch (IOException e) {
             logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
             logger.debug("LDAP disconnect failed.", e);
         }
 
     }
+    
+    /**
+     * Generate a SearchRequest object using the given Base DN and filter
+     * and retrieving other properties from the LDAP configuration service.
+     * 
+     * @param baseDn
+     *     The LDAP Base DN at which to search the search.
+     * 
+     * @param filter
+     *     A string representation of a LDAP filter to use for the search.
+     * 
+     * @return
+     *     The properly-configured SearchRequest object.
+     * 
+     * @throws GuacamoleException
+     *     If an error occurs retrieving any of the configuration values.
+     */
+    public SearchRequest getSearchRequest(Dn baseDn, ExprNode filter)
+            throws GuacamoleException {
+        
+        SearchRequest searchRequest = new SearchRequestImpl();
+        searchRequest.setBase(baseDn);
+        searchRequest.setDerefAliases(confService.getDereferenceAliases());
+        searchRequest.setScope(SearchScope.SUBTREE);
+        searchRequest.setFilter(filter);
+        searchRequest.setSizeLimit(confService.getMaxResults());
+        searchRequest.setTimeLimit(confService.getOperationTimeout());
+        searchRequest.setTypesOnly(false);
+        
+        if (confService.getFollowReferrals())
+            searchRequest.followReferrals();
+        
+        return searchRequest;
+    }
 
 }
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
index 2196c2f..ebf9792 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
@@ -20,18 +20,29 @@
 package org.apache.guacamole.auth.ldap;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
-import com.novell.ldap.LDAPReferralException;
-import com.novell.ldap.LDAPSearchResults;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
+import org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.AndNode;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.OrNode;
+import org.apache.directory.api.ldap.model.message.Referral;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.url.LdapUrl;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.net.auth.Identifiable;
@@ -50,19 +61,13 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
-
+    private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
+    
     /**
-     * Service for escaping parts of LDAP queries.
+     * Service for connecting to LDAP directory.
      */
     @Inject
-    private EscapingService escapingService;
-
-    /**
-     * Service for retrieving LDAP server configuration information.
-     */
-    @Inject
-    private ConfigurationService confService;
+    private LDAPConnectionService ldapService;
 
     /**
      * Returns the identifier of the object represented by the given LDAP
@@ -86,14 +91,18 @@
      *     The identifier of the object represented by the given LDAP entry, or
      *     null if no attributes declared as containing the identifier of the
      *     object are present on the entry.
+     * 
+     * @throws LdapInvalidAttributeValueException
+     *     If an error occurs retrieving the value of the identifier attribute.
      */
-    public String getIdentifier(LDAPEntry entry, Collection<String> attributes) {
+    public String getIdentifier(Entry entry, Collection<String> attributes) 
+            throws LdapInvalidAttributeValueException {
 
         // Retrieve the first value of the highest priority identifier attribute
         for (String identifierAttribute : attributes) {
-            LDAPAttribute identifier = entry.getAttribute(identifierAttribute);
+            Attribute identifier = entry.get(identifierAttribute);
             if (identifier != null)
-                return identifier.getStringValue();
+                return identifier.getString();
         }
 
         // No identifier attribute is present on the entry
@@ -125,42 +134,25 @@
      *     An LDAP query which will search for arbitrary LDAP objects having at
      *     least one of the given attributes set to the specified value.
      */
-    public String generateQuery(String filter,
+    public ExprNode generateQuery(ExprNode filter,
             Collection<String> attributes, String attributeValue) {
 
         // Build LDAP query for objects having at least one attribute and with
         // the given search filter
-        StringBuilder ldapQuery = new StringBuilder();
-        ldapQuery.append("(&");
-        ldapQuery.append(filter);
+        AndNode searchFilter = new AndNode();
+        searchFilter.addNode(filter);
 
         // Include all attributes within OR clause if there are more than one
-        if (attributes.size() > 1)
-            ldapQuery.append("(|");
-
+        OrNode attributeFilter = new OrNode();
+       
         // Add equality comparison for each possible attribute
-        for (String attribute : attributes) {
-            ldapQuery.append("(");
-            ldapQuery.append(escapingService.escapeLDAPSearchFilter(attribute));
+        attributes.forEach(attribute ->
+            attributeFilter.addNode(new EqualityNode(attribute, attributeValue))
+        );
 
-            if (attributeValue != null) {
-                ldapQuery.append("=");
-                ldapQuery.append(escapingService.escapeLDAPSearchFilter(attributeValue));
-                ldapQuery.append(")");
-            }
-            else
-                ldapQuery.append("=*)");
-
-        }
-
-        // Close OR clause, if any
-        if (attributes.size() > 1)
-            ldapQuery.append(")");
-
-        // Close overall query (AND clause)
-        ldapQuery.append(")");
-
-        return ldapQuery.toString();
+        searchFilter.addNode(attributeFilter);
+        
+        return searchFilter;
 
     }
 
@@ -178,6 +170,10 @@
      *
      * @param query
      *     The LDAP query to execute.
+     * 
+     * @param searchHop
+     *     The current level of referral depth for this search, used for
+     *     limiting the maximum depth to which referrals can go.
      *
      * @return
      *     A list of all results accessible to the user currently bound under
@@ -188,43 +184,40 @@
      *     information required to execute the query cannot be read from
      *     guacamole.properties.
      */
-    public List<LDAPEntry> search(LDAPConnection ldapConnection,
-            String baseDN, String query) throws GuacamoleException {
+    public List<Entry> search(LdapNetworkConnection ldapConnection,
+            Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException {
 
         logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
 
-        try {
+        LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
+            
+        // Search within subtree of given base DN
+        SearchRequest request = ldapService.getSearchRequest(baseDN,
+                query);
+            
+        // Produce list of all entries in the search result, automatically
+        // following referrals if configured to do so
+        List<Entry> entries = new ArrayList<>();
+            
+        try (SearchCursor results = ldapConnection.search(request)) {
+            while (results.next()) {
 
-            // Search within subtree of given base DN
-            LDAPSearchResults results = ldapConnection.search(baseDN,
-                    LDAPConnection.SCOPE_SUB, query, null, false,
-                    confService.getLDAPSearchConstraints());
-
-            // Produce list of all entries in the search result, automatically
-            // following referrals if configured to do so
-            List<LDAPEntry> entries = new ArrayList<>(results.getCount());
-            while (results.hasMore()) {
-
-                try {
-                    entries.add(results.next());
+                if (results.isEntry()) {
+                    entries.add(results.getEntry());
                 }
+                else if (results.isReferral() && request.isFollowReferrals()) {
 
-                // Warn if referrals cannot be followed
-                catch (LDAPReferralException e) {
-                    if (confService.getFollowReferrals()) {
-                        logger.error("Could not follow referral: {}", e.getFailedReferral());
-                        logger.debug("Error encountered trying to follow referral.", e);
-                        throw new GuacamoleServerException("Could not follow LDAP referral.", e);
+                    Referral referral = results.getReferral();
+                    for (String url : referral.getLdapUrls()) {
+                        LdapNetworkConnection referralConnection =
+                                ldapService.getReferralConnection(
+                                        new LdapUrl(url),
+                                        ldapConnectionConfig, searchHop++
+                                );
+                        entries.addAll(search(referralConnection, baseDN, query,
+                                searchHop));
                     }
-                    else {
-                        logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
-                        logger.debug("Got a referral, but configured to not follow them.", e);
-                    }
-                }
-                
-                catch (LDAPException e) {
-                  logger.warn("Failed to process an LDAP search result. Error was: {}", e.resultCodeToString());
-                  logger.debug("Error processing LDAPEntry search result.", e);
+
                 }
 
             }
@@ -232,7 +225,7 @@
             return entries;
 
         }
-        catch (LDAPException | GuacamoleException e) {
+        catch (CursorException | IOException | LdapException e) {
             throw new GuacamoleServerException("Unable to query list of "
                     + "objects from LDAP directory.", e);
         }
@@ -274,11 +267,11 @@
      *     information required to execute the query cannot be read from
      *     guacamole.properties.
      */
-    public List<LDAPEntry> search(LDAPConnection ldapConnection, String baseDN,
-            String filter, Collection<String> attributes, String attributeValue)
+    public List<Entry> search(LdapNetworkConnection ldapConnection, Dn baseDN,
+            ExprNode filter, Collection<String> attributes, String attributeValue)
             throws GuacamoleException {
-        String query = generateQuery(filter, attributes, attributeValue);
-        return search(ldapConnection, baseDN, query);
+        ExprNode query = generateQuery(filter, attributes, attributeValue);
+        return search(ldapConnection, baseDN, query, 0);
     }
 
     /**
@@ -302,15 +295,15 @@
      *     {@link Map} under its corresponding identifier.
      */
     public <ObjectType extends Identifiable> Map<String, ObjectType>
-        asMap(List<LDAPEntry> entries, Function<LDAPEntry, ObjectType> mapper) {
+        asMap(List<Entry> entries, Function<Entry, ObjectType> mapper) {
 
         // Convert each entry to the corresponding Guacamole API object
         Map<String, ObjectType> objects = new HashMap<>(entries.size());
-        for (LDAPEntry entry : entries) {
+        for (Entry entry : entries) {
 
             ObjectType object = mapper.apply(entry);
             if (object == null) {
-                logger.debug("Ignoring object \"{}\".", entry.getDN());
+                logger.debug("Ignoring object \"{}\".", entry.getDn().toString());
                 continue;
             }
 
@@ -320,7 +313,7 @@
             if (objects.putIfAbsent(identifier, object) != null)
                 logger.warn("Multiple objects ambiguously map to the "
                         + "same identifier (\"{}\"). Ignoring \"{}\" as "
-                        + "a duplicate.", identifier, entry.getDN());
+                        + "a duplicate.", identifier, entry.getDn().toString());
 
         }
 
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
deleted file mode 100644
index a5e359a..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
+++ /dev/null
@@ -1,79 +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.ldap;
-
-import com.novell.ldap.LDAPAuthHandler;
-import com.novell.ldap.LDAPAuthProvider;
-import java.io.UnsupportedEncodingException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Class that implements the necessary authentication handling
- * for following referrals in LDAP connections.
- */
-public class ReferralAuthHandler implements LDAPAuthHandler {
-
-    /**
-     * Logger for this class.
-     */
-    private final Logger logger = LoggerFactory.getLogger(ReferralAuthHandler.class);
-
-    /**
-     * The LDAPAuthProvider object that will be set and returned to the referral handler.
-     */
-    private final LDAPAuthProvider ldapAuth;
-
-    /**
-     * Creates a ReferralAuthHandler object to handle authentication when
-     * following referrals in a LDAP connection, using the provided dn and
-     * password.
-     * 
-     * @param dn
-     *     The distinguished name to use for the referral login.
-     * 
-     * @param password
-     *     The password to use for the referral login.
-     */
-    public ReferralAuthHandler(String dn, String password) {
-        byte[] passwordBytes;
-        try {
-
-            // Convert password into corresponding byte array
-            if (password != null)
-                passwordBytes = password.getBytes("UTF-8");
-            else
-                passwordBytes = null;
-
-        }   
-        catch (UnsupportedEncodingException e) {
-            logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
-            logger.debug("Support for UTF-8 (as required by Java spec) not found.", e); 
-            throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
-        }
-        ldapAuth = new LDAPAuthProvider(dn, passwordBytes);
-    }
-
-    @Override
-    public LDAPAuthProvider getAuthProvider(String host, int port) {
-        return ldapAuth;
-    }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
similarity index 87%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
index e8ea0ac..588c60d 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
@@ -17,16 +17,17 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPSearchConstraints;
 import java.util.Collections;
 import java.util.List;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.PresenceNode;
+import org.apache.directory.api.ldap.model.message.AliasDerefMode;
+import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.environment.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Service for retrieving configuration information regarding the LDAP server.
@@ -34,11 +35,6 @@
 public class ConfigurationService {
 
     /**
-     * Logger for this class.
-     */
-    private final Logger logger = LoggerFactory.getLogger(ConfigurationService.class);
-
-    /**
      * The Guacamole server environment.
      */
     @Inject
@@ -113,7 +109,7 @@
      *     If guacamole.properties cannot be parsed, or if the user base DN
      *     property is not specified.
      */
-    public String getUserBaseDN() throws GuacamoleException {
+    public Dn getUserBaseDN() throws GuacamoleException {
         return environment.getRequiredProperty(
             LDAPGuacamoleProperties.LDAP_USER_BASE_DN
         );
@@ -132,7 +128,7 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getConfigurationBaseDN() throws GuacamoleException {
+    public Dn getConfigurationBaseDN() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
         );
@@ -168,7 +164,7 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getGroupBaseDN() throws GuacamoleException {
+    public Dn getGroupBaseDN() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN
         );
@@ -187,7 +183,7 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getSearchBindDN() throws GuacamoleException {
+    public Dn getSearchBindDN() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN
         );
@@ -242,7 +238,7 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    private int getMaxResults() throws GuacamoleException {
+    public int getMaxResults() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS,
             1000
@@ -262,10 +258,10 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    private DereferenceAliasesMode getDereferenceAliases() throws GuacamoleException {
+    public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES,
-            DereferenceAliasesMode.NEVER
+            AliasDerefMode.NEVER_DEREF_ALIASES
         );
     }
 
@@ -288,28 +284,8 @@
     }
 
     /**
-     * Returns a set of LDAPSearchConstraints to apply globally
-     * to all LDAP searches.
-     *
-     * @return
-     *     A LDAPSearchConstraints object containing constraints
-     *     to be applied to all LDAP search operations.
-     *
-     * @throws GuacamoleException
-     *     If guacamole.properties cannot be parsed.
-     */
-    public LDAPSearchConstraints getLDAPSearchConstraints() throws GuacamoleException {
-
-        LDAPSearchConstraints constraints = new LDAPSearchConstraints();
-
-        constraints.setMaxResults(getMaxResults());
-        constraints.setDereference(getDereferenceAliases().DEREF_VALUE);
-
-        return constraints;
-    }
-
-    /**
-     * Returns the maximum number of referral hops to follow.
+     * Returns the maximum number of referral hops to follow.  By default
+     * a maximum of 5 hops is allowed.
      *
      * @return
      *     The maximum number of referral hops to follow
@@ -328,20 +304,20 @@
     /**
      * Returns the search filter that should be used when querying the
      * LDAP server for Guacamole users.  If no filter is specified,
-     * a default of "(objectClass=*)" is returned.
+     * a default of "(objectClass=user)" is returned.
      *
      * @return
      *     The search filter that should be used when querying the
      *     LDAP server for users that are valid in Guacamole, or
-     *     "(objectClass=*)" if not specified.
+     *     "(objectClass=user)" if not specified.
      *
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getUserSearchFilter() throws GuacamoleException {
+    public ExprNode getUserSearchFilter() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER,
-            "(objectClass=*)"
+            new PresenceNode("objectClass")
         );
     }
 
@@ -363,7 +339,8 @@
     }
 
     /**
-     * Returns names for custom LDAP user attributes.
+     * Returns names for custom LDAP user attributes.  By default no
+     * attributes will be returned.
      *
      * @return
      *     Custom LDAP user attributes as configured in guacamole.properties.
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
similarity index 75%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
index 60b89c4..b33aa19 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
@@ -17,21 +17,22 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
+import org.apache.directory.api.ldap.model.message.AliasDerefMode;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.properties.GuacamoleProperty;
 
 /**
- * A GuacamoleProperty with a value of DereferenceAliases. The possible strings
- * "never", "searching", "finding", and "always" are mapped to their values as a
- * DereferenceAliases enum. Anything else results in a parse error.
+ * A GuacamoleProperty with a value of AliasDerefMode. The possible strings
+ * "never", "searching", "finding", and "always" are mapped to their values as
+ * an AliasDerefMode object. Anything else results in a parse error.
  */
-public abstract class DereferenceAliasesProperty implements GuacamoleProperty<DereferenceAliasesMode> {
+public abstract class DereferenceAliasesProperty implements GuacamoleProperty<AliasDerefMode> {
 
     @Override
-    public DereferenceAliasesMode parseValue(String value) throws GuacamoleException {
+    public AliasDerefMode parseValue(String value) throws GuacamoleException {
 
         // No value provided, so return null.
         if (value == null)
@@ -39,19 +40,19 @@
 
         // Never dereference aliases
         if (value.equals("never"))
-            return DereferenceAliasesMode.NEVER;
+            return AliasDerefMode.NEVER_DEREF_ALIASES;
 
         // Dereference aliases during search operations, but not at base
         if (value.equals("searching"))
-            return DereferenceAliasesMode.SEARCHING;
+            return AliasDerefMode.DEREF_IN_SEARCHING;
 
         // Dereference aliases to locate base, but not during searches
         if (value.equals("finding"))
-            return DereferenceAliasesMode.FINDING;
+            return AliasDerefMode.DEREF_FINDING_BASE_OBJ;
 
         // Always dereference aliases
         if (value.equals("always"))
-            return DereferenceAliasesMode.ALWAYS;
+            return AliasDerefMode.DEREF_ALWAYS;
 
         // Anything else is invalid and results in an error
         throw new GuacamoleServerException("Dereference aliases must be one of \"never\", \"searching\", \"finding\", or \"always\".");
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
index 6ae5b01..95c93af 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
 /**
  * All possible encryption methods which may be used when connecting to an LDAP
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
index 5753756..d76cc4d 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
similarity index 82%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
index 7529956..e5f44f0 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
 import org.apache.guacamole.properties.BooleanGuacamoleProperty;
 import org.apache.guacamole.properties.IntegerGuacamoleProperty;
@@ -39,7 +39,8 @@
     /**
      * The base DN to search for Guacamole configurations.
      */
-    public static final StringGuacamoleProperty LDAP_CONFIG_BASE_DN = new StringGuacamoleProperty() {
+    public static final LdapDnGuacamoleProperty LDAP_CONFIG_BASE_DN =
+            new LdapDnGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-config-base-dn"; }
@@ -52,7 +53,8 @@
      * credentials for querying other LDAP users, all users must be direct
      * children of this base DN, varying only by LDAP_USERNAME_ATTRIBUTE.
      */
-    public static final StringGuacamoleProperty LDAP_USER_BASE_DN = new StringGuacamoleProperty() {
+    public static final LdapDnGuacamoleProperty LDAP_USER_BASE_DN =
+            new LdapDnGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-user-base-dn"; }
@@ -64,7 +66,8 @@
      * will be used for RBAC must be contained somewhere within the subtree of
      * this DN.
      */
-    public static final StringGuacamoleProperty LDAP_GROUP_BASE_DN = new StringGuacamoleProperty() {
+    public static final LdapDnGuacamoleProperty LDAP_GROUP_BASE_DN =
+            new LdapDnGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-group-base-dn"; }
@@ -79,7 +82,8 @@
      * one attribute, and the concatenation of that attribute and the value of
      * LDAP_USER_BASE_DN must equal the user's full DN.
      */
-    public static final StringListProperty LDAP_USERNAME_ATTRIBUTE = new StringListProperty() {
+    public static final StringListProperty LDAP_USERNAME_ATTRIBUTE =
+            new StringListProperty() {
 
         @Override
         public String getName() { return "ldap-username-attribute"; }
@@ -91,7 +95,8 @@
      * attributes must be present within each Guacamole user group's record in
      * the LDAP directory for that group to be visible.
      */
-    public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE = new StringListProperty() {
+    public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE =
+            new StringListProperty() {
 
         @Override
         public String getName() { return "ldap-group-name-attribute"; }
@@ -101,7 +106,8 @@
     /**
      * The port on the LDAP server to connect to when authenticating users.
      */
-    public static final IntegerGuacamoleProperty LDAP_PORT = new IntegerGuacamoleProperty() {
+    public static final IntegerGuacamoleProperty LDAP_PORT =
+            new IntegerGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-port"; }
@@ -111,7 +117,8 @@
     /**
      * The hostname of the LDAP server to connect to when authenticating users.
      */
-    public static final StringGuacamoleProperty LDAP_HOSTNAME = new StringGuacamoleProperty() {
+    public static final StringGuacamoleProperty LDAP_HOSTNAME =
+            new StringGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-hostname"; }
@@ -124,7 +131,8 @@
      * specified, the DNs of users attempting to log in will be derived from
      * the LDAP_BASE_DN and LDAP_USERNAME_ATTRIBUTE directly.
      */
-    public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN = new StringGuacamoleProperty() {
+    public static final LdapDnGuacamoleProperty LDAP_SEARCH_BIND_DN =
+            new LdapDnGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-search-bind-dn"; }
@@ -137,7 +145,8 @@
      * property has no effect. If this property is not specified, no password
      * will be provided when attempting to bind as LDAP_SEARCH_BIND_DN.
      */
-    public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD = new StringGuacamoleProperty() {
+    public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD =
+            new StringGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-search-bind-password"; }
@@ -149,7 +158,8 @@
      * The chosen method will also dictate the default port if not already
      * explicitly specified via LDAP_PORT.
      */
-    public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD = new EncryptionMethodProperty() {
+    public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD =
+            new EncryptionMethodProperty() {
 
         @Override
         public String getName() { return "ldap-encryption-method"; }
@@ -159,7 +169,8 @@
     /**
      * The maximum number of results a LDAP query can return.
      */
-    public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS = new IntegerGuacamoleProperty() {
+    public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS =
+            new IntegerGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-max-search-results"; }
@@ -170,7 +181,8 @@
      * Property that controls whether or not the LDAP connection follows
      * (dereferences) aliases as it searches the tree.
      */
-    public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES = new DereferenceAliasesProperty() {
+    public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES =
+            new DereferenceAliasesProperty() {
 
         @Override
         public String getName() { return "ldap-dereference-aliases"; }
@@ -180,7 +192,8 @@
     /**
      * A search filter to apply to user LDAP queries.
      */
-    public static final StringGuacamoleProperty LDAP_USER_SEARCH_FILTER = new StringGuacamoleProperty() {
+    public static final LdapFilterGuacamoleProperty LDAP_USER_SEARCH_FILTER =
+            new LdapFilterGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-user-search-filter"; }
@@ -190,7 +203,8 @@
     /**
      * Whether or not we should follow referrals.
      */
-    public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS = new BooleanGuacamoleProperty() {
+    public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS =
+            new BooleanGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-follow-referrals"; }
@@ -200,7 +214,8 @@
     /**
      * Maximum number of referral hops to follow.
      */
-    public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS = new IntegerGuacamoleProperty() {
+    public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS =
+            new IntegerGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-max-referral-hops"; }
@@ -210,7 +225,8 @@
     /**
      * Number of seconds to wait for LDAP operations to complete.
      */
-    public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT = new IntegerGuacamoleProperty() {
+    public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT =
+            new IntegerGuacamoleProperty() {
 
         @Override
         public String getName() { return "ldap-operation-timeout"; }
@@ -221,7 +237,8 @@
      * Custom attribute or attributes to query from Guacamole user's record in
      * the LDAP directory.
      */
-    public static final StringListProperty LDAP_USER_ATTRIBUTES = new StringListProperty() {
+    public static final StringListProperty LDAP_USER_ATTRIBUTES =
+            new StringListProperty() {
 
         @Override
         public String getName() { return "ldap-user-attributes"; }
@@ -231,7 +248,8 @@
     /**
      * LDAP attribute used to enumerate members of a group in the LDAP directory.
      */
-    public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE = new StringGuacamoleProperty() {
+    public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE =
+            new StringGuacamoleProperty() {
       
         @Override
         public String getName() { return "ldap-member-attribute"; }
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java
new file mode 100644
index 0000000..c782c97
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ldap.conf;
+
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty that converts a string to a Dn that can be used
+ * in LDAP connections.  An exception is thrown if the provided DN is invalid
+ * and cannot be parsed.
+ */
+public abstract class LdapDnGuacamoleProperty implements GuacamoleProperty<Dn> {
+
+    @Override
+    public Dn parseValue(String value) throws GuacamoleException {
+
+        if (value == null)
+            return null;
+
+        try {
+            return new Dn(value);
+        }
+        catch (LdapInvalidDnException e) {
+            throw new GuacamoleServerException("The DN \"" + value + "\" is invalid.", e);
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java
new file mode 100644
index 0000000..01b41c9
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ldap.conf;
+
+import java.text.ParseException;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty with a value of an ExprNode query filter.  The string
+ * provided is passed through the FilterParser returning the ExprNode object,
+ * or an exception is thrown if the filter is invalid and cannot be correctly
+ * parsed.
+ */
+public abstract class LdapFilterGuacamoleProperty implements GuacamoleProperty<ExprNode> {
+
+    @Override
+    public ExprNode parseValue(String value) throws GuacamoleException {
+
+        // No value provided, so return null.
+        if (value == null)
+            return null;
+
+        try {
+            return FilterParser.parse(value);
+        }
+        catch (ParseException e) {
+            throw new GuacamoleServerException("\"" + value + "\" is not a valid LDAP filter.", e);
+        }
+
+    }
+
+}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
index 908d922..f7057e9 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
 
 import java.util.Arrays;
 import java.util.List;
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
index 2f2b674..6b2d840 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
@@ -20,17 +20,22 @@
 package org.apache.guacamole.auth.ldap.connection;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
 import java.util.Collections;
-import java.util.Enumeration;
 import java.util.List;
 import java.util.Map;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.AndNode;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.OrNode;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
-import org.apache.guacamole.auth.ldap.EscapingService;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.auth.ldap.ObjectQueryService;
@@ -53,13 +58,7 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
-
-    /**
-     * Service for escaping parts of LDAP queries.
-     */
-    @Inject
-    private EscapingService escapingService;
+    private static final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
 
     /**
      * Service for retrieving LDAP server configuration information.
@@ -100,17 +99,18 @@
      *     If an error occurs preventing retrieval of connections.
      */
     public Map<String, Connection> getConnections(AuthenticatedUser user,
-            LDAPConnection ldapConnection) throws GuacamoleException {
+            LdapNetworkConnection ldapConnection) throws GuacamoleException {
 
         // Do not return any connections if base DN is not specified
-        String configurationBaseDN = confService.getConfigurationBaseDN();
+        Dn configurationBaseDN = confService.getConfigurationBaseDN();
         if (configurationBaseDN == null)
             return Collections.<String, Connection>emptyMap();
 
         try {
 
             // Pull the current user DN from the LDAP connection
-            String userDN = ldapConnection.getAuthenticationDN();
+            LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
+            Dn userDN = new Dn(ldapConnectionConfig.getName());
 
             // getConnections() will only be called after a connection has been
             // authenticated (via non-anonymous bind), thus userDN cannot
@@ -119,46 +119,77 @@
 
             // Get the search filter for finding connections accessible by the
             // current user
-            String connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
+            ExprNode connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
 
             // Find all Guacamole connections for the given user by
             // looking for direct membership in the guacConfigGroup
             // and possibly any groups the user is a member of that are
             // referred to in the seeAlso attribute of the guacConfigGroup.
-            List<LDAPEntry> results = queryService.search(ldapConnection, configurationBaseDN, connectionSearchFilter);
+            List<Entry> results = queryService.search(ldapConnection,
+                    configurationBaseDN, connectionSearchFilter, 0);
 
             // Return a map of all readable connections
             return queryService.asMap(results, (entry) -> {
 
                 // Get common name (CN)
-                LDAPAttribute cn = entry.getAttribute("cn");
+                Attribute cn = entry.get("cn");
+                
                 if (cn == null) {
                     logger.warn("guacConfigGroup is missing a cn.");
                     return null;
                 }
+                
+                String cnName;
+                
+                try {
+                    cnName = cn.getString();
+                }
+                catch (LdapInvalidAttributeValueException e) {
+                    logger.error("Invalid value for CN attribute: {}",
+                            e.getMessage());
+                    logger.debug("LDAP exception while getting CN attribute.", e);
+                    return null;
+                }
 
                 // Get associated protocol
-                LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
+                Attribute protocol = entry.get("guacConfigProtocol");
                 if (protocol == null) {
                     logger.warn("guacConfigGroup \"{}\" is missing the "
                               + "required \"guacConfigProtocol\" attribute.",
-                            cn.getStringValue());
+                            cnName);
                     return null;
                 }
 
                 // Set protocol
                 GuacamoleConfiguration config = new GuacamoleConfiguration();
-                config.setProtocol(protocol.getStringValue());
+                try {
+                    config.setProtocol(protocol.getString());
+                }
+                catch (LdapInvalidAttributeValueException e) {
+                    logger.error("Invalid value of the protocol entry: {}",
+                            e.getMessage());
+                    logger.debug("LDAP exception when getting protocol value.", e);
+                    return null;
+                }
 
                 // Get parameters, if any
-                LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
+                Attribute parameterAttribute = entry.get("guacConfigParameter");
                 if (parameterAttribute != null) {
 
                     // For each parameter
-                    Enumeration<?> parameters = parameterAttribute.getStringValues();
-                    while (parameters.hasMoreElements()) {
-
-                        String parameter = (String) parameters.nextElement();
+                    while (parameterAttribute.size() > 0) {
+                        String parameter;
+                        try {
+                            parameter = parameterAttribute.getString();
+                        }
+                        catch (LdapInvalidAttributeValueException e) {
+                            logger.warn("Parameter value not valid for {}: {}",
+                                    cnName, e.getMessage());
+                            logger.debug("LDAP exception when getting parameter value.",
+                                    e);
+                            return null;
+                        }
+                        parameterAttribute.remove(parameter);
 
                         // Parse parameter
                         int equals = parameter.indexOf('=');
@@ -177,8 +208,7 @@
                 }
 
                 // Store connection using cn for both identifier and name
-                String name = cn.getStringValue();
-                Connection connection = new SimpleConnection(name, name, config, true);
+                Connection connection = new SimpleConnection(cnName, cnName, config, true);
                 connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
 
                 // Inject LDAP-specific tokens only if LDAP handled user
@@ -192,7 +222,7 @@
             });
 
         }
-        catch (LDAPException e) {
+        catch (LdapException e) {
             throw new GuacamoleServerException("Error while querying for connections.", e);
         }
 
@@ -213,40 +243,39 @@
      *     An LDAP search filter which queries all guacConfigGroup objects
      *     accessible by the user having the given DN.
      *
-     * @throws LDAPException
+     * @throws LdapException
      *     If an error occurs preventing retrieval of user groups.
      *
      * @throws GuacamoleException
      *     If an error occurs retrieving the group base DN.
      */
-    private String getConnectionSearchFilter(String userDN,
-            LDAPConnection ldapConnection)
-            throws LDAPException, GuacamoleException {
+    private ExprNode getConnectionSearchFilter(Dn userDN,
+            LdapNetworkConnection ldapConnection)
+            throws LdapException, GuacamoleException {
 
-        // Create a search filter for the connection search
-        StringBuilder connectionSearchFilter = new StringBuilder();
+        AndNode searchFilter = new AndNode();
 
         // Add the prefix to the search filter, prefix filter searches for guacConfigGroups with the userDN as the member attribute value
-        connectionSearchFilter.append("(&(objectClass=guacConfigGroup)");
-        connectionSearchFilter.append("(|(");
-        connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(
-                confService.getMemberAttribute()));
-        connectionSearchFilter.append("=");
-        connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN));
-        connectionSearchFilter.append(")");
+        searchFilter.addNode(new EqualityNode("objectClass","guacConfigGroup"));
+        
+        // Apply group filters
+        OrNode groupFilter = new OrNode();
+        groupFilter.addNode(new EqualityNode(confService.getMemberAttribute(),
+            userDN.toString()));
 
         // Additionally filter by group membership if the current user is a
         // member of any user groups
-        List<LDAPEntry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
+        List<Entry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
         if (!userGroups.isEmpty()) {
-            for (LDAPEntry entry : userGroups)
-                connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
+            userGroups.forEach(entry ->
+                groupFilter.addNode(new EqualityNode("seeAlso",entry.getDn().toString()))
+            );
         }
 
         // Complete the search filter.
-        connectionSearchFilter.append("))");
+        searchFilter.addNode(groupFilter);
 
-        return connectionSearchFilter.toString();
+        return searchFilter;
     }
 
 }
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
index 3315beb..0628006 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
@@ -20,15 +20,21 @@
 package org.apache.guacamole.auth.ldap.group;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.NotNode;
+import org.apache.directory.api.ldap.model.filter.PresenceNode;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.ldap.ObjectQueryService;
 import org.apache.guacamole.net.auth.UserGroup;
@@ -45,7 +51,7 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
+    private static final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
 
     /**
      * Service for retrieving LDAP server configuration information.
@@ -72,17 +78,17 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    private String getGroupSearchFilter() throws GuacamoleException {
+    private ExprNode getGroupSearchFilter() throws GuacamoleException {
 
         // Explicitly exclude guacConfigGroup object class only if it should
         // be assumed to be defined (query may fail due to no such object
         // class existing otherwise)
         if (confService.getConfigurationBaseDN() != null)
-            return "(!(objectClass=guacConfigGroup))";
+            return new NotNode(new EqualityNode("objectClass","guacConfigGroup"));
 
         // Read any object as a group if LDAP is not being used for connection
         // storage (guacConfigGroup)
-        return "(objectClass=*)";
+        return new PresenceNode("objectClass");
 
     }
 
@@ -102,17 +108,17 @@
      * @throws GuacamoleException
      *     If an error occurs preventing retrieval of user groups.
      */
-    public Map<String, UserGroup> getUserGroups(LDAPConnection ldapConnection)
+    public Map<String, UserGroup> getUserGroups(LdapNetworkConnection ldapConnection)
             throws GuacamoleException {
 
         // Do not return any user groups if base DN is not specified
-        String groupBaseDN = confService.getGroupBaseDN();
+        Dn groupBaseDN = confService.getGroupBaseDN();
         if (groupBaseDN == null)
             return Collections.emptyMap();
 
         // Retrieve all visible user groups which are not guacConfigGroups
         Collection<String> attributes = confService.getGroupNameAttributes();
-        List<LDAPEntry> results = queryService.search(
+        List<Entry> results = queryService.search(
             ldapConnection,
             groupBaseDN,
             getGroupSearchFilter(),
@@ -125,13 +131,18 @@
         return queryService.asMap(results, entry -> {
 
             // Translate entry into UserGroup object having proper identifier
-            String name = queryService.getIdentifier(entry, attributes);
-            if (name != null)
-                return new SimpleUserGroup(name);
+            try {
+                String name = queryService.getIdentifier(entry, attributes);
+                if (name != null)
+                    return new SimpleUserGroup(name);
+            }
+            catch (LdapInvalidAttributeValueException e) {
+                return null;
+            }
 
             // Ignore user groups which lack a name attribute
             logger.debug("User group \"{}\" is missing a name attribute "
-                    + "and will be ignored.", entry.getDN());
+                    + "and will be ignored.", entry.getDn().toString());
             return null;
 
         });
@@ -157,11 +168,11 @@
      * @throws GuacamoleException
      *     If an error occurs preventing retrieval of user groups.
      */
-    public List<LDAPEntry> getParentUserGroupEntries(LDAPConnection ldapConnection,
-            String userDN) throws GuacamoleException {
+    public List<Entry> getParentUserGroupEntries(LdapNetworkConnection ldapConnection,
+            Dn userDN) throws GuacamoleException {
 
         // Do not return any user groups if base DN is not specified
-        String groupBaseDN = confService.getGroupBaseDN();
+        Dn groupBaseDN = confService.getGroupBaseDN();
         if (groupBaseDN == null)
             return Collections.emptyList();
 
@@ -172,7 +183,7 @@
             groupBaseDN,
             getGroupSearchFilter(),
             Collections.singleton(confService.getMemberAttribute()),
-            userDN
+            userDN.toString()
         );
 
     }
@@ -196,24 +207,31 @@
      * @throws GuacamoleException
      *     If an error occurs preventing retrieval of user groups.
      */
-    public Set<String> getParentUserGroupIdentifiers(LDAPConnection ldapConnection,
-            String userDN) throws GuacamoleException {
+    public Set<String> getParentUserGroupIdentifiers(LdapNetworkConnection ldapConnection,
+            Dn userDN) throws GuacamoleException {
 
         Collection<String> attributes = confService.getGroupNameAttributes();
-        List<LDAPEntry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
+        List<Entry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
 
         Set<String> identifiers = new HashSet<>(userGroups.size());
         userGroups.forEach(entry -> {
 
             // Determine unique identifier for user group
-            String name = queryService.getIdentifier(entry, attributes);
-            if (name != null)
-                identifiers.add(name);
+            try {
+                String name = queryService.getIdentifier(entry, attributes);
+                if (name != null)
+                    identifiers.add(name);
 
-            // Ignore user groups which lack a name attribute
-            else
-                logger.debug("User group \"{}\" is missing a name attribute "
-                        + "and will be ignored.", entry.getDN());
+                // Ignore user groups which lack a name attribute
+                else
+                    logger.debug("User group \"{}\" is missing a name attribute "
+                            + "and will be ignored.", entry.getDn().toString());
+            }
+            catch (LdapInvalidAttributeValueException e) {
+                logger.error("User group missing identifier: {}",
+                        e.getMessage());
+                logger.debug("LDAP exception while getting group identifier.", e);
+            }
 
         });
 
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
index cafc461..4429643 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
@@ -23,6 +23,7 @@
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
@@ -56,6 +57,11 @@
      * available to this user.
      */
     private Set<String> effectiveGroups;
+    
+    /**
+     * The LDAP DN used to bind this user.
+     */
+    private Dn bindDn;
 
     /**
      * Initializes this AuthenticatedUser with the given credentials,
@@ -71,14 +77,19 @@
      * @param effectiveGroups
      *     The unique identifiers of all user groups which affect the
      *     permissions available to this user.
+     * 
+     * @param bindDn
+     *     The LDAP DN used to bind this user.
      */
-    public void init(Credentials credentials, Map<String, String> tokens, Set<String> effectiveGroups) {
+    public void init(Credentials credentials, Map<String, String> tokens,
+            Set<String> effectiveGroups, Dn bindDn) {
         this.credentials = credentials;
         this.tokens = Collections.unmodifiableMap(tokens);
         this.effectiveGroups = effectiveGroups;
+        this.bindDn = bindDn;
         setIdentifier(credentials.getUsername());
     }
-
+    
     /**
      * Returns a Map of all name/value pairs that should be applied as
      * parameter tokens when connections are established using this
@@ -92,6 +103,16 @@
     public Map<String, String> getTokens() {
         return tokens;
     }
+    
+    /**
+     * Returns the LDAP DN used to bind this user.
+     * 
+     * @return 
+     *     The LDAP DN used to bind this user.
+     */
+    public Dn getBindDn() {
+        return bindDn;
+    }
 
     @Override
     public AuthenticationProvider getAuthenticationProvider() {
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
index 5505f7e..b5c789e 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
@@ -20,8 +20,8 @@
 package org.apache.guacamole.auth.ldap.user;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
 import java.util.Collections;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.auth.ldap.connection.ConnectionService;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
@@ -39,8 +39,6 @@
 import org.apache.guacamole.net.auth.simple.SimpleDirectory;
 import org.apache.guacamole.net.auth.simple.SimpleObjectPermissionSet;
 import org.apache.guacamole.net.auth.simple.SimpleUser;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * An LDAP-specific implementation of UserContext which queries all Guacamole
@@ -49,11 +47,6 @@
 public class LDAPUserContext extends AbstractUserContext {
 
     /**
-     * Logger for this class.
-     */
-    private final Logger logger = LoggerFactory.getLogger(LDAPUserContext.class);
-
-    /**
      * Service for retrieving Guacamole connections from the LDAP server.
      */
     @Inject
@@ -109,7 +102,7 @@
 
     /**
      * Initializes this UserContext using the provided AuthenticatedUser and
-     * LDAPConnection.
+     * LdapNetworkConnection.
      *
      * @param user
      *     The AuthenticatedUser representing the user that authenticated. This
@@ -124,7 +117,7 @@
      *     If associated data stored within the LDAP directory cannot be
      *     queried due to an error.
      */
-    public void init(AuthenticatedUser user, LDAPConnection ldapConnection)
+    public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection)
             throws GuacamoleException {
 
         // Query all accessible users
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
index 3f12ae8..ba29983 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
@@ -20,16 +20,20 @@
 package org.apache.guacamole.auth.ldap.user;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
-import org.apache.guacamole.auth.ldap.EscapingService;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.ldap.LDAPGuacamoleProperties;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
 import org.apache.guacamole.auth.ldap.ObjectQueryService;
 import org.apache.guacamole.net.auth.User;
 import org.apache.guacamole.net.auth.simple.SimpleUser;
@@ -45,13 +49,7 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(UserService.class);
-
-    /**
-     * Service for escaping parts of LDAP queries.
-     */
-    @Inject
-    private EscapingService escapingService;
+    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
 
     /**
      * Service for retrieving LDAP server configuration information.
@@ -81,12 +79,12 @@
      * @throws GuacamoleException
      *     If an error occurs preventing retrieval of users.
      */
-    public Map<String, User> getUsers(LDAPConnection ldapConnection)
+    public Map<String, User> getUsers(LdapNetworkConnection ldapConnection)
             throws GuacamoleException {
 
         // Retrieve all visible user objects
         Collection<String> attributes = confService.getUsernameAttributes();
-        List<LDAPEntry> results = queryService.search(ldapConnection,
+        List<Entry> results = queryService.search(ldapConnection,
                 confService.getUserBaseDN(),
                 confService.getUserSearchFilter(),
                 attributes,
@@ -96,15 +94,21 @@
         return queryService.asMap(results, entry -> {
 
             // Get username from record
-            String username = queryService.getIdentifier(entry, attributes);
-            if (username == null) {
-                logger.warn("User \"{}\" is missing a username attribute "
-                        + "and will be ignored.", entry.getDN());
+            try {
+                String username = queryService.getIdentifier(entry, attributes);
+                if (username == null) {
+                    logger.warn("User \"{}\" is missing a username attribute "
+                            + "and will be ignored.", entry.getDn().toString());
+                    return null;
+                }
+                
+                return new SimpleUser(username);
+            }
+            catch (LdapInvalidAttributeValueException e) {
+                
                 return null;
             }
 
-            return new SimpleUser(username);
-
         });
 
     }
@@ -130,19 +134,19 @@
      *     If an error occurs while querying the user DNs, or if the username
      *     attribute property cannot be parsed within guacamole.properties.
      */
-    public List<String> getUserDNs(LDAPConnection ldapConnection,
+    public List<Dn> getUserDNs(LdapNetworkConnection ldapConnection,
             String username) throws GuacamoleException {
 
         // Retrieve user objects having a matching username
-        List<LDAPEntry> results = queryService.search(ldapConnection,
+        List<Entry> results = queryService.search(ldapConnection,
                 confService.getUserBaseDN(),
                 confService.getUserSearchFilter(),
                 confService.getUsernameAttributes(),
                 username);
 
         // Build list of all DNs for retrieved users
-        List<String> userDNs = new ArrayList<>(results.size());
-        results.forEach(entry -> userDNs.add(entry.getDN()));
+        List<Dn> userDNs = new ArrayList<>(results.size());
+        results.forEach(entry -> userDNs.add(entry.getDn()));
 
         return userDNs;
 
@@ -164,7 +168,7 @@
      *     If required properties are missing, and thus the user DN cannot be
      *     determined.
      */
-    public String deriveUserDN(String username)
+    public Dn deriveUserDN(String username)
             throws GuacamoleException {
 
         // Pull username attributes from properties
@@ -181,10 +185,13 @@
         }
 
         // Derive user DN from base DN
-        return
-                    escapingService.escapeDN(usernameAttributes.get(0))
-            + "=" + escapingService.escapeDN(username)
-            + "," + confService.getUserBaseDN();
+        try {
+            return new Dn(new Rdn(usernameAttributes.get(0), username),
+                confService.getUserBaseDN());
+        }
+        catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) {
+            throw new GuacamoleServerException("Error trying to derive user DN.", e);
+        }
 
     }
 
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
index 4fd37f1..fee4357 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
@@ -25,7 +25,7 @@
 import java.util.Arrays;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.auth.radius.user.AuthenticatedUser;
-import org.apache.guacamole.auth.radius.form.RadiusChallengeResponseField;
+import org.apache.guacamole.auth.radius.form.GuacamoleRadiusChallenge;
 import org.apache.guacamole.auth.radius.form.RadiusStateField;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.form.Field;
@@ -42,6 +42,7 @@
 import net.jradius.packet.AccessChallenge;
 import net.jradius.packet.AccessReject;
 import net.jradius.packet.attribute.RadiusAttribute;
+import org.apache.guacamole.form.PasswordField;
 
 /**
  * Service providing convenience functions for the RADIUS AuthenticationProvider
@@ -53,6 +54,12 @@
      * Logger for this class.
      */
     private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
+    
+    /**
+     * The name of the password field where the user will enter a response to
+     * the RADIUS challenge.
+     */
+    private static final String CHALLENGE_RESPONSE_PARAM = "radiusChallenge";
 
     /**
      * Service for creating and managing connections to RADIUS servers.
@@ -67,18 +74,23 @@
     private Provider<AuthenticatedUser> authenticatedUserProvider;
 
     /**
-     * Returns the expected credentials from a RADIUS challenge.
+     * Returns an object containing the challenge message and the expected
+     * credentials from a RADIUS challenge, or null if either state or reply
+     * attributes are missing from the challenge.
      *
      * @param challengePacket
      *     The AccessChallenge RadiusPacket received from the RADIUS 
      *     server.
      *
      * @return
-     *     A CredentialsInfo object that represents fields that need to
-     *     be presented to the user in order to complete authentication.
-     *     One of these must be the RADIUS state.
+     *     A GuacamoleRadiusChallenge object that contains the challenge message
+     *     sent by the RADIUS server and the expected credentials that should
+     *     be requested of the user in order to continue authentication.  One
+     *     of the expected credentials *must* be the RADIUS state.  If either
+     *     state or the reply are missing from the challenge this method will
+     *     return null.
      */
-    private CredentialsInfo getRadiusChallenge(RadiusPacket challengePacket) {
+    private GuacamoleRadiusChallenge getRadiusChallenge(RadiusPacket challengePacket) {
 
         // Try to get the state attribute - if it's not there, we have a problem
         RadiusAttribute stateAttr = challengePacket.findAttribute(Attr_State.TYPE);
@@ -97,13 +109,16 @@
         }
 
         // We have the required attributes - convert to strings and then generate the additional login box/field
-        String replyMsg = replyAttr.toString();
+        String replyMsg = replyAttr.getValue().toString();
         String radiusState = BaseEncoding.base16().encode(stateAttr.getValue().getBytes());
-        Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
+        Field radiusResponseField = new PasswordField(CHALLENGE_RESPONSE_PARAM);
         Field radiusStateField = new RadiusStateField(radiusState);
 
-        // Return the CredentialsInfo object that has the state and the expected response.
-        return new CredentialsInfo(Arrays.asList(radiusResponseField,radiusStateField));
+        // Return the GuacamoleRadiusChallenge object that has the state
+        // and the expected response.
+        return new GuacamoleRadiusChallenge(replyMsg,
+                new CredentialsInfo(Arrays.asList(radiusResponseField,
+                        radiusStateField)));
     }
 
     /**
@@ -134,7 +149,7 @@
 
         // Grab HTTP request object and a response to a challenge.
         HttpServletRequest request = credentials.getRequest();
-        String challengeResponse = request.getParameter(RadiusChallengeResponseField.PARAMETER_NAME);
+        String challengeResponse = request.getParameter(CHALLENGE_RESPONSE_PARAM);
 
         // RadiusPacket object to store response from server.
         RadiusPacket radPack;
@@ -200,12 +215,14 @@
 
         // Received AccessChallenge packet, more credentials required to complete authentication
         else if (radPack instanceof AccessChallenge) {
-            CredentialsInfo expectedCredentials = getRadiusChallenge(radPack);
+            GuacamoleRadiusChallenge challenge = getRadiusChallenge(radPack);
 
-            if (expectedCredentials == null)
+            if (challenge == null)
                 throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
 
-            throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
+            throw new GuacamoleInsufficientCredentialsException(
+                    challenge.getChallengeText(),
+                    challenge.getExpectedCredentials());
         }
 
         // Something unanticipated happened, so panic and go back to login.
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
index 37ecb79..4224f77 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
@@ -20,10 +20,17 @@
 package org.apache.guacamole.auth.radius;
 
 import com.google.inject.AbstractModule;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
+import org.apache.guacamole.auth.radius.conf.RadiusGuacamoleProperties;
 import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.environment.LocalEnvironment;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
 /**
  * Guice module which configures RADIUS-specific injections.
@@ -57,6 +64,23 @@
 
         // Get local environment
         this.environment = new LocalEnvironment();
+        
+        // Check for MD4 requirement
+        RadiusAuthenticationProtocol authProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL);
+        RadiusAuthenticationProtocol innerProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL);
+        if (authProtocol == RadiusAuthenticationProtocol.MSCHAPv1 
+                    || authProtocol == RadiusAuthenticationProtocol.MSCHAPv2
+                    || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv1 
+                    || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv2) {
+            
+            try {
+                MessageDigest.getInstance("MD4");
+            }
+            catch (NoSuchAlgorithmException e) {
+                Security.addProvider(new BouncyCastleProvider());
+            }
+            
+        }
 
         // Store associated auth provider
         this.authProvider = authProvider;
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
index ec82a63..c8a21d6 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
@@ -27,6 +27,8 @@
 import java.security.NoSuchAlgorithmException;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import net.jradius.client.RadiusClient;
@@ -62,8 +64,7 @@
      */
     @Inject
     private ConfigurationService confService;
-
-
+    
     /**
      * Creates a new instance of RadiusClient, configured with parameters
      * from guacamole.properties.
@@ -115,8 +116,8 @@
      *     not configured when the client is set up for a tunneled
      *     RADIUS connection.
      */
-    private RadiusAuthenticator setupRadiusAuthenticator(RadiusClient radiusClient)
-            throws GuacamoleException {
+    private RadiusAuthenticator setupRadiusAuthenticator(
+            RadiusClient radiusClient) throws GuacamoleException {
 
         // If we don't have a radiusClient object, yet, don't go any further.
         if (radiusClient == null) {
@@ -125,7 +126,9 @@
             return null;
         }
 
-        RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(confService.getRadiusAuthProtocol());
+        RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(
+                confService.getRadiusAuthProtocol().toString());
+        
         if (radAuth == null)
             throw new GuacamoleException("Could not get a valid RadiusAuthenticator for specified protocol: " + confService.getRadiusAuthProtocol());
 
@@ -157,11 +160,13 @@
 
         // If we're using EAP-TTLS, we need to define tunneled protocol
         if (radAuth instanceof EAPTTLSAuthenticator) {
-            String innerProtocol = confService.getRadiusEAPTTLSInnerProtocol();
+            RadiusAuthenticationProtocol innerProtocol =
+                    confService.getRadiusEAPTTLSInnerProtocol();
+            
             if (innerProtocol == null)
-                throw new GuacamoleException("Trying to use EAP-TTLS, but no inner protocol specified.");
+                throw new GuacamoleException("Missing or invalid inner protocol for EAP-TTLS.");
 
-            ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol);
+            ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol.toString());
         }
 
         return radAuth;
@@ -236,14 +241,21 @@
 
             radAuth.setupRequest(radiusClient, radAcc);
             radAuth.processRequest(radAcc);
-            RadiusResponse reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+            RadiusResponse reply = radiusClient.sendReceive(radAcc,
+                    confService.getRadiusMaxRetries());
 
             // We receive a Challenge not asking for user input, so silently process the challenge
-            while((reply instanceof AccessChallenge) && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+            while((reply instanceof AccessChallenge) 
+                    && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+                
                 radAuth.processChallenge(radAcc, reply);
-                reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+                reply = radiusClient.sendReceive(radAcc,
+                        confService.getRadiusMaxRetries());
+                
             }
+            
             return reply;
+            
         }
         catch (RadiusException e) {
             logger.error("Unable to complete authentication.", e.getMessage());
@@ -282,8 +294,8 @@
      * @throws GuacamoleException
      *     If an error is encountered trying to talk to the RADIUS server.
      */
-    public RadiusPacket sendChallengeResponse(String username, String response, byte[] state)
-            throws GuacamoleException {
+    public RadiusPacket sendChallengeResponse(String username, String response,
+            byte[] state) throws GuacamoleException {
 
         if (username == null || username.isEmpty()) {
             logger.error("Challenge/response to RADIUS requires a username.");
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
similarity index 92%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
index 381ea13..2809f7c 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
@@ -17,11 +17,12 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
 
 import com.google.inject.Inject;
 import java.io.File;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.environment.Environment;
 
 /**
@@ -123,8 +124,9 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getRadiusAuthProtocol() throws GuacamoleException {
-        return environment.getProperty(
+    public RadiusAuthenticationProtocol getRadiusAuthProtocol()
+            throws GuacamoleException {
+        return environment.getRequiredProperty(
             RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL
         );
     }
@@ -309,12 +311,21 @@
      *     an EAP-TTLS RADIUS connection. 
      *     
      * @throws GuacamoleException
-     *     If guacamole.properties cannot be parsed.
+     *     If guacamole.properties cannot be parsed, or if EAP-TTLS is specified
+     *     as the inner protocol.
      */
-    public String getRadiusEAPTTLSInnerProtocol() throws GuacamoleException {
-        return environment.getProperty(
+    public RadiusAuthenticationProtocol getRadiusEAPTTLSInnerProtocol()
+            throws GuacamoleException {
+        
+        RadiusAuthenticationProtocol authProtocol = environment.getProperty(
             RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL
         );
+        
+        if (authProtocol == RadiusAuthenticationProtocol.EAP_TTLS)
+            throw new GuacamoleServerException("Invalid inner protocol specified for EAP-TTLS.");
+        
+        return authProtocol;
+        
     }
 
 }
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
new file mode 100644
index 0000000..e64a695
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
@@ -0,0 +1,118 @@
+/*
+ * 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.radius.conf;
+
+/**
+ * This enum represents supported RADIUS authentication protocols for
+ * the guacamole-auth-radius extension.
+ */
+public enum RadiusAuthenticationProtocol {
+    
+    /**
+     * Password Authentication Protocol (PAP)
+     */
+    PAP("pap"),
+    
+    /**
+     * Challenge-Handshake Authentication Protocol (CHAP)
+     */
+    CHAP("chap"),
+    
+    /**
+     * Microsoft implementation of CHAP, Version 1 (MS-CHAPv1)
+     */
+    MSCHAPv1("mschapv1"),
+    
+    /**
+     * Microsoft implementation of CHAP, Version 2 (MS-CHAPv2)
+     */
+    MSCHAPv2("mschapv2"),
+    
+    /**
+     * Extensible Authentication Protocol (EAP) with MD5 Hashing (EAP-MD5)
+     */
+    EAP_MD5("eap-md5"),
+
+    /**
+     * Extensible Authentication Protocol (EAP) with TLS encryption (EAP-TLS).
+     */
+    EAP_TLS("eap-tls"),
+
+    /**
+     * Extensible Authentication Protocol (EAP) with Tunneled TLS (EAP-TTLS).
+     */
+    EAP_TTLS("eap-ttls");
+
+    /**
+     * This variable stores the string value of the protocol, and is also
+     * used within the extension to pass to JRadius for configuring the
+     * library to talk to the RADIUS server.
+     */
+    private final String strValue;
+    
+    /**
+     * Create a new RadiusAuthenticationProtocol object having the
+     * given string value.
+     * 
+     * @param strValue
+     *     The value of the protocol to store as a string, which will be used
+     *     in specifying the protocol within the guacamole.properties file, and
+     *     will also be used by the JRadius library for its configuration.
+     */
+    RadiusAuthenticationProtocol(String strValue) {
+        this.strValue = strValue;
+    }
+    
+    /**
+    * {@inheritDoc}
+    * <p>
+    * This function returns the stored string values of the selected RADIUS
+    * protocol, which is used both in Guacamole configuration and also to pass
+    * on to the JRadius library for its configuration.
+    * 
+    * @return
+    *     The string value stored for the selected RADIUS protocol.
+    */
+    @Override
+    public String toString() {
+        return strValue;
+    }
+    
+    /**
+     * For a given String value, return the enum value that matches that string,
+     * or null if no matchi is found.
+     * 
+     * @param value
+     *     The string value to search for in the list of enums.
+     * 
+     * @return
+     *     The RadiusAuthenticationProtocol value that is identified by the
+     *     provided String value.
+     */
+    public static RadiusAuthenticationProtocol getEnum(String value) {
+    
+        for (RadiusAuthenticationProtocol v : values())
+            if(v.toString().equals(value))
+                return v;
+        
+        return null;
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
new file mode 100644
index 0000000..c92c0a3
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
@@ -0,0 +1,54 @@
+/*
+ * 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.radius.conf;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty whose value is a RadiusAuthenticationProtocol.
+ */
+public abstract class RadiusAuthenticationProtocolProperty
+        implements GuacamoleProperty<RadiusAuthenticationProtocol> {
+    
+    @Override
+    public RadiusAuthenticationProtocol parseValue(String value)
+            throws GuacamoleException {
+        
+        // Nothing provided, nothing returned
+        if (value == null)
+            return null;
+        
+        // Attempt to parse the string value
+        RadiusAuthenticationProtocol authProtocol = 
+                RadiusAuthenticationProtocol.getEnum(value);
+        
+        // Throw an exception if nothing matched.
+        if (authProtocol == null)
+            throw new GuacamoleServerException(
+                    "Invalid or unsupported RADIUS authentication protocol.");
+        
+        // Return the answer
+        return authProtocol;
+        
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
similarity index 93%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
index aaa445e..af6839b 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
 
 import org.apache.guacamole.properties.BooleanGuacamoleProperty;
 import org.apache.guacamole.properties.FileGuacamoleProperty;
@@ -81,7 +81,8 @@
     /**
      * The authentication protocol of the RADIUS server to connect to when authenticating users.
      */
-    public static final StringGuacamoleProperty RADIUS_AUTH_PROTOCOL = new StringGuacamoleProperty() {
+    public static final RadiusAuthenticationProtocolProperty RADIUS_AUTH_PROTOCOL =
+            new RadiusAuthenticationProtocolProperty() {
 
         @Override
         public String getName() { return "radius-auth-protocol"; }
@@ -181,7 +182,8 @@
     /**
      * The tunneled protocol to use inside a RADIUS EAP-TTLS connection.
      */
-    public static final StringGuacamoleProperty RADIUS_EAP_TTLS_INNER_PROTOCOL = new StringGuacamoleProperty() {
+    public static final RadiusAuthenticationProtocolProperty RADIUS_EAP_TTLS_INNER_PROTOCOL =
+            new RadiusAuthenticationProtocolProperty() {
 
         @Override
         public String getName() { return "radius-eap-ttls-inner-protocol"; }
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
new file mode 100644
index 0000000..4589794
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
@@ -0,0 +1,77 @@
+/*
+ * 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.radius.form;
+
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+
+/**
+ * Stores the RADIUS challenge message and expected credentials in a single
+ * object.
+ */
+public class GuacamoleRadiusChallenge {
+    
+    /**
+     * The challenge text sent by the RADIUS server.
+     */
+    private final String challengeText;
+    
+    /**
+     * The expected credentials that need to be provided to satisfy the
+     * RADIUS authentication challenge.
+     */
+    private final CredentialsInfo expectedCredentials;
+    
+    /**
+     * Creates a new GuacamoleRadiusChallenge object with the provided
+     * challenge message and expected credentials.
+     * 
+     * @param challengeText
+     *     The challenge message sent by the RADIUS server.
+     * 
+     * @param expectedCredentials 
+     *     The credentials required to complete the challenge.
+     */
+    public GuacamoleRadiusChallenge(String challengeText,
+            CredentialsInfo expectedCredentials) {
+        this.challengeText = challengeText;
+        this.expectedCredentials = expectedCredentials;
+    }
+    
+    /**
+     * Returns the challenge message provided by the RADIUS server.
+     * 
+     * @return
+     *     The challenge message provided by the RADIUS server.
+     */
+    public String getChallengeText() {
+        return challengeText;
+    }
+    
+    /**
+     * Returns the credentials required to satisfy the RADIUS challenge.
+     * 
+     * @return 
+     *     The credentials required to satisfy the RADIUS challenge.
+     */
+    public CredentialsInfo getExpectedCredentials() {
+        return expectedCredentials;
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
deleted file mode 100644
index 32ceb90..0000000
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
+++ /dev/null
@@ -1,68 +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.radius.form;
-
-import org.apache.guacamole.form.Field;
-
-/**
- * A form used to prompt the user for additional information when
- * the RADIUS server sends a challenge back to the user with a reply
- * message.
- */
-public class RadiusChallengeResponseField extends Field {
-
-    /**
-     * The field returned by the RADIUS challenge/response.
-     */
-    public static final String PARAMETER_NAME = "guac-radius-challenge-response";
-
-    /**
-     * The type of field to initialize for the challenge/response.
-     */
-    private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE";
-
-    /**
-     * The message the RADIUS server sent back in the challenge.
-     */
-    private final String challenge;
-
-    /**
-     * Initialize the field with the challenge sent back by the RADIUS server.
-     *
-     * @param challenge
-     *     The challenge message sent back by the RADIUS server.
-     */
-    public RadiusChallengeResponseField(String challenge) {
-        super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
-        this.challenge = challenge;
-
-    }
-
-    /**
-     * Get the challenge sent by the RADIUS server.
-     *
-     * @return
-     *     A String that indicates the challenge returned
-     *     by the RADIUS server.
-     */
-    public String getChallenge() {
-        return challenge;
-    }
-}
diff --git a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
index dab0ffc..a3d72bf 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
+++ b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
@@ -23,13 +23,6 @@
 angular.module('guacRadius').config(['formServiceProvider',
         function guacRadiusConfig(formServiceProvider) {
 
-    // Define field for the challenge from the RADIUS service
-    formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
-        module      : 'guacRadius',
-        controller  : 'radiusResponseController',
-        templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
-    });
-
     // Define the hidden field for the RADIUS state
     formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
         module      : 'guacRadius',
diff --git a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js b/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
deleted file mode 100644
index 4782b20..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
+++ /dev/null
@@ -1,30 +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_RADIUS_CHALLENGE_RESPONSE" field which
- * passes the RADIUS server challenge to the user and takes the response.
- */
-angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$injector',
-        function radiusResponseController($scope, $injector) {
-
-    // Populate the reply message field
-    $scope.radiusPlaceholder = $scope.field.challenge;
-
-}]);
diff --git a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
index 6e8e078..707f233 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -19,7 +19,6 @@
     ],
 
     "resources" : {
-        "templates/radiusResponseField.html" : "text/html",
         "templates/radiusStateField.html"    : "text/html"
     }
 
diff --git a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html b/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
deleted file mode 100644
index eec760f..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
+++ /dev/null
@@ -1 +0,0 @@
-<input type="password" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off" placeholder="{{radiusPlaceholder}}" />
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
index c068a70..1ba2623 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
@@ -5,9 +5,8 @@
     },
 
     "LOGIN" : {
-        "FIELD_HEADER_GUAC_RADIUS_CHALLENGE_RESPONSE" : "",
         "FIELD_HEADER_GUAC_RADIUS_STATE"              : "",
-        "INFO_RADIUS_ADDL_REQUIRED"                   : "Please supply additional credentials"
+        "FIELD_HEADER_RADIUSCHALLENGE"                : ""
     }
 
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
index 1b45c95..b1d4865 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
@@ -53,7 +53,7 @@
      * Array of all known protocol names.
      */
     private static final String[] KNOWN_PROTOCOLS = new String[]{
-        "vnc", "rdp", "ssh", "telnet"};
+        "vnc", "rdp", "ssh", "telnet", "kubernetes"};
 
     /**
      * The hostname to use when connecting to guacd if no hostname is provided
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
index 9fe76a4..d35facd 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
@@ -117,6 +117,12 @@
          */
         public static String QUERY_PARAMETER = "QUERY_PARAMETER";
 
+        /**
+         * A color scheme accepted by the Guacamole server terminal emulator
+         * and protocols which leverage it.
+         */
+        public static String TERMINAL_COLOR_SCHEME = "TERMINAL_COLOR_SCHEME";
+
     }
 
     /**
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
new file mode 100644
index 0000000..ccd7ce5
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
@@ -0,0 +1,39 @@
+/*
+ * 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.form;
+
+/**
+ * Represents a terminal color scheme field. The field may contain only valid
+ * terminal color schemes as used by the Guacamole server terminal emulator
+ * and protocols which leverage it (SSH, telnet, Kubernetes).
+ */
+public class TerminalColorSchemeField extends Field {
+
+    /**
+     * Creates a new TerminalColorSchemeField with the given name.
+     *
+     * @param name
+     *     The unique name to associate with this field.
+     */
+    public TerminalColorSchemeField(String name) {
+        super(name, Field.Type.TERMINAL_COLOR_SCHEME);
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
index ad1d6d3..9bcce3e 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
@@ -20,13 +20,18 @@
 package org.apache.guacamole.net.auth;
 
 import java.util.Date;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
 
 /**
  * A pairing of username and GuacamoleTunnel representing an active usage of a
  * particular connection.
  */
-public interface ActiveConnection extends Identifiable, Shareable<SharingProfile> {
+public interface ActiveConnection extends Identifiable, Connectable,
+        Shareable<SharingProfile> {
 
     /**
      * Returns the identifier of the connection being actively used. Unlike the
@@ -136,5 +141,31 @@
      *     The connected GuacamoleTunnel, or null if permission is denied.
      */
     void setTunnel(GuacamoleTunnel tunnel);
+
+    /**
+     * Returns whether this ActiveConnection may be joined through a call to
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)}
+     * by the user that retrieved this ActiveConnection.
+     *
+     * @return
+     *     true if the user that retrieved this ActiveConnection may join the
+     *     ActiveConnection through a call to
+     *     {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)},
+     *     false otherwise.
+     */
+    default boolean isConnectable() {
+        return false;
+    }
+
+    @Override
+    default GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        throw new GuacamoleSecurityException("Permission denied.");
+    }
+
+    @Override
+    default int getActiveConnections() {
+        return 0;
+    }
     
 }
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json
new file mode 100644
index 0000000..17df48a
--- /dev/null
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json
@@ -0,0 +1,174 @@
+{
+    "name"  : "kubernetes",
+
+    "connectionForms" : [
+
+        {
+            "name"  : "network",
+            "fields" : [
+                {
+                    "name" : "hostname",
+                    "type" : "TEXT"
+                },
+                {
+                    "name" : "port",
+                    "type" : "NUMERIC"
+                },
+                {
+                    "name"    : "use-ssl",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"    : "ignore-cert",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name" : "ca-cert",
+                    "type" : "MULTILINE"
+                }
+            ]
+        },
+
+        {
+            "name" : "container",
+            "fields" : [
+                {
+                    "name" : "namespace",
+                    "type" : "TEXT"
+                },
+                {
+                    "name" : "pod",
+                    "type" : "TEXT"
+                },
+                {
+                    "name" : "container",
+                    "type" : "TEXT"
+                }
+            ]
+        },
+
+        {
+            "name"  : "authentication",
+            "fields" : [
+                {
+                    "name" : "client-cert",
+                    "type" : "MULTILINE"
+                },
+                {
+                    "name" : "client-key",
+                    "type" : "MULTILINE"
+                }
+            ]
+        },
+
+        {
+            "name"  : "display",
+            "fields" : [
+                {
+                    "name"  : "color-scheme",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
+                    "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
+                },
+                {
+                    "name"  : "font-name",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "font-size",
+                    "type"  : "ENUM",
+                    "options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ]
+                },
+                {
+                    "name"  : "scrollback",
+                    "type"  : "NUMERIC"
+                },
+                {
+                    "name"    : "read-only",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        },
+
+        {
+            "name" : "behavior",
+            "fields" : [
+                {
+                    "name"    : "backspace",
+                    "type"    : "ENUM",
+                    "options" : [ "", "127", "8" ]
+                }
+            ]
+        },
+
+        {
+            "name" : "typescript",
+            "fields" : [
+                {
+                    "name"  : "typescript-path",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "typescript-name",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "create-typescript-path",
+                    "type"  : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        },
+
+        {
+            "name" : "recording",
+            "fields" : [
+                {
+                    "name"  : "recording-path",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "recording-name",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "recording-exclude-output",
+                    "type"  : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"  : "recording-exclude-mouse",
+                    "type"  : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"  : "recording-include-keys",
+                    "type"  : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"  : "create-recording-path",
+                    "type"  : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        }
+
+    ],
+    "sharingProfileForms" : [
+
+        {
+            "name"  : "display",
+            "fields" : [
+                {
+                    "name"    : "read-only",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        }
+
+    ]
+}
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
index 25537da..a1d01ed 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
@@ -47,7 +47,7 @@
             "fields" : [
                 {
                     "name"  : "color-scheme",
-                    "type"  : "TEXT",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
                     "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
                 },
                 {
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
index 02f9a20..0f70f05 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
@@ -51,7 +51,7 @@
             "fields" : [
                 {
                     "name"  : "color-scheme",
-                    "type"  : "TEXT",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
                     "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
                 },
                 {
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index 5552e4b..d7cf465 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -508,6 +508,13 @@
             <version>1.0.10</version>
         </dependency>
 
+        <!-- Pickr (JavaScript color picker) -->
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>simonwep__pickr</artifactId>
+            <version>1.2.6</version>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index c3d308d..2995811 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -697,6 +697,37 @@
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
+Pickr (https://simonwep.github.io/pickr/)
+-----------------------------------------
+
+    Version: 1.2.6
+    From: 'Simon Reinisch' (https://github.com/Simonwep/)
+    License(s):
+        MIT (bundled/pickr-1.2.6/LICENSE)
+
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
 Simple Logging Facade for Java (http://slf4j.org/)
 --------------------------------------------------
 
diff --git a/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
new file mode 100644
index 0000000..e02b384
--- /dev/null
+++ b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
index 0041a03..1634378 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
@@ -55,6 +55,11 @@
     private final String username;
 
     /**
+     * Whether this active connection may be connected to.
+     */
+    private final boolean connectable;
+
+    /**
      * Creates a new APIActiveConnection, copying the information from the given
      * active connection.
      *
@@ -67,6 +72,7 @@
         this.startDate            = connection.getStartDate();
         this.remoteHost           = connection.getRemoteHost();
         this.username             = connection.getUsername();
+        this.connectable          = connection.isConnectable();
     }
 
     /**
@@ -121,5 +127,16 @@
     public String getIdentifier() {
         return identifier;
     }
-    
+
+    /***
+     * Returns whether this active connection may be connected to, just as a
+     * normal connection.
+     *
+     * @return
+     *     true if this active connection may be connected to, false otherwise.
+     */
+    public boolean isConnectable() {
+        return connectable;
+    }
+
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
index 7f38857..b168514 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
@@ -36,6 +36,7 @@
 import org.apache.guacamole.net.auth.UserContext;
 import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
 import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
 import org.apache.guacamole.net.event.AuthenticationFailureEvent;
 import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
@@ -170,7 +171,13 @@
                     return authenticatedUser;
             }
 
-            // First failure takes priority for now
+            // Insufficient credentials should take precedence
+            catch (GuacamoleInsufficientCredentialsException e) {
+                if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException)
+                    authFailure = e;
+            }
+            
+            // Catch other credentials exceptions and assign the first one
             catch (GuacamoleCredentialsException e) {
                 if (authFailure == null)
                     authFailure = e;
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
index 74e3b4d..fe58e18 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
@@ -21,8 +21,6 @@
 
 import java.util.List;
 import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleException;
 
 /**
@@ -102,40 +100,6 @@
     public static final String TIMEZONE_PARAMETER = "GUAC_TIMEZONE";
 
     /**
-     * All supported object types that can be used as the destination of a
-     * tunnel.
-     */
-    public static enum Type {
-
-        /**
-         * A Guacamole connection.
-         */
-        CONNECTION("c"),
-
-        /**
-         * A Guacamole connection group.
-         */
-        CONNECTION_GROUP("g");
-
-        /**
-         * The parameter value which denotes a destination object of this type.
-         */
-        final String PARAMETER_VALUE;
-        
-        /**
-         * Defines a Type having the given corresponding parameter value.
-         *
-         * @param value
-         *     The parameter value which denotes a destination object of this
-         *     type.
-         */
-        Type(String value) {
-            PARAMETER_VALUE = value;
-        }
-
-    };
-
-    /**
      * Returns the value of the parameter having the given name.
      *
      * @param name
@@ -257,18 +221,11 @@
      *     If the type was not present in the request, or if the type requested
      *     is in the wrong format.
      */
-    public Type getType() throws GuacamoleException {
+    public TunnelRequestType getType() throws GuacamoleException {
 
-        String type = getRequiredParameter(TYPE_PARAMETER);
-
-        // For each possible object type
-        for (Type possibleType : Type.values()) {
-
-            // Match against defined parameter value
-            if (type.equals(possibleType.PARAMETER_VALUE))
-                return possibleType;
-
-        }
+        TunnelRequestType type = TunnelRequestType.parseType(getRequiredParameter(TYPE_PARAMETER));
+        if (type != null)
+            return type;
 
         throw new GuacamoleClientException("Illegal identifier - unknown type.");
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
index 598a4e5..fa56b19 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
@@ -24,15 +24,13 @@
 import java.util.List;
 import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.GuacamoleSecurityException;
+import org.apache.guacamole.GuacamoleResourceNotFoundException;
 import org.apache.guacamole.GuacamoleSession;
 import org.apache.guacamole.GuacamoleUnauthorizedException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
-import org.apache.guacamole.net.auth.Connection;
-import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.Connectable;
 import org.apache.guacamole.net.auth.Credentials;
-import org.apache.guacamole.net.auth.Directory;
 import org.apache.guacamole.net.auth.UserContext;
 import org.apache.guacamole.net.event.TunnelCloseEvent;
 import org.apache.guacamole.net.event.TunnelConnectEvent;
@@ -204,58 +202,20 @@
      *     If an error occurs while creating the tunnel.
      */
     protected GuacamoleTunnel createConnectedTunnel(UserContext context,
-            final TunnelRequest.Type type, String id,
+            final TunnelRequestType type, String id,
             GuacamoleClientInformation info, Map<String, String> tokens)
             throws GuacamoleException {
 
-        // Create connected tunnel from identifier
-        GuacamoleTunnel tunnel = null;
-        switch (type) {
+        // Retrieve requested destination object
+        Connectable connectable = type.getConnectable(context, id);
+        if (connectable == null)
+            throw new GuacamoleResourceNotFoundException("Requested tunnel "
+                    + "destination does not exist.");
 
-            // Connection identifiers
-            case CONNECTION: {
-
-                // Get connection directory
-                Directory<Connection> directory = context.getConnectionDirectory();
-
-                // Get authorized connection
-                Connection connection = directory.get(id);
-                if (connection == null) {
-                    logger.info("Connection \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
-                    throw new GuacamoleSecurityException("Requested connection is not authorized.");
-                }
-
-                // Connect tunnel
-                tunnel = connection.connect(info, tokens);
-                logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id);
-                break;
-            }
-
-            // Connection group identifiers
-            case CONNECTION_GROUP: {
-
-                // Get connection group directory
-                Directory<ConnectionGroup> directory = context.getConnectionGroupDirectory();
-
-                // Get authorized connection group
-                ConnectionGroup group = directory.get(id);
-                if (group == null) {
-                    logger.info("Connection group \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
-                    throw new GuacamoleSecurityException("Requested connection group is not authorized.");
-                }
-
-                // Connect tunnel
-                tunnel = group.connect(info, tokens);
-                logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id);
-                break;
-            }
-
-            // Type is guaranteed to be one of the above
-            default:
-                assert(false);
-
-        }
-
+        // Connect tunnel to destination
+        GuacamoleTunnel tunnel = connectable.connect(info, tokens);
+        logger.info("User \"{}\" connected to {} \"{}\".",
+                context.self().getIdentifier(), type.NAME, id);
         return tunnel;
 
     }
@@ -297,7 +257,7 @@
      */
     protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleTunnel tunnel,
             final String authToken, final GuacamoleSession session,
-            final UserContext context, final TunnelRequest.Type type,
+            final UserContext context, final TunnelRequestType type,
             final String id) throws GuacamoleException {
 
         // Monitor tunnel closure and data
@@ -320,26 +280,9 @@
                 long connectionEndTime = System.currentTimeMillis();
                 long duration = connectionEndTime - connectionStartTime;
 
-                // Log closure
-                switch (type) {
-
-                    // Connection identifiers
-                    case CONNECTION:
-                        logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds",
-                                session.getAuthenticatedUser().getIdentifier(), id, duration);
-                        break;
-
-                    // Connection group identifiers
-                    case CONNECTION_GROUP:
-                        logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds",
-                                session.getAuthenticatedUser().getIdentifier(), id, duration);
-                        break;
-
-                    // Type is guaranteed to be one of the above
-                    default:
-                        assert(false);
-
-                }
+                logger.info("User \"{}\" disconnected from {} \"{}\". Duration: {} milliseconds",
+                        session.getAuthenticatedUser().getIdentifier(),
+                        type.NAME, id, duration);
 
                 try {
 
@@ -390,7 +333,7 @@
         // Parse request parameters
         String authToken                = request.getAuthenticationToken();
         String id                       = request.getIdentifier();
-        TunnelRequest.Type type         = request.getType();
+        TunnelRequestType type          = request.getType();
         String authProviderIdentifier   = request.getAuthenticationProviderIdentifier();
         GuacamoleClientInformation info = getClientInformation(request);
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java
new file mode 100644
index 0000000..2c98708
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java
@@ -0,0 +1,154 @@
+/*
+ * 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.tunnel;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActiveConnection;
+import org.apache.guacamole.net.auth.Connectable;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.UserContext;
+
+/**
+ * All supported object types that can be used as the destination of a tunnel.
+ *
+ * @see TunnelRequest#TYPE_PARAMETER
+ * @see TunnelRequest#getType()
+ */
+public enum TunnelRequestType {
+
+    /**
+     * A Guacamole connection.
+     */
+    CONNECTION("c", "connection") {
+
+        @Override
+        public Connection getConnectable(UserContext userContext,
+                String identifier) throws GuacamoleException {
+            return userContext.getConnectionDirectory().get(identifier);
+        }
+
+    },
+
+    /**
+     * A Guacamole connection group.
+     */
+    CONNECTION_GROUP("g", "connection group") {
+
+        @Override
+        public ConnectionGroup getConnectable(UserContext userContext,
+                String identifier) throws GuacamoleException {
+            return userContext.getConnectionGroupDirectory().get(identifier);
+        }
+
+    },
+
+    /**
+     * An active Guacamole connection.
+     */
+    ACTIVE_CONNECTION("a", "active connection") {
+
+        @Override
+        public ActiveConnection getConnectable(UserContext userContext,
+                String identifier) throws GuacamoleException {
+            return userContext.getActiveConnectionDirectory().get(identifier);
+        }
+
+    };
+
+    /**
+     * The parameter value which denotes a destination object of this type
+     * within a tunnel request.
+     *
+     * @see TunnelRequest#TYPE_PARAMETER
+     * @see TunnelRequest#getType()
+     */
+    public final String PARAMETER_VALUE;
+
+    /**
+     * A human-readable, descriptive name of the type of destination object.
+     */
+    public final String NAME;
+
+    /**
+     * Defines a tunnel request type having the given corresponding parameter
+     * value and human-readable name.
+     *
+     * @param value
+     *     The parameter value which denotes a destination object of this
+     *     type.
+     *
+     * @param name
+     *     A human-readable, descriptive name of the type of destination
+     *     object.
+     */
+    private TunnelRequestType(String value, String name) {
+        PARAMETER_VALUE = value;
+        NAME = name;
+    }
+
+    /**
+     * Retrieves the object having the given identifier from the given
+     * UserContext, where the type of object retrieved is the type of object
+     * represented by this tunnel request type.
+     *
+     * @param userContext
+     *     The UserContext to retrieve the object from.
+     *
+     * @param identifier
+     *     The identifier of the object to retrieve.
+     *
+     * @return
+     *     The object having the given identifier, or null if no such object
+     *     exists.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs retrieving the requested object, or if permission
+     *     to retrieve the object is denied.
+     */
+    public abstract Connectable getConnectable(UserContext userContext,
+            String identifier) throws GuacamoleException;
+
+    /**
+     * Parses the given tunnel request type string, returning the
+     * TunnelRequestType which matches that string, as declared by
+     * {@link #PARAMETER_VALUE}. If no such type exists, null is returned.
+     *
+     * @param type
+     *     The type string to parse.
+     *
+     * @return
+     *     The TunnelRequestType which specifies the given string as its
+     *     {@link #PARAMETER_VALUE}, or null if no such type exists.
+     */
+    public static TunnelRequestType parseType(String type) {
+
+        // Locate type with given parameter value
+        for (TunnelRequestType possibleType : values()) {
+            if (type.equals(possibleType.PARAMETER_VALUE))
+                return possibleType;
+        }
+
+        // No such type
+        return null;
+
+    }
+
+}
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index f787918..c8941b3 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -28,6 +28,7 @@
     var ManagedClient      = $injector.get('ManagedClient');
     var ManagedClientState = $injector.get('ManagedClientState');
     var ManagedFilesystem  = $injector.get('ManagedFilesystem');
+    var Protocol           = $injector.get('Protocol');
     var ScrollState        = $injector.get('ScrollState');
 
     // Required services
@@ -251,7 +252,15 @@
          *
          * @type ScrollState
          */
-        scrollState : new ScrollState()
+        scrollState : new ScrollState(),
+
+        /**
+         * The current desired values of all editable connection parameters as
+         * a set of name/value pairs, including any changes made by the user.
+         *
+         * @type {Object.<String, String>}
+         */
+        connectionParameters : {}
 
     };
 
@@ -261,6 +270,16 @@
     };
 
     /**
+     * Applies any changes to connection parameters made by the user within the
+     * Guacamole menu.
+     */
+    $scope.applyParameterChanges = function applyParameterChanges() {
+        angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
+            ManagedClient.setArgument($scope.client, name, value);
+        });
+    };
+
+    /**
      * The client which should be attached to the client UI.
      *
      * @type ManagedClient
@@ -490,12 +509,20 @@
 
     });
 
+    // Update client state/behavior as visibility of the Guacamole menu changes
     $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
         
-        // Send clipboard data if menu is hidden
-        if (!menuShown && menuShownPreviousState)
+        // Send clipboard and argument value data once menu is hidden
+        if (!menuShown && menuShownPreviousState) {
             $scope.$broadcast('guacClipboard', $scope.client.clipboardData);
-        
+            $scope.applyParameterChanges();
+        }
+
+        // Obtain snapshot of current editable connection parameters when menu
+        // is opened
+        else if (menuShown)
+            $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
+
         // Disable client keyboard if the menu is shown
         $scope.client.clientProperties.keyboardEnabled = !menuShown;
 
@@ -873,6 +900,11 @@
     $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ];
 
     /**
+     * @borrows Protocol.getNamespace
+     */
+    $scope.getProtocolNamespace = Protocol.getNamespace;
+
+    /**
      * The currently-visible filesystem within the filesystem menu, if the
      * filesystem menu is open. If no filesystem is currently visible, this
      * will be null.
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index 3f56a9d..5325b47 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -119,6 +119,14 @@
                     </div>
                 </div>
 
+                <!-- Connection parameters which may be modified while the connection is open -->
+                <div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
+                    <guac-form namespace="getProtocolNamespace(client.protocol)"
+                               content="client.forms"
+                               model="menu.connectionParameters"
+                               model-only="true"></guac-form>
+                </div>
+
                 <!-- Input method -->
                 <div class="menu-section" id="keyboard-settings">
                     <h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedArgument.js b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
new file mode 100644
index 0000000..247d9f6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides the ManagedArgument class used by ManagedClient.
+ */
+angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
+
+    /**
+     * Object which represents an argument (connection parameter) which may be
+     * changed by the user while the connection is open.
+     * 
+     * @constructor
+     * @param {ManagedArgument|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     ManagedArgument.
+     */
+    var ManagedArgument = function ManagedArgument(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The name of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.name = template.name;
+
+        /**
+         * The current value of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.value = template.value;
+
+        /**
+         * A valid, open output stream which may be used to apply a new value
+         * to the connection parameter.
+         *
+         * @type {Guacamole.OutputStream}
+         */
+        this.stream = template.stream;
+
+    };
+
+    /**
+     * Requests editable access to a given connection parameter, returning a
+     * promise which is resolved with a ManagedArgument instance that provides
+     * such access if the parameter is indeed editable.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the connection for which
+     *     an editable version of the connection parameter is being retrieved.
+     *
+     * @param {String} name
+     *     The name of the connection parameter.
+     *
+     * @param {String} value
+     *     The current value of the connection parameter, as received from a
+     *     prior, inbound "argv" stream.
+     *
+     * @returns {Promise.<ManagedArgument>}
+     *     A promise which is resolved with the new ManagedArgument instance
+     *     once the requested parameter has been verified as editable.
+     */
+    ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
+
+        var deferred = $q.defer();
+
+        // Create internal, fully-populated instance of ManagedArgument, to be
+        // returned only once mutability of the associated connection parameter
+        // has been verified
+        var managedArgument = new ManagedArgument({
+            name   : name,
+            value  : value,
+            stream : managedClient.client.createArgumentValueStream('text/plain', name)
+        });
+
+        // The connection parameter is editable only if a successful "ack" is
+        // received
+        managedArgument.stream.onack = function ackReceived(status) {
+            if (status.isError())
+                deferred.reject(status);
+            else
+                deferred.resolve(managedArgument);
+        };
+
+        return deferred.promise;
+
+    };
+
+    /**
+     * Sets the given editable argument (connection parameter) to the given
+     * value, updating the behavior of the associated connection in real-time.
+     * If successful, the ManagedArgument provided cannot be used for future
+     * calls to setValue() and must be replaced with a new instance. This
+     * function only has an effect if the new parameter value is different from
+     * the current value.
+     *
+     * @param {ManagedArgument} managedArgument
+     *     The ManagedArgument instance associated with the connection
+     *     parameter being modified.
+     *
+     * @param {String} value
+     *     The new value to assign to the connection parameter.
+     *
+     * @returns {Boolean}
+     *     true if the connection parameter was sent and the provided
+     *     ManagedArgument instance may no longer be used for future setValue()
+     *     calls, false if the connection parameter was NOT sent as it has not
+     *     changed.
+     */
+    ManagedArgument.setValue = function setValue(managedArgument, value) {
+
+        // Stream new value only if value has changed
+        if (value !== managedArgument.value) {
+
+            var writer = new Guacamole.StringWriter(managedArgument.stream);
+            writer.sendText(value);
+            writer.sendEnd();
+
+            // ManagedArgument instance is no longer usable
+            return true;
+
+        }
+
+        // No parameter value change was attempted and the ManagedArgument
+        // instance may be reused
+        return false;
+
+    };
+
+    return ManagedArgument;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index de679d6..c1eccdd 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -27,6 +27,7 @@
     var ClientProperties       = $injector.get('ClientProperties');
     var ClientIdentifier       = $injector.get('ClientIdentifier');
     var ClipboardData          = $injector.get('ClipboardData');
+    var ManagedArgument        = $injector.get('ManagedArgument');
     var ManagedClientState     = $injector.get('ManagedClientState');
     var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
     var ManagedDisplay         = $injector.get('ManagedDisplay');
@@ -35,20 +36,22 @@
     var ManagedShareLink       = $injector.get('ManagedShareLink');
 
     // Required services
-    var $document              = $injector.get('$document');
-    var $q                     = $injector.get('$q');
-    var $rootScope             = $injector.get('$rootScope');
-    var $window                = $injector.get('$window');
-    var authenticationService  = $injector.get('authenticationService');
-    var connectionGroupService = $injector.get('connectionGroupService');
-    var connectionService      = $injector.get('connectionService');
-    var preferenceService      = $injector.get('preferenceService');
-    var requestService         = $injector.get('requestService');
-    var tunnelService          = $injector.get('tunnelService');
-    var guacAudio              = $injector.get('guacAudio');
-    var guacHistory            = $injector.get('guacHistory');
-    var guacImage              = $injector.get('guacImage');
-    var guacVideo              = $injector.get('guacVideo');
+    var $document               = $injector.get('$document');
+    var $q                      = $injector.get('$q');
+    var $rootScope              = $injector.get('$rootScope');
+    var $window                 = $injector.get('$window');
+    var activeConnectionService = $injector.get('activeConnectionService');
+    var authenticationService   = $injector.get('authenticationService');
+    var connectionGroupService  = $injector.get('connectionGroupService');
+    var connectionService       = $injector.get('connectionService');
+    var preferenceService       = $injector.get('preferenceService');
+    var requestService          = $injector.get('requestService');
+    var schemaService           = $injector.get('schemaService');
+    var tunnelService           = $injector.get('tunnelService');
+    var guacAudio               = $injector.get('guacAudio');
+    var guacHistory             = $injector.get('guacHistory');
+    var guacImage               = $injector.get('guacImage');
+    var guacVideo               = $injector.get('guacVideo');
 
     /**
      * The minimum amount of time to wait between updates to the client
@@ -128,6 +131,23 @@
         this.title = template.title;
 
         /**
+         * The name which uniquely identifies the protocol of the connection in
+         * use. If the protocol cannot be determined, such as when a connection
+         * group is in use, this will be null.
+         *
+         * @type {String}
+         */
+        this.protocol = template.protocol || null;
+
+        /**
+         * An array of forms describing all known parameters for the connection
+         * in use, including those which may not be editable.
+         *
+         * @type {Form[]}
+         */
+        this.forms = template.forms || [];
+
+        /**
          * The most recently-generated thumbnail for this connection, as
          * stored within the local connection history. If no thumbnail is
          * stored, this will be null.
@@ -189,6 +209,17 @@
          */
         this.clientProperties = template.clientProperties || new ClientProperties();
 
+        /**
+         * All editable arguments (connection parameters), stored by their
+         * names. Arguments will only be present within this set if their
+         * current values have been exposed by the server via an inbound "argv"
+         * stream and the server has confirmed that the value may be changed
+         * through a successful "ack" to an outbound "argv" stream.
+         *
+         * @type {Object.<String, ManagedArgument>}
+         */
+        this.arguments = template.arguments || {};
+
     };
 
     /**
@@ -458,6 +489,33 @@
 
         };
 
+        // Test for argument mutability whenever an argument value is
+        // received
+        client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
+
+            // Ignore arguments which do not use a mimetype currently supported
+            // by the web application
+            if (mimetype !== 'text/plain')
+                return;
+
+            var reader = new Guacamole.StringReader(stream);
+
+            // Assemble received data into a single string
+            var value = '';
+            reader.ontext = function textReceived(text) {
+                value += text;
+            };
+
+            // Test mutability once stream is finished, storing the current
+            // value for the argument only if it is mutable
+            reader.onend = function textComplete() {
+                ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
+                    managedClient.arguments[name] = argument;
+                }, function ignoreImmutableArguments() {});
+            };
+
+        };
+
         // Handle any received clipboard data
         client.onclipboard = function clientClipboardReceived(stream, mimetype) {
 
@@ -532,11 +590,16 @@
             client.connect(connectString);
         });
 
-        // If using a connection, pull connection name
+        // If using a connection, pull connection name and protocol information
         if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
-            connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
-            .then(function connectionRetrieved(connection) {
-                managedClient.name = managedClient.title = connection.name;
+            $q.all({
+                connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
+                protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
+            })
+            .then(function dataRetrieved(values) {
+                managedClient.name = managedClient.title = values.connection.name;
+                managedClient.protocol = values.connection.protocol;
+                managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
             }, requestService.WARN);
         }
         
@@ -548,6 +611,29 @@
             }, requestService.WARN);
         }
 
+        // If using an active connection, pull corresponding connection, then
+        // pull connection name and protocol information from that
+        else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) {
+            activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id)
+            .then(function activeConnectionRetrieved(activeConnection) {
+
+                // Attempt to retrieve connection details only if the
+                // underlying connection is known
+                if (activeConnection.connectionIdentifier) {
+                    $q.all({
+                        connection : connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier),
+                        protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
+                    })
+                    .then(function dataRetrieved(values) {
+                        managedClient.name = managedClient.title = values.connection.name;
+                        managedClient.protocol = values.connection.protocol;
+                        managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
+                    }, requestService.WARN);
+                }
+
+            }, requestService.WARN);
+        }
+
         return managedClient;
 
     };
@@ -630,6 +716,52 @@
     };
 
     /**
+     * Assigns the given value to the connection parameter having the given
+     * name, updating the behavior of the connection in real-time. If the
+     * connection parameter is not editable, this function has no effect.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the active connection
+     *     being modified.
+     *
+     * @param {String} name
+     *     The name of the connection parameter to modify.
+     *
+     * @param {String} value
+     *     The value to attempt to assign to the given connection parameter.
+     */
+    ManagedClient.setArgument = function setArgument(managedClient, name, value) {
+        var managedArgument = managedClient.arguments[name];
+        if (managedArgument && ManagedArgument.setValue(managedArgument, value))
+            delete managedClient.arguments[name];
+    };
+
+    /**
+     * Retrieves the current values of all editable connection parameters as a
+     * set of name/value pairs suitable for use as the model of a form which
+     * edits those parameters.
+     *
+     * @param {ManagedClient} client
+     *     The ManagedClient instance associated with the active connection
+     *     whose parameter values are being retrieved.
+     *
+     * @returns {Object.<String, String>}
+     *     A new set of name/value pairs containing the current values of all
+     *     editable parameters.
+     */
+    ManagedClient.getArgumentModel = function getArgumentModel(client) {
+
+        var model = {};
+
+        angular.forEach(client.arguments, function addModelEntry(managedArgument) {
+            model[managedArgument.name] = managedArgument.value;
+        });
+
+        return model;
+
+    };
+
+    /**
      * Produces a sharing link for the given ManagedClient using the given
      * sharing profile. The resulting sharing link, and any required login
      * information, can be retrieved from the <code>shareLinks</code> property
diff --git a/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js b/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js
new file mode 100644
index 0000000..fb85a50
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js
@@ -0,0 +1,142 @@
+/*
+ * 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 terminal color scheme fields.
+ */
+angular.module('form').controller('terminalColorSchemeFieldController', ['$scope', '$injector',
+    function terminalColorSchemeFieldController($scope, $injector) {
+
+    // Required types
+    var ColorScheme = $injector.get('ColorScheme');
+
+    /**
+     * The currently selected color scheme. If a pre-defined color scheme is
+     * selected, this will be the connection parameter value associated with
+     * that color scheme. If a custom color scheme is selected, this will be
+     * the string "custom".
+     *
+     * @type String
+     */
+    $scope.selectedColorScheme = '';
+
+    /**
+     * The current custom color scheme, if a custom color scheme has been
+     * specified. If no custom color scheme has yet been specified, this will
+     * be a ColorScheme instance that has been initialized to the default
+     * colors.
+     *
+     * @type ColorScheme
+     */
+    $scope.customColorScheme = new ColorScheme();
+
+    /**
+     * The array of colors to include within the color picker as pre-defined
+     * options for convenience.
+     *
+     * @type String[]
+     */
+    $scope.defaultPalette = new ColorScheme().colors;
+
+    /**
+     * Whether the raw details of the custom color scheme should be shown. By
+     * default, such details are hidden.
+     *
+     * @type Boolean
+     */
+    $scope.detailsShown = false;
+
+    /**
+     * The palette indices of all colors which are considered low-intensity.
+     *
+     * @type Number[]
+     */
+    $scope.lowIntensity = [ 0, 1, 2, 3, 4, 5, 6, 7 ];
+
+    /**
+     * The palette indices of all colors which are considered high-intensity.
+     *
+     * @type Number[]
+     */
+    $scope.highIntensity = [ 8, 9, 10, 11, 12, 13, 14, 15 ];
+
+    /**
+     * The string value which is assigned to selectedColorScheme if a custom
+     * color scheme is selected.
+     *
+     * @constant
+     * @type String
+     */
+    var CUSTOM_COLOR_SCHEME = 'custom';
+
+    /**
+     * Returns whether a custom color scheme has been selected.
+     *
+     * @returns {Boolean}
+     *     true if a custom color scheme has been selected, false otherwise.
+     */
+    $scope.isCustom = function isCustom() {
+        return $scope.selectedColorScheme === CUSTOM_COLOR_SCHEME;
+    };
+
+    /**
+     * Shows the raw details of the custom color scheme. If the details are
+     * already shown, this function has no effect.
+     */
+    $scope.showDetails = function showDetails() {
+        $scope.detailsShown = true;
+    };
+
+    /**
+     * Hides the raw details of the custom color scheme. If the details are
+     * already hidden, this function has no effect.
+     */
+    $scope.hideDetails = function hideDetails() {
+        $scope.detailsShown = false;
+    };
+
+    // Keep selected color scheme and custom color scheme in sync with changes
+    // to model
+    $scope.$watch('model', function modelChanged(model) {
+        if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME || (model && !_.includes($scope.field.options, model))) {
+            $scope.customColorScheme = ColorScheme.fromString(model);
+            $scope.selectedColorScheme = CUSTOM_COLOR_SCHEME;
+        }
+        else
+            $scope.selectedColorScheme = model || '';
+    });
+
+    // Keep model in sync with changes to selected color scheme
+    $scope.$watch('selectedColorScheme', function selectedColorSchemeChanged(selectedColorScheme) {
+        if (!selectedColorScheme)
+            $scope.model = '';
+        else if (selectedColorScheme === CUSTOM_COLOR_SCHEME)
+            $scope.model = ColorScheme.toString($scope.customColorScheme);
+        else
+            $scope.model = selectedColorScheme;
+    });
+
+    // Keep model in sync with changes to custom color scheme
+    $scope.$watch('customColorScheme', function customColorSchemeChanged(customColorScheme) {
+        if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME)
+            $scope.model = ColorScheme.toString(customColorScheme);
+    }, true);
+
+}]);
diff --git a/guacamole/src/main/webapp/app/form/controllers/textFieldController.js b/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
index b5bc753..8dd134a 100644
--- a/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
+++ b/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
@@ -35,6 +35,6 @@
 
     // Generate unique ID for datalist, if applicable
     if ($scope.field.options && $scope.field.options.length)
-        $scope.dataListId = $scope.field.name + '-datalist';
+        $scope.dataListId = $scope.fieldId + '-datalist';
 
 }]);
diff --git a/guacamole/src/main/webapp/app/form/directives/formField.js b/guacamole/src/main/webapp/app/form/directives/formField.js
index cf3efd0..fbc0cfe 100644
--- a/guacamole/src/main/webapp/app/form/directives/formField.js
+++ b/guacamole/src/main/webapp/app/form/directives/formField.js
@@ -88,6 +88,18 @@
             var fieldContent = $element.find('.form-field');
 
             /**
+             * An ID value which is reasonably likely to be unique relative to
+             * other elements on the page. This ID should be used to associate
+             * the relevant input element with the label provided by the
+             * guacFormField directive, if there is such an input element.
+             *
+             * @type String
+             */
+            $scope.fieldId = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() {
+                return Math.floor(Math.random() * 36).toString(36);
+            }) + '-' + new Date().getTime().toString(36);
+
+            /**
              * Produces the translation string for the header of the current
              * field. The translation string will be of the form:
              *
@@ -173,4 +185,4 @@
         }] // end controller
     };
     
-}]);
\ No newline at end of file
+}]);
diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
new file mode 100644
index 0000000..1762c31
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+/**
+ * A directive which implements a color input field. If the underlying color
+ * picker implementation cannot be used due to a lack of browser support, this
+ * directive will become read-only, functioning essentially as a color preview.
+ *
+ * @see colorPickerService
+ */
+angular.module('form').directive('guacInputColor', [function guacInputColor() {
+
+    var config = {
+        restrict: 'E',
+        replace: true,
+        templateUrl: 'app/form/templates/guacInputColor.html',
+        transclude: true
+    };
+
+    config.scope = {
+
+        /**
+         * The current selected color value, in standard 6-digit hexadecimal
+         * RGB notation. When the user selects a different color using this
+         * directive, this value will updated accordingly.
+         *
+         * @type String
+         */
+        model: '=',
+
+        /**
+         * An optional array of colors to include within the color picker as a
+         * convenient selection of pre-defined colors. The colors within the
+         * array must be in standard 6-digit hexadecimal RGB notation.
+         *
+         * @type String[]
+         */
+        palette: '='
+
+    };
+
+    config.controller = ['$scope', '$element', '$injector',
+        function guacInputColorController($scope, $element, $injector) {
+
+        // Required services
+        var colorPickerService = $injector.get('colorPickerService');
+
+        /**
+         * @borrows colorPickerService.isAvailable()
+         */
+        $scope.isColorPickerAvailable = colorPickerService.isAvailable;
+
+        /**
+         * Returns whether the color currently selected is "dark" in the sense
+         * that the color white will have higher contrast against it than the
+         * color black.
+         *
+         * @returns {Boolean}
+         *     true if the currently selected color is relatively dark (white
+         *     text would provide better contrast than black), false otherwise.
+         */
+        $scope.isDark = function isDark() {
+
+            // Assume not dark if color is invalid or undefined
+            var rgb = $scope.model && /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec($scope.model);
+            if (!rgb)
+                return false;
+
+            // Parse color component values as hexadecimal
+            var red = parseInt(rgb[1], 16);
+            var green = parseInt(rgb[2], 16);
+            var blue = parseInt(rgb[3], 16);
+
+            // Convert RGB to luminance in HSL space (as defined by the
+            // relative luminance formula given by the W3C for accessibility)
+            var luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
+
+            // Consider the background to be dark if white text over that
+            // background would provide better contrast than black
+            return luminance <= 153; // 153 is the component value 0.6 converted from 0-1 to the 0-255 range
+
+        };
+
+        /**
+         * Prompts the user to choose a color by displaying a color selection
+         * dialog. If the user chooses a color, this directive's model is
+         * automatically updated. If the user cancels the dialog, the model is
+         * left untouched.
+         */
+        $scope.selectColor = function selectColor() {
+            colorPickerService.selectColor($element[0], $scope.model, $scope.palette)
+            .then(function colorSelected(color) {
+                $scope.model = color;
+            }, angular.noop);
+        };
+
+    }];
+
+    return config;
+
+}]);
diff --git a/guacamole/src/main/webapp/app/form/services/colorPickerService.js b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
new file mode 100644
index 0000000..cb9e63f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ */
+
+/**
+ * A service for prompting the user to choose a color using the "Pickr" color
+ * picker. As the Pickr color picker might not be available if the JavaScript
+ * features it requires are not supported by the browser (Internet Explorer),
+ * the isAvailable() function should be used to test for usability.
+ */
+angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() {
+
+    /**
+     * A singleton instance of the "Pickr" color picker, shared by all users of
+     * this service. Pickr does not initialize synchronously, nor is it
+     * supported by all browsers. If Pickr is not yet initialized, or is
+     * unsupported, this will be null.
+     *
+     * @type {Pickr}
+     */
+    var pickr = null;
+
+    /**
+     * Whether Pickr has completed initialization.
+     *
+     * @type {Boolean}
+     */
+    var pickrInitComplete = false;
+
+    /**
+     * The HTML element to provide to Pickr as the root element.
+     *
+     * @type {HTMLDivElement}
+     */
+    var pickerContainer = document.createElement('div');
+    pickerContainer.className = 'shared-color-picker';
+
+    /**
+     * An instance of Deferred which represents an active request for the
+     * user to choose a color. The promise associated with the Deferred will
+     * be resolved with the chosen color once a color is chosen, and rejected
+     * if the request is cancelled or Pickr is not available. If no request is
+     * active, this will be null.
+     *
+     * @type {Deferred}
+     */
+    var activeRequest = null;
+
+    /**
+     * Resolves the current active request with the given color value. If no
+     * color value is provided, the active request is rejected. If no request
+     * is active, this function has no effect.
+     *
+     * @param {String} [color]
+     *     The color value to resolve the active request with.
+     */
+    var completeActiveRequest = function completeActiveRequest(color) {
+        if (activeRequest) {
+
+            // Hide color picker, if shown
+            pickr.hide();
+
+            // Resolve/reject active request depending on value provided
+            if (color)
+                activeRequest.resolve(color);
+            else
+                activeRequest.reject();
+
+            // No active request
+            activeRequest = null;
+
+        }
+    };
+
+    try {
+        pickr = Pickr.create({
+
+            // Bind color picker to the container element
+            el : pickerContainer,
+
+            // Wrap color picker dialog in Guacamole-specific class for
+            // sake of additional styling
+            appClass : 'guac-input-color-picker',
+
+            'default' : '#000000',
+
+            // Display color details as hex
+            defaultRepresentation : 'HEX',
+
+            // Use "monolith" theme, as a nice balance between "nano" (does
+            // not work in Internet Explorer) and "classic" (too big)
+            theme : 'monolith',
+
+            // Leverage the container element as the button which shows the
+            // picker, relying on our own styling for that button
+            useAsButton  : true,
+            appendToBody : true,
+
+            // Do not include opacity controls
+            lockOpacity : true,
+
+            // Include a selection of palette entries for convenience and
+            // reference
+            swatches : [],
+
+            components: {
+
+                // Include hue and color preview controls
+                preview : true,
+                hue     : true,
+
+                // Display only a text color input field and the save and
+                // cancel buttons (no clear button)
+                interaction: {
+                    input  : true,
+                    save   : true,
+                    cancel : true
+                }
+
+            }
+
+        });
+
+        // Hide color picker after user clicks "cancel"
+        pickr.on('cancel', function colorChangeCanceled() {
+            completeActiveRequest();
+        });
+
+        // Keep model in sync with changes to the color picker
+        pickr.on('save', function colorChanged(color) {
+            completeActiveRequest(color.toHEXA().toString());
+            activeRequest = null;
+        });
+
+        // Keep color picker in sync with changes to the model
+        pickr.on('init', function pickrReady() {
+            pickrInitComplete = true;
+        });
+    }
+    catch (e) {
+        // If the "Pickr" color picker cannot be loaded (Internet Explorer),
+        // the available flag will remain set to false
+    }
+
+    // Factory method required by provider
+    this.$get = ['$injector', function colorPickerServiceFactory($injector) {
+
+        // Required services
+        var $q         = $injector.get('$q');
+        var $translate = $injector.get('$translate');
+
+        var service = {};
+
+        /**
+         * Promise which is resolved when Pickr initialization has completed
+         * and rejected if Pickr cannot be used.
+         *
+         * @type {Promise}
+         */
+        var pickrPromise = (function getPickr() {
+
+            var deferred = $q.defer();
+
+            // Resolve promise when Pickr has completed initialization
+            if (pickrInitComplete)
+                deferred.resolve();
+            else if (pickr)
+                pickr.on('init', deferred.resolve);
+
+            // Reject promise if Pickr cannot be used at all
+            else
+                deferred.reject();
+
+            return deferred.promise;
+
+        })();
+
+        /**
+         * Returns whether the underlying color picker (Pickr) can be used by
+         * calling selectColor(). If the browser cannot support the color
+         * picker, false is returned.
+         *
+         * @returns {Boolean}
+         *     true if the underlying color picker can be used by calling
+         *     selectColor(), false otherwise.
+         */
+        service.isAvailable = function isAvailable() {
+            return !!pickr;
+        };
+
+        /**
+         * Prompts the user to choose a color, returning the color chosen via a
+         * Promise.
+         *
+         * @param {Element} element
+         *     The element that the user interacted with to indicate their
+         *     desire to choose a color.
+         *
+         * @param {String} current
+         *     The color that should be selected by default, in standard
+         *     6-digit hexadecimal RGB format, including "#" prefix.
+         *
+         * @param {String[]} [palette]
+         *     An array of color choices which should be exposed to the user
+         *     within the color chooser for convenience. Each color must be in
+         *     standard 6-digit hexadecimal RGB format, including "#" prefix.
+         *
+         * @returns {Promise.<String>}
+         *     A Promise which is resolved with the color chosen by the user,
+         *     in standard 6-digit hexadecimal RGB format with "#" prefix, and
+         *     rejected if the selection operation was cancelled or the color
+         *     picker cannot be used.
+         */
+        service.selectColor = function selectColor(element, current, palette) {
+
+            // Show picker once the relevant translation strings have been
+            // retrieved and Pickr is ready for use
+            return $q.all({
+                'saveString'   : $translate('APP.ACTION_SAVE'),
+                'cancelString' : $translate('APP.ACTION_CANCEL'),
+                'pickr'        : pickrPromise
+            }).then(function dependenciesReady(deps) {
+
+                // Cancel any active request
+                completeActiveRequest();
+
+                // Reset state of color picker to provided parameters
+                pickr.setColor(current);
+                element.appendChild(pickerContainer);
+
+                // Assign translated strings to button text
+                var pickrRoot = pickr.getRoot();
+                pickrRoot.interaction.save.value = deps.saveString;
+                pickrRoot.interaction.cancel.value = deps.cancelString;
+
+                // Replace all color swatches with the palette of colors given
+                while (pickr.removeSwatch(0)) {}
+                angular.forEach(palette, pickr.addSwatch.bind(pickr));
+
+                // Show color picker and wait for user to complete selection
+                activeRequest = $q.defer();
+                pickr.show();
+                return activeRequest.promise;
+
+            });
+
+        };
+
+        return service;
+
+    }];
+
+});
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/form/services/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js
index 7e9c1cb..1df49dc 100644
--- a/guacamole/src/main/webapp/app/form/services/formService.js
+++ b/guacamole/src/main/webapp/app/form/services/formService.js
@@ -179,6 +179,19 @@
             module      : 'form',
             controller  : 'timeFieldController',
             templateUrl : 'app/form/templates/timeField.html'
+        },
+
+        /**
+         * Field type which allows selection of color schemes accepted by the
+         * Guacamole server terminal emulator and protocols which leverage it.
+         *
+         * @see {@link Field.Type.TERMINAL_COLOR_SCHEME}
+         * @type FieldType
+         */
+        'TERMINAL_COLOR_SCHEME' : {
+            module      : 'form',
+            controller  : 'terminalColorSchemeFieldController',
+            templateUrl : 'app/form/templates/terminalColorSchemeField.html'
         }
 
     };
@@ -207,11 +220,46 @@
         var $q               = $injector.get('$q');
         var $templateRequest = $injector.get('$templateRequest');
 
+        /**
+         * Map of module name to the injector instance created for that module.
+         *
+         * @type {Object.<String, injector>}
+         */
+        var injectors = {};
+
         var service = {};
 
         service.fieldTypes = provider.fieldTypes;
 
         /**
+         * Given the name of a module, returns an injector instance which
+         * injects dependencies within that module. A new injector may be
+         * created and initialized if no such injector has yet been requested.
+         * If the injector available to formService already includes the
+         * requested module, that injector will simply be returned.
+         *
+         * @param {String} module
+         *     The name of the module to produce an injector for.
+         *
+         * @returns {injector}
+         *     An injector instance which injects dependencies for the given
+         *     module.
+         */
+        var getInjector = function getInjector(module) {
+
+            // Use the formService's injector if possible
+            if ($injector.modules[module])
+                return $injector;
+
+            // If the formService's injector does not include the requested
+            // module, create the necessary injector, reusing that injector for
+            // future calls
+            injectors[module] = injectors[module] || angular.injector(['ng', module]);
+            return injectors[module];
+
+        };
+
+        /**
          * Compiles and links the field associated with the given name to the given
          * scope, producing a distinct and independent DOM Element which functions
          * as an instance of that field. The scope object provided must include at
@@ -221,6 +269,11 @@
          *     A String which defines the unique namespace associated the
          *     translation strings used by the form using a field of this type.
          *
+         * fieldId:
+         *     A String value which is reasonably likely to be unique and may
+         *     be used to associate the main element of the field with its
+         *     label.
+         *
          * field:
          *     The Field object that is being rendered, representing a field of
          *     this type.
@@ -286,7 +339,7 @@
 
                 // Populate scope using defined controller
                 if (fieldType.module && fieldType.controller) {
-                    var $controller = angular.injector(['ng', fieldType.module]).get('$controller');
+                    var $controller = getInjector(fieldType.module).get('$controller');
                     $controller(fieldType.controller, {
                         '$scope'   : scope,
                         '$element' : angular.element(fieldContainer.childNodes)
diff --git a/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css b/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css
new file mode 100644
index 0000000..01eac1a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+.terminal-color-scheme-field {
+    max-width: 320px;
+}
+
+.terminal-color-scheme-field select {
+    width: 100%;
+}
+
+.terminal-color-scheme-field .custom-color-scheme {
+    background: #EEE;
+    padding: 0.5em;
+    border: 1px solid silver;
+    border-spacing: 0;
+    margin-top: -2px;
+    width: 100%;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-section  {
+    display: -ms-flexbox;
+    display: -moz-box;
+    display: -webkit-box;
+    display: -webkit-flex;
+    display: flex;
+}
+
+.terminal-color-scheme-field .guac-input-color {
+
+    display: block;
+    margin: 2px;
+    width: 1.5em;
+    height: 1.5em;
+    min-width: 1.25em;
+    border-radius: 0.15em;
+    line-height: 1.5em;
+    text-align: center;
+    font-size: 0.75em;
+    cursor: pointer;
+    color: black;
+
+    -ms-flex: 1;
+    -moz-box-flex: 1;
+    -webkit-box-flex: 1;
+    -webkit-flex: 1;
+    flex: 1;
+
+}
+
+.terminal-color-scheme-field .guac-input-color.read-only {
+    cursor: not-allowed;
+}
+
+.terminal-color-scheme-field .guac-input-color.dark {
+    color: white;
+}
+
+.terminal-color-scheme-field .palette .guac-input-color {
+    font-weight: bold;
+}
+
+/* Hide palette numbers unless color scheme details are visible */
+.terminal-color-scheme-field.custom-color-scheme-details-hidden .custom-color-scheme .palette .guac-input-color {
+    color: transparent;
+}
+
+/*
+ * Custom color scheme details header
+ */
+
+.terminal-color-scheme-field .custom-color-scheme-details-header {
+    font-size: 0.8em;
+    margin: 0.5em 0;
+    padding: 0;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-details-header::before {
+    content: 'â–¸ ';
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details-header::before {
+    content: 'â–¾ ';
+}
+
+/*
+ * Details show/hide link
+ */
+
+/* Render show/hide as a link */
+.terminal-color-scheme-field .custom-color-scheme-hide-details,
+.terminal-color-scheme-field .custom-color-scheme-show-details {
+    color: blue;
+    text-decoration: underline;
+    cursor: pointer;
+    margin: 0 0.25em;
+    font-weight: normal;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-hide-details {
+    display: none;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-hide-details {
+    display: inline;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-show-details {
+    display: none;
+}
+
+/*
+ * Color scheme details
+ */
+
+.terminal-color-scheme-field .custom-color-scheme-details {
+    display: none;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details {
+    display: block;
+    width: 100%;
+    margin: 0.5em 0;
+}
+
+/*
+ * Color picker
+ */
+
+/* Increase width of color picker to allow two even rows of eight color
+ * swatches */
+.guac-input-color-picker[data-theme="monolith"] {
+    width: 16.25em;
+}
+
+/* Remove Guacamole-specific styles inherited from the generic button rules */
+.guac-input-color-picker[data-theme="monolith"] button {
+    min-width: 0;
+    padding: 0;
+    margin: 0;
+    box-shadow: none;
+}
diff --git a/guacamole/src/main/webapp/app/form/templates/checkboxField.html b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
index 252441b..e906f7d 100644
--- a/guacamole/src/main/webapp/app/form/templates/checkboxField.html
+++ b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
@@ -1 +1,7 @@
-<input type="checkbox" ng-disabled="disabled" ng-model="typedValue" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+<input type="checkbox"
+       ng-attr-id="{{ fieldId }}"
+       ng-disabled="disabled"
+       ng-model="typedValue"
+       guac-focus="focused"
+       autocorrect="off"
+       autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/dateField.html b/guacamole/src/main/webapp/app/form/templates/dateField.html
index 6fd38da..7673e36 100644
--- a/guacamole/src/main/webapp/app/form/templates/dateField.html
+++ b/guacamole/src/main/webapp/app/form/templates/dateField.html
@@ -1,6 +1,7 @@
 <div class="date-field">
     <input type="date"
            ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="typedValue"
            ng-model-options="modelOptions"
            guac-lenient-date
diff --git a/guacamole/src/main/webapp/app/form/templates/emailField.html b/guacamole/src/main/webapp/app/form/templates/emailField.html
index 3eb31e7..cbfbb90 100644
--- a/guacamole/src/main/webapp/app/form/templates/emailField.html
+++ b/guacamole/src/main/webapp/app/form/templates/emailField.html
@@ -1,10 +1,11 @@
 <div class="email-field">
     <input type="email"
            ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="model"
            ng-hide="readOnly"
            guac-focus="focused"
            autocorrect="off"
            autocapitalize="off"/>
     <a href="mailto:{{model}}" ng-show="readOnly">{{model}}</a>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/formField.html b/guacamole/src/main/webapp/app/form/templates/formField.html
index 45cf6b9..3e45d4c 100644
--- a/guacamole/src/main/webapp/app/form/templates/formField.html
+++ b/guacamole/src/main/webapp/app/form/templates/formField.html
@@ -1,9 +1,11 @@
-<label class="labeled-field" ng-class="{empty: !model}" ng-show="isFieldVisible()">
+<div class="labeled-field" ng-class="{empty: !model}" ng-show="isFieldVisible()">
 
     <!-- Field header -->
-    <span class="field-header">{{getFieldHeader() | translate}}</span>
+    <div class="field-header">
+        <label ng-attr-for="{{ fieldId }}">{{getFieldHeader() | translate}}</label>
+    </div>
 
     <!-- Field content -->
     <div class="form-field"></div>
 
-</label>
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
new file mode 100644
index 0000000..eae1f66
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
@@ -0,0 +1,11 @@
+<div class="guac-input-color"
+     ng-class="{
+         'dark' : isDark(),
+         'read-only' : !isColorPickerAvailable()
+     }"
+     ng-click="selectColor()"
+     ng-style="{
+        'background-color' : model
+     }">
+    <ng-transclude></ng-transclude>
+</div>
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/form/templates/languageField.html b/guacamole/src/main/webapp/app/form/templates/languageField.html
index 5af4e75..2a22ff2 100644
--- a/guacamole/src/main/webapp/app/form/templates/languageField.html
+++ b/guacamole/src/main/webapp/app/form/templates/languageField.html
@@ -1 +1,4 @@
-<select guac-focus="focused" ng-model="model" ng-options="language.key as language.value for language in languages | toArray | orderBy: key"></select>
\ No newline at end of file
+<select guac-focus="focused"
+        ng-attr-id="{{ fieldId }}"
+        ng-model="model"
+        ng-options="language.key as language.value for language in languages | toArray | orderBy: key"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/numberField.html b/guacamole/src/main/webapp/app/form/templates/numberField.html
index 9e4c28b..c86fb8e 100644
--- a/guacamole/src/main/webapp/app/form/templates/numberField.html
+++ b/guacamole/src/main/webapp/app/form/templates/numberField.html
@@ -1 +1,7 @@
-<input type="number" ng-disabled="disabled" ng-model="typedValue" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+<input type="number"
+       ng-disabled="disabled"
+       ng-attr-id="{{ fieldId }}"
+       ng-model="typedValue"
+       guac-focus="focused"
+       autocorrect="off"
+       autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/passwordField.html b/guacamole/src/main/webapp/app/form/templates/passwordField.html
index 68d36f7..35eba9e 100644
--- a/guacamole/src/main/webapp/app/form/templates/passwordField.html
+++ b/guacamole/src/main/webapp/app/form/templates/passwordField.html
@@ -1,4 +1,11 @@
 <div class="password-field">
-    <input type="{{passwordInputType}}" ng-disabled="disabled" ng-model="model" ng-trim="false" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+    <input type="{{passwordInputType}}"
+           ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
+           ng-model="model"
+           ng-trim="false"
+           guac-focus="focused"
+           autocorrect="off"
+           autocapitalize="off"/>
     <div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/selectField.html b/guacamole/src/main/webapp/app/form/templates/selectField.html
index f173d54..2c672aa 100644
--- a/guacamole/src/main/webapp/app/form/templates/selectField.html
+++ b/guacamole/src/main/webapp/app/form/templates/selectField.html
@@ -1,2 +1,5 @@
-<select ng-model="model" ng-disabled="disabled" guac-focus="focused"
+<select ng-attr-id="{{ fieldId }}"
+        ng-disabled="disabled"
+        guac-focus="focused"
+        ng-model="model"
         ng-options="option as getFieldOption(option) | translate for option in field.options | orderBy: value"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
new file mode 100644
index 0000000..a8425e4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
@@ -0,0 +1,63 @@
+<div class="terminal-color-scheme-field" ng-class="{
+        'custom-color-scheme-details-visible' : detailsShown,
+        'custom-color-scheme-details-hidden' : !detailsShown
+    }">
+
+    <!-- Pre-defined color scheme options -->
+    <select ng-attr-id="{{ fieldId }}" ng-model="selectedColorScheme">
+        <option ng-repeat="option in field.options | orderBy: value"
+                ng-value="option">{{ getFieldOption(option) | translate }}</option>
+        <option value="custom">{{ 'COLOR_SCHEME.FIELD_OPTION_CUSTOM' | translate }}</option>
+    </select>
+
+    <!-- Custom color scheme -->
+    <div class="custom-color-scheme" ng-show="isCustom()">
+
+        <!-- Default foreground color -->
+        <div class="custom-color-scheme-section default-color foreground">
+            <guac-input-color model="customColorScheme.foreground"
+                              palette="defaultPalette">
+                {{ 'COLOR_SCHEME.FIELD_HEADER_FOREGROUND' | translate }}
+            </guac-input-color>
+        </div>
+
+        <!-- Default background color -->
+        <div class="custom-color-scheme-section default-color background">
+            <guac-input-color model="customColorScheme.background"
+                              palette="defaultPalette">
+                {{ 'COLOR_SCHEME.FIELD_HEADER_BACKGROUND' | translate }}
+            </guac-input-color>
+        </div>
+
+        <!-- Low intensity portion of 16-color palette -->
+        <div class="custom-color-scheme-section palette low-intensity">
+            <guac-input-color ng-repeat="index in lowIntensity"
+                              model="customColorScheme.colors[index]"
+                              palette="defaultPalette">
+                {{ index }}
+            </guac-input-color>
+        </div>
+
+        <!-- High intensity portion of 16-color palette -->
+        <div class="custom-color-scheme-section palette high-intensity">
+            <guac-input-color ng-repeat="index in highIntensity"
+                              model="customColorScheme.colors[index]"
+                              palette="defaultPalette">
+                {{ index }}
+            </guac-input-color>
+        </div>
+
+    </div>
+
+    <!-- Show/hide details -->
+    <h4 class="custom-color-scheme-details-header" ng-show="isCustom()">
+        {{'COLOR_SCHEME.SECTION_HEADER_DETAILS' | translate}}
+        <a class="custom-color-scheme-show-details" ng-click="showDetails()">{{'COLOR_SCHEME.ACTION_SHOW_DETAILS' | translate}}</a>
+        <a class="custom-color-scheme-hide-details" ng-click="hideDetails()">{{'COLOR_SCHEME.ACTION_HIDE_DETAILS' | translate}}</a>
+    </h4>
+
+    <!-- Custom color scheme details (internal representation -->
+    <textarea class="custom-color-scheme-details" spellcheck="false"
+              ng-model="model" ng-show="isCustom()"></textarea>
+
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/textAreaField.html b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
index 6614bfb..9761af7 100644
--- a/guacamole/src/main/webapp/app/form/templates/textAreaField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
@@ -1 +1,6 @@
-<textarea ng-model="model" guac-focus="focused" autocorrect="off" autocapitalize="off" ng-disabled="disabled"></textarea>
+<textarea ng-attr-id="{{ fieldId }}"
+          ng-model="model"
+          ng-disabled="disabled"
+          guac-focus="focused"
+          autocorrect="off"
+          autocapitalize="off"></textarea>
diff --git a/guacamole/src/main/webapp/app/form/templates/textField.html b/guacamole/src/main/webapp/app/form/templates/textField.html
index 6abd4f6..3aea2bc 100644
--- a/guacamole/src/main/webapp/app/form/templates/textField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textField.html
@@ -1,8 +1,14 @@
 <div class="text-field">
-    <input type="text" ng-model="model" autocorrect="off" autocapitalize="off" guac-focus="focused"
-           ng-disabled="disabled" ng-attr-list="{{ dataListId }}"/>
-    <datalist ng-if="dataListId" id="{{ dataListId }}">
+    <input type="text"
+           ng-attr-id="{{ fieldId }}"
+           ng-attr-list="{{ dataListId }}"
+           ng-model="model"
+           ng-disabled="disabled"
+           guac-focus="focused"
+           autocorrect="off"
+           autocapitalize="off"/>
+    <datalist ng-if="dataListId" ng-attr-id="{{ dataListId }}">
         <option ng-repeat="option in field.options | orderBy: option"
                 value="{{ option }}">{{ getFieldOption(option) | translate }}</option>
     </datalist>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/timeField.html b/guacamole/src/main/webapp/app/form/templates/timeField.html
index 6f7201f..2a88230 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeField.html
@@ -1,6 +1,7 @@
 <div class="time-field">
     <input type="time"
            ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="typedValue"
            ng-model-options="modelOptions"
            guac-focus="focused"
diff --git a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
index c318a17..ecab57d 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
@@ -4,6 +4,7 @@
     <select class="time-zone-region"
             ng-disabled="disabled"
             guac-focus="focused"
+            ng-attr-id="{{ fieldId }}"
             ng-model="region"
             ng-options="name for name in regions | orderBy: name"></select>
 
diff --git a/guacamole/src/main/webapp/app/form/types/ColorScheme.js b/guacamole/src/main/webapp/app/form/types/ColorScheme.js
new file mode 100644
index 0000000..f51a667
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/types/ColorScheme.js
@@ -0,0 +1,949 @@
+/*
+ * 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.
+ */
+
+/**
+ * Service which defines the ColorScheme class.
+ */
+angular.module('form').factory('ColorScheme', [function defineColorScheme() {
+ 
+    /**
+     * Intermediate representation of a custom color scheme which can be
+     * converted to the color scheme format used by Guacamole's terminal
+     * emulator. All colors must be represented in the six-digit hexadecimal
+     * RGB notation used by HTML ("#000000" for black, etc.).
+     * 
+     * @constructor
+     * @param {ColorScheme|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     ColorScheme.
+     */
+    var ColorScheme = function ColorScheme(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The terminal background color. This will be the default foreground
+         * color of the Guacamole terminal emulator ("#000000") by default.
+         *
+         * @type {String}
+         */
+        this.background = template.background || '#000000';
+
+        /**
+         * The terminal foreground color. This will be the default foreground
+         * color of the Guacamole terminal emulator ("#999999") by default.
+         *
+         * @type {String}
+         */
+        this.foreground = template.foreground || '#999999';
+
+        /**
+         * The terminal color palette. Default values are provided for the
+         * normal 16 terminal colors using the default values of the Guacamole
+         * terminal emulator, however the terminal emulator and this
+         * representation support up to 256 colors.
+         *
+         * @type {String[]}
+         */
+        this.colors = template.colors || [
+
+            // Normal colors
+            '#000000', // Black
+            '#993E3E', // Red
+            '#3E993E', // Green
+            '#99993E', // Brown
+            '#3E3E99', // Blue
+            '#993E99', // Magenta
+            '#3E9999', // Cyan
+            '#999999', // White
+
+            // Intense colors
+            '#3E3E3E', // Black
+            '#FF6767', // Red
+            '#67FF67', // Green
+            '#FFFF67', // Brown
+            '#6767FF', // Blue
+            '#FF67FF', // Magenta
+            '#67FFFF', // Cyan
+            '#FFFFFF'  // White
+
+        ];
+
+        /**
+         * The string which was parsed to produce this ColorScheme instance, if
+         * ColorScheme.fromString() was used to produce this ColorScheme.
+         *
+         * @private
+         * @type {String}
+         */
+        this._originalString = template._originalString;
+
+    };
+
+    /**
+     * Given a color string in the standard 6-digit hexadecimal RGB format,
+     * returns a X11 color spec which represents the same color.
+     *
+     * @param {String} color
+     *     The hexadecimal color string to convert.
+     *
+     * @returns {String}
+     *     The X11 color spec representing the same color as the given
+     *     hexadecimal string, or null if the given string is not a valid
+     *     6-digit hexadecimal RGB color.
+     */
+    var fromHexColor = function fromHexColor(color) {
+
+        var groups = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(color);
+        if (!groups)
+            return null;
+
+        return 'rgb:' + groups[1] + '/' + groups[2] + '/' + groups[3];
+
+    };
+
+    /**
+     * Parses the same subset of the X11 color spec supported by the Guacamole
+     * terminal emulator (the "rgb:*" format), returning the equivalent 6-digit
+     * hexadecimal color string supported by the ColorScheme representation.
+     * The X11 color spec defined by Xlib's XParseColor(). The human-readable
+     * color names supported by the Guacamole terminal emulator (the same color
+     * names as supported by xterm) may also be used.
+     *
+     * @param {String} color
+     *     The X11 color spec to parse, or the name of a known named color.
+     *
+     * @returns {String}
+     *     The 6-digit hexadecimal color string which represents the same color
+     *     as the given X11 color spec/name, or null if the given spec/name is
+     *     invalid.
+     */
+    var toHexColor = function toHexColor(color) {
+
+        /**
+         * Shifts or truncates the given hexadecimal string such that it
+         * contains exactly two hexadecimal digits, as required by any
+         * individual color component of the 6-digit hexadecimal RGB format.
+         *
+         * @param {String} component
+         *     The hexadecimal string to shift or truncate to two digits.
+         *
+         * @returns {String}
+         *     A new 2-digit hexadecimal string containing the same digits as
+         *     the provided string, shifted or truncated as necessary to fit
+         *     within the 2-digit length limit.
+         */
+        var toHexComponent = function toHexComponent(component) {
+            return (component + '0').substring(0, 2).toUpperCase();
+        };
+
+        // Attempt to parse any non-RGB color as a named color
+        var groups = /^rgb:([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})$/.exec(color);
+        if (!groups)
+            return ColorScheme.NAMED_COLORS[color.toLowerCase()] || null;
+
+        // Convert to standard 6-digit hexadecimal RGB format
+        return '#' + toHexComponent(groups[1]) + toHexComponent(groups[2]) + toHexComponent(groups[3]);
+
+    };
+
+    /**
+     * Converts the given string representation of a color scheme which is
+     * supported by the Guacamole terminal emulator to a corresponding,
+     * intermediate ColorScheme object.
+     *
+     * @param {String} str
+     *     An arbitrary color scheme, in the string format supported by the
+     *     Guacamole terminal emulator.
+     *
+     * @returns {ColorScheme}
+     *     A new ColorScheme instance which represents the same color scheme as
+     *     the given string.
+     */
+    ColorScheme.fromString = function fromString(str) {
+
+        var scheme = new ColorScheme({ _originalString : str });
+
+        // For each semicolon-separated statement in the provided color scheme
+        var statements = str.split(/;/);
+        for (var i = 0; i < statements.length; i++) {
+
+            // Skip any statements which cannot be parsed
+            var statement = statements[i];
+            var groups = /^\s*(background|foreground|color([0-9]+))\s*:\s*(\S*)\s*$/.exec(statement);
+            if (!groups)
+                continue;
+
+            // If the statement is valid and contains a valid color, map that
+            // color to the appropriate property of the ColorScheme object
+            var color = toHexColor(groups[3]);
+            if (color) {
+                if (groups[1] === 'background')
+                    scheme.background = color;
+                else if (groups[1] === 'foreground')
+                    scheme.foreground = color;
+                else
+                    scheme.colors[parseInt(groups[2])] = color;
+            }
+
+        }
+
+        return scheme;
+
+    };
+
+    /**
+     * Returns whether the two given color schemes define the exact same
+     * colors.
+     *
+     * @param {ColorScheme} a
+     *     The first ColorScheme to compare.
+     *
+     * @param {ColorScheme} b
+     *     The second ColorScheme to compare.
+     *
+     * @returns {Boolean}
+     *     true if both color schemes contain the same colors, false otherwise.
+     */
+    ColorScheme.equals = function equals(a, b) {
+        return a.foreground === b.foreground
+            && a.background === b.background
+            && _.isEqual(a.colors, b.colors);
+    };
+
+    /**
+     * Converts the given ColorScheme to a string representation which is
+     * supported by the Guacamole terminal emulator.
+     *
+     * @param {ColorScheme} scheme
+     *     The ColorScheme to convert to a string.
+     *
+     * @returns {String}
+     *     The given color scheme, converted to the string format supported by
+     *     the Guacamole terminal emulator.
+     */
+    ColorScheme.toString = function toString(scheme) {
+
+        // Use originally-provided string if it equates to the exact same color scheme
+        if (!_.isUndefined(scheme._originalString) && ColorScheme.equals(scheme, ColorScheme.fromString(scheme._originalString)))
+            return scheme._originalString;
+
+        // Add background and foreground
+        var str = 'background: ' + fromHexColor(scheme.background) + ';\n'
+                + 'foreground: ' + fromHexColor(scheme.foreground) + ';';
+
+        // Add color definitions for each palette entry
+        for (var index in scheme.colors)
+            str += '\ncolor' + index + ': ' + fromHexColor(scheme.colors[index]) + ';';
+
+        return str;
+
+    };
+
+    /**
+     * The set of all named colors supported by the Guacamole terminal
+     * emulator and their corresponding 6-digit hexadecimal RGB
+     * representations. This set should contain all colors supported by xterm.
+     *
+     * @constant
+     * @type {Object.<String, String>}
+     */
+    ColorScheme.NAMED_COLORS = {
+        'aliceblue'             : '#F0F8FF',
+        'antiquewhite'          : '#FAEBD7',
+        'antiquewhite1'         : '#FFEFDB',
+        'antiquewhite2'         : '#EEDFCC',
+        'antiquewhite3'         : '#CDC0B0',
+        'antiquewhite4'         : '#8B8378',
+        'aqua'                  : '#00FFFF',
+        'aquamarine'            : '#7FFFD4',
+        'aquamarine1'           : '#7FFFD4',
+        'aquamarine2'           : '#76EEC6',
+        'aquamarine3'           : '#66CDAA',
+        'aquamarine4'           : '#458B74',
+        'azure'                 : '#F0FFFF',
+        'azure1'                : '#F0FFFF',
+        'azure2'                : '#E0EEEE',
+        'azure3'                : '#C1CDCD',
+        'azure4'                : '#838B8B',
+        'beige'                 : '#F5F5DC',
+        'bisque'                : '#FFE4C4',
+        'bisque1'               : '#FFE4C4',
+        'bisque2'               : '#EED5B7',
+        'bisque3'               : '#CDB79E',
+        'bisque4'               : '#8B7D6B',
+        'black'                 : '#000000',
+        'blanchedalmond'        : '#FFEBCD',
+        'blue'                  : '#0000FF',
+        'blue1'                 : '#0000FF',
+        'blue2'                 : '#0000EE',
+        'blue3'                 : '#0000CD',
+        'blue4'                 : '#00008B',
+        'blueviolet'            : '#8A2BE2',
+        'brown'                 : '#A52A2A',
+        'brown1'                : '#FF4040',
+        'brown2'                : '#EE3B3B',
+        'brown3'                : '#CD3333',
+        'brown4'                : '#8B2323',
+        'burlywood'             : '#DEB887',
+        'burlywood1'            : '#FFD39B',
+        'burlywood2'            : '#EEC591',
+        'burlywood3'            : '#CDAA7D',
+        'burlywood4'            : '#8B7355',
+        'cadetblue'             : '#5F9EA0',
+        'cadetblue1'            : '#98F5FF',
+        'cadetblue2'            : '#8EE5EE',
+        'cadetblue3'            : '#7AC5CD',
+        'cadetblue4'            : '#53868B',
+        'chartreuse'            : '#7FFF00',
+        'chartreuse1'           : '#7FFF00',
+        'chartreuse2'           : '#76EE00',
+        'chartreuse3'           : '#66CD00',
+        'chartreuse4'           : '#458B00',
+        'chocolate'             : '#D2691E',
+        'chocolate1'            : '#FF7F24',
+        'chocolate2'            : '#EE7621',
+        'chocolate3'            : '#CD661D',
+        'chocolate4'            : '#8B4513',
+        'coral'                 : '#FF7F50',
+        'coral1'                : '#FF7256',
+        'coral2'                : '#EE6A50',
+        'coral3'                : '#CD5B45',
+        'coral4'                : '#8B3E2F',
+        'cornflowerblue'        : '#6495ED',
+        'cornsilk'              : '#FFF8DC',
+        'cornsilk1'             : '#FFF8DC',
+        'cornsilk2'             : '#EEE8CD',
+        'cornsilk3'             : '#CDC8B1',
+        'cornsilk4'             : '#8B8878',
+        'crimson'               : '#DC143C',
+        'cyan'                  : '#00FFFF',
+        'cyan1'                 : '#00FFFF',
+        'cyan2'                 : '#00EEEE',
+        'cyan3'                 : '#00CDCD',
+        'cyan4'                 : '#008B8B',
+        'darkblue'              : '#00008B',
+        'darkcyan'              : '#008B8B',
+        'darkgoldenrod'         : '#B8860B',
+        'darkgoldenrod1'        : '#FFB90F',
+        'darkgoldenrod2'        : '#EEAD0E',
+        'darkgoldenrod3'        : '#CD950C',
+        'darkgoldenrod4'        : '#8B6508',
+        'darkgray'              : '#A9A9A9',
+        'darkgreen'             : '#006400',
+        'darkgrey'              : '#A9A9A9',
+        'darkkhaki'             : '#BDB76B',
+        'darkmagenta'           : '#8B008B',
+        'darkolivegreen'        : '#556B2F',
+        'darkolivegreen1'       : '#CAFF70',
+        'darkolivegreen2'       : '#BCEE68',
+        'darkolivegreen3'       : '#A2CD5A',
+        'darkolivegreen4'       : '#6E8B3D',
+        'darkorange'            : '#FF8C00',
+        'darkorange1'           : '#FF7F00',
+        'darkorange2'           : '#EE7600',
+        'darkorange3'           : '#CD6600',
+        'darkorange4'           : '#8B4500',
+        'darkorchid'            : '#9932CC',
+        'darkorchid1'           : '#BF3EFF',
+        'darkorchid2'           : '#B23AEE',
+        'darkorchid3'           : '#9A32CD',
+        'darkorchid4'           : '#68228B',
+        'darkred'               : '#8B0000',
+        'darksalmon'            : '#E9967A',
+        'darkseagreen'          : '#8FBC8F',
+        'darkseagreen1'         : '#C1FFC1',
+        'darkseagreen2'         : '#B4EEB4',
+        'darkseagreen3'         : '#9BCD9B',
+        'darkseagreen4'         : '#698B69',
+        'darkslateblue'         : '#483D8B',
+        'darkslategray'         : '#2F4F4F',
+        'darkslategray1'        : '#97FFFF',
+        'darkslategray2'        : '#8DEEEE',
+        'darkslategray3'        : '#79CDCD',
+        'darkslategray4'        : '#528B8B',
+        'darkslategrey'         : '#2F4F4F',
+        'darkturquoise'         : '#00CED1',
+        'darkviolet'            : '#9400D3',
+        'deeppink'              : '#FF1493',
+        'deeppink1'             : '#FF1493',
+        'deeppink2'             : '#EE1289',
+        'deeppink3'             : '#CD1076',
+        'deeppink4'             : '#8B0A50',
+        'deepskyblue'           : '#00BFFF',
+        'deepskyblue1'          : '#00BFFF',
+        'deepskyblue2'          : '#00B2EE',
+        'deepskyblue3'          : '#009ACD',
+        'deepskyblue4'          : '#00688B',
+        'dimgray'               : '#696969',
+        'dimgrey'               : '#696969',
+        'dodgerblue'            : '#1E90FF',
+        'dodgerblue1'           : '#1E90FF',
+        'dodgerblue2'           : '#1C86EE',
+        'dodgerblue3'           : '#1874CD',
+        'dodgerblue4'           : '#104E8B',
+        'firebrick'             : '#B22222',
+        'firebrick1'            : '#FF3030',
+        'firebrick2'            : '#EE2C2C',
+        'firebrick3'            : '#CD2626',
+        'firebrick4'            : '#8B1A1A',
+        'floralwhite'           : '#FFFAF0',
+        'forestgreen'           : '#228B22',
+        'fuchsia'               : '#FF00FF',
+        'gainsboro'             : '#DCDCDC',
+        'ghostwhite'            : '#F8F8FF',
+        'gold'                  : '#FFD700',
+        'gold1'                 : '#FFD700',
+        'gold2'                 : '#EEC900',
+        'gold3'                 : '#CDAD00',
+        'gold4'                 : '#8B7500',
+        'goldenrod'             : '#DAA520',
+        'goldenrod1'            : '#FFC125',
+        'goldenrod2'            : '#EEB422',
+        'goldenrod3'            : '#CD9B1D',
+        'goldenrod4'            : '#8B6914',
+        'gray'                  : '#BEBEBE',
+        'gray0'                 : '#000000',
+        'gray1'                 : '#030303',
+        'gray10'                : '#1A1A1A',
+        'gray100'               : '#FFFFFF',
+        'gray11'                : '#1C1C1C',
+        'gray12'                : '#1F1F1F',
+        'gray13'                : '#212121',
+        'gray14'                : '#242424',
+        'gray15'                : '#262626',
+        'gray16'                : '#292929',
+        'gray17'                : '#2B2B2B',
+        'gray18'                : '#2E2E2E',
+        'gray19'                : '#303030',
+        'gray2'                 : '#050505',
+        'gray20'                : '#333333',
+        'gray21'                : '#363636',
+        'gray22'                : '#383838',
+        'gray23'                : '#3B3B3B',
+        'gray24'                : '#3D3D3D',
+        'gray25'                : '#404040',
+        'gray26'                : '#424242',
+        'gray27'                : '#454545',
+        'gray28'                : '#474747',
+        'gray29'                : '#4A4A4A',
+        'gray3'                 : '#080808',
+        'gray30'                : '#4D4D4D',
+        'gray31'                : '#4F4F4F',
+        'gray32'                : '#525252',
+        'gray33'                : '#545454',
+        'gray34'                : '#575757',
+        'gray35'                : '#595959',
+        'gray36'                : '#5C5C5C',
+        'gray37'                : '#5E5E5E',
+        'gray38'                : '#616161',
+        'gray39'                : '#636363',
+        'gray4'                 : '#0A0A0A',
+        'gray40'                : '#666666',
+        'gray41'                : '#696969',
+        'gray42'                : '#6B6B6B',
+        'gray43'                : '#6E6E6E',
+        'gray44'                : '#707070',
+        'gray45'                : '#737373',
+        'gray46'                : '#757575',
+        'gray47'                : '#787878',
+        'gray48'                : '#7A7A7A',
+        'gray49'                : '#7D7D7D',
+        'gray5'                 : '#0D0D0D',
+        'gray50'                : '#7F7F7F',
+        'gray51'                : '#828282',
+        'gray52'                : '#858585',
+        'gray53'                : '#878787',
+        'gray54'                : '#8A8A8A',
+        'gray55'                : '#8C8C8C',
+        'gray56'                : '#8F8F8F',
+        'gray57'                : '#919191',
+        'gray58'                : '#949494',
+        'gray59'                : '#969696',
+        'gray6'                 : '#0F0F0F',
+        'gray60'                : '#999999',
+        'gray61'                : '#9C9C9C',
+        'gray62'                : '#9E9E9E',
+        'gray63'                : '#A1A1A1',
+        'gray64'                : '#A3A3A3',
+        'gray65'                : '#A6A6A6',
+        'gray66'                : '#A8A8A8',
+        'gray67'                : '#ABABAB',
+        'gray68'                : '#ADADAD',
+        'gray69'                : '#B0B0B0',
+        'gray7'                 : '#121212',
+        'gray70'                : '#B3B3B3',
+        'gray71'                : '#B5B5B5',
+        'gray72'                : '#B8B8B8',
+        'gray73'                : '#BABABA',
+        'gray74'                : '#BDBDBD',
+        'gray75'                : '#BFBFBF',
+        'gray76'                : '#C2C2C2',
+        'gray77'                : '#C4C4C4',
+        'gray78'                : '#C7C7C7',
+        'gray79'                : '#C9C9C9',
+        'gray8'                 : '#141414',
+        'gray80'                : '#CCCCCC',
+        'gray81'                : '#CFCFCF',
+        'gray82'                : '#D1D1D1',
+        'gray83'                : '#D4D4D4',
+        'gray84'                : '#D6D6D6',
+        'gray85'                : '#D9D9D9',
+        'gray86'                : '#DBDBDB',
+        'gray87'                : '#DEDEDE',
+        'gray88'                : '#E0E0E0',
+        'gray89'                : '#E3E3E3',
+        'gray9'                 : '#171717',
+        'gray90'                : '#E5E5E5',
+        'gray91'                : '#E8E8E8',
+        'gray92'                : '#EBEBEB',
+        'gray93'                : '#EDEDED',
+        'gray94'                : '#F0F0F0',
+        'gray95'                : '#F2F2F2',
+        'gray96'                : '#F5F5F5',
+        'gray97'                : '#F7F7F7',
+        'gray98'                : '#FAFAFA',
+        'gray99'                : '#FCFCFC',
+        'green'                 : '#00FF00',
+        'green1'                : '#00FF00',
+        'green2'                : '#00EE00',
+        'green3'                : '#00CD00',
+        'green4'                : '#008B00',
+        'greenyellow'           : '#ADFF2F',
+        'grey'                  : '#BEBEBE',
+        'grey0'                 : '#000000',
+        'grey1'                 : '#030303',
+        'grey10'                : '#1A1A1A',
+        'grey100'               : '#FFFFFF',
+        'grey11'                : '#1C1C1C',
+        'grey12'                : '#1F1F1F',
+        'grey13'                : '#212121',
+        'grey14'                : '#242424',
+        'grey15'                : '#262626',
+        'grey16'                : '#292929',
+        'grey17'                : '#2B2B2B',
+        'grey18'                : '#2E2E2E',
+        'grey19'                : '#303030',
+        'grey2'                 : '#050505',
+        'grey20'                : '#333333',
+        'grey21'                : '#363636',
+        'grey22'                : '#383838',
+        'grey23'                : '#3B3B3B',
+        'grey24'                : '#3D3D3D',
+        'grey25'                : '#404040',
+        'grey26'                : '#424242',
+        'grey27'                : '#454545',
+        'grey28'                : '#474747',
+        'grey29'                : '#4A4A4A',
+        'grey3'                 : '#080808',
+        'grey30'                : '#4D4D4D',
+        'grey31'                : '#4F4F4F',
+        'grey32'                : '#525252',
+        'grey33'                : '#545454',
+        'grey34'                : '#575757',
+        'grey35'                : '#595959',
+        'grey36'                : '#5C5C5C',
+        'grey37'                : '#5E5E5E',
+        'grey38'                : '#616161',
+        'grey39'                : '#636363',
+        'grey4'                 : '#0A0A0A',
+        'grey40'                : '#666666',
+        'grey41'                : '#696969',
+        'grey42'                : '#6B6B6B',
+        'grey43'                : '#6E6E6E',
+        'grey44'                : '#707070',
+        'grey45'                : '#737373',
+        'grey46'                : '#757575',
+        'grey47'                : '#787878',
+        'grey48'                : '#7A7A7A',
+        'grey49'                : '#7D7D7D',
+        'grey5'                 : '#0D0D0D',
+        'grey50'                : '#7F7F7F',
+        'grey51'                : '#828282',
+        'grey52'                : '#858585',
+        'grey53'                : '#878787',
+        'grey54'                : '#8A8A8A',
+        'grey55'                : '#8C8C8C',
+        'grey56'                : '#8F8F8F',
+        'grey57'                : '#919191',
+        'grey58'                : '#949494',
+        'grey59'                : '#969696',
+        'grey6'                 : '#0F0F0F',
+        'grey60'                : '#999999',
+        'grey61'                : '#9C9C9C',
+        'grey62'                : '#9E9E9E',
+        'grey63'                : '#A1A1A1',
+        'grey64'                : '#A3A3A3',
+        'grey65'                : '#A6A6A6',
+        'grey66'                : '#A8A8A8',
+        'grey67'                : '#ABABAB',
+        'grey68'                : '#ADADAD',
+        'grey69'                : '#B0B0B0',
+        'grey7'                 : '#121212',
+        'grey70'                : '#B3B3B3',
+        'grey71'                : '#B5B5B5',
+        'grey72'                : '#B8B8B8',
+        'grey73'                : '#BABABA',
+        'grey74'                : '#BDBDBD',
+        'grey75'                : '#BFBFBF',
+        'grey76'                : '#C2C2C2',
+        'grey77'                : '#C4C4C4',
+        'grey78'                : '#C7C7C7',
+        'grey79'                : '#C9C9C9',
+        'grey8'                 : '#141414',
+        'grey80'                : '#CCCCCC',
+        'grey81'                : '#CFCFCF',
+        'grey82'                : '#D1D1D1',
+        'grey83'                : '#D4D4D4',
+        'grey84'                : '#D6D6D6',
+        'grey85'                : '#D9D9D9',
+        'grey86'                : '#DBDBDB',
+        'grey87'                : '#DEDEDE',
+        'grey88'                : '#E0E0E0',
+        'grey89'                : '#E3E3E3',
+        'grey9'                 : '#171717',
+        'grey90'                : '#E5E5E5',
+        'grey91'                : '#E8E8E8',
+        'grey92'                : '#EBEBEB',
+        'grey93'                : '#EDEDED',
+        'grey94'                : '#F0F0F0',
+        'grey95'                : '#F2F2F2',
+        'grey96'                : '#F5F5F5',
+        'grey97'                : '#F7F7F7',
+        'grey98'                : '#FAFAFA',
+        'grey99'                : '#FCFCFC',
+        'honeydew'              : '#F0FFF0',
+        'honeydew1'             : '#F0FFF0',
+        'honeydew2'             : '#E0EEE0',
+        'honeydew3'             : '#C1CDC1',
+        'honeydew4'             : '#838B83',
+        'hotpink'               : '#FF69B4',
+        'hotpink1'              : '#FF6EB4',
+        'hotpink2'              : '#EE6AA7',
+        'hotpink3'              : '#CD6090',
+        'hotpink4'              : '#8B3A62',
+        'indianred'             : '#CD5C5C',
+        'indianred1'            : '#FF6A6A',
+        'indianred2'            : '#EE6363',
+        'indianred3'            : '#CD5555',
+        'indianred4'            : '#8B3A3A',
+        'indigo'                : '#4B0082',
+        'ivory'                 : '#FFFFF0',
+        'ivory1'                : '#FFFFF0',
+        'ivory2'                : '#EEEEE0',
+        'ivory3'                : '#CDCDC1',
+        'ivory4'                : '#8B8B83',
+        'khaki'                 : '#F0E68C',
+        'khaki1'                : '#FFF68F',
+        'khaki2'                : '#EEE685',
+        'khaki3'                : '#CDC673',
+        'khaki4'                : '#8B864E',
+        'lavender'              : '#E6E6FA',
+        'lavenderblush'         : '#FFF0F5',
+        'lavenderblush1'        : '#FFF0F5',
+        'lavenderblush2'        : '#EEE0E5',
+        'lavenderblush3'        : '#CDC1C5',
+        'lavenderblush4'        : '#8B8386',
+        'lawngreen'             : '#7CFC00',
+        'lemonchiffon'          : '#FFFACD',
+        'lemonchiffon1'         : '#FFFACD',
+        'lemonchiffon2'         : '#EEE9BF',
+        'lemonchiffon3'         : '#CDC9A5',
+        'lemonchiffon4'         : '#8B8970',
+        'lightblue'             : '#ADD8E6',
+        'lightblue1'            : '#BFEFFF',
+        'lightblue2'            : '#B2DFEE',
+        'lightblue3'            : '#9AC0CD',
+        'lightblue4'            : '#68838B',
+        'lightcoral'            : '#F08080',
+        'lightcyan'             : '#E0FFFF',
+        'lightcyan1'            : '#E0FFFF',
+        'lightcyan2'            : '#D1EEEE',
+        'lightcyan3'            : '#B4CDCD',
+        'lightcyan4'            : '#7A8B8B',
+        'lightgoldenrod'        : '#EEDD82',
+        'lightgoldenrod1'       : '#FFEC8B',
+        'lightgoldenrod2'       : '#EEDC82',
+        'lightgoldenrod3'       : '#CDBE70',
+        'lightgoldenrod4'       : '#8B814C',
+        'lightgoldenrodyellow'  : '#FAFAD2',
+        'lightgray'             : '#D3D3D3',
+        'lightgreen'            : '#90EE90',
+        'lightgrey'             : '#D3D3D3',
+        'lightpink'             : '#FFB6C1',
+        'lightpink1'            : '#FFAEB9',
+        'lightpink2'            : '#EEA2AD',
+        'lightpink3'            : '#CD8C95',
+        'lightpink4'            : '#8B5F65',
+        'lightsalmon'           : '#FFA07A',
+        'lightsalmon1'          : '#FFA07A',
+        'lightsalmon2'          : '#EE9572',
+        'lightsalmon3'          : '#CD8162',
+        'lightsalmon4'          : '#8B5742',
+        'lightseagreen'         : '#20B2AA',
+        'lightskyblue'          : '#87CEFA',
+        'lightskyblue1'         : '#B0E2FF',
+        'lightskyblue2'         : '#A4D3EE',
+        'lightskyblue3'         : '#8DB6CD',
+        'lightskyblue4'         : '#607B8B',
+        'lightslateblue'        : '#8470FF',
+        'lightslategray'        : '#778899',
+        'lightslategrey'        : '#778899',
+        'lightsteelblue'        : '#B0C4DE',
+        'lightsteelblue1'       : '#CAE1FF',
+        'lightsteelblue2'       : '#BCD2EE',
+        'lightsteelblue3'       : '#A2B5CD',
+        'lightsteelblue4'       : '#6E7B8B',
+        'lightyellow'           : '#FFFFE0',
+        'lightyellow1'          : '#FFFFE0',
+        'lightyellow2'          : '#EEEED1',
+        'lightyellow3'          : '#CDCDB4',
+        'lightyellow4'          : '#8B8B7A',
+        'lime'                  : '#00FF00',
+        'limegreen'             : '#32CD32',
+        'linen'                 : '#FAF0E6',
+        'magenta'               : '#FF00FF',
+        'magenta1'              : '#FF00FF',
+        'magenta2'              : '#EE00EE',
+        'magenta3'              : '#CD00CD',
+        'magenta4'              : '#8B008B',
+        'maroon'                : '#B03060',
+        'maroon1'               : '#FF34B3',
+        'maroon2'               : '#EE30A7',
+        'maroon3'               : '#CD2990',
+        'maroon4'               : '#8B1C62',
+        'mediumaquamarine'      : '#66CDAA',
+        'mediumblue'            : '#0000CD',
+        'mediumorchid'          : '#BA55D3',
+        'mediumorchid1'         : '#E066FF',
+        'mediumorchid2'         : '#D15FEE',
+        'mediumorchid3'         : '#B452CD',
+        'mediumorchid4'         : '#7A378B',
+        'mediumpurple'          : '#9370DB',
+        'mediumpurple1'         : '#AB82FF',
+        'mediumpurple2'         : '#9F79EE',
+        'mediumpurple3'         : '#8968CD',
+        'mediumpurple4'         : '#5D478B',
+        'mediumseagreen'        : '#3CB371',
+        'mediumslateblue'       : '#7B68EE',
+        'mediumspringgreen'     : '#00FA9A',
+        'mediumturquoise'       : '#48D1CC',
+        'mediumvioletred'       : '#C71585',
+        'midnightblue'          : '#191970',
+        'mintcream'             : '#F5FFFA',
+        'mistyrose'             : '#FFE4E1',
+        'mistyrose1'            : '#FFE4E1',
+        'mistyrose2'            : '#EED5D2',
+        'mistyrose3'            : '#CDB7B5',
+        'mistyrose4'            : '#8B7D7B',
+        'moccasin'              : '#FFE4B5',
+        'navajowhite'           : '#FFDEAD',
+        'navajowhite1'          : '#FFDEAD',
+        'navajowhite2'          : '#EECFA1',
+        'navajowhite3'          : '#CDB38B',
+        'navajowhite4'          : '#8B795E',
+        'navy'                  : '#000080',
+        'navyblue'              : '#000080',
+        'oldlace'               : '#FDF5E6',
+        'olive'                 : '#808000',
+        'olivedrab'             : '#6B8E23',
+        'olivedrab1'            : '#C0FF3E',
+        'olivedrab2'            : '#B3EE3A',
+        'olivedrab3'            : '#9ACD32',
+        'olivedrab4'            : '#698B22',
+        'orange'                : '#FFA500',
+        'orange1'               : '#FFA500',
+        'orange2'               : '#EE9A00',
+        'orange3'               : '#CD8500',
+        'orange4'               : '#8B5A00',
+        'orangered'             : '#FF4500',
+        'orangered1'            : '#FF4500',
+        'orangered2'            : '#EE4000',
+        'orangered3'            : '#CD3700',
+        'orangered4'            : '#8B2500',
+        'orchid'                : '#DA70D6',
+        'orchid1'               : '#FF83FA',
+        'orchid2'               : '#EE7AE9',
+        'orchid3'               : '#CD69C9',
+        'orchid4'               : '#8B4789',
+        'palegoldenrod'         : '#EEE8AA',
+        'palegreen'             : '#98FB98',
+        'palegreen1'            : '#9AFF9A',
+        'palegreen2'            : '#90EE90',
+        'palegreen3'            : '#7CCD7C',
+        'palegreen4'            : '#548B54',
+        'paleturquoise'         : '#AFEEEE',
+        'paleturquoise1'        : '#BBFFFF',
+        'paleturquoise2'        : '#AEEEEE',
+        'paleturquoise3'        : '#96CDCD',
+        'paleturquoise4'        : '#668B8B',
+        'palevioletred'         : '#DB7093',
+        'palevioletred1'        : '#FF82AB',
+        'palevioletred2'        : '#EE799F',
+        'palevioletred3'        : '#CD6889',
+        'palevioletred4'        : '#8B475D',
+        'papayawhip'            : '#FFEFD5',
+        'peachpuff'             : '#FFDAB9',
+        'peachpuff1'            : '#FFDAB9',
+        'peachpuff2'            : '#EECBAD',
+        'peachpuff3'            : '#CDAF95',
+        'peachpuff4'            : '#8B7765',
+        'peru'                  : '#CD853F',
+        'pink'                  : '#FFC0CB',
+        'pink1'                 : '#FFB5C5',
+        'pink2'                 : '#EEA9B8',
+        'pink3'                 : '#CD919E',
+        'pink4'                 : '#8B636C',
+        'plum'                  : '#DDA0DD',
+        'plum1'                 : '#FFBBFF',
+        'plum2'                 : '#EEAEEE',
+        'plum3'                 : '#CD96CD',
+        'plum4'                 : '#8B668B',
+        'powderblue'            : '#B0E0E6',
+        'purple'                : '#A020F0',
+        'purple1'               : '#9B30FF',
+        'purple2'               : '#912CEE',
+        'purple3'               : '#7D26CD',
+        'purple4'               : '#551A8B',
+        'rebeccapurple'         : '#663399',
+        'red'                   : '#FF0000',
+        'red1'                  : '#FF0000',
+        'red2'                  : '#EE0000',
+        'red3'                  : '#CD0000',
+        'red4'                  : '#8B0000',
+        'rosybrown'             : '#BC8F8F',
+        'rosybrown1'            : '#FFC1C1',
+        'rosybrown2'            : '#EEB4B4',
+        'rosybrown3'            : '#CD9B9B',
+        'rosybrown4'            : '#8B6969',
+        'royalblue'             : '#4169E1',
+        'royalblue1'            : '#4876FF',
+        'royalblue2'            : '#436EEE',
+        'royalblue3'            : '#3A5FCD',
+        'royalblue4'            : '#27408B',
+        'saddlebrown'           : '#8B4513',
+        'salmon'                : '#FA8072',
+        'salmon1'               : '#FF8C69',
+        'salmon2'               : '#EE8262',
+        'salmon3'               : '#CD7054',
+        'salmon4'               : '#8B4C39',
+        'sandybrown'            : '#F4A460',
+        'seagreen'              : '#2E8B57',
+        'seagreen1'             : '#54FF9F',
+        'seagreen2'             : '#4EEE94',
+        'seagreen3'             : '#43CD80',
+        'seagreen4'             : '#2E8B57',
+        'seashell'              : '#FFF5EE',
+        'seashell1'             : '#FFF5EE',
+        'seashell2'             : '#EEE5DE',
+        'seashell3'             : '#CDC5BF',
+        'seashell4'             : '#8B8682',
+        'sienna'                : '#A0522D',
+        'sienna1'               : '#FF8247',
+        'sienna2'               : '#EE7942',
+        'sienna3'               : '#CD6839',
+        'sienna4'               : '#8B4726',
+        'silver'                : '#C0C0C0',
+        'skyblue'               : '#87CEEB',
+        'skyblue1'              : '#87CEFF',
+        'skyblue2'              : '#7EC0EE',
+        'skyblue3'              : '#6CA6CD',
+        'skyblue4'              : '#4A708B',
+        'slateblue'             : '#6A5ACD',
+        'slateblue1'            : '#836FFF',
+        'slateblue2'            : '#7A67EE',
+        'slateblue3'            : '#6959CD',
+        'slateblue4'            : '#473C8B',
+        'slategray'             : '#708090',
+        'slategray1'            : '#C6E2FF',
+        'slategray2'            : '#B9D3EE',
+        'slategray3'            : '#9FB6CD',
+        'slategray4'            : '#6C7B8B',
+        'slategrey'             : '#708090',
+        'snow'                  : '#FFFAFA',
+        'snow1'                 : '#FFFAFA',
+        'snow2'                 : '#EEE9E9',
+        'snow3'                 : '#CDC9C9',
+        'snow4'                 : '#8B8989',
+        'springgreen'           : '#00FF7F',
+        'springgreen1'          : '#00FF7F',
+        'springgreen2'          : '#00EE76',
+        'springgreen3'          : '#00CD66',
+        'springgreen4'          : '#008B45',
+        'steelblue'             : '#4682B4',
+        'steelblue1'            : '#63B8FF',
+        'steelblue2'            : '#5CACEE',
+        'steelblue3'            : '#4F94CD',
+        'steelblue4'            : '#36648B',
+        'tan'                   : '#D2B48C',
+        'tan1'                  : '#FFA54F',
+        'tan2'                  : '#EE9A49',
+        'tan3'                  : '#CD853F',
+        'tan4'                  : '#8B5A2B',
+        'teal'                  : '#008080',
+        'thistle'               : '#D8BFD8',
+        'thistle1'              : '#FFE1FF',
+        'thistle2'              : '#EED2EE',
+        'thistle3'              : '#CDB5CD',
+        'thistle4'              : '#8B7B8B',
+        'tomato'                : '#FF6347',
+        'tomato1'               : '#FF6347',
+        'tomato2'               : '#EE5C42',
+        'tomato3'               : '#CD4F39',
+        'tomato4'               : '#8B3626',
+        'turquoise'             : '#40E0D0',
+        'turquoise1'            : '#00F5FF',
+        'turquoise2'            : '#00E5EE',
+        'turquoise3'            : '#00C5CD',
+        'turquoise4'            : '#00868B',
+        'violet'                : '#EE82EE',
+        'violetred'             : '#D02090',
+        'violetred1'            : '#FF3E96',
+        'violetred2'            : '#EE3A8C',
+        'violetred3'            : '#CD3278',
+        'violetred4'            : '#8B2252',
+        'webgray'               : '#808080',
+        'webgreen'              : '#008000',
+        'webgrey'               : '#808080',
+        'webmaroon'             : '#800000',
+        'webpurple'             : '#800080',
+        'wheat'                 : '#F5DEB3',
+        'wheat1'                : '#FFE7BA',
+        'wheat2'                : '#EED8AE',
+        'wheat3'                : '#CDBA96',
+        'wheat4'                : '#8B7E66',
+        'white'                 : '#FFFFFF',
+        'whitesmoke'            : '#F5F5F5',
+        'x11gray'               : '#BEBEBE',
+        'x11green'              : '#00FF00',
+        'x11grey'               : '#BEBEBE',
+        'x11maroon'             : '#B03060',
+        'x11purple'             : '#A020F0',
+        'yellow'                : '#FFFF00',
+        'yellow1'               : '#FFFF00',
+        'yellow2'               : '#EEEE00',
+        'yellow3'               : '#CDCD00',
+        'yellow4'               : '#8B8B00',
+        'yellowgreen'           : '#9ACD32'
+    };
+
+    return ColorScheme;
+
+}]);
diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css
index 58406eb..b90e271 100644
--- a/guacamole/src/main/webapp/app/index/styles/ui.css
+++ b/guacamole/src/main/webapp/app/index/styles/ui.css
@@ -198,6 +198,7 @@
     background-image: url('images/protocol-icons/guac-plug.png');
 }
 
+.connection .icon.kubernetes,
 .connection .icon.ssh,
 .connection .icon.telnet {
     background-image: url('images/protocol-icons/guac-text.png');
diff --git a/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
index 8fe19d6..c5645fb 100644
--- a/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
+++ b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
@@ -29,6 +29,7 @@
     display: table;
     padding-left: .5em;
     border-left: 3px solid rgba(0,0,0,0.125);
+    width: 100%;
 }
 
 .connection-parameters .form .fields .labeled-field {
@@ -40,8 +41,11 @@
     display: table-cell;
     padding: 0.125em;
     vertical-align: top;
+    width: 100%;
 }
 
 .connection-parameters .form .fields .field-header {
     padding-right: 1em;
+    width: 0;
+    white-space: nowrap;
 }
diff --git a/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
index 23daef2..3f68924 100644
--- a/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
+++ b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
@@ -89,7 +89,14 @@
          *
          * @type String
          */
-        CONNECTION_GROUP : 'g'
+        CONNECTION_GROUP : 'g',
+
+        /**
+         * The type string for an active Guacamole connection.
+         *
+         * @type String
+         */
+        ACTIVE_CONNECTION : 'a'
 
     };
 
diff --git a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
index 5354bc1..3960b39 100644
--- a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
+++ b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
@@ -30,6 +30,38 @@
     var service = {};
 
     /**
+     * Makes a request to the REST API to get a single active connection,
+     * returning a promise that provides the corresponding
+     * @link{ActiveConnection} if successful.
+     *
+     * @param {String} dataSource
+     *     The identifier of the data source to retrieve the active connection
+     *     from.
+     *
+     * @param {String} id
+     *     The identifier of the active connection.
+     *
+     * @returns {Promise.<ActiveConnection>}
+     *     A promise which will resolve with a @link{ActiveConnection} upon
+     *     success.
+     */
+    service.getActiveConnection = function getActiveConnection(dataSource, id) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve active connection
+        return requestService({
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections/' + encodeURIComponent(id),
+            params  : httpParameters
+        });
+
+    };
+
+    /**
      * Makes a request to the REST API to get the list of active tunnels,
      * returning a promise that provides a map of @link{ActiveConnection}
      * objects if successful.
diff --git a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
index 6c5aac2..dc0b140 100644
--- a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
+++ b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
@@ -76,6 +76,14 @@
          */
         this.username = template.username;
 
+        /**
+         * Whether this active connection may be connected to, just as a
+         * normal connection.
+         *
+         * @type Boolean
+         */
+        this.connectable = template.connectable;
+
     };
 
     return ActiveConnection;
diff --git a/guacamole/src/main/webapp/app/rest/types/Field.js b/guacamole/src/main/webapp/app/rest/types/Field.js
index 84dfe13..195db82 100644
--- a/guacamole/src/main/webapp/app/rest/types/Field.js
+++ b/guacamole/src/main/webapp/app/rest/types/Field.js
@@ -168,7 +168,16 @@
          *
          * @type String
          */
-        QUERY_PARAMETER : 'QUERY_PARAMETER'
+        QUERY_PARAMETER : 'QUERY_PARAMETER',
+
+        /**
+         * The type string associated with parameters that may contain color
+         * schemes accepted by the Guacamole server terminal emulator and
+         * protocols which leverage it.
+         *
+         * @type String
+         */
+        TERMINAL_COLOR_SCHEME : 'TERMINAL_COLOR_SCHEME'
 
     };
 
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
index 5e1774d..30b8bde 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
@@ -35,6 +35,7 @@
 
             // Required types
             var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper');
+            var ClientIdentifier        = $injector.get('ClientIdentifier');
             var ConnectionGroup         = $injector.get('ConnectionGroup');
             var SortOrder               = $injector.get('SortOrder');
 
@@ -336,7 +337,37 @@
                     'actions'    : [ DELETE_ACTION, CANCEL_ACTION]
                 });
             };
-            
+
+            /**
+             * Returns the relative URL of the client page which accesses the
+             * given active connection. If the active connection is not
+             * connectable, null is returned.
+             *
+             * @param {String} dataSource
+             *     The unique identifier of the data source containing the
+             *     active connection.
+             *
+             * @param {String} activeConnection
+             *     The active connection to determine the relative URL of.
+             *
+             * @returns {String}
+             *     The relative URL of the client page which accesses the given
+             *     active connection, or null if the active connection is not
+             *     connectable.
+             */
+            $scope.getClientURL = function getClientURL(dataSource, activeConnection) {
+
+                if (!activeConnection.connectable)
+                    return null;
+
+                return '#/client/' + encodeURIComponent(ClientIdentifier.toString({
+                    dataSource : dataSource,
+                    type       : ClientIdentifier.Types.ACTIVE_CONNECTION,
+                    id         : activeConnection.identifier
+                }));
+
+            };
+
             /**
              * Returns whether the selected sessions can be deleted.
              * 
diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html b/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
index 184ff0b..698582c 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
@@ -40,7 +40,9 @@
                 <td><guac-user-item username="wrapper.activeConnection.username"></guac-user-item></td>
                 <td>{{wrapper.startDate}}</td>
                 <td>{{wrapper.activeConnection.remoteHost}}</td>
-                <td>{{wrapper.name}}</td>
+                <td><a ng-href="{{
+                    getClientURL(wrapper.dataSource, wrapper.activeConnection)
+                }}">{{wrapper.name}}</a></td>
             </tr>
         </tbody>
     </table>
diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html
index e675546..309f114 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -27,6 +27,7 @@
         <link rel="icon" type="image/png" href="images/logo-64.png"/>
         <link rel="icon" type="image/png" sizes="144x144" href="images/logo-144.png"/>
         <link rel="apple-touch-icon" type="image/png" href="images/logo-144.png"/>
+        <link rel="stylesheet" type="text/css" href="webjars/simonwep__pickr/1.2.6/dist/themes/monolith.min.css"/>
         <link rel="stylesheet" type="text/css" href="app.css?v=${project.version}">
         <title ng-bind="page.title | translate"></title>
     </head>
@@ -87,7 +88,10 @@
 
         <!-- JSTZ -->
         <script type="text/javascript" src="webjars/jstz/1.0.10/dist/jstz.min.js"></script>
-        
+
+        <!-- Pickr (color picker) -->
+        <script type="text/javascript" src="webjars/simonwep__pickr/1.2.6/dist/pickr.es5.min.js"></script>
+
         <!-- Polyfills for the "datalist" element, Blob and the FileSaver API -->
         <script type="text/javascript" src="webjars/blob-polyfill/1.0.20150320/Blob.js"></script>
         <script type="text/javascript" src="webjars/datalist-polyfill/1.14.0/datalist-polyfill.min.js"></script>
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index 598fd9e..63deae5 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -150,6 +150,22 @@
 
     },
 
+    "COLOR_SCHEME" : {
+
+        "ACTION_CANCEL"       : "@:APP.ACTION_CANCEL",
+        "ACTION_HIDE_DETAILS" : "Hide",
+        "ACTION_SAVE"         : "@:APP.ACTION_SAVE",
+        "ACTION_SHOW_DETAILS" : "Show",
+
+        "FIELD_HEADER_BACKGROUND" : "Background",
+        "FIELD_HEADER_FOREGROUND" : "Foreground",
+
+        "FIELD_OPTION_CUSTOM" : "Custom...",
+
+        "SECTION_HEADER_DETAILS" : "Details:"
+
+    },
+
     "DATA_SOURCE_DEFAULT" : {
         "NAME" : "Default (XML)"
     },
@@ -364,7 +380,73 @@
         "TEXT_CONFIRM_DELETE" : "Groups cannot be restored after they have been deleted. Are you sure you want to delete this group?"
 
     },
-    
+
+    "PROTOCOL_KUBERNETES" : {
+
+        "FIELD_HEADER_BACKSPACE"       : "Backspace key sends:",
+        "FIELD_HEADER_CA_CERT"         : "Certificate authority certificate:",
+        "FIELD_HEADER_CLIENT_CERT"     : "Client certificate:",
+        "FIELD_HEADER_CLIENT_KEY"      : "Client key:",
+        "FIELD_HEADER_COLOR_SCHEME"    : "Color scheme:",
+        "FIELD_HEADER_CONTAINER"       : "Container name:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"  : "Automatically create recording path:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatically create typescript path:",
+        "FIELD_HEADER_FONT_NAME"       : "Font name:",
+        "FIELD_HEADER_FONT_SIZE"       : "Font size:",
+        "FIELD_HEADER_HOSTNAME"        : "Hostname:",
+        "FIELD_HEADER_IGNORE_CERT"     : "Ignore server certificate:",
+        "FIELD_HEADER_NAMESPACE"       : "Namespace:",
+        "FIELD_HEADER_POD"             : "Pod name:",
+        "FIELD_HEADER_PORT"            : "Port:",
+        "FIELD_HEADER_READ_ONLY"       : "Read-only:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclude mouse:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Include key events:",
+        "FIELD_HEADER_RECORDING_NAME"  : "Recording name:",
+        "FIELD_HEADER_RECORDING_PATH"  : "Recording path:",
+        "FIELD_HEADER_SCROLLBACK"      : "Maximum scrollback size:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:",
+        "FIELD_HEADER_USE_SSL"         : "Use SSL/TLS",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Delete (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Black on white",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Gray on black",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Green on black",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "White on black",
+
+        "FIELD_OPTION_FONT_SIZE_8"     : "8",
+        "FIELD_OPTION_FONT_SIZE_9"     : "9",
+        "FIELD_OPTION_FONT_SIZE_10"    : "10",
+        "FIELD_OPTION_FONT_SIZE_11"    : "11",
+        "FIELD_OPTION_FONT_SIZE_12"    : "12",
+        "FIELD_OPTION_FONT_SIZE_14"    : "14",
+        "FIELD_OPTION_FONT_SIZE_18"    : "18",
+        "FIELD_OPTION_FONT_SIZE_24"    : "24",
+        "FIELD_OPTION_FONT_SIZE_30"    : "30",
+        "FIELD_OPTION_FONT_SIZE_36"    : "36",
+        "FIELD_OPTION_FONT_SIZE_48"    : "48",
+        "FIELD_OPTION_FONT_SIZE_60"    : "60",
+        "FIELD_OPTION_FONT_SIZE_72"    : "72",
+        "FIELD_OPTION_FONT_SIZE_96"    : "96",
+        "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
+
+        "NAME" : "Kubernetes",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Authentication",
+        "SECTION_HEADER_BEHAVIOR"       : "Terminal behavior",
+        "SECTION_HEADER_CONTAINER"      : "Container",
+        "SECTION_HEADER_DISPLAY"        : "Display",
+        "SECTION_HEADER_RECORDING"      : "Screen Recording",
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Text Session Recording)",
+        "SECTION_HEADER_NETWORK"        : "Network"
+
+    },
+
     "PROTOCOL_RDP" : {
 
         "FIELD_HEADER_CLIENT_NAME"     : "Client name:",