blob: 50156e1395859e742a0ff4b30d0dc09ca8345531 [file] [log] [blame]
<?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>&lt;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">
&lt;modelVersion>4.0.0&lt;/modelVersion>
&lt;groupId>org.glyptodon.guacamole&lt;/groupId>
&lt;artifactId>guacamole-auth-tutorial&lt;/artifactId>
&lt;packaging>jar&lt;/packaging>
&lt;version>0.8.0&lt;/version>
&lt;name>guacamole-auth-tutorial&lt;/name>
&lt;url>http://guac-dev.org/&lt;/url>
&lt;properties>
&lt;project.build.sourceEncoding>UTF-8&lt;/project.build.sourceEncoding>
&lt;/properties>
&lt;build>
&lt;plugins>
&lt;!-- Written for 1.6 -->
&lt;plugin>
&lt;groupId>org.apache.maven.plugins&lt;/groupId>
&lt;artifactId>maven-compiler-plugin&lt;/artifactId>
&lt;configuration>
&lt;source>1.6&lt;/source>
&lt;target>1.6&lt;/target>
&lt;/configuration>
&lt;/plugin>
&lt;/plugins>
&lt;/build>
&lt;dependencies>
&lt;!-- Guacamole Java API -->
&lt;dependency>
&lt;groupId>org.glyptodon.guacamole&lt;/groupId>
&lt;artifactId>guacamole-common&lt;/artifactId>
&lt;version>0.8.0&lt;/version>
&lt;/dependency>
&lt;!-- Guacamole Extension API -->
&lt;dependency>
&lt;groupId>org.glyptodon.guacamole&lt;/groupId>
&lt;artifactId>guacamole-ext&lt;/artifactId>
&lt;version>0.8.0&lt;/version>
&lt;/dependency>
&lt;/dependencies>
&lt;repositories>
&lt;!-- Central Guacamole repository -->
&lt;repository>
&lt;id>guac-dev&lt;/id>
&lt;url>http://guac-dev.org/repo&lt;/url>
&lt;/repository>
&lt;/repositories>
&lt;/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&lt;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&lt;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&lt;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&lt;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&lt;String, GuacamoleConfiguration> configs =
new HashMap&lt;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>