mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-07 21:51:23 +00:00
GUAC-586: Authenticate against multiple AuthenticationProviders (data not yet aggregated, however).
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2014 Glyptodon LLC
|
* Copyright (C) 2015 Glyptodon LLC
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -39,12 +39,14 @@ import javax.ws.rs.core.MediaType;
|
|||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.xml.bind.DatatypeConverter;
|
import javax.xml.bind.DatatypeConverter;
|
||||||
import org.glyptodon.guacamole.GuacamoleException;
|
import org.glyptodon.guacamole.GuacamoleException;
|
||||||
|
import org.glyptodon.guacamole.GuacamoleSecurityException;
|
||||||
import org.glyptodon.guacamole.environment.Environment;
|
import org.glyptodon.guacamole.environment.Environment;
|
||||||
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
|
import org.glyptodon.guacamole.net.auth.AuthenticatedUser;
|
||||||
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
import org.glyptodon.guacamole.net.auth.AuthenticationProvider;
|
||||||
import org.glyptodon.guacamole.net.auth.Credentials;
|
import org.glyptodon.guacamole.net.auth.Credentials;
|
||||||
import org.glyptodon.guacamole.net.auth.UserContext;
|
import org.glyptodon.guacamole.net.auth.UserContext;
|
||||||
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
|
import org.glyptodon.guacamole.net.auth.credentials.CredentialsInfo;
|
||||||
|
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleCredentialsException;
|
||||||
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
import org.glyptodon.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
|
||||||
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
|
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
|
||||||
import org.glyptodon.guacamole.net.basic.rest.APIError;
|
import org.glyptodon.guacamole.net.basic.rest.APIError;
|
||||||
@@ -58,6 +60,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
* A service for managing auth tokens via the Guacamole REST API.
|
* A service for managing auth tokens via the Guacamole REST API.
|
||||||
*
|
*
|
||||||
* @author James Muehlner
|
* @author James Muehlner
|
||||||
|
* @author Michael Jumper
|
||||||
*/
|
*/
|
||||||
@Path("/tokens")
|
@Path("/tokens")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@@ -139,12 +142,299 @@ public class TokenRESTService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the credentials associated with the given request, using the
|
||||||
|
* provided username and password.
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* The request to use to derive the credentials.
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* The username to associate with the credentials, or null if the
|
||||||
|
* username should be derived from the request.
|
||||||
|
*
|
||||||
|
* @param password
|
||||||
|
* The password to associate with the credentials, or null if the
|
||||||
|
* password should be derived from the request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A new Credentials object whose contents have been derived from the
|
||||||
|
* given request, along with the provided username and password.
|
||||||
|
*/
|
||||||
|
private Credentials getCredentials(HttpServletRequest request,
|
||||||
|
String username, String password) {
|
||||||
|
|
||||||
|
// If no username/password given, try Authorization header
|
||||||
|
if (username == null && password == null) {
|
||||||
|
|
||||||
|
String authorization = request.getHeader("Authorization");
|
||||||
|
if (authorization != null && authorization.startsWith("Basic ")) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Decode base64 authorization
|
||||||
|
String basicBase64 = authorization.substring(6);
|
||||||
|
String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8");
|
||||||
|
|
||||||
|
// Pull username/password from auth data
|
||||||
|
int colon = basicCredentials.indexOf(':');
|
||||||
|
if (colon != -1) {
|
||||||
|
username = basicCredentials.substring(0, colon);
|
||||||
|
password = basicCredentials.substring(colon + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.debug("Invalid HTTP Basic \"Authorization\" header received.");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 support is required by the Java specification
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end Authorization header fallback
|
||||||
|
|
||||||
|
// Build credentials
|
||||||
|
Credentials credentials = new Credentials();
|
||||||
|
credentials.setUsername(username);
|
||||||
|
credentials.setPassword(password);
|
||||||
|
credentials.setRequest(request);
|
||||||
|
credentials.setSession(request.getSession(true));
|
||||||
|
|
||||||
|
return credentials;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 GuacamoleException
|
||||||
|
* 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 {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First failure takes priority for now
|
||||||
|
catch (GuacamoleCredentialsException e) {
|
||||||
|
if (authFailure == null)
|
||||||
|
authFailure = 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
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 GuacamoleException
|
||||||
|
* If an error prevents the user from being re-authenticated.
|
||||||
|
*/
|
||||||
|
private AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser authenticatedUser,
|
||||||
|
Credentials credentials) throws GuacamoleException {
|
||||||
|
|
||||||
|
// 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.");
|
||||||
|
|
||||||
|
return authenticatedUser;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 GuacamoleException
|
||||||
|
* 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)
|
||||||
|
return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials);
|
||||||
|
|
||||||
|
// Otherwise, attempt authentication as a new user
|
||||||
|
AuthenticatedUser authenticatedUser = authenticateUser(credentials);
|
||||||
|
if (logger.isInfoEnabled())
|
||||||
|
logger.info("User \"{}\" successfully authenticated from {}.",
|
||||||
|
authenticatedUser.getIdentifier(),
|
||||||
|
getLoggableAddress(credentials.getRequest()));
|
||||||
|
|
||||||
|
return authenticatedUser;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log and rethrow any authentication errors
|
||||||
|
catch (GuacamoleException e) {
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A List of all UserContexts associated with the given
|
||||||
|
* AuthenticatedUser.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If an error occurs while creating or updating any UserContext.
|
||||||
|
*/
|
||||||
|
private List<UserContext> getUserContexts(GuacamoleSession existingSession,
|
||||||
|
AuthenticatedUser authenticatedUser) throws GuacamoleException {
|
||||||
|
|
||||||
|
List<UserContext> userContexts = new ArrayList<UserContext>(authProviders.size());
|
||||||
|
|
||||||
|
// If UserContexts already exist, update them and add to the list
|
||||||
|
if (existingSession != null) {
|
||||||
|
|
||||||
|
// Update all old user contexts
|
||||||
|
List<UserContext> oldUserContexts = existingSession.getUserContexts();
|
||||||
|
for (UserContext oldUserContext : oldUserContexts) {
|
||||||
|
|
||||||
|
// Update existing UserContext
|
||||||
|
AuthenticationProvider authProvider = oldUserContext.getAuthenticationProvider();
|
||||||
|
UserContext userContext = authProvider.updateUserContext(oldUserContext, authenticatedUser);
|
||||||
|
|
||||||
|
// Add to available data, if successful
|
||||||
|
if (userContext != null)
|
||||||
|
userContexts.add(userContext);
|
||||||
|
|
||||||
|
// 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 = authProvider.getUserContext(authenticatedUser);
|
||||||
|
|
||||||
|
// Add to available data, if successful
|
||||||
|
if (userContext != null)
|
||||||
|
userContexts.add(userContext);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return userContexts;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticates a user, generates an auth token, associates that auth token
|
* Authenticates a user, generates an auth token, associates that auth token
|
||||||
* with the user's UserContext for use by further requests. If an existing
|
* with the user's UserContext for use by further requests. If an existing
|
||||||
* token is provided, the authentication procedure will attempt to update
|
* token is provided, the authentication procedure will attempt to update
|
||||||
* or reuse the provided token.
|
* or reuse the provided token.
|
||||||
*
|
*
|
||||||
* @param username
|
* @param username
|
||||||
* The username of the user who is to be authenticated.
|
* The username of the user who is to be authenticated.
|
||||||
*
|
*
|
||||||
@@ -180,7 +470,7 @@ public class TokenRESTService {
|
|||||||
|
|
||||||
// Reconstitute the HTTP request with the map of parameters
|
// Reconstitute the HTTP request with the map of parameters
|
||||||
HttpServletRequest request = new APIRequest(consumedRequest, parameters);
|
HttpServletRequest request = new APIRequest(consumedRequest, parameters);
|
||||||
|
|
||||||
// Pull existing session if token provided
|
// Pull existing session if token provided
|
||||||
GuacamoleSession existingSession;
|
GuacamoleSession existingSession;
|
||||||
if (token != null)
|
if (token != null)
|
||||||
@@ -188,111 +478,12 @@ public class TokenRESTService {
|
|||||||
else
|
else
|
||||||
existingSession = null;
|
existingSession = null;
|
||||||
|
|
||||||
// If no username/password given, try Authorization header
|
// Build credentials from request
|
||||||
if (username == null && password == null) {
|
Credentials credentials = getCredentials(request, username, password);
|
||||||
|
|
||||||
String authorization = request.getHeader("Authorization");
|
// Get up-to-date AuthenticatedUser and associated UserContexts
|
||||||
if (authorization != null && authorization.startsWith("Basic ")) {
|
AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials);
|
||||||
|
List<UserContext> userContexts = getUserContexts(existingSession, authenticatedUser);
|
||||||
try {
|
|
||||||
|
|
||||||
// Decode base64 authorization
|
|
||||||
String basicBase64 = authorization.substring(6);
|
|
||||||
String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8");
|
|
||||||
|
|
||||||
// Pull username/password from auth data
|
|
||||||
int colon = basicCredentials.indexOf(':');
|
|
||||||
if (colon != -1) {
|
|
||||||
username = basicCredentials.substring(0, colon);
|
|
||||||
password = basicCredentials.substring(colon + 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
logger.debug("Invalid HTTP Basic \"Authorization\" header received.");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// UTF-8 support is required by the Java specification
|
|
||||||
catch (UnsupportedEncodingException e) {
|
|
||||||
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // end Authorization header fallback
|
|
||||||
|
|
||||||
// Build credentials
|
|
||||||
Credentials credentials = new Credentials();
|
|
||||||
credentials.setUsername(username);
|
|
||||||
credentials.setPassword(password);
|
|
||||||
credentials.setRequest(request);
|
|
||||||
credentials.setSession(request.getSession(true));
|
|
||||||
|
|
||||||
AuthenticatedUser authenticatedUser = null;
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Re-authenticate user if session exists
|
|
||||||
if (existingSession != null) {
|
|
||||||
authenticatedUser = existingSession.getAuthenticatedUser();
|
|
||||||
authenticatedUser = authenticatedUser.getAuthenticationProvider().updateAuthenticatedUser(authenticatedUser, credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, attempt authentication as a new user against each
|
|
||||||
// AuthenticationProvider, in deterministic order
|
|
||||||
else {
|
|
||||||
for (AuthenticationProvider authProvider : authProviders) {
|
|
||||||
|
|
||||||
// Attempt authentication
|
|
||||||
authenticatedUser = authProvider.authenticateUser(credentials);
|
|
||||||
|
|
||||||
// Stop after successful authentication
|
|
||||||
if (authenticatedUser != null && logger.isInfoEnabled()) {
|
|
||||||
logger.info("User \"{}\" successfully authenticated from {}.",
|
|
||||||
authenticatedUser.getIdentifier(), getLoggableAddress(request));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request standard username/password if no user was produced
|
|
||||||
if (authenticatedUser == null)
|
|
||||||
throw new GuacamoleInvalidCredentialsException("Permission Denied.",
|
|
||||||
CredentialsInfo.USERNAME_PASSWORD);
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (GuacamoleException e) {
|
|
||||||
|
|
||||||
// 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), username);
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get UserContexts from each available AuthenticationProvider
|
|
||||||
List<UserContext> userContexts = new ArrayList<UserContext>(authProviders.size());
|
|
||||||
for (AuthenticationProvider authProvider : authProviders) {
|
|
||||||
|
|
||||||
// Generate or update user context
|
|
||||||
UserContext userContext;
|
|
||||||
if (existingSession != null)
|
|
||||||
userContext = authProvider.updateUserContext(existingSession.getUserContext(), authenticatedUser);
|
|
||||||
else
|
|
||||||
userContext = authProvider.getUserContext(authenticatedUser);
|
|
||||||
|
|
||||||
// Add to available data, if successful
|
|
||||||
if (userContext != null)
|
|
||||||
userContexts.add(userContext);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update existing session, if it exists
|
// Update existing session, if it exists
|
||||||
String authToken;
|
String authToken;
|
||||||
@@ -306,9 +497,10 @@ public class TokenRESTService {
|
|||||||
else {
|
else {
|
||||||
authToken = authTokenGenerator.getToken();
|
authToken = authTokenGenerator.getToken();
|
||||||
tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
|
tokenSessionMap.put(authToken, new GuacamoleSession(environment, authenticatedUser, userContexts));
|
||||||
|
logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Login was successful for user \"{}\".", authenticatedUser.getIdentifier());
|
// Return possibly-new auth token
|
||||||
return new APIAuthToken(authToken, authenticatedUser.getIdentifier());
|
return new APIAuthToken(authToken, authenticatedUser.getIdentifier());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user