GUACAMOLE-990: Merge new guacamole-auth-ban extension to block brute-force auth attempts.
diff --git a/doc/licenses/caffeine-2.9.3/README b/doc/licenses/caffeine-2.9.3/README
new file mode 100644
index 0000000..c51ba74
--- /dev/null
+++ b/doc/licenses/caffeine-2.9.3/README
@@ -0,0 +1,8 @@
+Caffeine (https://github.com/ben-manes/caffeine)
+------------------------------------------------
+
+ Version: 2.9.3
+ From: 'Ben Manes' (https://github.com/ben-manes)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/caffeine-2.9.3/dep-coordinates.txt b/doc/licenses/caffeine-2.9.3/dep-coordinates.txt
new file mode 100644
index 0000000..feda8aa
--- /dev/null
+++ b/doc/licenses/caffeine-2.9.3/dep-coordinates.txt
@@ -0,0 +1 @@
+com.github.ben-manes.caffeine:caffeine:jar:2.9.3
diff --git a/doc/licenses/checker-qual-3.19.0/LICENSE.txt b/doc/licenses/checker-qual-3.19.0/LICENSE.txt
new file mode 100644
index 0000000..9837c6b
--- /dev/null
+++ b/doc/licenses/checker-qual-3.19.0/LICENSE.txt
@@ -0,0 +1,22 @@
+Checker Framework qualifiers
+Copyright 2004-present by the Checker Framework developers
+
+MIT License:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/doc/licenses/checker-qual-3.19.0/README b/doc/licenses/checker-qual-3.19.0/README
new file mode 100644
index 0000000..4a00db7
--- /dev/null
+++ b/doc/licenses/checker-qual-3.19.0/README
@@ -0,0 +1,8 @@
+Checker Framework qualifiers (https://checkerframework.org/)
+------------------------------------------------------------
+
+ Version: 3.19.0
+ From: 'Checker Framework developers' (https://checkerframework.org/)
+ License(s):
+ MIT (bundled/checker-qual-3.19.0/LICENSE.txt)
+
diff --git a/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt b/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt
new file mode 100644
index 0000000..322f395
--- /dev/null
+++ b/doc/licenses/checker-qual-3.19.0/dep-coordinates.txt
@@ -0,0 +1 @@
+org.checkerframework:checker-qual:jar:3.19.0
diff --git a/doc/licenses/error-prone-2.10.0/README b/doc/licenses/error-prone-2.10.0/README
new file mode 100644
index 0000000..f286aed
--- /dev/null
+++ b/doc/licenses/error-prone-2.10.0/README
@@ -0,0 +1,8 @@
+Error Prone (https://errorprone.info/)
+--------------------------------------
+
+ Version: 2.10.0
+ From: 'Google Inc.' (http://www.google.com/)
+ License(s):
+ Apache v2.0
+
diff --git a/doc/licenses/error-prone-2.10.0/dep-coordinates.txt b/doc/licenses/error-prone-2.10.0/dep-coordinates.txt
new file mode 100644
index 0000000..9473dfc
--- /dev/null
+++ b/doc/licenses/error-prone-2.10.0/dep-coordinates.txt
@@ -0,0 +1 @@
+com.google.errorprone:error_prone_annotations:jar:2.10.0
diff --git a/extensions/guacamole-auth-ban/.ratignore b/extensions/guacamole-auth-ban/.ratignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/extensions/guacamole-auth-ban/.ratignore
diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml
new file mode 100644
index 0000000..68b8d6f
--- /dev/null
+++ b/extensions/guacamole-auth-ban/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<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.apache.guacamole</groupId>
+ <artifactId>guacamole-auth-ban</artifactId>
+ <packaging>jar</packaging>
+ <version>1.4.0</version>
+ <name>guacamole-auth-ban</name>
+ <url>http://guacamole.apache.org/</url>
+
+ <parent>
+ <groupId>org.apache.guacamole</groupId>
+ <artifactId>extensions</artifactId>
+ <version>1.4.0</version>
+ <relativePath>../</relativePath>
+ </parent>
+
+ <dependencies>
+
+ <!-- Java servlet API -->
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ <version>2.5</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Guacamole Extension API -->
+ <dependency>
+ <groupId>org.apache.guacamole</groupId>
+ <artifactId>guacamole-ext</artifactId>
+ <version>1.4.0</version>
+ <scope>provided</scope>
+
+ <!-- Exclude transitive dependencies that will be overridden by
+ newer versions required by Caffeine -->
+ <exclusions>
+ <exclusion>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ </exclusion>
+ </exclusions>
+
+ </dependency>
+
+ <!-- Guava Base Libraries -->
+ <dependency>
+ <groupId>com.github.ben-manes.caffeine</groupId>
+ <artifactId>caffeine</artifactId>
+ <version>2.9.3</version>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git a/extensions/guacamole-auth-ban/src/main/assembly/dist.xml b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml
new file mode 100644
index 0000000..d046ae6
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<assembly
+ xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0
+ http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+
+ <id>dist</id>
+ <baseDirectory>${project.artifactId}-${project.version}</baseDirectory>
+
+ <!-- Output tar.gz -->
+ <formats>
+ <format>tar.gz</format>
+ </formats>
+
+ <!-- Include licenses and extension .jar -->
+ <fileSets>
+
+ <!-- Include licenses -->
+ <fileSet>
+ <outputDirectory></outputDirectory>
+ <directory>target/licenses</directory>
+ </fileSet>
+
+ <!-- Include extension .jar -->
+ <fileSet>
+ <directory>target</directory>
+ <outputDirectory></outputDirectory>
+ <includes>
+ <include>*.jar</include>
+ </includes>
+ </fileSet>
+
+ </fileSets>
+
+</assembly>
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java
new file mode 100644
index 0000000..4d8a3bb
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban;
+
+import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
+import org.apache.guacamole.net.event.AuthenticationFailureEvent;
+import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
+import org.apache.guacamole.net.event.listener.Listener;
+
+/**
+ * Listener implementation which automatically tracks authentication failures
+ * such that further authentication attempts may be automatically blocked by
+ * {@link BanningAuthenticationProvider} if they match configured criteria.
+ */
+public class BanningAuthenticationListener implements Listener {
+
+ /**
+ * Shared tracker of addresses that have repeatedly failed authentication.
+ */
+ private static AuthenticationFailureTracker tracker;
+
+ /**
+ * Assigns the shared tracker instance used by both the {@link BanningAuthenticationProvider}
+ * and this listener. This function MUST be invoked with the tracker
+ * created for BanningAuthenticationProvider as soon as possible (during
+ * construction of BanningAuthenticationProvider), or processing of
+ * received events will fail internally.
+ *
+ * @param tracker
+ * The tracker instance to use for received authentication events.
+ */
+ public static void setAuthenticationFailureTracker(AuthenticationFailureTracker tracker) {
+ BanningAuthenticationListener.tracker = tracker;
+ }
+
+ @Override
+ public void handleEvent(Object event) throws GuacamoleException {
+
+ if (event instanceof AuthenticationFailureEvent) {
+
+ AuthenticationFailureEvent failure = (AuthenticationFailureEvent) event;
+
+ // Requests for additional credentials are not failures per se,
+ // but continuations of a multi-request authentication attempt that
+ // has not yet succeeded OR failed
+ if (failure.getFailure() instanceof GuacamoleInsufficientCredentialsException) {
+ tracker.notifyAuthenticationRequestReceived(failure.getCredentials());
+ return;
+ }
+
+ // Consider all other errors to be failed auth attempts
+ tracker.notifyAuthenticationFailed(failure.getCredentials());
+
+ }
+
+ else if (event instanceof AuthenticationSuccessEvent) {
+ AuthenticationSuccessEvent success = (AuthenticationSuccessEvent) event;
+ tracker.notifyAuthenticationSuccess(success.getCredentials());
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java
new file mode 100644
index 0000000..1d115d3
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban;
+
+import org.apache.guacamole.auth.ban.status.InMemoryAuthenticationFailureTracker;
+import org.apache.guacamole.auth.ban.status.AuthenticationFailureTracker;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.ban.status.NullAuthenticationFailureTracker;
+import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.environment.LocalEnvironment;
+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;
+import org.apache.guacamole.properties.IntegerGuacamoleProperty;
+import org.apache.guacamole.properties.LongGuacamoleProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AuthenticationProvider implementation that blocks further authentication
+ * attempts that are related to past authentication failures flagged by
+ * {@link BanningAuthenticationListener}.
+ */
+public class BanningAuthenticationProvider extends AbstractAuthenticationProvider {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(BanningAuthenticationProvider.class);
+
+ /**
+ * The maximum number of failed authentication attempts allowed before an
+ * address is temporarily banned.
+ */
+ private static final IntegerGuacamoleProperty MAX_ATTEMPTS = new IntegerGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "ban-max-invalid-attempts";
+ }
+
+ };
+
+ /**
+ * The length of time that each address should be banned after reaching the
+ * maximum number of failed authentication attempts, in seconds.
+ */
+ private static final IntegerGuacamoleProperty IP_BAN_DURATION = new IntegerGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "ban-address-duration";
+ }
+
+ };
+
+ /**
+ * The maximum number of failed authentication attempts tracked at any
+ * given time. Once this number of addresses is exceeded, the oldest
+ * authentication attempts are rotated off on an LRU basis.
+ */
+ private static final LongGuacamoleProperty MAX_ADDRESSES = new LongGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "ban-max-addresses";
+ }
+
+ };
+
+ /**
+ * The default maximum number of failed authentication attempts allowed
+ * before an address is temporarily banned.
+ */
+ private static final int DEFAULT_MAX_ATTEMPTS = 5;
+
+ /**
+ * The default length of time that each address should be banned after
+ * reaching the maximum number of failed authentication attempts, in
+ * seconds.
+ */
+ private static final int DEFAULT_IP_BAN_DURATION = 300;
+
+ /**
+ * The maximum number of failed authentication attempts tracked at any
+ * given time. Once this number of addresses is exceeded, the oldest
+ * authentication attempts are rotated off on an LRU basis.
+ */
+ private static final long DEFAULT_MAX_ADDRESSES = 10485760;
+
+ /**
+ * Shared tracker of addresses that have repeatedly failed authentication.
+ */
+ private final AuthenticationFailureTracker tracker;
+
+ /**
+ * Creates a new BanningAuthenticationProvider which automatically bans
+ * further authentication attempts from addresses that have repeatedly
+ * failed to authenticate. The ban duration and maximum number of failed
+ * attempts allowed before banning are configured within
+ * guacamole.properties.
+ *
+ * @throws GuacamoleException
+ * If an error occurs parsing the configuration properties used by this
+ * extension.
+ */
+ public BanningAuthenticationProvider() throws GuacamoleException {
+
+ Environment environment = LocalEnvironment.getInstance();
+ int maxAttempts = environment.getProperty(MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS);
+ int banDuration = environment.getProperty(IP_BAN_DURATION, DEFAULT_IP_BAN_DURATION);
+ long maxAddresses = environment.getProperty(MAX_ADDRESSES, DEFAULT_MAX_ADDRESSES);
+
+ // Configure auth failure tracking behavior and inform administrator of
+ // ultimate result
+ if (maxAttempts <= 0) {
+ this.tracker = new NullAuthenticationFailureTracker();
+ logger.info("Maximum failed authentication attempts has been set "
+ + "to {}. Automatic banning of brute-force authentication "
+ + "attempts will be disabled.", maxAttempts);
+ }
+ else if (banDuration <= 0) {
+ this.tracker = new NullAuthenticationFailureTracker();
+ logger.info("Ban duration for addresses that repeatedly fail "
+ + "authentication has been set to {}. Automatic banning "
+ + "of brute-force authentication attempts will be "
+ + "disabled.", banDuration);
+ }
+ else if (maxAddresses <= 0) {
+ this.tracker = new NullAuthenticationFailureTracker();
+ logger.info("Maximum number of tracked addresses has been set to "
+ + "{}. Automatic banning of brute-force authentication "
+ + "attempts will be disabled.", maxAddresses);
+ }
+ else {
+ this.tracker = new InMemoryAuthenticationFailureTracker(maxAttempts, banDuration, maxAddresses);
+ logger.info("Addresses will be automatically banned for {} "
+ + "seconds after {} failed authentication attempts. Up "
+ + "to {} unique addresses will be tracked/banned at any "
+ + "given time.", banDuration, maxAttempts, maxAddresses);
+ }
+
+ BanningAuthenticationListener.setAuthenticationFailureTracker(tracker);
+
+ }
+
+ @Override
+ public String getIdentifier() {
+ return "ban";
+ }
+
+ @Override
+ public AuthenticatedUser authenticateUser(Credentials credentials) throws GuacamoleException {
+ tracker.notifyAuthenticationRequestReceived(credentials);
+ return null;
+ }
+
+ @Override
+ public UserContext getUserContext(AuthenticatedUser authenticatedUser) throws GuacamoleException {
+ tracker.notifyAuthenticationRequestReceived(authenticatedUser.getCredentials());
+ return null;
+ }
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java
new file mode 100644
index 0000000..3292d11
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureStatus.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban.status;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The current status of an authentication failure, including the number of
+ * times the failure has occurred.
+ */
+public class AuthenticationFailureStatus {
+
+ /**
+ * The timestamp of the last authentication failure, as returned by
+ * System.nanoTime().
+ */
+ private long lastFailure;
+
+ /**
+ * The number of failures that have occurred.
+ */
+ private final AtomicInteger failureCount;
+
+ /**
+ * The maximum number of failures that may occur before the user/address
+ * causing the failures is blocked.
+ */
+ private final int maxAttempts;
+
+ /**
+ * The amount of time that a user/address must remain blocked after they
+ * have reached the maximum number of failures. Unlike the value provided
+ * at construction time, this value is maintained in nanoseconds.
+ */
+ private final long duration;
+
+ /**
+ * Creates an AuthenticationFailureStatus that is initialized to zero
+ * failures and is subject to the given restrictions. Additional failures
+ * may be flagged after creation with {@link #notifyFailed()}.
+ *
+ * @param maxAttempts
+ * The maximum number of failures that may occur before the
+ * user/address causing the failures is blocked.
+ *
+ * @param duration
+ * The amount of time, in seconds, that a user/address must remain
+ * blocked after they have reached the maximum number of failures.
+ */
+ public AuthenticationFailureStatus(int maxAttempts, int duration) {
+ this.lastFailure = System.nanoTime();
+ this.failureCount = new AtomicInteger(0);
+ this.maxAttempts = maxAttempts;
+ this.duration = TimeUnit.SECONDS.toNanos(duration);
+ }
+
+ /**
+ * Updates this authentication failure, noting that the failure it
+ * represents has recurred.
+ */
+ public void notifyFailed() {
+ lastFailure = System.nanoTime();
+ failureCount.incrementAndGet();
+ }
+
+ /**
+ * Returns whether this authentication failure is recent enough that it
+ * should still be tracked. This function will return false for
+ * authentication failures that have not recurred for at least the duration
+ * provided at construction time.
+ *
+ * @return
+ * true if this authentication failure is recent enough that it should
+ * still be tracked, false otherwise.
+ */
+ public boolean isValid() {
+ return System.nanoTime() - lastFailure <= duration;
+ }
+
+ /**
+ * Returns whether the user/address causing this authentication failure
+ * should be blocked based on the restrictions provided at construction
+ * time.
+ *
+ * @return
+ * true if the user/address causing this failure should be blocked,
+ * false otherwise.
+ */
+ public boolean isBlocked() {
+ return isValid() && failureCount.get() >= maxAttempts;
+ }
+
+ /**
+ * Returns the total number of authentication failures that have been
+ * recorded through creating this object and invoking
+ * {@link #notifyFailed()}.
+ *
+ * @return
+ * The total number of failures that have occurred.
+ */
+ public int getFailures() {
+ return failureCount.get();
+ }
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java
new file mode 100644
index 0000000..9ea7f2c
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/AuthenticationFailureTracker.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban.status;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.Credentials;
+
+/**
+ * Tracks past authentication results, automatically blocking the IP addresses
+ * of machines that repeatedly fail to authenticate.
+ */
+public interface AuthenticationFailureTracker {
+
+ /**
+ * Reports that an authentication request has been received, but it is
+ * either not yet known whether the request has succeeded or failed. If the
+ * associated address is currently being blocked, an exception will be
+ * thrown.
+ *
+ * @param credentials
+ * The credentials associated with the authentication request.
+ *
+ * @throws GuacamoleException
+ * If the authentication request is being blocked due to brute force
+ * prevention rules.
+ */
+ void notifyAuthenticationRequestReceived(Credentials credentials)
+ throws GuacamoleException;
+
+ /**
+ * Reports that an authentication request has been received and has
+ * succeeded. If the associated address is currently being blocked, an
+ * exception will be thrown.
+ *
+ * @param credentials
+ * The credentials associated with the successful authentication
+ * request.
+ *
+ * @throws GuacamoleException
+ * If the authentication request is being blocked due to brute force
+ * prevention rules.
+ */
+ void notifyAuthenticationSuccess(Credentials credentials)
+ throws GuacamoleException;
+
+ /**
+ * Reports that an authentication request has been received and has
+ * failed. If the associated address is currently being blocked, an
+ * exception will be thrown.
+ *
+ * @param credentials
+ * The credentials associated with the failed authentication request.
+ *
+ * @throws GuacamoleException
+ * If the authentication request is being blocked due to brute force
+ * prevention rules.
+ */
+ void notifyAuthenticationFailed(Credentials credentials)
+ throws GuacamoleException;
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java
new file mode 100644
index 0000000..b655168
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/InMemoryAuthenticationFailureTracker.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban.status;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.language.TranslatableGuacamoleClientTooManyException;
+import org.apache.guacamole.net.auth.Credentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AuthenticationFailureTracker implementation that tracks the failure status
+ * of each IP address in memory. The maximum amount of memory consumed is
+ * bounded by the configured maximum number of addresses tracked.
+ */
+public class InMemoryAuthenticationFailureTracker implements AuthenticationFailureTracker {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(InMemoryAuthenticationFailureTracker.class);
+
+ /**
+ * All authentication failures currently being tracked, stored by the
+ * associated IP address.
+ */
+ private final Cache<String, AuthenticationFailureStatus> failures;
+
+ /**
+ * The maximum number of failed authentication attempts allowed before an
+ * address is temporarily banned.
+ */
+ private final int maxAttempts;
+
+ /**
+ * The length of time that each address should be banned after reaching the
+ * maximum number of failed authentication attempts, in seconds.
+ */
+ private final int banDuration;
+
+ /**
+ * Creates a new AuthenticationFailureTracker that automatically blocks
+ * authentication attempts based on the provided blocking criteria.
+ *
+ * @param maxAttempts
+ * The maximum number of failed authentication attempts allowed before
+ * an address is temporarily banned.
+ *
+ * @param banDuration
+ * The length of time that each address should be banned after reaching
+ * the maximum number of failed authentication attempts, in seconds.
+ *
+ * @param maxAddresses
+ * The maximum number of unique IP addresses that should be tracked
+ * before discarding older tracked failures.
+ */
+ public InMemoryAuthenticationFailureTracker(int maxAttempts, int banDuration,
+ long maxAddresses) {
+
+ this.maxAttempts = maxAttempts;
+ this.banDuration = banDuration;
+
+ // Limit maximum number of tracked addresses to configured upper bound
+ this.failures = Caffeine.newBuilder()
+ .maximumSize(maxAddresses)
+ .build();
+
+ }
+
+ /**
+ * Returns whether the given Credentials do not contain any specific
+ * authentication parameters, including HTTP parameters. An authentication
+ * request that contains no parameters whatsoever will tend to be the
+ * first, anonymous, credential-less authentication attempt that results in
+ * the initial login screen rendering.
+ *
+ * @param credentials
+ * The Credentials object to test.
+ *
+ * @return
+ * true if the given Credentials contain no authentication parameters
+ * whatsoever, false otherwise.
+ */
+ private boolean isEmpty(Credentials credentials) {
+
+ // An authentication request that contains an explicit username or
+ // password (even if blank) is non-empty, regardless of how the values
+ // were passed
+ if (credentials.getUsername() != null || credentials.getPassword() != null)
+ return false;
+
+ // All further tests depend on HTTP request details
+ HttpServletRequest request = credentials.getRequest();
+ if (request == null)
+ return true;
+
+ // An authentication request is non-empty if it contains any HTTP
+ // parameters at all or contains an authentication token
+ return !request.getParameterNames().hasMoreElements()
+ && request.getHeader("Guacamole-Token") == null;
+
+ }
+
+ /**
+ * Reports that the given address has just failed to authenticate and
+ * returns the AuthenticationFailureStatus that represents that failure. If
+ * the address isn't already being tracked, it will begin being tracked as
+ * of this call. If the address is already tracked, the returned
+ * AuthenticationFailureStatus will represent past authentication failures,
+ * as well.
+ *
+ * @param address
+ * The address that has failed to authenticate.
+ *
+ * @return
+ * An AuthenticationFailureStatus that represents this latest
+ * authentication failure for the given address, as well as any past
+ * failures.
+ */
+ private AuthenticationFailureStatus getAuthenticationFailure(String address) {
+
+ AuthenticationFailureStatus status = failures.get(address,
+ (addr) -> new AuthenticationFailureStatus(maxAttempts, banDuration));
+
+ status.notifyFailed();
+ return status;
+
+ }
+
+ /**
+ * Reports that an authentication request has been received, as well as
+ * whether that request is known to have failed. If the associated address
+ * is currently being blocked, an exception will be thrown.
+ *
+ * @param credentials
+ * The credentials associated with the authentication request.
+ *
+ * @param failed
+ * Whether the request is known to have failed. If the status of the
+ * request is not yet known, this should be false.
+ *
+ * @throws GuacamoleException
+ * If the authentication request is being blocked due to brute force
+ * prevention rules.
+ */
+ private void notifyAuthenticationStatus(Credentials credentials,
+ boolean failed) throws GuacamoleException {
+
+ // Ignore requests that do not contain explicit parameters of any kind
+ if (isEmpty(credentials))
+ return;
+
+ // Determine originating address of the authentication request
+ String address = credentials.getRemoteAddress();
+ if (address == null)
+ throw new GuacamoleServerException("Source address cannot be determined.");
+
+ // Get current failure status for the address associated with the
+ // authentication request, adding/updating that status if the request
+ // was itself a failure
+ AuthenticationFailureStatus status;
+ if (failed) {
+ status = getAuthenticationFailure(address);
+ logger.info("Authentication has failed for address \"{}\" (current total failures: {}/{}).",
+ address, status.getFailures(), maxAttempts);
+ }
+ else
+ status = failures.getIfPresent(address);
+
+ if (status != null) {
+
+ // Explicitly block further processing of authentication/authorization
+ // if too many failures have occurred
+ if (status.isBlocked()) {
+ logger.warn("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address);
+ throw new TranslatableGuacamoleClientTooManyException("Too "
+ + "many failed authentication attempts.",
+ "LOGIN.ERROR_TOO_MANY_ATTEMPTS");
+ }
+
+ // Clean up tracking of failures if the address is no longer
+ // relevant (all failures are sufficiently old)
+ else if (!status.isValid()) {
+ logger.debug("Removing address \"{}\" from tracking as there are no recent authentication failures.", address);
+ failures.invalidate(address);
+ }
+
+ }
+
+ }
+
+ @Override
+ public void notifyAuthenticationRequestReceived(Credentials credentials)
+ throws GuacamoleException {
+ notifyAuthenticationStatus(credentials, false);
+ }
+
+ @Override
+ public void notifyAuthenticationSuccess(Credentials credentials)
+ throws GuacamoleException {
+ notifyAuthenticationStatus(credentials, false);
+ }
+
+ @Override
+ public void notifyAuthenticationFailed(Credentials credentials)
+ throws GuacamoleException {
+ notifyAuthenticationStatus(credentials, true);
+ }
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java
new file mode 100644
index 0000000..9b50a30
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/status/NullAuthenticationFailureTracker.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.ban.status;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.Credentials;
+
+/**
+ * AuthenticationFailureTracker implementation that does nothing. All requests
+ * are ignored, regardless of status, and no tracking is performed.
+ */
+public class NullAuthenticationFailureTracker implements AuthenticationFailureTracker {
+
+ @Override
+ public void notifyAuthenticationRequestReceived(Credentials credentials)
+ throws GuacamoleException {
+ // Do nothing
+ }
+
+ @Override
+ public void notifyAuthenticationSuccess(Credentials credentials)
+ throws GuacamoleException {
+ // Do nothing
+ }
+
+ @Override
+ public void notifyAuthenticationFailed(Credentials credentials)
+ throws GuacamoleException {
+ // Do nothing
+ }
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json
new file mode 100644
index 0000000..1e6beac
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json
@@ -0,0 +1,20 @@
+{
+
+ "guacamoleVersion" : "1.4.0",
+
+ "name" : "Brute-force Authentication Detection/Prevention",
+ "namespace" : "ban",
+
+ "authProviders" : [
+ "org.apache.guacamole.auth.ban.BanningAuthenticationProvider"
+ ],
+
+ "listeners" : [
+ "org.apache.guacamole.auth.ban.BanningAuthenticationListener"
+ ],
+
+ "translations" : [
+ "translations/en.json"
+ ]
+
+}
diff --git a/extensions/guacamole-auth-ban/src/main/resources/translations/en.json b/extensions/guacamole-auth-ban/src/main/resources/translations/en.json
new file mode 100644
index 0000000..2ef8a37
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/resources/translations/en.json
@@ -0,0 +1,5 @@
+{
+ "LOGIN": {
+ "ERROR_TOO_MANY_ATTEMPTS" : "Too many failed authentication attempts. Please try again later."
+ }
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 3bab332..b16b3ed 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -40,6 +40,7 @@
<modules>
<!-- Authentication extensions -->
+ <module>guacamole-auth-ban</module>
<module>guacamole-auth-duo</module>
<module>guacamole-auth-header</module>
<module>guacamole-auth-jdbc</module>
diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh
index 9f1773d..6ffc866 100755
--- a/guacamole-docker/bin/build-guacamole.sh
+++ b/guacamole-docker/bin/build-guacamole.sh
@@ -198,3 +198,13 @@
mkdir -p "$DESTINATION/json"
cp extensions/guacamole-auth-json/target/guacamole-auth-json*.jar "$DESTINATION/json"
fi
+
+#
+# Copy automatic brute-force banning auth extension if it was built
+#
+
+if [ -f extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar ]; then
+ mkdir -p "$DESTINATION/ban"
+ cp extensions/guacamole-auth-ban/target/guacamole-auth-ban*.jar "$DESTINATION/ban"
+fi
+
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index c9d205b..632c3d0 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -1160,6 +1160,18 @@
associate_apisessiontimeout
fi
+# Apply any overrides for default address ban behavior
+set_optional_property "ban-address-duration" "$BAN_ADDRESS_DURATION"
+set_optional_property "ban-max-addresses" "$BAN_MAX_ADDRESSES"
+set_optional_property "ban-max-invalid-attempts" "$BAN_MAX_INVALID_ATTEMPTS"
+
+# Ensure guacamole-auth-ban always loads before other extensions unless
+# explicitly overridden via naming or EXTENSION_PRIORITY (allowing other
+# extensions to attempt authentication before guacamole-auth-ban has a chance
+# to enforce any bans could allow credentials to continue to be guessed even
+# after the address has been blocked via timing attacks)
+ln -s /opt/guacamole/ban/guacamole-auth-*.jar "$GUACAMOLE_EXT/_guacamole-auth-ban.jar"
+
# Set logback level if specified
if [ -n "$LOGBACK_LEVEL" ]; then
unzip -o -j /opt/guacamole/guacamole.war WEB-INF/classes/logback.xml -d $GUACAMOLE_HOME
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java
index 9808e70..8705900 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationFailureEvent.java
@@ -19,28 +19,91 @@
package org.apache.guacamole.net.event;
+import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
+import org.apache.guacamole.net.event.listener.Listener;
/**
* An event which is triggered whenever a user's credentials fail to be
* authenticated. The credentials that failed to be authenticated are included
* within this event, and can be retrieved using getCredentials().
*/
-public class AuthenticationFailureEvent implements CredentialEvent {
+public class AuthenticationFailureEvent implements AuthenticationProviderEvent,
+ CredentialEvent, FailureEvent {
/**
* The credentials which failed authentication.
*/
- private Credentials credentials;
+ private final Credentials credentials;
/**
- * Creates a new AuthenticationFailureEvent which represents the failure
- * to authenticate the given credentials.
+ * The AuthenticationProvider that encountered the failure. This may be
+ * null if the AuthenticationProvider is not known, such as if the failure
+ * is caused by every AuthenticationProvider passively refusing to
+ * authenticate the user but without explicitly rejecting the user
+ * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}),
+ * or if the failure is external to any installed AuthenticationProvider
+ * (such as within a {@link Listener}.
+ */
+ private final AuthenticationProvider authProvider;
+
+ /**
+ * The Throwable that was thrown resulting in the failure, if any. This
+ * may be null if authentication failed without a known error, such as if
+ * the failure is caused by every AuthenticationProvider passively refusing
+ * to authenticate the user but without explicitly rejecting the user
+ * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}).
+ */
+ private final Throwable failure;
+
+ /**
+ * Creates a new AuthenticationFailureEvent which represents a failure
+ * to authenticate the given credentials where there is no specific
+ * AuthenticationProvider nor Throwable associated with the failure.
*
- * @param credentials The credentials which failed authentication.
+ * @param credentials
+ * The credentials which failed authentication.
*/
public AuthenticationFailureEvent(Credentials credentials) {
+ this(credentials, null);
+ }
+
+ /**
+ * Creates a new AuthenticationFailureEvent which represents a failure
+ * to authenticate the given credentials where there is no specific
+ * AuthenticationProvider causing the failure.
+ *
+ * @param credentials
+ * The credentials which failed authentication.
+ *
+ * @param failure
+ * The Throwable that was thrown resulting in the failure, or null if
+ * there is no such Throwable.
+ */
+ public AuthenticationFailureEvent(Credentials credentials, Throwable failure) {
+ this(credentials, null, failure);
+ }
+
+ /**
+ * Creates a new AuthenticationFailureEvent which represents a failure
+ * to authenticate the given credentials.
+ *
+ * @param credentials
+ * The credentials which failed authentication.
+ *
+ * @param authProvider
+ * The AuthenticationProvider that caused the failure, or null if there
+ * is no such AuthenticationProvider.
+ *
+ * @param failure
+ * The Throwable that was thrown resulting in the failure, or null if
+ * there is no such Throwable.
+ */
+ public AuthenticationFailureEvent(Credentials credentials,
+ AuthenticationProvider authProvider, Throwable failure) {
this.credentials = credentials;
+ this.authProvider = authProvider;
+ this.failure = failure;
}
@Override
@@ -48,4 +111,35 @@
return credentials;
}
+ /**
+ * {@inheritDoc}
+ *
+ * <p>NOTE: In the case of an authentication failure, cases where this may
+ * be null include if authentication failed without a definite single
+ * AuthenticationProvider causing that failure, such as if the failure is
+ * caused by every AuthenticationProvider passively refusing to
+ * authenticate the user but without explicitly rejecting the user
+ * (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}),
+ * or if the failure is external to any installed AuthenticationProvider
+ * (such as within a {@link Listener}.
+ */
+ @Override
+ public AuthenticationProvider getAuthenticationProvider() {
+ return authProvider;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>NOTE: In the case of an authentication failure, cases where this may
+ * be null include if authentication failed without a known error, such as
+ * if the failure is caused by every AuthenticationProvider passively
+ * refusing to authenticate the user but without explicitly rejecting the
+ * user (returning null for calls to {@link AuthenticationProvider#authenticateUser(org.apache.guacamole.net.auth.Credentials)}).
+ */
+ @Override
+ public Throwable getFailure() {
+ return failure;
+ }
+
}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java
new file mode 100644
index 0000000..7faa876
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationProviderEvent.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.net.event;
+
+import org.apache.guacamole.net.auth.AuthenticationProvider;
+
+/**
+ * An event which may be dispatched due to a specific AuthenticationProvider.
+ */
+public interface AuthenticationProviderEvent {
+
+ /**
+ * Returns the AuthenticationProvider that resulted in the event, if any.
+ * If the event occurred without any definite causing
+ * AuthenticationProvider, this may be null.
+ *
+ * @return
+ * The AuthenticationProvider that resulted in the event, or null if no
+ * such AuthenticationProvider is known.
+ */
+ AuthenticationProvider getAuthenticationProvider();
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java
index 8b63bcf..a9b21dc 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java
@@ -20,6 +20,7 @@
package org.apache.guacamole.net.event;
import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
/**
@@ -32,7 +33,8 @@
* is effectively <em>vetoed</em> and will be subsequently processed as though the
* authentication failed.
*/
-public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent {
+public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent,
+ AuthenticationProviderEvent {
/**
* The AuthenticatedUser identifying the user that successfully
@@ -60,7 +62,12 @@
@Override
public Credentials getCredentials() {
- return authenticatedUser.getCredentials();
+ return getAuthenticatedUser().getCredentials();
+ }
+
+ @Override
+ public AuthenticationProvider getAuthenticationProvider() {
+ return getAuthenticatedUser().getAuthenticationProvider();
}
}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java
new file mode 100644
index 0000000..dfc3375
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/FailureEvent.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.net.event;
+
+/**
+ * An event which represents failure of an operation, where that failure may
+ * be associated with a particular Throwable.
+ */
+public interface FailureEvent {
+
+ /**
+ * Returns the Throwable that represents the failure that occurred, if any.
+ * If the failure was recognized but without a definite known error, this
+ * may be null.
+ *
+ * @return
+ * The Throwable that represents the failure that occurred, or null if
+ * no such Throwable is known.
+ */
+ Throwable getFailure();
+
+}
diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js
index 944fd4a..199d6cb 100644
--- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js
+++ b/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js
@@ -198,12 +198,16 @@
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
// Request credentials if provided credentials were invalid
- if (error.type === Error.Type.INVALID_CREDENTIALS)
+ if (error.type === Error.Type.INVALID_CREDENTIALS) {
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
+ clearAuthenticationResult();
+ }
// Request more credentials if provided credentials were not enough
- else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS)
+ else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) {
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
+ clearAuthenticationResult();
+ }
// Abort rendering of page if an internal error occurs
else if (error.type === Error.Type.INTERNAL_ERROR)
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
index ce8a9fb..8ed76c6 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
@@ -34,7 +34,6 @@
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.UserContext;
-import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
@@ -169,14 +168,15 @@
* The AuthenticatedUser given by the highest-priority
* AuthenticationProvider for which the given credentials are valid.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If the given credentials are not valid for any
* AuthenticationProvider, or if an error occurs while authenticating
* the user.
*/
private AuthenticatedUser authenticateUser(Credentials credentials)
- throws GuacamoleException {
+ throws GuacamoleAuthenticationProcessException {
+ AuthenticationProvider failedAuthProvider = null;
GuacamoleCredentialsException authFailure = null;
// Attempt authentication against each AuthenticationProvider
@@ -191,27 +191,29 @@
// Insufficient credentials should take precedence
catch (GuacamoleInsufficientCredentialsException e) {
- if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException)
+ if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException) {
+ failedAuthProvider = authProvider;
authFailure = e;
+ }
}
-
+
// Catch other credentials exceptions and assign the first one
catch (GuacamoleCredentialsException e) {
- if (authFailure == null)
+ if (authFailure == null) {
+ failedAuthProvider = authProvider;
authFailure = e;
+ }
+ }
+
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication was aborted.", authProvider, e);
}
}
- // If a specific failure occured, rethrow that
- if (authFailure != null)
- throw authFailure;
-
- // Otherwise, request standard username/password
- throw new GuacamoleInvalidCredentialsException(
- "Permission Denied.",
- CredentialsInfo.USERNAME_PASSWORD
- );
+ throw new GuacamoleAuthenticationProcessException("User authentication "
+ + "failed.", failedAuthProvider, authFailure);
}
@@ -230,51 +232,29 @@
* A AuthenticatedUser which may have been updated due to re-
* authentication.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If an error prevents the user from being re-authenticated.
*/
private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Get original AuthenticationProvider
AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider();
- // Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only
- authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
- if (authenticatedUser == null)
- throw new GuacamoleSecurityException("User re-authentication failed.");
+ try {
- return authenticatedUser;
+ // Re-authenticate the AuthenticatedUser against the original AuthenticationProvider only
+ authenticatedUser = authProvider.updateAuthenticatedUser(authenticatedUser, credentials);
+ if (authenticatedUser == null)
+ throw new GuacamoleSecurityException("User re-authentication failed.");
- }
+ return authenticatedUser;
- /**
- * Notify all bound listeners that a successful authentication
- * has occurred.
- *
- * @param authenticatedUser
- * The user that was successfully authenticated.
- *
- * @throws GuacamoleException
- * If thrown by a listener.
- */
- private void fireAuthenticationSuccessEvent(AuthenticatedUser authenticatedUser)
- throws GuacamoleException {
- listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser));
- }
+ }
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User re-authentication failed.", authProvider, e);
+ }
- /**
- * Notify all bound listeners that an authentication attempt has failed.
- *
- * @param credentials
- * The credentials that failed to authenticate.
- *
- * @throws GuacamoleException
- * If thrown by a listener.
- */
- private void fireAuthenticationFailedEvent(Credentials credentials)
- throws GuacamoleException {
- listenerService.handleEvent(new AuthenticationFailureEvent(credentials));
}
/**
@@ -292,61 +272,23 @@
* The AuthenticatedUser associated with the given session and
* credentials.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If an error occurs while authenticating or re-authenticating the
* user.
*/
private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
- try {
-
- // Re-authenticate user if session exists
- if (existingSession != null) {
- AuthenticatedUser updatedUser = updateAuthenticatedUser(
- existingSession.getAuthenticatedUser(), credentials);
- fireAuthenticationSuccessEvent(updatedUser);
- return updatedUser;
- }
-
- // Otherwise, attempt authentication as a new user
- AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials);
- fireAuthenticationSuccessEvent(authenticatedUser);
-
- if (logger.isInfoEnabled())
- logger.info("User \"{}\" successfully authenticated from {}.",
- authenticatedUser.getIdentifier(),
- getLoggableAddress(credentials.getRequest()));
-
- return authenticatedUser;
-
+ // Re-authenticate user if session exists
+ if (existingSession != null) {
+ AuthenticatedUser updatedUser = updateAuthenticatedUser(
+ existingSession.getAuthenticatedUser(), credentials);
+ return updatedUser;
}
- // Log and rethrow any authentication errors
- catch (GuacamoleException e) {
-
- fireAuthenticationFailedEvent(credentials);
-
- // Get request and username for sake of logging
- HttpServletRequest request = credentials.getRequest();
- String username = credentials.getUsername();
-
- // Log authentication failures with associated usernames
- if (username != null) {
- if (logger.isWarnEnabled())
- logger.warn("Authentication attempt from {} for user \"{}\" failed.",
- getLoggableAddress(request), username);
- }
-
- // Log anonymous authentication failures
- else if (logger.isDebugEnabled())
- logger.debug("Anonymous authentication attempt from {} failed.",
- getLoggableAddress(request));
-
- // Rethrow exception
- throw e;
-
- }
+ // Otherwise, attempt authentication as a new user
+ AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials);
+ return authenticatedUser;
}
@@ -371,15 +313,14 @@
* A List of all UserContexts associated with the given
* AuthenticatedUser.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If an error occurs while creating or updating any UserContext.
*/
private List<DecoratedUserContext> getUserContexts(GuacamoleSession existingSession,
AuthenticatedUser authenticatedUser, Credentials credentials)
- throws GuacamoleException {
+ throws GuacamoleAuthenticationProcessException {
- List<DecoratedUserContext> userContexts =
- new ArrayList<DecoratedUserContext>(authProviders.size());
+ List<DecoratedUserContext> userContexts = new ArrayList<>(authProviders.size());
// If UserContexts already exist, update them and add to the list
if (existingSession != null) {
@@ -392,7 +333,15 @@
// Update existing UserContext
AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider();
- UserContext updatedUserContext = authProvider.updateUserContext(oldUserContext, authenticatedUser, credentials);
+ UserContext updatedUserContext;
+ try {
+ updatedUserContext = authProvider.updateUserContext(oldUserContext, authenticatedUser, credentials);
+ }
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted during UserContext update.",
+ authProvider, e);
+ }
// Add to available data, if successful
if (updatedUserContext != null)
@@ -415,7 +364,15 @@
for (AuthenticationProvider authProvider : authProviders) {
// Generate new UserContext
- UserContext userContext = authProvider.getUserContext(authenticatedUser);
+ UserContext userContext;
+ try {
+ userContext = authProvider.getUserContext(authenticatedUser);
+ }
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted during initial "
+ + "UserContext creation.", authProvider, e);
+ }
// Add to available data, if successful
if (userContext != null)
@@ -453,7 +410,7 @@
* If the authentication or re-authentication attempt fails.
*/
public String authenticate(Credentials credentials, String token)
- throws GuacamoleException {
+ throws GuacamoleException {
// Pull existing session if token provided
GuacamoleSession existingSession;
@@ -462,25 +419,72 @@
else
existingSession = null;
- // Get up-to-date AuthenticatedUser and associated UserContexts
- AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials);
- List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, credentials);
-
- // Update existing session, if it exists
+ AuthenticatedUser authenticatedUser;
String authToken;
- if (existingSession != null) {
- authToken = token;
- existingSession.setAuthenticatedUser(authenticatedUser);
- existingSession.setUserContexts(userContexts);
+
+ try {
+
+ // Get up-to-date AuthenticatedUser and associated UserContexts
+ authenticatedUser = getAuthenticatedUser(existingSession, credentials);
+ List<DecoratedUserContext> userContexts = getUserContexts(existingSession, authenticatedUser, credentials);
+
+ // Update existing session, if it exists
+ if (existingSession != null) {
+ authToken = token;
+ existingSession.setAuthenticatedUser(authenticatedUser);
+ existingSession.setUserContexts(userContexts);
+ }
+
+ // If no existing session, generate a new token/session pair
+ else {
+ authToken = authTokenGenerator.getToken();
+ tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
+ logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
+ }
+
+ // Report authentication success
+ try {
+ listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser));
+ }
+ catch (GuacamoleException e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted by event listener.", null, e);
+ }
+
}
- // If no existing session, generate a new token/session pair
- else {
- authToken = authTokenGenerator.getToken();
- tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
- logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
+ // Log and rethrow any authentication errors
+ catch (GuacamoleAuthenticationProcessException e) {
+
+ // Get request and username for sake of logging
+ HttpServletRequest request = credentials.getRequest();
+ String username = credentials.getUsername();
+
+ listenerService.handleEvent(new AuthenticationFailureEvent(credentials,
+ e.getAuthenticationProvider(), e.getCause()));
+
+ // Log authentication failures with associated usernames
+ if (username != null) {
+ if (logger.isWarnEnabled())
+ logger.warn("Authentication attempt from {} for user \"{}\" failed.",
+ getLoggableAddress(request), username);
+ }
+
+ // Log anonymous authentication failures
+ else if (logger.isDebugEnabled())
+ logger.debug("Anonymous authentication attempt from {} failed.",
+ getLoggableAddress(request));
+
+ // Rethrow exception
+ throw e.getCauseAsGuacamoleException();
+
}
+ if (logger.isInfoEnabled())
+ logger.info("User \"{}\" successfully authenticated from {}.",
+ authenticatedUser.getIdentifier(),
+ getLoggableAddress(credentials.getRequest()));
+
return authToken;
}
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java
index d773068..2be283a 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecoratedUserContext.java
@@ -76,21 +76,29 @@
* given AuthenticationProvider, or the original UserContext if the
* given AuthenticationProvider originated the UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If the given AuthenticationProvider fails while decorating the
* UserContext.
*/
private static UserContext decorate(AuthenticationProvider authProvider,
UserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Skip the AuthenticationProvider which produced the UserContext
// being decorated
if (authProvider != userContext.getAuthenticationProvider()) {
// Apply layer of wrapping around UserContext
- UserContext decorated = authProvider.decorate(userContext,
- authenticatedUser, credentials);
+ UserContext decorated;
+ try {
+ decorated = authProvider.decorate(userContext,
+ authenticatedUser, credentials);
+ }
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted by decorating UserContext.",
+ authProvider, e);
+ }
// Do not allow misbehaving extensions to wipe out the
// UserContext entirely
@@ -130,13 +138,13 @@
* given AuthenticationProvider, or the original UserContext if the
* given AuthenticationProvider originated the UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If the given AuthenticationProvider fails while decorating the
* UserContext.
*/
private static UserContext redecorate(DecoratedUserContext decorated,
UserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
AuthenticationProvider authProvider = decorated.getDecoratingAuthenticationProvider();
@@ -145,8 +153,16 @@
if (authProvider != userContext.getAuthenticationProvider()) {
// Apply next layer of wrapping around UserContext
- UserContext redecorated = authProvider.redecorate(decorated,
- userContext, authenticatedUser, credentials);
+ UserContext redecorated;
+ try {
+ redecorated = authProvider.redecorate(decorated.getDelegateUserContext(),
+ userContext, authenticatedUser, credentials);
+ }
+ catch (GuacamoleException | RuntimeException | Error e) {
+ throw new GuacamoleAuthenticationProcessException("User "
+ + "authentication aborted by redecorating UserContext.",
+ authProvider, e);
+ }
// Do not allow misbehaving extensions to wipe out the
// UserContext entirely
@@ -181,13 +197,13 @@
* The credentials associated with the request which produced the given
* UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any of the given AuthenticationProviders fails while decorating
* the UserContext.
*/
public DecoratedUserContext(AuthenticationProvider authProvider,
UserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Wrap the result of invoking decorate() on the given AuthenticationProvider
super(decorate(authProvider, userContext, authenticatedUser, credentials));
@@ -221,13 +237,13 @@
* The credentials associated with the request which produced the given
* UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any of the given AuthenticationProviders fails while decorating
* the UserContext.
*/
public DecoratedUserContext(AuthenticationProvider authProvider,
DecoratedUserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Wrap the result of invoking decorate() on the given AuthenticationProvider
super(decorate(authProvider, userContext, authenticatedUser, credentials));
@@ -261,13 +277,13 @@
* The credentials associated with the request which produced the given
* UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any of the given AuthenticationProviders fails while decorating
* the UserContext.
*/
public DecoratedUserContext(DecoratedUserContext decorated,
UserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Wrap the result of invoking redecorate() on the given AuthenticationProvider
super(redecorate(decorated, userContext, authenticatedUser, credentials));
@@ -303,13 +319,13 @@
* The credentials associated with the request which produced the given
* UserContext.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any of the given AuthenticationProviders fails while decorating
* the UserContext.
*/
public DecoratedUserContext(DecoratedUserContext decorated,
DecoratedUserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Wrap the result of invoking redecorate() on the given AuthenticationProvider
super(redecorate(decorated, userContext, authenticatedUser, credentials));
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java
index b28dc03..0b7fc12 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/DecorationService.java
@@ -23,7 +23,6 @@
import java.util.List;
import javax.inject.Inject;
-import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
@@ -65,12 +64,12 @@
* A new DecoratedUserContext which has been decorated by all
* AuthenticationProviders.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any AuthenticationProvider fails while decorating the UserContext.
*/
public DecoratedUserContext decorate(UserContext userContext,
AuthenticatedUser authenticatedUser, Credentials credentials)
- throws GuacamoleException {
+ throws GuacamoleAuthenticationProcessException {
// Get first AuthenticationProvider in list
Iterator<AuthenticationProvider> current = authProviders.iterator();
@@ -119,12 +118,12 @@
* A new DecoratedUserContext which has been decorated by all
* AuthenticationProviders.
*
- * @throws GuacamoleException
+ * @throws GuacamoleAuthenticationProcessException
* If any AuthenticationProvider fails while decorating the UserContext.
*/
public DecoratedUserContext redecorate(DecoratedUserContext decorated,
UserContext userContext, AuthenticatedUser authenticatedUser,
- Credentials credentials) throws GuacamoleException {
+ Credentials credentials) throws GuacamoleAuthenticationProcessException {
// If the given DecoratedUserContext contains further decorated layers,
// redecorate those first
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java
new file mode 100644
index 0000000..ba1d72a
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/GuacamoleAuthenticationProcessException.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.rest.auth;
+
+import java.io.Serializable;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.net.auth.AuthenticationProvider;
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
+import org.apache.guacamole.protocol.GuacamoleStatus;
+
+/**
+ * An exception that occurs during Guacamole's authentication and authorization
+ * process, possibly associated with a specific AuthenticationProvider.
+ */
+public class GuacamoleAuthenticationProcessException extends GuacamoleException {
+
+ /**
+ * Internal identifier unique to this version of
+ * GuacamoleAuthenticationProcessException, as required by Java's
+ * {@link Serializable} interface.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The AuthenticationProvider that caused the failure, or null if there is
+ * no such specific AuthenticationProvider involved in this failure.
+ */
+ private final transient AuthenticationProvider authProvider;
+
+ /**
+ * A GuacamoleException representation of the failure that occurred. If
+ * the cause provided when this GuacamoleAuthenticationProcessException
+ * was created was a GuacamoleException, this will just be that exception.
+ * Otherwise, this will be a GuacamoleServerException wrapping the cause
+ * or a generic GuacamoleInvalidCredentialsException requesting a
+ * username/password if there is no specific cause at all.
+ */
+ private final GuacamoleException guacCause;
+
+ /**
+ * Converts the given Throwable to a GuacamoleException representing the
+ * failure that occurred. If the Throwable already is a GuacamoleException,
+ * this will just be that Throwable. For all other cases, a new
+ * GuacamoleException will be created that best represents the provided
+ * failure. If no failure is provided at all, a generic
+ * GuacamoleInvalidCredentialsException requesting a username/password is
+ * created.
+ *
+ * @param message
+ * A human-readable message describing the failure that occurred.
+ *
+ * @param cause
+ * The Throwable cause of the failure that occurred, if any, or null if
+ * the cause is not known to be a specific Throwable.
+ *
+ * @return
+ * A GuacamoleException representation of the message and cause
+ * provided.
+ */
+ private static GuacamoleException toGuacamoleException(String message,
+ Throwable cause) {
+
+ // Create generic invalid username/password exception if we have no
+ // specific cause
+ if (cause == null)
+ return new GuacamoleInvalidCredentialsException(
+ "Permission Denied.",
+ CredentialsInfo.USERNAME_PASSWORD
+ );
+
+ // If the specific cause is already a GuacamoleException, there's
+ // nothing for us to do here
+ if (cause instanceof GuacamoleException)
+ return (GuacamoleException) cause;
+
+ // Wrap all other Throwables as generic internal errors
+ return new GuacamoleServerException(message, cause);
+
+ }
+
+ /**
+ * Creates a new GuacamoleAuthenticationProcessException with the given
+ * message, associated AuthenticationProvider, and cause.
+ *
+ * @param message
+ * A human readable description of the exception that occurred.
+ *
+ * @param authProvider
+ * The AuthenticationProvider that caused the failure, or null if there
+ * is no such specific AuthenticationProvider involved in this failure.
+ *
+ * @param cause
+ * The cause of this exception, or null if the cause is unknown or
+ * there is no such cause.
+ */
+ public GuacamoleAuthenticationProcessException(String message,
+ AuthenticationProvider authProvider, Throwable cause) {
+ super(message, cause);
+ this.authProvider = authProvider;
+ this.guacCause = toGuacamoleException(message, cause);
+ }
+
+ /**
+ * Returns the AuthenticationProvider that caused the failure, if any. If
+ * there is no specific AuthenticationProvider involved in this failure,
+ * including if the failure is due to multiple AuthenticationProviders,
+ * this will be null.
+ *
+ * @return
+ * The AuthenticationProvider that caused the failure, or null if there
+ * is no such specific AuthenticationProvider involved in this failure.
+ */
+ public AuthenticationProvider getAuthenticationProvider() {
+ return authProvider;
+ }
+
+ /**
+ * Returns a GuacamoleException that represents the user-facing cause of
+ * this exception. A GuacamoleException will be returned by this function
+ * in all cases, including if no specific cause was given.
+ *
+ * @return
+ * A GuacamoleException that represents the user-facing cause of this
+ * exception.
+ */
+ public GuacamoleException getCauseAsGuacamoleException() {
+ return guacCause;
+ }
+
+ @Override
+ public GuacamoleStatus getStatus() {
+ return getCauseAsGuacamoleException().getStatus();
+ }
+
+ @Override
+ public int getHttpStatusCode() {
+ return getCauseAsGuacamoleException().getHttpStatusCode();
+ }
+
+ @Override
+ public int getWebSocketCode() {
+ return getCauseAsGuacamoleException().getWebSocketCode();
+ }
+
+}