diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java index dcf3bd705..563203fd7 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java @@ -51,14 +51,14 @@ public class GuacamoleSession { private static final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); /** - * The credentials provided when the user logged in. + * The credentials provided when the user authenticated. */ - private final Credentials credentials; + private Credentials credentials; /** * The user context associated with this session. */ - private final UserContext userContext; + private UserContext userContext; /** * Collection of all event listeners configured in guacamole.properties. @@ -139,15 +139,27 @@ public class GuacamoleSession { /** * Returns the credentials used when the user associated with this session - * logged in. + * authenticated. * - * @return The credentials used when the user associated with this session - * logged in. + * @return + * The credentials used when the user associated with this session + * authenticated. */ public Credentials getCredentials() { return credentials; } + /** + * Replaces the credentials associated with this session with the given + * credentials. + * + * @param credentials + * The credentials to associate with this session. + */ + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } + /** * Returns the UserContext associated with this session. * @@ -157,6 +169,17 @@ public class GuacamoleSession { return userContext; } + /** + * Replaces the user context associated with this session with the given + * user context. + * + * @param userContext + * The user context to associate with this session. + */ + public void setUserContext(UserContext userContext) { + this.userContext = userContext; + } + /** * Returns the ClipboardState associated with this session. * diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java index a2ae39d51..f2de45e3b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/properties/BasicGuacamoleProperties.java @@ -49,18 +49,6 @@ public class BasicGuacamoleProperties { }; - /** - * Whether HTTP "Authorization" headers should be taken into account when - * authenticating the user. By default, "Authorization" headers are - * ignored. - */ - public static final BooleanGuacamoleProperty ENABLE_HTTP_AUTH = new BooleanGuacamoleProperty() { - - @Override - public String getName() { return "enable-http-auth"; } - - }; - /** * The directory to search for authentication provider classes. */ 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 6db943908..e46b54d1a 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 @@ -23,6 +23,7 @@ package org.glyptodon.guacamole.net.basic.rest.auth; import com.google.inject.Inject; +import java.io.UnsupportedEncodingException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -33,6 +34,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response.Status; +import javax.xml.bind.DatatypeConverter; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; @@ -77,20 +79,73 @@ public class TokenRESTService { /** * Authenticates a user, generates an auth token, associates that auth token - * with the user's UserContext for use by further requests. + * 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. - * @param password The password of the user who is to be authenticated. - * @param request The HttpServletRequest associated with the login attempt. + * @param username + * The username of the user who is to be authenticated. + * + * @param password + * The password of the user who is to be authenticated. + * + * @param token + * An optional existing auth token for the user who is to be + * authenticated. + * + * @param request + * The HttpServletRequest associated with the login attempt. + * * @return The auth token for the newly logged-in user. * @throws GuacamoleException If an error prevents successful login. */ @POST @AuthProviderRESTExposure public APIAuthToken createToken(@FormParam("username") String username, - @FormParam("password") String password, + @FormParam("password") String password, + @FormParam("token") String token, @Context HttpServletRequest request) throws GuacamoleException { + + // Pull existing session if token provided + GuacamoleSession existingSession; + if (token != null) + existingSession = tokenSessionMap.get(token); + else + existingSession = null; + + // 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); @@ -99,7 +154,15 @@ public class TokenRESTService { UserContext userContext; try { - userContext = authProvider.getUserContext(credentials); + + // Update existing user context if session already exists + if (existingSession != null) + userContext = authProvider.updateUserContext(existingSession.getUserContext(), credentials); + + /// Otherwise, generate a new user context + else + userContext = authProvider.getUserContext(credentials); + } catch(GuacamoleException e) { logger.error("Exception caught while authenticating user.", e); @@ -110,13 +173,23 @@ public class TokenRESTService { // Authentication failed. if (userContext == null) throw new HTTPException(Status.UNAUTHORIZED, "Permission Denied."); - - String authToken = authTokenGenerator.getToken(); - - tokenSessionMap.put(authToken, new GuacamoleSession(credentials, userContext)); + + // Update existing session, if it exists + String authToken; + if (existingSession != null) { + authToken = token; + existingSession.setCredentials(credentials); + existingSession.setUserContext(userContext); + } + + // If no existing session, generate a new token/session pair + else { + authToken = authTokenGenerator.getToken(); + tokenSessionMap.put(authToken, new GuacamoleSession(credentials, userContext)); + } logger.debug("Login was successful for user \"{}\".", userContext.self().getUsername()); - return new APIAuthToken(authToken, username); + return new APIAuthToken(authToken, userContext.self().getUsername()); } diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js index 2070b25aa..9b1983821 100644 --- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -37,7 +37,81 @@ angular.module('auth').factory('authenticationService', ['$http', '$cookieStore' var AUTH_COOKIE_ID = "GUAC_AUTH"; /** - * Makes a request to authenticate a user using the token REST API endpoint, + * Makes a request to authenticate a user using the token REST API endpoint + * and given arbitrary parameters, returning a promise that succeeds only + * if the authentication operation was successful. The resulting + * authentication data can be retrieved later via getCurrentToken() or + * getCurrentUserID(). + * + * The provided parameters can be virtually any object, as each property + * will be sent as an HTTP parameter in the authentication request. + * Standard parameters include "username" for the user's username, + * "password" for the user's associated password, and "token" for the + * auth token to check/update. + * + * If a token is provided, it will be reused if possible. + * + * @param {Object} parameters + * Arbitrary parameters to authenticate with. + * + * @returns {Promise} + * A promise which succeeds only if the login operation was successful. + */ + service.authenticate = function authenticate(parameters) { + return $http({ + method: 'POST', + url: 'api/tokens', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $.param(parameters), + }).success(function success(data, status, headers, config) { + $cookieStore.put(AUTH_COOKIE_ID, { + authToken : data.authToken, + userID : data.userID + }); + }); + }; + + /** + * Makes a request to update the current auth token, if any, using the + * token REST API endpoint. If the optional parameters object is provided, + * its properties will be included as parameters in the update request. + * This function returns a promise that succeeds only if the authentication + * operation was successful. The resulting authentication data can be + * retrieved later via getCurrentToken() or getCurrentUserID(). + * + * If there is no current auth token, this function behaves identically to + * authenticate(), and makes a general authentication request. + * + * @param {Object} [parameters] + * Arbitrary parameters to authenticate with, if any. + * + * @returns {Promise} + * A promise which succeeds only if the login operation was successful. + */ + service.updateCurrentToken = function updateCurrentToken(parameters) { + + // HTTP parameters for the authentication request + var httpParameters = {}; + + // Add token parameter if current token is known + var token = service.getCurrentToken(); + if (token) + httpParameters.token = service.getCurrentToken(); + + // Add any additional parameters + if (parameters) + angular.extend(httpParameters, parameters); + + // Make the request + return service.authenticate(httpParameters); + + }; + + /** + * Makes a request to authenticate a user using the token REST API endpoint + * with a username and password, ignoring any currently-stored token, * returning a promise that succeeds only if the login operation was * successful. The resulting authentication data can be retrieved later * via getCurrentToken() or getCurrentUserID(). @@ -52,21 +126,9 @@ angular.module('auth').factory('authenticationService', ['$http', '$cookieStore' * A promise which succeeds only if the login operation was successful. */ service.login = function login(username, password) { - return $http({ - method: 'POST', - url: 'api/tokens', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param({ - username: username, - password: password - }) - }).success(function success(data, status, headers, config) { - $cookieStore.put(AUTH_COOKIE_ID, { - authToken : data.authToken, - userID : data.userID - }); + return service.authenticate({ + username: username, + password: password }); }; diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index a739d2fdd..8d6c77286 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -25,55 +25,113 @@ */ angular.module('index').config(['$routeProvider', '$locationProvider', function indexRouteConfig($routeProvider, $locationProvider) { - + // Disable HTML5 mode (use # for routing) $locationProvider.html5Mode(false); - - $routeProvider - .when('/', { - title: 'APP.NAME', - bodyClassName: 'home', - templateUrl: 'app/home/templates/home.html', - controller: 'homeController' - }) - .when('/manage/', { - title: 'APP.NAME', - bodyClassName: 'manage', - templateUrl: 'app/manage/templates/manage.html', - controller: 'manageController' - }) - .when('/manage/connections/:id?', { - title: 'APP.NAME', - bodyClassName: 'manage', - templateUrl: 'app/manage/templates/manageConnection.html', - controller: 'manageConnectionController' - }) - .when('/manage/connectionGroups/:id?', { - title: 'APP.NAME', - bodyClassName: 'manage', - templateUrl: 'app/manage/templates/manageConnectionGroup.html', - controller: 'manageConnectionGroupController' - }) - .when('/manage/users/:id', { - title: 'APP.NAME', - bodyClassName: 'manage', - templateUrl: 'app/manage/templates/manageUser.html', - controller: 'manageUserController' - }) - .when('/login/', { - title: 'APP.NAME', - bodyClassName: 'login', - templateUrl: 'app/login/templates/login.html', - controller: 'loginController' - }) - .when('/client/:type/:id/:params?', { - bodyClassName: 'client', - templateUrl: 'app/client/templates/client.html', - controller: 'clientController' - }) - .otherwise({ - redirectTo: '/' + + /** + * Attempts to re-authenticate with the Guacamole server, sending any + * query parameters in the URL, along with the current auth token, and + * updating locally stored token if necessary. + * + * @param {Service} $injector + * The Angular $injector service. + * + * @returns {Promise} + * A promise which resolves successfully only after an attempt to + * re-authenticate has been made. + */ + var updateCurrentToken = ['$injector', function updateCurrentToken($injector) { + + // Required services + var $location = $injector.get('$location'); + var $q = $injector.get('$q'); + var authenticationService = $injector.get('authenticationService'); + + // Promise for authentication attempt + var authAttempt = $q.defer(); + + // Re-authenticate including any parameters in URL + authenticationService.updateCurrentToken($location.search()) + ['finally'](function authenticationAttemptComplete() { + authAttempt.resolve(); }); + + // Return promise that will resolve regardless of success/failure + return authAttempt.promise; + + }]; + + // Configure each possible route + $routeProvider + + // Home screen + .when('/', { + title : 'APP.NAME', + bodyClassName : 'home', + templateUrl : 'app/home/templates/home.html', + controller : 'homeController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // Management screen + .when('/manage/', { + title : 'APP.NAME', + bodyClassName : 'manage', + templateUrl : 'app/manage/templates/manage.html', + controller : 'manageController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // Connection editor + .when('/manage/connections/:id?', { + title : 'APP.NAME', + bodyClassName : 'manage', + templateUrl : 'app/manage/templates/manageConnection.html', + controller : 'manageConnectionController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // Connection group editor + .when('/manage/connectionGroups/:id?', { + title : 'APP.NAME', + bodyClassName : 'manage', + templateUrl : 'app/manage/templates/manageConnectionGroup.html', + controller : 'manageConnectionGroupController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // User editor + .when('/manage/users/:id', { + title : 'APP.NAME', + bodyClassName : 'manage', + templateUrl : 'app/manage/templates/manageUser.html', + controller : 'manageUserController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // Login screen + .when('/login/', { + title : 'APP.NAME', + bodyClassName : 'login', + templateUrl : 'app/login/templates/login.html', + controller : 'loginController' + // No need to update token here - the login screen ignores all auth + }) + + // Client view + .when('/client/:type/:id/:params?', { + bodyClassName : 'client', + templateUrl : 'app/client/templates/client.html', + controller : 'clientController', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // Redirect to home screen if page not found + .otherwise({ + redirectTo : '/' + }); + }]);