GUACAMOLE-990: Fire auth success/failure events only after authentication has absolutely succeeded or failed, including the details of any failure.

Previously, these events were fired only after the user's identity had
been determined (or failed to be determined). If we don't wait until
after the user contexts have also been successfully obtained (or failed
to be obtained), then things like MFA will not be taken into account
for auth events.
This commit is contained in:
Michael Jumper
2022-08-18 12:20:32 -07:00
parent 0e5a3cb74f
commit e6a61b7223
8 changed files with 509 additions and 146 deletions

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.getDelegateUserContext(),
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();
}
}