From 508a476e2effc0aaa4f76be2d0bd547f12501170 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 12:03:53 -0800 Subject: [PATCH 1/6] GUAC-971: Allow existing sessions/tokens to be updated through the login process. --- .../guacamole/net/basic/GuacamoleSession.java | 26 +++++++- .../net/basic/rest/auth/TokenRESTService.java | 61 +++++++++++++++---- 2 files changed, 74 insertions(+), 13 deletions(-) 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..0455b8436 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 @@ -53,12 +53,12 @@ public class GuacamoleSession { /** * The credentials provided when the user logged in. */ - 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. @@ -148,6 +148,17 @@ public class GuacamoleSession { 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 +168,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/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index 6db943908..740f74b38 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 @@ -77,20 +77,41 @@ 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; + + // Build credentials Credentials credentials = new Credentials(); credentials.setUsername(username); credentials.setPassword(password); @@ -99,7 +120,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,10 +139,20 @@ 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); From 83bf9d32c1c5a112b0f6249c6a187b2d25c5063d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 13:21:36 -0800 Subject: [PATCH 2/6] GUAC-971: Add re-authentication functions to authenticationService. --- .../app/auth/service/authenticationService.js | 94 +++++++++++++++---- 1 file changed, 78 insertions(+), 16 deletions(-) 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 }); }; From 98d5c19b1d82dca5825e8429dbf72aef723f928f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 13:54:57 -0800 Subject: [PATCH 3/6] GUAC-971: Re-authenticate with server for each page visited. --- .../app/index/config/indexRouteConfig.js | 150 ++++++++++++------ 1 file changed, 104 insertions(+), 46 deletions(-) diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index a739d2fdd..ef62e79e8 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', + resolve : { updateCurrentToken: updateCurrentToken } + }) + + // 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 : '/' + }); + }]); From f1c5adfba8f5366843c1b9b6483b22489de3d04b Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 15:29:39 -0800 Subject: [PATCH 4/6] GUAC-971: No need to re-authenticate when loading the login screen. The login screen is intended to allow fresh auth, overriding existing auth if necessary, and ignores any existing auth anyway. --- .../src/main/webapp/app/index/config/indexRouteConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js index ef62e79e8..8d6c77286 100644 --- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js @@ -115,8 +115,8 @@ angular.module('index').config(['$routeProvider', '$locationProvider', title : 'APP.NAME', bodyClassName : 'login', templateUrl : 'app/login/templates/login.html', - controller : 'loginController', - resolve : { updateCurrentToken: updateCurrentToken } + controller : 'loginController' + // No need to update token here - the login screen ignores all auth }) // Client view From 58f1093c582ad95e42e78678ef98de7a625b25b2 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 15:37:24 -0800 Subject: [PATCH 5/6] GUAC-971: Pull username and password from HTTP "Authorization" header, if present, when username and password are not provided via parameters. --- .../properties/BasicGuacamoleProperties.java | 12 ------- .../net/basic/rest/auth/TokenRESTService.java | 36 ++++++++++++++++++- 2 files changed, 35 insertions(+), 13 deletions(-) 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 740f74b38..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; @@ -111,6 +113,38 @@ public class TokenRESTService { 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); @@ -155,7 +189,7 @@ public class TokenRESTService { } logger.debug("Login was successful for user \"{}\".", userContext.self().getUsername()); - return new APIAuthToken(authToken, username); + return new APIAuthToken(authToken, userContext.self().getUsername()); } From 354e3180dbcc3edea3bb80ce0d3e8c5cf4baf81c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 5 Jan 2015 16:42:59 -0800 Subject: [PATCH 6/6] GUAC-971: Clarify documentation surrounding the credentials associated with a Guacamole session. --- .../glyptodon/guacamole/net/basic/GuacamoleSession.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 0455b8436..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,7 +51,7 @@ 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 Credentials credentials; @@ -139,10 +139,11 @@ 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;