diff --git a/extensions/guacamole-auth-ban/.ratignore b/extensions/guacamole-auth-ban/.ratignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/guacamole-auth-ban/pom.xml b/extensions/guacamole-auth-ban/pom.xml
new file mode 100644
index 000000000..2be082fc2
--- /dev/null
+++ b/extensions/guacamole-auth-ban/pom.xml
@@ -0,0 +1,60 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-auth-ban
+ jar
+ 1.4.0
+ guacamole-auth-ban
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ extensions
+ 1.4.0
+ ../
+
+
+
+
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+
+
+ org.apache.guacamole
+ guacamole-ext
+ 1.4.0
+ provided
+
+
+
+
+
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 000000000..d046ae699
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/assembly/dist.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ dist
+ ${project.artifactId}-${project.version}
+
+
+
+ tar.gz
+
+
+
+
+
+
+
+
+ target/licenses
+
+
+
+
+ target
+
+
+ *.jar
+
+
+
+
+
+
diff --git a/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureStatus.java
new file mode 100644
index 000000000..87bdc5515
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/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;
+
+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 represents a single failure
+ * 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(1);
+ 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/AuthenticationFailureTracker.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java
new file mode 100644
index 000000000..4f77875fc
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/AuthenticationFailureTracker.java
@@ -0,0 +1,278 @@
+/*
+ * 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 java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.guacamole.GuacamoleClientTooManyException;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.net.auth.Credentials;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides automated tracking and blocking of IP addresses that repeatedly
+ * fail authentication.
+ */
+public class AuthenticationFailureTracker {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(AuthenticationFailureTracker.class);
+
+ /**
+ * All authentication failures currently being tracked, stored by the
+ * associated IP address.
+ */
+ private final ConcurrentMap failures =
+ new ConcurrentHashMap<>();
+
+ /**
+ * 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.
+ */
+ public AuthenticationFailureTracker(int maxAttempts, int banDuration) {
+ this.maxAttempts = maxAttempts;
+ this.banDuration = banDuration;
+
+ // Inform administrator of configured behavior
+ if (maxAttempts <= 0) {
+ 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) {
+ 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 {
+ logger.info("Addresses will be automatically banned for {} "
+ + "seconds after {} failed authentication attempts.",
+ banDuration, maxAttempts);
+ }
+
+ }
+
+ /**
+ * 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 newFailure = new AuthenticationFailureStatus(maxAttempts, banDuration);
+ AuthenticationFailureStatus status = failures.putIfAbsent(address, newFailure);
+ if (status == null)
+ return newFailure;
+
+ 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 {
+
+ // Do not track/ban if tracking or banning are disabled
+ if (maxAttempts <= 0 || banDuration <= 0)
+ return;
+
+ // 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.debug("Authentication has failed for address \"{}\" (current total failures: {}/{}).",
+ address, status.getFailures(), maxAttempts);
+ }
+ else
+ status = failures.get(address);
+
+ if (status != null) {
+
+ // Explicitly block further processing of authentication/authorization
+ // if too many failures have occurred
+ if (status.isBlocked()) {
+ logger.debug("Blocking authentication attempt from address \"{}\" due to number of authentication failures.", address);
+ throw new GuacamoleClientTooManyException("Too many failed "
+ + "authentication attempts. Please try again later.");
+ }
+
+ // 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.remove(address);
+ }
+
+ }
+
+ }
+
+ /**
+ * 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.
+ */
+ public void notifyAuthenticationRequestReceived(Credentials credentials)
+ throws GuacamoleException {
+ notifyAuthenticationStatus(credentials, false);
+ }
+
+ /**
+ * 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.
+ */
+ public void notifyAuthenticationSuccess(Credentials credentials)
+ throws GuacamoleException {
+ notifyAuthenticationStatus(credentials, false);
+ }
+
+ /**
+ * 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.
+ */
+ 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/BanningAuthenticationListener.java b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java
new file mode 100644
index 000000000..38fd575fd
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationListener.java
@@ -0,0 +1,81 @@
+/*
+ * 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.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 000000000..12195f5e4
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/java/org/apache/guacamole/auth/ban/BanningAuthenticationProvider.java
@@ -0,0 +1,121 @@
+/*
+ * 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.GuacamoleException;
+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;
+
+/**
+ * AuthenticationProvider implementation that blocks further authentication
+ * attempts that are related to past authentication failures flagged by
+ * {@link BanningAuthenticationListener}.
+ */
+public class BanningAuthenticationProvider extends AbstractAuthenticationProvider {
+
+ /**
+ * 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 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;
+
+ /**
+ * 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);
+
+ tracker = new AuthenticationFailureTracker(maxAttempts, banDuration);
+ 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/resources/guac-manifest.json b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json
new file mode 100644
index 000000000..87f75ea61
--- /dev/null
+++ b/extensions/guacamole-auth-ban/src/main/resources/guac-manifest.json
@@ -0,0 +1,16 @@
+{
+
+ "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"
+ ]
+
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 3bab33257..b16b3ed53 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -40,6 +40,7 @@
+ guacamole-auth-ban
guacamole-auth-duo
guacamole-auth-header
guacamole-auth-jdbc