GUACAMOLE-990: Merge new guacamole-auth-ban extension to block brute-force auth attempts.

This commit is contained in:
James Muehlner
2022-08-22 15:57:33 -07:00
committed by GitHub
30 changed files with 1492 additions and 148 deletions

View File

@@ -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

View File

@@ -0,0 +1 @@
com.github.ben-manes.caffeine:caffeine:jar:2.9.3

View File

@@ -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.

View File

@@ -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)

View File

@@ -0,0 +1 @@
org.checkerframework:checker-qual:jar:3.19.0

View File

@@ -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

View File

@@ -0,0 +1 @@
com.google.errorprone:error_prone_annotations:jar:2.10.0

View File

View File

@@ -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>

View File

@@ -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>

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,5 @@
{
"LOGIN": {
"ERROR_TOO_MANY_ATTEMPTS" : "Too many failed authentication attempts. Please try again later."
}
}

View File

@@ -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>

View File

@@ -198,3 +198,13 @@ if [ -f extensions/guacamole-auth-json/target/guacamole-auth-json*.jar ]; then
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

View File

@@ -1160,6 +1160,18 @@ if [ -n "$API_SESSION_TIMEOUT" ]; then
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

View File

@@ -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 @@ public class AuthenticationFailureEvent implements CredentialEvent {
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;
}
}

View File

@@ -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();
}

View File

@@ -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 @@ import org.apache.guacamole.net.auth.Credentials;
* 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 @@ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent {
@Override
public Credentials getCredentials() {
return authenticatedUser.getCredentials();
return getAuthenticatedUser().getCredentials();
}
@Override
public AuthenticationProvider getAuthenticationProvider() {
return getAuthenticatedUser().getAuthenticationProvider();
}
}

View File

@@ -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();
}

View File

@@ -198,12 +198,16 @@ angular.module('auth').factory('authenticationService', ['$injector',
['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)

View File

@@ -34,7 +34,6 @@ import org.apache.guacamole.net.auth.AuthenticatedUser;
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 @@ public class AuthenticationService {
* 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 @@ public class AuthenticationService {
// 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 @@ public class AuthenticationService {
* 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 @@ public class AuthenticationService {
* 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 {
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;
Credentials credentials) throws GuacamoleAuthenticationProcessException {
// 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 @@ public class AuthenticationService {
* 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 @@ public class AuthenticationService {
// 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 @@ public class AuthenticationService {
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 @@ public class AuthenticationService {
* 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 @@ public class AuthenticationService {
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;
}

View File

@@ -76,21 +76,29 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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 @@ public class DecoratedUserContext extends DelegatingUserContext {
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 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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 @@ public class DecoratedUserContext extends DelegatingUserContext {
* 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));

View File

@@ -23,7 +23,6 @@ import java.util.Iterator;
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 @@ public class DecorationService {
* 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 @@ public class DecorationService {
* 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

View File

@@ -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();
}
}