blob: 305073de39252010e8e19d19d6685057598b319c [file] [log] [blame]
/*
* 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.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.GuacamoleSession;
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.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationRequestReceivedEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
import org.apache.guacamole.rest.event.ListenerService;
import org.glassfish.jersey.server.ContainerRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A service for performing authentication checks in REST endpoints.
*/
public class AuthenticationService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class);
/**
* All configured authentication providers which can be used to
* authenticate users or retrieve data associated with authenticated users.
*/
@Inject
private List<AuthenticationProvider> authProviders;
/**
* The map of auth tokens to sessions for the REST endpoints.
*/
@Inject
private TokenSessionMap tokenSessionMap;
/**
* A generator for creating new auth tokens.
*/
@Inject
private AuthTokenGenerator authTokenGenerator;
/**
* Service for applying or reapplying layers of decoration.
*/
@Inject
private DecorationService decorationService;
/**
* The service to use to notify registered authentication listeners.
*/
@Inject
private ListenerService listenerService;
/**
* The name of the HTTP header that may contain the authentication token
* used by the Guacamole REST API.
*/
public static final String TOKEN_HEADER_NAME = "Guacamole-Token";
/**
* The name of the query parameter that may contain the authentication
* token used by the Guacamole REST API.
*/
public static final String TOKEN_PARAMETER_NAME = "token";
/**
* Attempts authentication against all AuthenticationProviders, in order,
* using the provided credentials. The first authentication failure takes
* priority, but remaining AuthenticationProviders are attempted. If any
* AuthenticationProvider succeeds, the resulting AuthenticatedUser is
* returned, and no further AuthenticationProviders are tried.
*
* @param credentials
* The credentials to use for authentication.
*
* @return
* The AuthenticatedUser given by the highest-priority
* AuthenticationProvider for which the given credentials are valid.
*
* @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 GuacamoleAuthenticationProcessException {
AuthenticationProvider failedAuthProvider = null;
GuacamoleCredentialsException authFailure = null;
// Attempt authentication against each AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Attempt authentication
try {
AuthenticatedUser authenticatedUser = authProvider.authenticateUser(credentials);
if (authenticatedUser != null)
return authenticatedUser;
}
// Insufficient credentials should take precedence
catch (GuacamoleInsufficientCredentialsException e) {
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) {
failedAuthProvider = authProvider;
authFailure = e;
}
}
catch (GuacamoleException | RuntimeException | Error e) {
throw new GuacamoleAuthenticationProcessException("User "
+ "authentication was aborted.", authProvider, e);
}
}
throw new GuacamoleAuthenticationProcessException("User authentication "
+ "failed.", failedAuthProvider, authFailure);
}
/**
* Re-authenticates the given AuthenticatedUser against the
* AuthenticationProvider that originally created it, using the given
* Credentials.
*
* @param authenticatedUser
* The AuthenticatedUser to re-authenticate.
*
* @param credentials
* The Credentials to use to re-authenticate the user.
*
* @return
* A AuthenticatedUser which may have been updated due to re-
* authentication.
*
* @throws GuacamoleAuthenticationProcessException
* If an error prevents the user from being re-authenticated.
*/
private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Get original AuthenticationProvider
AuthenticationProvider authProvider = authenticatedUser.getAuthenticationProvider();
try {
// 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;
}
catch (GuacamoleException | RuntimeException | Error e) {
throw new GuacamoleAuthenticationProcessException("User re-authentication failed.", authProvider, e);
}
}
/**
* Returns the AuthenticatedUser associated with the given session and
* credentials, performing a fresh authentication and creating a new
* AuthenticatedUser if necessary.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param credentials
* The Credentials to use to authenticate the user.
*
* @return
* The AuthenticatedUser associated with the given session and
* credentials.
*
* @throws GuacamoleAuthenticationProcessException
* If an error occurs while authenticating or re-authenticating the
* user.
*/
private AuthenticatedUser getAuthenticatedUser(GuacamoleSession existingSession,
Credentials credentials) throws GuacamoleAuthenticationProcessException {
// Re-authenticate user if session exists
if (existingSession != null) {
AuthenticatedUser updatedUser = updateAuthenticatedUser(
existingSession.getAuthenticatedUser(), credentials);
return updatedUser;
}
// Otherwise, attempt authentication as a new user
AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials);
return authenticatedUser;
}
/**
* Returns all UserContexts associated with the given AuthenticatedUser,
* updating existing UserContexts, if any. If no UserContexts are yet
* associated with the given AuthenticatedUser, new UserContexts are
* generated by polling each available AuthenticationProvider.
*
* @param existingSession
* The current GuacamoleSession, or null if no session exists yet.
*
* @param authenticatedUser
* The AuthenticatedUser that has successfully authenticated or re-
* authenticated.
*
* @param credentials
* The Credentials provided by the user in the most recent
* authentication attempt.
*
* @return
* A List of all UserContexts associated with the given
* AuthenticatedUser.
*
* @throws GuacamoleAuthenticationProcessException
* If an error occurs while creating or updating any UserContext.
*/
private List<DecoratedUserContext> getUserContexts(GuacamoleSession existingSession,
AuthenticatedUser authenticatedUser, Credentials credentials)
throws GuacamoleAuthenticationProcessException {
List<DecoratedUserContext> userContexts = new ArrayList<>(authProviders.size());
// If UserContexts already exist, update them and add to the list
if (existingSession != null) {
// Update all old user contexts
List<DecoratedUserContext> oldUserContexts = existingSession.getUserContexts();
for (DecoratedUserContext userContext : oldUserContexts) {
UserContext oldUserContext = userContext.getUndecoratedUserContext();
// Update existing UserContext
AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider();
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)
userContexts.add(decorationService.redecorate(userContext,
updatedUserContext, authenticatedUser, credentials));
// If unsuccessful, log that this happened, as it may be a bug
else
logger.debug("AuthenticationProvider \"{}\" retroactively destroyed its UserContext.",
authProvider.getClass().getName());
}
}
// Otherwise, create new UserContexts from available AuthenticationProviders
else {
// Get UserContexts from each available AuthenticationProvider
for (AuthenticationProvider authProvider : authProviders) {
// Generate new UserContext
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)
userContexts.add(decorationService.decorate(userContext,
authenticatedUser, credentials));
}
}
return userContexts;
}
/**
* Authenticates a user using the given credentials and optional
* authentication token, returning the authentication token associated with
* the user's Guacamole session, which may be newly generated. If an
* existing token is provided, the authentication procedure will attempt to
* update or reuse the provided token, but it is possible that a new token
* will be returned. Note that this function CANNOT return null.
*
* @param credentials
* The credentials to use when authenticating the user.
*
* @param token
* The authentication token to use if attempting to re-authenticate an
* existing session, or null to request a new token.
*
* @return
* The authentication token associated with the newly created or
* existing session.
*
* @throws GuacamoleException
* If the authentication or re-authentication attempt fails.
*/
public String authenticate(Credentials credentials, String token)
throws GuacamoleException {
// Fire pre-authentication event before ANY authn/authz occurs at all
listenerService.handleEvent((AuthenticationRequestReceivedEvent) () -> credentials);
// Pull existing session if token provided
GuacamoleSession existingSession;
if (token != null)
existingSession = tokenSessionMap.get(token);
else
existingSession = null;
AuthenticatedUser authenticatedUser;
String authToken;
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(listenerService, authenticatedUser, userContexts));
}
// Report authentication success
try {
listenerService.handleEvent(new AuthenticationSuccessEvent(authenticatedUser,
existingSession != null));
}
catch (GuacamoleException e) {
throw new GuacamoleAuthenticationProcessException("User "
+ "authentication aborted by event listener.", null, e);
}
}
// Log and rethrow any authentication errors
catch (GuacamoleAuthenticationProcessException e) {
listenerService.handleEvent(new AuthenticationFailureEvent(credentials,
e.getAuthenticationProvider(), e.getCause()));
// Rethrow exception
e.rethrowCause();
// This line SHOULD be unreachable unless a bug causes
// rethrowCause() to not actually rethrow the underlying failure
Throwable cause = e.getCause();
if (cause != null) {
logger.warn("An underlying internal error was not correctly rethrown by rethrowCause(): {}", cause.getMessage());
logger.debug("Internal error not rethrown by rethrowCause().", cause);
}
else
logger.warn("An underlying internal error was not correctly rethrown by rethrowCause().");
throw e.getCauseAsGuacamoleException();
}
return authToken;
}
/**
* Finds the Guacamole session for a given auth token, if the auth token
* represents a currently logged in user. Throws an unauthorized error
* otherwise.
*
* @param authToken The auth token to check against the map of logged in users.
* @return The session that corresponds to the provided auth token.
* @throws GuacamoleException If the auth token does not correspond to any
* logged in user.
*/
public GuacamoleSession getGuacamoleSession(String authToken)
throws GuacamoleException {
// Try to get the session from the map of logged in users.
GuacamoleSession session = tokenSessionMap.get(authToken);
// Authentication failed.
if (session == null)
throw new GuacamoleUnauthorizedException("Permission Denied.");
return session;
}
/**
* Invalidates a specific authentication token and its corresponding
* Guacamole session, effectively logging out the associated user. If the
* authentication token is not valid, this function has no effect.
*
* @param authToken
* The token being invalidated.
*
* @return
* true if the given authentication token was valid and the
* corresponding Guacamole session was destroyed, false if the given
* authentication token was not valid and no action was taken.
*/
public boolean destroyGuacamoleSession(String authToken) {
// Remove corresponding GuacamoleSession if the token is valid
GuacamoleSession session = tokenSessionMap.remove(authToken);
if (session == null)
return false;
// Invalidate the removed session
session.invalidate();
return true;
}
/**
* Returns all UserContexts associated with a given auth token, if the auth
* token represents a currently logged in user. Throws an unauthorized
* error otherwise.
*
* @param authToken
* The auth token to check against the map of logged in users.
*
* @return
* A List of all UserContexts associated with the provided auth token.
*
* @throws GuacamoleException
* If the auth token does not correspond to any logged in user.
*/
public List<DecoratedUserContext> getUserContexts(String authToken)
throws GuacamoleException {
return getGuacamoleSession(authToken).getUserContexts();
}
/**
* Returns the authentication token sent within the given request, if
* present, or null if otherwise. Authentication tokens may be sent via
* the "Guacamole-Token" header or the "token" query parameter. If both
* the header and a parameter are used, the header is given priority.
*
* @param request
* The HTTP request to retrieve the authentication token from.
*
* @return
* The authentication token within the given request, or null if no
* token is present.
*/
public String getAuthenticationToken(ContainerRequest request) {
// Give priority to token within HTTP header
String token = request.getHeaderString(TOKEN_HEADER_NAME);
if (token != null && !token.isEmpty())
return token;
// If no token was provided via HTTP headers, fall back to using
// query parameters
token = request.getUriInfo().getQueryParameters().getFirst(TOKEN_PARAMETER_NAME);
if (token != null && !token.isEmpty())
return token;
return null;
}
}