| <?xml version="1.0" encoding="UTF-8"?> |
| |
| <chapter xml:id="custom-authentication" xmlns="http://docbook.org/ns/docbook" version="5.0" |
| xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude"> |
| <title>Custom authentication</title> |
| <indexterm xmlns:xl="http://www.w3.org/1999/xlink"> |
| <primary>authentication</primary> |
| <secondary>custom</secondary> |
| </indexterm> |
| <para>Guacamole's authentication layer is designed to be extendable such that users can |
| integrate Guacamole into existing authentication systems without having to resort to writing |
| their own web application around the Guacamole API.</para> |
| <para>The web application comes with a default authentication mechanism which uses an XML file |
| to associate users with connections. Plugins for Guacamole that provide LDAP-based |
| authentication or database-based authentication have also been developed.</para> |
| <para>To demonstrate the principles involved, we will implement a very simple authentication |
| plugin which associates a single user/password pair with a single connection, with all this |
| information saved in properties inside the <filename>guacamole.properties</filename> |
| file.</para> |
| <para>In general, all other authentication plugins for Guacamole will use the principles |
| demonstrated here. However, as of Guacamole 0.8.0, the authentication model has been |
| significantly enhanced, and supports more than simply translating a username/password pair |
| into a set of authorized configurations. This tutorial demonstrates the simplest way to |
| create an authentication plugin for Guacamole - an authentication plugin that does not |
| support management of users and connections via the web interface.</para> |
| <section xml:id="auth-model"> |
| <title>Guacamole's authentication model</title> |
| <para>When you view any page in Guacamole, whether that be the login screen or the client |
| interface, the page makes an authentication attempt with the web application, sending |
| all available credentials. After entering your username and password, the exact same |
| process occurs, except the web application receives the username and password as |
| well.</para> |
| <para>The web application handles this authentication attempt by collecting all credentials |
| available and passing them to a designated class called the "authentication provider". |
| This class is designated via a property in the <filename>guacamole.properties</filename> |
| file. Given the set of credentials, the specified authentication provider returns a |
| context object that provides restricted access to other users and connections, if |
| any.</para> |
| </section> |
| <section xml:id="client-plugin-skeleton"> |
| <title>A Guacamole plugin skeleton</title> |
| <para>For simplicity's sake, and because this is how things are done upstream in the |
| Guacamole project, we will use Maven to build our plugin.</para> |
| <para>The bare minimum required for a Guacamole authentication plugin is a |
| <filename>pom.xml</filename> file listing guacamole-ext as a dependency, and a |
| single .java file implementing our stub of an authentication provider.</para> |
| <para>In our stub, we won't actually do any authentication yet; we'll just universally |
| reject all authentication attempts by returning <varname>null</varname> for any |
| credentials given. You can verify that this is what happens by checking the server |
| logs.</para> |
| <example> |
| <title>Barebones <filename>pom.xml</filename> required for a simple authentication |
| plugin.</title> |
| <programlisting><project xmlns="http://maven.apache.org/POM/4.0.0" |
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 |
| http://maven.apache.org/maven-v4_0_0.xsd"> |
| |
| <modelVersion>4.0.0</modelVersion> |
| <groupId>org.glyptodon.guacamole</groupId> |
| <artifactId>guacamole-auth-tutorial</artifactId> |
| <packaging>jar</packaging> |
| <version>0.8.0</version> |
| <name>guacamole-auth-tutorial</name> |
| <url>http://guac-dev.org/</url> |
| |
| <properties> |
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
| </properties> |
| |
| <build> |
| <plugins> |
| |
| <!-- Written for 1.6 --> |
| <plugin> |
| <groupId>org.apache.maven.plugins</groupId> |
| <artifactId>maven-compiler-plugin</artifactId> |
| <configuration> |
| <source>1.6</source> |
| <target>1.6</target> |
| </configuration> |
| </plugin> |
| |
| </plugins> |
| </build> |
| |
| <dependencies> |
| |
| <!-- Guacamole Java API --> |
| <dependency> |
| <groupId>org.glyptodon.guacamole</groupId> |
| <artifactId>guacamole-common</artifactId> |
| <version>0.8.0</version> |
| </dependency> |
| |
| <!-- Guacamole Extension API --> |
| <dependency> |
| <groupId>org.glyptodon.guacamole</groupId> |
| <artifactId>guacamole-ext</artifactId> |
| <version>0.8.0</version> |
| </dependency> |
| |
| </dependencies> |
| |
| <repositories> |
| |
| <!-- Central Guacamole repository --> |
| <repository> |
| <id>guac-dev</id> |
| <url>http://guac-dev.org/repo</url> |
| </repository> |
| |
| </repositories> |
| |
| </project></programlisting> |
| </example> |
| <para>We won't need to update this <filename>pom.xml</filename> throughout the rest of the |
| tutorial. Even after adding new files, Maven will just find them and compile as |
| necessary.</para> |
| <para>Naturally, we need the actual authentication plugin skeleton code. While you can put |
| this in whatever file and package you want, for the sake of this tutorial, we will |
| assume you are using |
| <classname>org.glyptodon.guacamole.auth.TutorialAuthenticationProvider</classname>.</para> |
| <example> |
| <title>A skeleton <classname>TutorialAuthenticationProvider</classname></title> |
| <programlisting>package org.glyptodon.guacamole.auth; |
| |
| import java.util.Map; |
| import org.glyptodon.guacamole.GuacamoleException; |
| import org.glyptodon.guacamole.net.auth.simple.SimpleAuthenticationProvider; |
| import org.glyptodon.guacamole.net.auth.Credentials; |
| import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; |
| |
| public class TutorialAuthenticationProvider extends SimpleAuthenticationProvider { |
| |
| @Override |
| public Map<String, GuacamoleConfiguration> |
| getAuthorizedConfigurations(Credentials credentials) |
| throws GuacamoleException { |
| |
| // Do nothing ... yet |
| return null; |
| |
| } |
| |
| }</programlisting> |
| </example> |
| <para>To conform with Maven, this skeleton file must be placed within |
| <filename>src/main/java/net/sourceforge/guacamole/auth</filename> as |
| <filename>TutorialAuthenticationProvider.java</filename>.</para> |
| <para>Notice how simple the authentication provider is. The |
| <classname>AuthenticationProvider</classname> interface requires nothing more than a |
| single <methodname>getAuthorizedConfigurations()</methodname> implementation, which must |
| return a <classname>Map</classname> of <classname>GuacamoleConfiguration</classname> |
| each associated with some arbitrary unique ID. This unique ID will be presented to the |
| user in the connection list after they log in.</para> |
| <para>For now, we just return <varname>null</varname>, which will cause Guacamole to report |
| an invalid login for every attempt. Note that there is a difference in semantics between |
| returning an empty map and returning <varname>null</varname>, as the former indicates |
| the credentials are authorized but simply have no associated configurations, while the |
| latter indicates the credentials are not authorized at all.</para> |
| </section> |
| <section xml:id="user-auth-example"> |
| <title>Actually authenticating the user</title> |
| <para>Once we receive credentials, we need to validate those credentials against the |
| associated properties in <filename>guacamole.properties</filename> (our source of |
| authentication information for the sake of this tutorial).</para> |
| <para>We will define four properties:<variablelist> |
| <varlistentry> |
| <term><property>tutorial-user</property></term> |
| <listitem> |
| <para>The name of the only user we accept.</para> |
| </listitem> |
| </varlistentry> |
| <varlistentry> |
| <term><property>tutorial-password</property></term> |
| <listitem> |
| <para>The password we require for the user specified to be |
| authenticated.</para> |
| </listitem> |
| </varlistentry> |
| <varlistentry> |
| <term><property>tutorial-protocol</property></term> |
| <listitem> |
| <para>The protocol of the configuration this user is authorized to use, |
| which will be sent to guacd when the user logs in and selects their |
| connection.</para> |
| </listitem> |
| </varlistentry> |
| <varlistentry> |
| <term><property>tutorial-parameters</property></term> |
| <listitem> |
| <para>A comma-delimited list of |
| <code><replaceable>name</replaceable>=<replaceable>value</replaceable></code> |
| pairs. For the sake of simplicity, we'll assume there will never be any |
| commas in the values.</para> |
| </listitem> |
| </varlistentry> |
| </variablelist></para> |
| <para>If the username and password match what is stored in the file, we read the |
| configuration information, store it in a <classname>GuacamoleConfiguration</classname>, |
| and return the configuration within a set, telling Guacamole that this user is |
| authorized but only to access the configurations returned.</para> |
| <para>Upstream, we always place the properties of authentication providers in their own |
| class, and so we will also do that here in this tutorial, as it keeps things |
| organized.</para> |
| <example> |
| <title><filename>TutorialProperties.java</filename>, a class containing property |
| definitions</title> |
| <programlisting>package org.glyptodon.guacamole.auth; |
| |
| import org.glyptodon.guacamole.properties.StringGuacamoleProperty; |
| |
| public class TutorialGuacamoleProperties { |
| |
| /** |
| * This class should not be instantiated. |
| */ |
| private TutorialGuacamoleProperties() {} |
| |
| /** |
| * The only user to allow. |
| */ |
| public static final StringGuacamoleProperty TUTORIAL_USER = |
| new StringGuacamoleProperty() { |
| |
| @Override |
| public String getName() { return "tutorial-user"; } |
| |
| }; |
| |
| /** |
| * The password required for the specified user. |
| */ |
| public static final StringGuacamoleProperty TUTORIAL_PASSWORD = |
| new StringGuacamoleProperty() { |
| |
| @Override |
| public String getName() { return "tutorial-password"; } |
| |
| }; |
| |
| |
| /** |
| * The protocol to use when connecting. |
| */ |
| public static final StringGuacamoleProperty TUTORIAL_PROTOCOL = |
| new StringGuacamoleProperty() { |
| |
| @Override |
| public String getName() { return "tutorial-protocol"; } |
| |
| }; |
| |
| |
| /** |
| * All parameters associated with the connection, as a comma-delimited |
| * list of name="value" |
| */ |
| public static final StringGuacamoleProperty TUTORIAL_PARAMETERS = |
| new StringGuacamoleProperty() { |
| |
| @Override |
| public String getName() { return "tutorial-parameters"; } |
| |
| }; |
| |
| }</programlisting> |
| </example> |
| <para>Normally, we would define a new type of <classname>GuacamoleProperty</classname> to |
| handle the parsing of the parameters required by <varname>TUTORIAL_PARAMETERS</varname>, |
| but for the sake of simplicity, parsing of this parameter will be embedded in the |
| authentication function later.</para> |
| <para>You will need to modify your existing <filename>guacamole.properties</filename> file, |
| adding each of the above properties to describe one of your available |
| connections.</para> |
| <example> |
| <title>Properties describing a user and connection, as required by this tutorial</title> |
| <programlisting># Username and password |
| tutorial-user: <replaceable>tutorial</replaceable> |
| tutorial-password: <replaceable>password</replaceable> |
| |
| # Connection information |
| tutorial-protocol: <replaceable>vnc</replaceable> |
| tutorial-parameters: <replaceable>hostname=localhost, port=5900</replaceable></programlisting> |
| </example> |
| <para>Once these properties and their accessor class are in place, it's simple enough to |
| read the properties within <methodname>getAuthorizedConfigurations()</methodname> and |
| authenticate the user based on their username and password.</para> |
| <example> |
| <title>Checking the credentials against the properties</title> |
| <programlisting>@Override |
| public Map<String, GuacamoleConfiguration> |
| getAuthorizedConfigurations(Credentials credentials) |
| throws GuacamoleException { |
| |
| // Get username |
| String username = GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_USER |
| ); |
| |
| // If wrong username, fail |
| if (!username.equals(credentials.getUsername())) |
| return null; |
| |
| // Get password |
| String password = GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_PASSWORD |
| ); |
| |
| // If wrong password, fail |
| if (!password.equals(credentials.getPassword())) |
| return null; |
| |
| // Successful login. Return configurations (STUB) |
| return new HashMap<String, GuacamoleConfiguration>(); |
| |
| }</programlisting> |
| </example> |
| <para>As is, the authentication provider will work in its current state in that the correct |
| username and password will authenticate the user, while an incorrect username or |
| password will not, but we still aren't returning an actual map of configurations. We |
| need to construct the configuration based on the properties in the |
| <filename>guacamole.properties</filename> file after the user has been |
| authenticated, and return that configuration to the web application.</para> |
| </section> |
| <section xml:id="parse-conf-example"> |
| <title>Parsing the configuration</title> |
| <para>The only remaining task before we have a fully-functioning authentication provider is |
| to parse the configuration from the <filename>guacamole.properties</filename> |
| file.</para> |
| <example> |
| <title>Parsing and returning a <classname>GuacamoleConfiguration</classname></title> |
| <programlisting>@Override |
| public Map<String, GuacamoleConfiguration> |
| getAuthorizedConfigurations(Credentials credentials) |
| throws GuacamoleException { |
| |
| // Get username |
| String username = GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_USER |
| ); |
| |
| // If wrong username, fail |
| if (!username.equals(credentials.getUsername())) |
| return null; |
| |
| // Get password |
| String password = GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_PASSWORD |
| ); |
| |
| // If wrong password, fail |
| if (!password.equals(credentials.getPassword())) |
| return null; |
| |
| // Successful login. Return configurations. |
| Map<String, GuacamoleConfiguration> configs = |
| new HashMap<String, GuacamoleConfiguration>(); |
| |
| // Create new configuration |
| GuacamoleConfiguration config = new GuacamoleConfiguration(); |
| |
| // Set protocol specified in properties |
| config.setProtocol(GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_PROTOCOL |
| )); |
| |
| // Set all parameters, splitting at commas |
| for (String parameterValue : GuacamoleProperties.getRequiredProperty( |
| TutorialProperties.TUTORIAL_PARAMETERS |
| ).split(",\\s*")) { |
| |
| // Find the equals sign |
| int equals = parameterValue.indexOf('='); |
| if (equals == -1) |
| throw new GuacamoleException("Required equals sign missing"); |
| |
| // Get name and value from parameter string |
| String name = parameterValue.substring(0, equals); |
| String value = parameterValue.substring(equals+1); |
| |
| // Set parameter as specified |
| config.setParameter(name, value); |
| |
| } |
| |
| configs.put("DEFAULT", config); |
| return configs; |
| |
| }</programlisting> |
| </example> |
| </section> |
| </chapter> |