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

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