GUACAMOLE-1218: Copy guacamole-auth-json source tree from glyptodon/guacamole-auth-json at commit f7b2eaf6a65b7cd25fd73437360e36fe46e0bcb9.

This commit is contained in:
Michael Jumper
2020-11-20 13:40:07 -08:00
parent cbcac3a5d5
commit cec53a24e6
23 changed files with 2919 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
target/
*~

View File

@@ -0,0 +1,19 @@
Copyright (C) 2015 Glyptodon LLC
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.

View File

@@ -0,0 +1,189 @@
guacamole-auth-json
===================
guacamole-auth-json is an authentication extension for [Apache
Guacamole](http://guacamole.apache.org/) which authenticates users using JSON
which has been signed using **HMAC/SHA-256** and encrypted with **128-bit AES
in CBC mode**. This JSON contains all information describing the user being
authenticated, as well as any connections they have access to.
Configuring Guacamole to accept encrypted JSON
----------------------------------------------
To verify and decrypt the received signed and encrypted JSON, a secret key must
be generated which will be shared by both the Guacamole server and systems that
will generate the JSON data. As guacamole-auth-json uses 128-bit AES, this key
must be 128 bits.
An easy way of generating such a key is to echo a passphrase through the
"md5sum" utility. This is the technique OpenSSL itself uses to generate 128-bit
keys from passphrases. For example:
$ echo -n "ThisIsATest" | md5sum
4c0b569e4c96df157eee1b65dd0e4d41 -
The generated key must then be saved within `guacamole.properties` as the full
32-digit hex value using the `json-secret-key` property:
json-secret-key: 4c0b569e4c96df157eee1b65dd0e4d41
JSON format
-----------
The general format of the JSON (prior to being encrypted, signed, and sent to
Guacamole), is as follows:
{
"username" : "arbitraryUsername",
"expires" : TIMESTAMP,
"connections" : {
"Connection Name" : {
"protocol" : "PROTOCOL",
"parameters" : {
"name1" : "value1",
"name2" : "value2",
...
}
},
...
}
}
where `TIMESTAMP` is a standard UNIX epoch timestamp with millisecond
resolution (the number of milliseconds since midnight of January 1, 1970 UTC)
and `PROTOCOL` is the internal name of any of Guacamole's supported protocols,
such as `vnc`, `rdp`, or `ssh`.
The JSON will cease to be accepted as valid after the server time passes the
timestamp. If no timestamp is specified, the data will not expire.
The top-level JSON object which must be submitted to Guacamole has the
following properties:
Property name | Type | Description
--------------|----------|------------
`username` | `string` | The unique username of the user authenticated by the JSON. If the user is anonymous, this should be the empty string (`""`).
`expires` | `number` | The absolute time after which the JSON should no longer be accepted, even if the signature is valid, as a standard UNIX epoch timestamp with millisecond resolution (the number of milliseconds since midnight of January 1, 1970 UTC).
`connections` | `object` | The set of connections which should be exposed to the user by their corresponding, unique names. If no connections will be exposed to the user, this can simply be an empty object (`{}`).
Each normal connection defined within each submitted JSON object has the
following properties:
Property name | Type | Description
--------------|----------|------------
`id` | `string` | An optional opaque value which uniquely identifies this connection across all other connections which may be active at any given time. This property is only required if you wish to allow the connection to be shared or shadowed.
`protocol` | `string` | The internal name of a supported protocol, such as `vnc`, `rdp`, or `ssh`.
`parameters` | `object` | An object representing the connection parameter name/value pairs to apply to the connection, as documented in the [Guacamole manual](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#connection-configuration).
Connections which share or shadow other connections use a `join` property
instead of a `protocol` property, where `join` contains the value of the `id`
property of the connection being joined:
Property name | Type | Description
--------------|----------|------------
`id` | `string` | An optional opaque value which uniquely identifies this connection across all other connections which may be active at any given time. This property is only required if you wish to allow the connection to be shared or shadowed. (Yes, a connection which shadows another connection may itself be shadowed.)
`join` | `string` | The opaque ID given within the `id` property of the connection being joined (shared / shadowed).
`parameters` | `object` | An object representing the connection parameter name/value pairs to apply to the connection, as documented in the [Guacamole manual](https://guacamole.apache.org/doc/gug/configuring-guacamole.html#connection-configuration). Most of the connection configuration is inherited from the connection being joined. In general, the only property relevant to joining connections is `read-only`.
If a connection is configured to join another connection, that connection will
only be usable if the connection being joined is currently active. If two
connections are established having the same `id` value, only the last
connection will be joinable using the given `id`.
Generating encrypted JSON
-------------------------
To authenticate a user with the above JSON format, the JSON must be both signed
and encrypted using the same 128-bit secret key specified with the
`json-secret-key` within `guacamole.properties`:
1. Generate JSON in the format described above
2. Sign the JSON using the secret key (the same 128-bit key stored within
`guacamole.properties` with the `json-secret-key` property) with
**HMAC/SHA-256**. Prepend the binary result of the signing process to the
plaintext JSON that was signed.
3. Encrypt the result of (2) above using **AES in CBC mode**, with the initial
vector (IV) set to all zero bytes.
4. Encode the encrypted result using base64.
5. POST the encrypted result to the `/api/tokens` REST endpoint as the value of
an HTTP parameter named `data` (or include it in the URL of any Guacamole
page as a query parameter named `data`).
For example, if Guacamole is running on localhost at `/guacamole`, and
`BASE64_RESULT` is the result of the above process, the equivalent run of
the "curl" utility would be:
$ curl --data-urlencode "data=BASE64_RESULT" http://localhost:8080/guacamole/api/tokens
**NOTE:** Be sure to URL-encode the base64-encoded result prior to POSTing
it to `/api/tokens` or including it in the URL. Base64 can contain both "+"
and "=" characters, which have special meaning within URLs.
If the data is invalid in any way, if the signature does not match, if
decryption or signature verification fails, or if the submitted data has
expired, the REST service will return an invalid credentials error and fail
without user-visible explanation. Details describing the error that occurred
will be in the Tomcat logs, however.
Reference implementation
------------------------
The source includes a shell script, `doc/encrypt-json.sh`, which uses the
OpenSSL command-line utility to encrypt and sign JSON in the manner that
guacamole-auth-json requires. It is thoroughly commented and should work well
as a reference implementation, for testing, and as a point of comparison for
development. The script is run as:
$ ./encrypt-json.sh HEX_ENCRYPTION_KEY file-to-sign-and-encrypt.json
For example, if you have a file called `auth.json` containing the following:
{
"username" : "test",
"expires" : "1446323765000",
"connections" : {
"My Connection" : {
"protocol" : "rdp",
"parameters" : {
"hostname" : "10.10.209.63",
"port" : "3389"
}
},
"My OTHER Connection" : {
"protocol" : "rdp",
"parameters" : {
"hostname" : "10.10.209.64",
"port" : "3389"
}
}
}
}
and you run:
$ ./encrypt-json.sh 4C0B569E4C96DF157EEE1B65DD0E4D41 auth.json
You will receive the following output:
le2Ug6YIo4perD2GV17QtWvOdfSemVDDtCOdRYJlbdUf3fhN+63LpQa1RDkzU7Zc
DW3+OtyTCBGQ7OLO+HpG6pHNom76BXpmnHSRx1UdQ3WVZelPUXEDzxe74aN6DUP9
G9isXhBMdLUhZwEJf4k4Gpzt9MHAH5PufSKq3DO1UHnrRjdGbKKddug2BcuDrwJM
UJf1tRX9CAEC11/gWEwrHDOhH/abeyeDyElbaEG/oOY8EdoFNYgUsjI2x31OpCuB
sEv7FOFafL05wEoIFv0/pPft0DHk7GuvHBBCqXuK98yMEo3d0zD5D+IsOY8Rmm1+
0CoWkX22mqyRQMFS2fTp/fClCN4QLb0aNn+unweTimd2SXN9cjREmZknXf7Tj8oU
/FNXc37i0HEfG5aVgp5znMCwwRAOFnFhLqG3K2yaTRE+hLNBxltIjLfFmNG5TZZA
gUdKyuegsOd0KS5iHdW6tPI01AwfRO9y2z20t3flsgDp50EGWjT2/TTA5Nkjnnjk
JXNzCOfM7DCI/ioEz6Ga140qXfOX/g8SGiukpwt+j0ANI573TdVt7nsp7MZX2qKg
2GcoNqjBqQxqpqI5ZYz4KVfD4cYu8KDZ9MiFMzbUwwKNSzYxiep1KJwiG0HQThHg
oX2FJYOFCFcinQgGkUOaBJK1K0bo1ouaBSe4iGPjd54=
The resulting base64 data above, if submitted using the `data` parameter to
Guacamole, will authenticate a user and grant them access to the connections
described in the JSON (at least until the expires timestamp is reached, at
which point the JSON will no longer be accepted).

View File

@@ -0,0 +1,119 @@
#!/bin/bash -e
#
# Copyright (C) 2015 Glyptodon LLC
#
# 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.
#
##
## @fn encrypt-json.sh
##
## Encrypts and signs JSON using the provided key, returning base64-encoded
## data ready to be submitted to Guacamole and used by the guacamole-auth-json
## authentication provider. Beware that this base64-encoded must still be
## URL-encoded prior to submission to /api/tokens via POST. Base64 encoding may
## contain + and = characters, which have special meaning in URLs.
##
## To submit the resulting data easily via curl, the following will work:
##
## curl --data-urlencode "data=$(<file_containing_result)" GUAC_URL/api/tokens
##
## @param SECRET_KEY
## The key to encrypt and sign the JSON file with, as a 16-byte (32-digit)
## hexadecimal value. This key must match the key specified within
## guacamole.properties using the "json-secret-key" property.
##
## @param JSON_FILENAME
## The filename of the JSON to encrypt and sign.
##
##
## Encryption/signing key.
##
SECRET_KEY="$1"
##
## The filename of the JSON data being signed and encrypted.
##
JSON_FILENAME="$2"
##
## A null (all zeroes) IV.
##
NULL_IV="00000000000000000000000000000000"
##
## Signs the contents of the given file using the given key. The signature is
## created using HMAC/SHA-256, and is output in binary form to STDOUT, followed
## by the raw contents of the file.
##
## @param KEY
## The key to use to sign the contents of the given file with HMAC/SHA-256.
##
## @param FILENAME
## The filename of the file to sign.
##
sign() {
KEY="$1"
FILENAME="$2"
#
# Write out signature
#
openssl dgst \
-sha256 -mac HMAC -macopt hexkey:"$KEY" \
-binary "$FILENAME"
#
# Write out file contents
#
cat "$FILENAME"
}
##
## Encrypts all data received through STDIN using the provided key. Data is
## encrypted using 128-bit AES in CBC mode (with a null IV). The encrypted
## result is printed to STDOUT encoded with base64.
##
## @param KEY
## The key to encrypt STDIN with, as a 16-byte (32-digit) hexadecimal
## value.
##
encrypt() {
KEY="$1"
#
# Encrypt STDIN
#
openssl enc -aes-128-cbc -K "$KEY" -iv "$NULL_IV" -nosalt -a
}
#
# Sign and encrypt file using secret key
#
sign "$SECRET_KEY" "$JSON_FILENAME" | encrypt "$SECRET_KEY"

View File

@@ -0,0 +1,104 @@
<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-json</artifactId>
<packaging>jar</packaging>
<version>1.0.0-1</version>
<name>guacamole-auth-json</name>
<url>http://glyptodon.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>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg>
</compilerArgs>
<fork>true</fork>
</configuration>
</plugin>
<!-- Copy dependencies prior to packaging -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Guacamole Extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.0.0</version>
<scope>provided</scope>
</dependency>
<!-- Guice -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-multibindings</artifactId>
<version>3.0</version>
</dependency>
<!-- Jackson for JSON support -->
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<version>1.9.2</version>
</dependency>
<!-- Java servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<!-- Spring security library (required for IP address matching) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.0.4.RELEASE</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.glyptodon.guacamole.auth.json.user.AuthenticatedUser;
import org.glyptodon.guacamole.auth.json.user.UserContext;
import org.glyptodon.guacamole.auth.json.user.UserData;
import org.glyptodon.guacamole.auth.json.user.UserDataService;
/**
* Service providing convenience functions for the JSONAuthenticationProvider.
*
* @author Michael Jumper
*/
public class AuthenticationProviderService {
/**
* Service for deriving Guacamole extension API data from UserData objects.
*/
@Inject
private UserDataService userDataService;
/**
* Provider for AuthenticatedUser objects.
*/
@Inject
private Provider<AuthenticatedUser> authenticatedUserProvider;
/**
* Provider for UserContext objects.
*/
@Inject
private Provider<UserContext> userContextProvider;
/**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* An AuthenticatedUser representing the user authenticated by the
* given credentials.
*
* @throws GuacamoleException
* If an error occurs while authenticating the user, or if access is
* denied.
*/
public AuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
// Pull UserData from credentials, if possible
UserData userData = userDataService.fromCredentials(credentials);
if (userData == null)
throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.EMPTY);
// Produce AuthenticatedUser associated with derived UserData
AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
authenticatedUser.init(credentials, userData);
return authenticatedUser;
}
/**
* Returns a UserContext object initialized with data accessible to the
* given AuthenticatedUser.
*
* @param authenticatedUser
* The AuthenticatedUser to retrieve data for.
*
* @return
* A UserContext object initialized with data accessible to the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If the UserContext cannot be created due to an error.
*/
public UserContext getUserContext(org.apache.guacamole.net.auth.AuthenticatedUser authenticatedUser)
throws GuacamoleException {
// The JSONAuthenticationProvider only provides data for users it has
// authenticated itself
if (!(authenticatedUser instanceof AuthenticatedUser))
return null;
// Return UserContext containing data from the authenticated user's
// associated UserData object
UserContext userContext = userContextProvider.get();
userContext.init(((AuthenticatedUser) authenticatedUser).getUserData());
return userContext;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import javax.xml.bind.DatatypeConverter;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is a byte array. The bytes of the byte array
* must be represented as a hexadecimal string within the property value. The
* hexadecimal string is case-insensitive.
*
* @author Michael Jumper
*/
public abstract class ByteArrayProperty implements GuacamoleProperty<byte[]> {
@Override
public byte[] parseValue(String value) throws GuacamoleException {
// If no property provided, return null.
if (value == null)
return null;
// Return value parsed from hex
try {
return DatatypeConverter.parseHexBinary(value);
}
// Fail parse if hex invalid
catch (IllegalArgumentException e) {
throw new GuacamoleServerException("Invalid hexadecimal value.", e);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import com.google.inject.Inject;
import java.util.Collection;
import java.util.Collections;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
/**
* Service for retrieving configuration information regarding the JSON
* authentication provider.
*
* @author Michael Jumper
*/
public class ConfigurationService {
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* The encryption key to use for all decryption and signature verification.
*/
private static final ByteArrayProperty JSON_SECRET_KEY = new ByteArrayProperty() {
@Override
public String getName() {
return "json-secret-key";
}
};
/**
* A comma-separated list of all IP addresses or CIDR subnets which should
* be allowed to perform authentication. If not specified, ALL address will
* be allowed.
*/
private static final StringListProperty JSON_TRUSTED_NETWORKS = new StringListProperty() {
@Override
public String getName() {
return "json-trusted-networks";
}
};
/**
* Returns the symmetric key which will be used to encrypt and sign all
* JSON data and should be used to decrypt and verify any received JSON
* data. This is dictated by the "json-secret-key" property specified
* within guacamole.properties.
*
* @return
* The key which should be used to decrypt received JSON data.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed, or if the
* "json-secret-key" property is missing.
*/
public byte[] getSecretKey() throws GuacamoleException {
return environment.getRequiredProperty(JSON_SECRET_KEY);
}
/**
* Returns a collection of all IP address or CIDR subnets which should be
* allowed to submit authentication requests. If empty, authentication
* attempts will be allowed through without restriction.
*
* @return
* A collection of all IP address or CIDR subnets which should be
* allowed to submit authentication requests.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
public Collection<String> getTrustedNetworks() throws GuacamoleException {
return environment.getProperty(JSON_TRUSTED_NETWORKS, Collections.<String>emptyList());
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
/**
* Service for handling cryptography-related operations, such as decrypting
* encrypted data.
*
* @author Michael Jumper
*/
public class CryptoService {
/**
* The length of all signatures, in bytes.
*/
public static final int SIGNATURE_LENGTH = 32;
/**
* The name of the key generation algorithm used for decryption.
*/
private static final String DECRYPTION_KEY_GENERATION_ALGORITHM_NAME = "AES";
/**
* The name of the cipher transformation that should be used to decrypt any
* String provided to decrypt().
*/
private static final String DECRYPTION_CIPHER_NAME = "AES/CBC/PKCS5Padding";
/**
* The name of the key generation algorithm used for verifying signatures.
*/
private static final String SIGNATURE_KEY_GENERATION_ALGORITHM_NAME = "HmacSHA256";
/**
* The name of the MAC algorithm used for verifying signatures.
*/
private static final String SIGNATURE_MAC_ALGORITHM_NAME = "HmacSHA256";
/**
* IV which is all null bytes (all binary zeroes). Usually, using a null IV
* is a horrible idea. As our plaintext will always be prepended with the
* HMAC signature of the rest of the message, we are effectively using the
* HMAC signature itself as the IV. For our purposes, where the encrypted
* value becomes an authentication token, this is OK.
*/
private static final IvParameterSpec NULL_IV = new IvParameterSpec(new byte[] {
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0
});
/**
* Creates a new key suitable for decryption using the provided raw key
* bytes. The algorithm used to generate this key is dictated by
* DECRYPTION_KEY_GENERATION_ALGORITHM_NAME and must match the algorithm
* used by decrypt().
*
* @param keyBytes
* The raw bytes from which the encryption/decryption key should be
* generated.
*
* @return
* A new key suitable for encryption or decryption, generated from the
* given bytes.
*/
public SecretKey createEncryptionKey(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, DECRYPTION_KEY_GENERATION_ALGORITHM_NAME);
}
/**
* Creates a new key suitable for signature verification using the provided
* raw key bytes. The algorithm used to generate this key is dictated by
* SIGNATURE_KEY_GENERATION_ALGORITHM_NAME and must match the algorithm
* used by sign().
*
* @param keyBytes
* The raw bytes from which the signature verification key should be
* generated.
*
* @return
* A new key suitable for signature verification, generated from the
* given bytes.
*/
public SecretKey createSignatureKey(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, SIGNATURE_KEY_GENERATION_ALGORITHM_NAME);
}
/**
* Decrypts the given ciphertext using the provided key, returning the
* resulting plaintext. If any error occurs during decryption at all, a
* GuacamoleException is thrown. The IV used for the decryption process is
* a null IV (all binary zeroes).
*
* @param key
* The key to use to decrypt the provided ciphertext.
*
* @param cipherText
* The ciphertext to decrypt.
*
* @return
* The plaintext which results from decrypting the ciphertext with the
* provided key.
*
* @throws GuacamoleException
* If any error at all occurs during decryption.
*/
public byte[] decrypt(Key key, byte[] cipherText) throws GuacamoleException {
try {
// Init cipher for descryption using secret key
Cipher cipher = Cipher.getInstance(DECRYPTION_CIPHER_NAME);
cipher.init(Cipher.DECRYPT_MODE, key, NULL_IV);
// Perform decryption
return cipher.doFinal(cipherText);
}
// Rethrow all decryption failures identically
catch (InvalidAlgorithmParameterException e) {
throw new GuacamoleServerException(e);
}
catch (NoSuchAlgorithmException e) {
throw new GuacamoleServerException(e);
}
catch (NoSuchPaddingException e) {
throw new GuacamoleServerException(e);
}
catch (InvalidKeyException e) {
throw new GuacamoleServerException(e);
}
catch (IllegalBlockSizeException e) {
throw new GuacamoleServerException(e);
}
catch (BadPaddingException e) {
throw new GuacamoleServerException(e);
}
}
/**
* Signs the given arbitrary data using the provided key, returning the
* resulting signature. If any error occurs during signing at all, a
* GuacamoleException is thrown.
*
* @param key
* The key to use to sign the provided data.
*
* @param data
* The arbitrary data to sign.
*
* @return
* The signature which results from signing the arbitrary data with the
* provided key.
*
* @throws GuacamoleException
* If any error at all occurs during signing.
*/
public byte[] sign(Key key, byte[] data) throws GuacamoleException {
try {
// Init MAC for signing using secret key
Mac mac = Mac.getInstance(SIGNATURE_MAC_ALGORITHM_NAME);
mac.init(key);
// Sign provided data
return mac.doFinal(data);
}
// Rethrow all signature failures identically
catch (NoSuchAlgorithmException e) {
throw new GuacamoleServerException(e);
}
catch (InvalidKeyException e) {
throw new GuacamoleServerException(e);
}
catch (IllegalStateException e) {
throw new GuacamoleServerException(e);
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import com.google.inject.Guice;
import com.google.inject.Injector;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
/**
* Allows users to be authenticated using encrypted blobs of JSON data. The
* username of the user, all available connections, and the parameters
* associated with those connections are all determined by the contents of the
* provided JSON. The JSON itself is authorized by virtue of being properly
* encrypted with a shared key.
*
* @author Michael Jumper
*/
public class JSONAuthenticationProvider extends AbstractAuthenticationProvider {
/**
* Injector which will manage the object graph of this authentication
* provider.
*/
private final Injector injector;
/**
* Creates a new JSON AuthenticationProvider that authenticates users
* using encrypted blobs of JSON data.
*
* @throws GuacamoleException
* If a required property is missing, or an error occurs while parsing
* a property.
*/
public JSONAuthenticationProvider() throws GuacamoleException {
// Set up Guice injector.
injector = Guice.createInjector(new JSONAuthenticationProviderModule(this));
}
@Override
public String getIdentifier() {
return "json";
}
@Override
public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException {
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.authenticateUser(credentials);
}
@Override
public UserContext getUserContext(AuthenticatedUser authenticatedUser)
throws GuacamoleException {
AuthenticationProviderService authProviderService = injector.getInstance(AuthenticationProviderService.class);
return authProviderService.getUserContext(authenticatedUser);
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import com.google.inject.AbstractModule;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.glyptodon.guacamole.auth.json.connection.ConnectionService;
import org.glyptodon.guacamole.auth.json.user.UserDataService;
/**
* Guice module which configures injections specific to the JSON authentication
* provider.
*
* @author Michael Jumper
*/
public class JSONAuthenticationProviderModule extends AbstractModule {
/**
* Guacamole server environment.
*/
private final Environment environment;
/**
* A reference to the JSONAuthenticationProvider on behalf of which this
* module has configured injection.
*/
private final AuthenticationProvider authProvider;
/**
* Creates a new JSON authentication provider module which configures
* injection for the JSONAuthenticationProvider.
*
* @param authProvider
* The AuthenticationProvider for which injection is being configured.
*
* @throws GuacamoleException
* If an error occurs while retrieving the Guacamole server
* environment.
*/
public JSONAuthenticationProviderModule(AuthenticationProvider authProvider)
throws GuacamoleException {
// Get local environment
this.environment = new LocalEnvironment();
// Store associated auth provider
this.authProvider = authProvider;
}
@Override
protected void configure() {
// Bind core implementations of guacamole-ext classes
bind(AuthenticationProvider.class).toInstance(authProvider);
bind(Environment.class).toInstance(environment);
// Bind JSON-specific services
bind(ConfigurationService.class);
bind(ConnectionService.class);
bind(CryptoService.class);
bind(RequestValidationService.class);
bind(UserDataService.class);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.web.util.matcher.IpAddressMatcher;
/**
* Service for testing the validity of received HTTP requests.
*
* @author Michael Jumper
*/
public class RequestValidationService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(RequestValidationService.class);
/**
* Service for retrieving configuration information regarding the
* JSONAuthenticationProvider.
*/
@Inject
private ConfigurationService confService;
/**
* Returns whether the given request can be used for authentication, taking
* into account restrictions specified within guacamole.properties.
*
* @param request
* The HTTP request to test.
*
* @return
* true if the given request comes from a trusted source and can be
* used for authentication, false otherwise.
*/
public boolean isAuthenticationAllowed(HttpServletRequest request) {
// Pull list of all trusted networks
Collection<String> trustedNetworks;
try {
trustedNetworks = confService.getTrustedNetworks();
}
// Deny all requests if restrictions cannot be parsed
catch (GuacamoleException e) {
logger.warn("Authentication request from \"{}\" is DENIED due to parse error: {}", request.getRemoteAddr(), e.getMessage());
logger.debug("Error parsing authentication request restrictions from guacamole.properties.", e);
return false;
}
// All requests are allowed if no restrictions are defined
if (trustedNetworks.isEmpty()) {
logger.debug("Authentication request from \"{}\" is ALLOWED (no restrictions).", request.getRemoteAddr());
return true;
}
// Build matchers for each trusted network
Collection<IpAddressMatcher> matchers = new ArrayList<IpAddressMatcher>(trustedNetworks.size());
for (String network : trustedNetworks)
matchers.add(new IpAddressMatcher(network));
// Otherwise ensure at least one subnet matches
for (IpAddressMatcher matcher : matchers) {
// Request is allowed if any subnet matches
if (matcher.matches(request)) {
logger.debug("Authentication request from \"{}\" is ALLOWED (matched subnet).", request.getRemoteAddr());
return true;
}
}
// Otherwise request is denied - no subnets matched
logger.debug("Authentication request from \"{}\" is DENIED (did not match subnet).", request.getRemoteAddr());
return false;
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
* A GuacamoleProperty whose value is a List of Strings. The string value
* parsed to produce this list is a comma-delimited list. Duplicate values are
* ignored, as is any whitespace following delimiters. To maintain
* compatibility with the behavior of Java properties in general, only
* whitespace at the beginning of each value is ignored; trailing whitespace
* becomes part of the value.
*
* @author Michael Jumper
*/
public abstract class StringListProperty implements GuacamoleProperty<List<String>> {
/**
* A pattern which matches against the delimiters between values. This is
* currently simply a comma and any following whitespace. Parts of the
* input string which match this pattern will not be included in the parsed
* result.
*/
private static final Pattern DELIMITER_PATTERN = Pattern.compile(",\\s*");
@Override
public List<String> parseValue(String values) throws GuacamoleException {
// If no property provided, return null.
if (values == null)
return null;
// Split string into a list of individual values
List<String> stringValues = Arrays.asList(DELIMITER_PATTERN.split(values));
if (stringValues.isEmpty())
return null;
return stringValues;
}
}

View File

@@ -0,0 +1,333 @@
/*
* Copyright (C) 2018 Glyptodon, Inc.
*
* 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.
*/
/*
* NOTE: The code implemented provided here for establishing connections is
* based upon the connect() function of the SimpleConnection class, part of the
* "guacamole-ext" library, which is part of Apache Guacamole. The relevant
* code has been modified to suit the purposes of this extension.
*/
/*
* 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.glyptodon.guacamole.auth.json.connection;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.io.GuacamoleReader;
import org.apache.guacamole.io.GuacamoleWriter;
import org.apache.guacamole.net.GuacamoleSocket;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.InetGuacamoleSocket;
import org.apache.guacamole.net.SSLGuacamoleSocket;
import org.apache.guacamole.net.SimpleGuacamoleTunnel;
import org.apache.guacamole.net.auth.GuacamoleProxyConfiguration;
import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.glyptodon.guacamole.auth.json.user.UserData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service which provides a centralized means of establishing connections,
* tracking/joining active connections, and retrieving associated data.
*
* @author Michael Jumper
*/
@Singleton
public class ConnectionService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* The Guacamole server environment.
*/
@Inject
private Environment environment;
/**
* Mapping of the unique IDs of active connections (as specified within the
* UserData.Connection object) to the underlying connection ID (as returned
* via the Guacamole protocol handshake). Only connections with defined IDs
* are tracked here.
*/
private final ConcurrentHashMap<String, String> activeConnections =
new ConcurrentHashMap<>();
/**
* Mapping of the connection IDs of joinable connections (as returned via
* the Guacamole protocol handshake) to the Collection of tunnels shadowing
* those connections.
*/
private final ConcurrentHashMap<String, Collection<GuacamoleTunnel>> shadowers =
new ConcurrentHashMap<>();
/**
* Generates a new GuacamoleConfiguration from the associated protocol and
* parameters of the given UserData.Connection. If the configuration cannot
* be generated (because a connection is being joined by that connection is
* not actually active), null is returned.
*
* @param connection
* The UserData.Connection whose protocol and parameters should be used
* to construct the new GuacamoleConfiguration.
*
* @return
* A new GuacamoleConfiguration generated from the associated protocol
* and parameters of the given UserData.Connection, or null if the
* configuration cannot be generated.
*/
public GuacamoleConfiguration getConfiguration(UserData.Connection connection) {
GuacamoleConfiguration config = new GuacamoleConfiguration();
// Set connection ID if joining an active connection
String primaryConnection = connection.getPrimaryConnection();
if (primaryConnection != null) {
// Verify that the connection being joined actually exists
String id = activeConnections.get(primaryConnection);
if (id == null)
return null;
config.setConnectionID(id);
}
// Otherwise, require protocol
else
config.setProtocol(connection.getProtocol());
// Add all parameter name/value pairs
Map<String, String> parameters = connection.getParameters();
if (parameters != null)
config.setParameters(parameters);
return config;
}
/**
* Closes all tunnels within the given connection. If a GuacamoleException
* is thrown by any tunnel during closure, that exception is ignored.
*
* @param tunnels
* The Collection of tunnels to close.
*/
private void closeAll(Collection<GuacamoleTunnel> tunnels) {
for (GuacamoleTunnel tunnel : tunnels) {
try {
tunnel.close();
}
catch (GuacamoleException e) {
logger.debug("Failure to close tunnel masked by closeAll().", e);
}
}
}
/**
* Establishes a connection to guacd using the information associated with
* the given connection object. The resulting connection will be provided
* the given client information during the Guacamole protocol handshake.
*
* @param connection
* The connection object describing the nature of the connection to be
* established.
*
* @param info
* Information associated with the connecting client.
*
* @return
* A fully-established GuacamoleTunnel.
*
* @throws GuacamoleException
* If an error occurs while connecting to guacd, or if permission to
* connect is denied.
*/
public GuacamoleTunnel connect(UserData.Connection connection,
GuacamoleClientInformation info) throws GuacamoleException {
// Retrieve proxy configuration from environment
GuacamoleProxyConfiguration proxyConfig = environment.getDefaultGuacamoleProxyConfiguration();
// Get guacd connection parameters
String hostname = proxyConfig.getHostname();
int port = proxyConfig.getPort();
// Generate and verify connection configuration
GuacamoleConfiguration config = getConfiguration(connection);
if (config == null) {
logger.debug("Configuration for connection could not be "
+ "generated. Perhaps the connection being joined is not "
+ "active?");
throw new GuacamoleResourceNotFoundException("No such connection");
}
// Determine socket type based on required encryption method
final ConfiguredGuacamoleSocket socket;
switch (proxyConfig.getEncryptionMethod()) {
// If guacd requires SSL, use it
case SSL:
socket = new ConfiguredGuacamoleSocket(
new SSLGuacamoleSocket(hostname, port),
config, info
);
break;
// Connect directly via TCP if encryption is not enabled
case NONE:
socket = new ConfiguredGuacamoleSocket(
new InetGuacamoleSocket(hostname, port),
config, info
);
break;
// Abort if encryption method is unknown
default:
throw new GuacamoleServerException("Unimplemented encryption method.");
}
final GuacamoleTunnel tunnel;
// If the current connection is not being tracked (no ID) just use a
// normal, non-tracking tunnel
final String id = connection.getId();
if (id == null)
tunnel = new SimpleGuacamoleTunnel(socket);
// Otherwise, create a tunnel with proper tracking which can be joined
else {
// Allow connection to be joined
final String connectionID = socket.getConnectionID();
final Collection<GuacamoleTunnel> existingTunnels = shadowers.putIfAbsent(connectionID,
Collections.synchronizedList(new ArrayList<>()));
// Duplicate connection IDs cannot exist
assert(existingTunnels == null);
// If the current connection is intended to be tracked (an ID was
// provided), but a connection is already in progress with that ID,
// log a warning that the original connection will no longer be tracked
String activeConnection = activeConnections.put(id, connectionID);
if (activeConnection != null)
logger.warn("A connection with ID \"{}\" is already in progress, "
+ "but another attempt to use this ID has been made. The "
+ "original connection will no longer be joinable.", id);
// Return a tunnel which automatically tracks the active connection
tunnel = new SimpleGuacamoleTunnel(new GuacamoleSocket() {
@Override
public GuacamoleReader getReader() {
return socket.getReader();
}
@Override
public GuacamoleWriter getWriter() {
return socket.getWriter();
}
@Override
public void close() throws GuacamoleException {
// Stop connection from being joined further
activeConnections.remove(id, connectionID);
// Close all connections sharing the closed connection
Collection<GuacamoleTunnel> tunnels = shadowers.remove(connectionID);
if (tunnels != null)
closeAll(tunnels);
socket.close();
}
@Override
public boolean isOpen() {
return socket.isOpen();
}
});
}
// Track tunnels which join connections, such that they can be
// automatically closed when the joined connection closes
String joinedConnection = config.getConnectionID();
if (joinedConnection != null) {
// Track shadower of joined connection if possible
Collection<GuacamoleTunnel> tunnels = shadowers.get(joinedConnection);
if (tunnels != null)
tunnels.add(tunnel);
// Close this tunnel in ALL CASES if the joined connection has
// closed. Note that it is insufficient to simply check whether the
// retrieved Collection is null here, as it may have been removed
// after retrieval. We must ensure that the tunnel is closed in any
// case where it will not automatically be closed due to the
// closure of the shadowed connection.
if (!shadowers.containsKey(joinedConnection))
tunnel.close();
}
return tunnel;
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import com.google.inject.Inject;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
/**
* An implementation of AuthenticatedUser specific to the
* JSONAuthenticationProvider, providing access to the decrypted contents of
* the JSON provided during authentication.
*
* @author Michael Jumper
*/
public class AuthenticatedUser extends AbstractAuthenticatedUser {
/**
* Reference to the authentication provider associated with this
* authenticated user.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* The credentials provided when this user was authenticated.
*/
private Credentials credentials;
/**
* The UserData object derived from the data submitted when this user was
* authenticated.
*/
private UserData userData;
/**
* Initializes this AuthenticatedUser using the given credentials and
* UserData object. The provided UserData object MUST have been derived
* from the data submitted when the user authenticated.
*
* @param credentials
* The credentials provided when this user was authenticated.
*
* @param userData
* The UserData object derived from the data submitted when this user
* was authenticated.
*/
public void init(Credentials credentials, UserData userData) {
this.credentials = credentials;
this.userData = userData;
setIdentifier(userData.getUsername());
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Credentials getCredentials() {
return credentials;
}
/**
* Returns the UserData object derived from the data submitted when this
* user was authenticated.
*
* @return
* The UserData object derived from the data submitted when this user
* was authenticated.
*/
public UserData getUserData() {
return userData;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import com.google.inject.Inject;
import org.apache.guacamole.net.auth.AbstractUserContext;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
/**
* An implementation of UserContext specific to the JSONAuthenticationProvider
* which obtains all data from the encrypted JSON provided during
* authentication.
*
* @author Michael Jumper
*/
public class UserContext extends AbstractUserContext {
/**
* The identifier reserved for the root connection group.
*/
public static final String ROOT_CONNECTION_GROUP = DEFAULT_ROOT_CONNECTION_GROUP;
/**
* Reference to the AuthenticationProvider associated with this
* UserContext.
*/
@Inject
private AuthenticationProvider authProvider;
/**
* Service for deriving Guacamole extension API data from UserData objects.
*/
@Inject
private UserDataService userDataService;
/**
* The UserData object associated with the user to whom this UserContext
* belongs.
*/
private UserData userData;
/**
* Initializes this UserContext using the data associated with the provided
* UserData object.
*
* @param userData
* The UserData object derived from the JSON data received when the
* user authenticated.
*/
public void init(UserData userData) {
this.userData = userData;
}
@Override
public User self() {
return userDataService.getUser(userData);
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return authProvider;
}
@Override
public Directory<Connection> getConnectionDirectory() {
return userDataService.getConnectionDirectory(userData);
}
}

View File

@@ -0,0 +1,393 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonProperty;
/**
* All data associated with a particular user, as parsed from the JSON supplied
* within the encrypted blob provided during authentication.
*
* @author Michael Jumper
*/
public class UserData {
/**
* The username of the user associated with this data.
*/
private String username;
/**
* The time after which this data is no longer valid and must not be used.
* This is a UNIX-style epoch timestamp, stored as the number of
* milliseconds since midnight of January 1, 1970 UTC.
*/
private Long expires;
/**
* Whether this data can only be used once. If set to true, reuse of the
* associated signed data will not be allowed. This is only valid if the
* expiration timestamp has been set.
*/
private boolean singleUse = false;
/**
* All connections accessible by this user. The key of each entry is both
* the connection identifier and the connection name.
*/
private ConcurrentMap<String, Connection> connections;
/**
* The data associated with a Guacamole connection stored within a UserData
* object.
*
* @author Michael Jumper
*/
public static class Connection {
/**
* An arbitrary, opaque, unique ID for this connection. If specified
* via the "join" (primaryConnection) property of another connection,
* that connection may be used to join this connection.
*/
private String id;
/**
* The protocol that this connection should use, such as "vnc" or "rdp".
*/
private String protocol;
/**
* The opaque ID of the connection being joined (shared), as given with
* the "id" property. If specified, the provided protocol is ignored.
* This value is exposed via the "join" property within JSON.
*/
private String primaryConnection;
/**
* Map of all connection parameter values, where each key is the parameter
* name. Legal parameter names are dictated by the specified protocol and
* are documented within the Guacamole manual:
*
* http://guac-dev.org/doc/gug/configuring-guacamole.html#connection-configuration
*/
private Map<String, String> parameters;
/**
* Whether this connection can only be used once. If set to true, the
* connection will be removed from the connections directory
* immediately upon use.
*/
private boolean singleUse = false;
/**
* Returns an arbitrary, opaque, unique ID for this connection. If
* defined, this ID may be used via the "join" (primaryConnection)
* property of another connection to join (share) this connection while
* it is in progress.
*
* @return
* An arbitrary, opaque, unique ID for this connection.
*/
public String getId() {
return id;
}
/**
* Sets an arbitrary, opaque ID which uniquely identifies this
* connection. This ID may be used via the "join" (primaryConnection)
* property of another connection to join (share) this connection while
* it is in progress.
*
* @param id
* An arbitrary, opaque, unique ID for this connection.
*/
public void setId(String id) {
this.id = id;
}
/**
* Returns the protocol that this connection should use, such as "vnc"
* or "rdp".
*
* @return
* The name of the protocol to use, such as "vnc" or "rdp".
*/
public String getProtocol() {
return protocol;
}
/**
* Sets the protocol that this connection should use, such as "vnc"
* or "rdp".
*
* @param protocol
* The name of the protocol to use, such as "vnc" or "rdp".
*/
public void setProtocol(String protocol) {
this.protocol = protocol;
}
/**
* Returns the opaque ID of the connection being joined (shared), if
* any. If specified, any provided protocol is ignored. This value is
* exposed via the "join" property within JSON.
*
* @return
* The opaque ID of the connection being joined (shared), if any.
*/
@JsonProperty("join")
public String getPrimaryConnection() {
return primaryConnection;
}
/**
* Sets the opaque ID of the connection being joined (shared). If
* specified, any provided protocol is ignored. This is exposed via the
* "join" property within JSON.
*
* @param primaryConnection
* The opaque ID of the connection being joined (shared).
*/
@JsonProperty("join")
public void setPrimaryConnection(String primaryConnection) {
this.primaryConnection = primaryConnection;
}
/**
* Returns a map of all parameter name/value pairs, where the key of
* each entry in the map is the corresponding parameter name. Changes
* to this map directly affect the parameters associated with this
* connection.
*
* @return
* A map of all parameter name/value pairs associated with this
* connection.
*/
public Map<String, String> getParameters() {
return parameters;
}
/**
* Replaces all parameters associated with this connection with the
* name/value pairs in the provided map, where the key of each entry
* in the map is the corresponding parameter name. Changes to this map
* directly affect the parameters associated with this connection.
*
* @param parameters
* The map of all parameter name/value pairs to associate with this
* connection.
*/
public void setParameters(Map<String, String> parameters) {
this.parameters = parameters;
}
/**
* Returns whether this connection is intended for single-use only. A
* single-use connection cannot be used more than once.
*
* After a single-use connection is used, it should be automatically
* and atomically removed from any underlying data (such as with
* UserData.removeConnection()).
*
* @return
* true if this connection is intended for single-use only, false
* otherwise.
*/
public boolean isSingleUse() {
return singleUse;
}
/**
* Sets whether this connection is intended for single-use only. A
* single-use connection cannot be used more than once. By default,
* connections are NOT single-use.
*
* After a single-use connection is used, it should be automatically
* and atomically removed from any underlying data (such as with
* UserData.removeConnection()).
*
* @param singleUse
* true if this connection is intended for single-use only, false
* otherwise.
*/
public void setSingleUse(boolean singleUse) {
this.singleUse = singleUse;
}
}
/**
* Returns the username of the user associated with the data stored in this
* object.
*
* @return
* The username of the user associated with the data stored in this
* object.
*/
public String getUsername() {
return username;
}
/**
* Sets the username of the user associated with the data stored in this
* object.
*
* @param username
* The username of the user to associate with the data stored in this
* object.
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Returns the time after which the data stored in this object is invalid
* and must not be used. The time returned is a UNIX-style epoch timestamp
* whose value is the number of milliseconds since midnight of January 1,
* 1970 UTC. If this object does not expire, null is returned.
*
* @return
* The time after which the data stored in this object is invalid and
* must not be used, or null if this object does not expire.
*/
public Long getExpires() {
return expires;
}
/**
* Sets the time after which the data stored in this object is invalid
* and must not be used. The time provided MUST be a UNIX-style epoch
* timestamp whose value is the number of milliseconds since midnight of
* January 1, 1970 UTC. If this object should not expire, the value
* provided should be null.
*
* @param expires
* The time after which the data stored in this object is invalid and
* must not be used, or null if this object does not expire.
*/
public void setExpires(Long expires) {
this.expires = expires;
}
/**
* Returns whether this user data is intended for single-use only.
* Single-use data cannot be used more than once. This flag only has
* meaning if the data also has an expires timestamp.
*
* @return
* true if this data is intended for single-use only, false
* otherwise.
*/
public boolean isSingleUse() {
return singleUse;
}
/**
* Sets whether this user data is intended for single-use only. Single-use
* data cannot be used more than once. This flag only has meaning if the
* data also has an expires timestamp. By default, user data is NOT
* single-use.
*
* @param singleUse
* true if this data is intended for single-use only, false
* otherwise.
*/
public void setSingleUse(boolean singleUse) {
this.singleUse = singleUse;
}
/**
* Returns all connections stored within this UserData object as an
* unmodifiable map. Each of these connections is accessible by the user
* specified by getUsername(). The key of each entry within the map is the
* identifier and human-readable name of the corresponding connection.
*
* @return
* An unmodifiable map of all connections stored within this
* UserData object, where the key of each entry is the identifier of
* the corresponding connection.
*/
public Map<String, Connection> getConnections() {
return connections == null ? null : Collections.unmodifiableMap(connections);
}
/**
* Replaces all connections stored within this UserData object with the
* given connections. Each of these connections will be accessible by the
* user specified by getUsername(). The key of each entry within the map is
* the identifier and human-readable name of the corresponding connection.
*
* @param connections
* A map of all connections to be stored within this UserData object,
* where the key of each entry is the identifier of the corresponding
* connection.
*/
public void setConnections(Map<String, Connection> connections) {
this.connections = new ConcurrentHashMap<String, Connection>(connections);
}
/**
* Removes the connection having the given identifier from the overall map
* of connections, such that it cannot be used further. This operation is
* atomic.
*
* @param identifier
* The identifier of the connection to remove.
*
* @return
* The connection that was removed, or null if no such connection
* exists.
*/
public Connection removeConnection(String identifier) {
return connections.remove(identifier);
}
/**
* Returns whether the data within this UserData object is expired, and
* thus must not be used, according to the timestamp returned by
* getExpires().
*
* @return
* true if the data within this UserData object is expired and must not
* be used, false otherwise.
*/
@JsonIgnore
public boolean isExpired() {
// Do not bother comparing if this UserData object does not expire
Long expirationTimestamp = getExpires();
if (expirationTimestamp == null)
return false;
// Otherwise, compare expiration timestamp against system time
return System.currentTimeMillis() > expirationTimestamp;
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright (C) 2016 Glyptodon, Inc.
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.xml.bind.DatatypeConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Atomic blacklist of UserData objects, stored by their associated
* cryptographic signatures. UserData objects stored within this blacklist MUST
* have an associated expiration timestamp, and will automatically be removed
* from the blacklist once they have expired.
*
* @author Michael Jumper
*/
public class UserDataBlacklist {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserDataBlacklist.class);
/**
* All blacklisted UserData objects, stored by their associated
* cryptographic signatures. NOTE: Each key into this map is the hex
* string produced by encoding the binary signature using DatatypeConverter.
* A byte[] cannot be used directly.
*/
private final ConcurrentMap<String, UserData> blacklist =
new ConcurrentHashMap<String, UserData>();
/**
* Removes all expired UserData objects from the blacklist. This will
* automatically be invoked whenever new UserData is added to the blacklist.
*/
public void removeExpired() {
// Remove expired data from blacklist
Iterator<Map.Entry<String, UserData>> current = blacklist.entrySet().iterator();
while (current.hasNext()) {
// Remove entry from map if its associated with expired data
Map.Entry<String, UserData> entry = current.next();
if (entry.getValue().isExpired())
current.remove();
}
}
/**
* Adds the given UserData to the blacklist, storing it according to the
* provided cryptographic signature. The UserData MUST have an associated
* expiration timestamp. If any UserData objects already within the
* blacklist have expired, they will automatically be removed when this
* function is invoked.
*
* @param data
* The UserData to store within the blacklist.
*
* @param signature
* The cryptographic signature associated with the UserData.
*
* @return
* true if the UserData was not already blacklisted and has
* successfully been added, false otherwise.
*/
public boolean add(UserData data, byte[] signature) {
// Expiration timestamps must be provided
if (data.getExpires() == null) {
logger.warn("An expiration timestamp MUST be provided for "
+ "single-use data.");
return false;
}
// Remove any expired entries
removeExpired();
// Expired user data is implicitly blacklisted
if (data.isExpired())
return false;
// Add to blacklist only if not already present
String signatureHex = DatatypeConverter.printHexBinary(signature);
return blacklist.putIfAbsent(signatureHex, data) == null;
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2016 Glyptodon, Inc.
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import com.google.inject.Inject;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionRecord;
import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.apache.guacamole.protocol.GuacamoleConfiguration;
import org.glyptodon.guacamole.auth.json.connection.ConnectionService;
/**
* Connection implementation which automatically manages related UserData if
* the connection is used. Connections which are marked as single-use will
* be removed from the given UserData such that only the first connection
* attempt can succeed.
*
* @author Michael Jumper
*/
public class UserDataConnection implements Connection {
/**
* Service for establishing and managing connections.
*/
@Inject
private ConnectionService connectionService;
/**
* A human-readable value which both uniquely identifies this connection
* and serves as the connection display name.
*/
private String identifier;
/**
* The UserData associated with this connection. This UserData will be
* automatically updated as this connection is used.
*/
private UserData data;
/**
* The connection entry for this connection within the associated UserData.
*/
private UserData.Connection connection;
/**
* Initializes this UserDataConnection with the given data, unique
* identifier, and connection information. This function MUST be invoked
* before any particular UserDataConnection is actually used.
*
* @param data
* The UserData that this connection should manage.
*
* @param identifier
* The identifier associated with this connection within the given
* UserData.
*
* @param connection
* The connection data associated with this connection within the given
* UserData.
*
* @return
* A reference to this UserDataConnection.
*/
public UserDataConnection init(UserData data, String identifier,
UserData.Connection connection) {
this.identifier = identifier;
this.data = data;
this.connection = connection;
return this;
}
@Override
public String getIdentifier() {
return identifier;
}
@Override
public void setIdentifier(String identifier) {
throw new UnsupportedOperationException("UserDataConnection is immutable.");
}
@Override
public String getName() {
return identifier;
}
@Override
public void setName(String name) {
throw new UnsupportedOperationException("UserDataConnection is immutable.");
}
@Override
public String getParentIdentifier() {
return UserContext.ROOT_CONNECTION_GROUP;
}
@Override
public void setParentIdentifier(String parentIdentifier) {
throw new UnsupportedOperationException("UserDataConnection is immutable.");
}
@Override
public GuacamoleConfiguration getConfiguration() {
// Generate configuration, using a skeleton configuration if generation
// fails
GuacamoleConfiguration config = connectionService.getConfiguration(connection);
if (config == null)
config = new GuacamoleConfiguration();
return config;
}
@Override
public void setConfiguration(GuacamoleConfiguration config) {
throw new UnsupportedOperationException("UserDataConnection is immutable.");
}
@Override
public Map<String, String> getAttributes() {
return Collections.<String, String>emptyMap();
}
@Override
public void setAttributes(Map<String, String> attributes) {
throw new UnsupportedOperationException("UserDataConnection is immutable.");
}
@Override
public Date getLastActive() {
return null;
}
@Override
public List<? extends ConnectionRecord> getHistory() throws GuacamoleException {
return Collections.<ConnectionRecord>emptyList();
}
@Override
public Set<String> getSharingProfileIdentifiers() throws GuacamoleException {
return Collections.<String>emptySet();
}
@Override
public int getActiveConnections() {
return 0;
}
@Override
public GuacamoleTunnel connect(GuacamoleClientInformation info)
throws GuacamoleException {
// Prevent future use immediately upon connect
if (connection.isSingleUse()) {
// Deny access if another user already used the connection
if (data.removeConnection(getIdentifier()) == null)
throw new GuacamoleSecurityException("Permission denied");
}
// Perform connection operation
return connectionService.connect(connection, info);
}
}

View File

@@ -0,0 +1,378 @@
/*
* Copyright (C) 2015 Glyptodon LLC
*
* 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.
*/
package org.glyptodon.guacamole.auth.json.user;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.xml.bind.DatatypeConverter;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
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.codehaus.jackson.map.ObjectMapper;
import org.glyptodon.guacamole.auth.json.ConfigurationService;
import org.glyptodon.guacamole.auth.json.CryptoService;
import org.glyptodon.guacamole.auth.json.RequestValidationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service for deriving Guacamole extension API data from UserData objects.
*
* @author Michael Jumper
*/
@Singleton
public class UserDataService {
/**
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(UserDataService.class);
/**
* ObjectMapper for deserializing UserData objects.
*/
private static final ObjectMapper mapper = new ObjectMapper();
/**
* Blacklist of single-use user data objects which have already been used.
*/
private final UserDataBlacklist blacklist = new UserDataBlacklist();
/**
* Service for retrieving configuration information regarding the
* JSONAuthenticationProvider.
*/
@Inject
private ConfigurationService confService;
/**
* Service for testing the validity of HTTP requests.
*/
@Inject
private RequestValidationService requestService;
/**
* Service for handling cryptography-related operations.
*/
@Inject
private CryptoService cryptoService;
/**
* Provider for UserDataConnection instances.
*/
@Inject
private Provider<UserDataConnection> userDataConnectionProvider;
/**
* The name of the HTTP parameter from which base64-encoded, encrypted JSON
* data should be read. The value of this parameter, when decoded and
* decrypted, must be valid JSON prepended with the 32-byte raw binary
* signature generated through signing the JSON with the secret key using
* HMAC/SHA-256.
*/
public static final String ENCRYPTED_DATA_PARAMETER = "data";
/**
* Derives a new UserData object from the data contained within the given
* Credentials. If no such data is present, or the data present is invalid,
* null is returned.
*
* @param credentials
* The Credentials from which the new UserData object should be
* derived.
*
* @return
* A new UserData object derived from the data contained within the
* given Credentials, or null if no such data is present or if the data
* present is invalid.
*/
public UserData fromCredentials(Credentials credentials) {
String json;
byte[] correctSignature;
// Pull HTTP request, if available
HttpServletRequest request = credentials.getRequest();
if (request == null)
return null;
// Abort if the request itself is not allowed
if (!requestService.isAuthenticationAllowed(request))
return null;
// Pull base64-encoded, encrypted JSON data from HTTP request, if any
// such data is present
String base64 = request.getParameter(ENCRYPTED_DATA_PARAMETER);
if (base64 == null)
return null;
// Decrypt base64-encoded parameter
try {
// Decrypt using defined encryption key
byte[] decrypted = cryptoService.decrypt(
cryptoService.createEncryptionKey(confService.getSecretKey()),
DatatypeConverter.parseBase64Binary(base64)
);
// Abort if decrypted value cannot possibly have a signature AND data
if (decrypted.length <= CryptoService.SIGNATURE_LENGTH) {
logger.warn("Submitted data is too small to contain both a signature and JSON.");
return null;
}
// Split data into signature and JSON portions
byte[] receivedSignature = Arrays.copyOf(decrypted, CryptoService.SIGNATURE_LENGTH);
byte[] receivedJSON = Arrays.copyOfRange(decrypted, CryptoService.SIGNATURE_LENGTH, decrypted.length);
// Produce signature for decrypted data
correctSignature = cryptoService.sign(
cryptoService.createSignatureKey(confService.getSecretKey()),
receivedJSON
);
// Verify signatures
if (!Arrays.equals(receivedSignature, correctSignature)) {
logger.warn("Signature of submitted data is incorrect.");
return null;
}
// Convert from UTF-8
json = new String(receivedJSON, "UTF-8");
}
// Fail if base64 data is not valid
catch (IllegalArgumentException e) {
logger.warn("Submitted data is not proper base64.");
logger.debug("Invalid base64 data.", e);
return null;
}
// Handle lack of standard UTF-8 support (should never happen)
catch (UnsupportedEncodingException e) {
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
logger.debug("Unable to decode base64 data as UTF-8.", e);
return null;
}
// Fail if decryption or key retrieval fails for any reason
catch (GuacamoleException e) {
logger.error("Decryption of received data failed: {}", e.getMessage());
logger.debug("Unable to decrypt received data.", e);
return null;
}
// Deserialize UserData from submitted JSON data
try {
// Deserialize UserData, but reject if expired
UserData userData = mapper.readValue(json, UserData.class);
if (userData.isExpired())
return null;
// Reject if data is single-use and already present in the blacklist
if (userData.isSingleUse() && !blacklist.add(userData, correctSignature))
return null;
return userData;
}
// Fail UserData creation if JSON is invalid/unreadable
catch (IOException e) {
logger.error("Received JSON is invalid: {}", e.getMessage());
logger.debug("Error parsing UserData JSON.", e);
return null;
}
}
/**
* Returns the identifiers of all users readable by the user whose data is
* given by the provided UserData object. As users of the
* JSONAuthenticationProvider can only see themselves, this will always
* simply be a set of the user's own username.
*
* @param userData
* All data associated with the user whose accessible user identifiers
* are being retrieved.
*
* @return
* A set containing the identifiers of all users readable by the user
* whose data is given by the provided UserData object.
*/
public Set<String> getUserIdentifiers(UserData userData) {
// Each user can only see themselves
return Collections.singleton(userData.getUsername());
}
/**
* Returns the user object of the user to whom the given UserData object
* belongs.
*
* @param userData
* All data associated with the user whose own user object is being
* retrieved.
*
* @return
* The user object of the user to whom the given UserData object
* belongs.
*/
public User getUser(UserData userData) {
// Build user object with READ access to all available data
return new SimpleUser(userData.getUsername()) {
@Override
public ObjectPermissionSet getUserPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(getUserIdentifiers(userData));
}
@Override
public ObjectPermissionSet getConnectionPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(getConnectionIdentifiers(userData));
}
@Override
public ObjectPermissionSet getConnectionGroupPermissions() throws GuacamoleException {
return new SimpleObjectPermissionSet(getConnectionGroupIdentifiers(userData));
}
};
}
/**
* Returns the identifiers of all connections readable by the user whose
* data is given by the provided UserData object. If the provided UserData
* is not expired, this will be the set of all connection identifiers
* within the UserData. If the UserData is expired, this will be an empty
* set.
*
* @param userData
* All data associated with the user whose accessible connection
* identifiers are being retrieved.
*
* @return
* A set containing the identifiers of all connections readable by the
* user whose data is given by the provided UserData object.
*/
public Set<String> getConnectionIdentifiers(UserData userData) {
// Do not return any connections if empty or expired
Map<String, UserData.Connection> connections = userData.getConnections();
if (connections == null || userData.isExpired())
return Collections.<String>emptySet();
// Return all available connection identifiers
return connections.keySet();
}
/**
* Returns a Directory containing all connections accessible by the user
* whose data is given by the provided UserData object. If the given
* UserData object is not expired, this Directory will contain absolutely
* all connections defined within the given UserData. If the given UserData
* object is expired, this Directory will be empty.
*
* @param userData
* All data associated with the user whose connection directory is
* being retrieved.
*
* @return
* A Directory containing all connections accessible by the user whose
* data is given by the provided UserData object.
*/
public Directory<Connection> getConnectionDirectory(UserData userData) {
// Do not return any connections if empty or expired
Map<String, UserData.Connection> connections = userData.getConnections();
if (connections == null || userData.isExpired())
return new SimpleDirectory<>();
// Convert UserData.Connection objects to normal Connections
Map<String, Connection> directoryContents = new HashMap<>();
for (Map.Entry<String, UserData.Connection> entry : connections.entrySet()) {
// Pull connection and associated identifier
String identifier = entry.getKey();
UserData.Connection connection = entry.getValue();
// Create Guacamole connection containing the defined identifier
// and parameters
Connection guacConnection = userDataConnectionProvider.get().init(
userData,
identifier,
connection
);
// Add corresponding Connection to directory
directoryContents.put(identifier, guacConnection);
}
return new SimpleDirectory<>(directoryContents);
}
/**
* Returns the identifiers of all connection groups readable by the user
* whose data is given by the provided UserData object. This will always be
* a set containing only the root connection group identifier. The
* JSONAuthenticationProvider does not define any other connection groups.
*
* @param userData
* All data associated with the user whose accessible connection group
* identifiers are being retrieved.
*
* @return
* A set containing the identifiers of all connection groups readable
* by the user whose data is given by the provided UserData object.
*/
public Set<String> getConnectionGroupIdentifiers(UserData userData) {
// The only connection group available is the root group
return Collections.singleton(UserContext.ROOT_CONNECTION_GROUP);
}
}

View File

@@ -0,0 +1,16 @@
{
"guacamoleVersion" : "1.0.0",
"name" : "Encrypted JSON Authentication",
"namespace" : "guac-json",
"authProviders" : [
"org.glyptodon.guacamole.auth.json.JSONAuthenticationProvider"
],
"translations" : [
"translations/en.json"
]
}

View File

@@ -0,0 +1,7 @@
{
"DATA_SOURCE_JSON" : {
"NAME" : "JSON"
}
}

View File

@@ -53,6 +53,7 @@
<module>extensions/guacamole-auth-duo</module>
<module>extensions/guacamole-auth-header</module>
<module>extensions/guacamole-auth-jdbc</module>
<module>extensions/guacamole-auth-json</module>
<module>extensions/guacamole-auth-ldap</module>
<module>extensions/guacamole-auth-openid</module>
<module>extensions/guacamole-auth-quickconnect</module>