From 7a5690b605c79eabdc049a7d9f0291249b0fc63b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 26 Aug 2015 17:00:47 -0700 Subject: [PATCH] GUAC-586: Authenticate against multiple AuthenticationProviders (data not yet aggregated, however). --- .../net/basic/rest/auth/TokenRESTService.java | 410 +++++++++++++----- 1 file changed, 301 insertions(+), 109 deletions(-) diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index f0130905a..3e2432ad2 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java @@ -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 * 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.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.auth.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; 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.basic.GuacamoleSession; 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. * * @author James Muehlner + * @author Michael Jumper */ @Path("/tokens") @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 getUserContexts(GuacamoleSession existingSession, + AuthenticatedUser authenticatedUser) throws GuacamoleException { + + List 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 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 * with the user's UserContext for use by further requests. If an existing * token is provided, the authentication procedure will attempt to update * or reuse the provided token. - * + * * @param username * 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 HttpServletRequest request = new APIRequest(consumedRequest, parameters); - + // Pull existing session if token provided GuacamoleSession existingSession; if (token != null) @@ -188,111 +478,12 @@ public class TokenRESTService { else existingSession = null; - // If no username/password given, try Authorization header - if (username == null && password == null) { + // Build credentials from request + Credentials credentials = getCredentials(request, username, password); - 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)); - - 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 userContexts = new ArrayList(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); - - } + // Get up-to-date AuthenticatedUser and associated UserContexts + AuthenticatedUser authenticatedUser = getAuthenticatedUser(existingSession, credentials); + List userContexts = getUserContexts(existingSession, authenticatedUser); // Update existing session, if it exists String authToken; @@ -306,9 +497,10 @@ public class TokenRESTService { else { authToken = authTokenGenerator.getToken(); 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()); }