diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index 6394b22ce..6d8487fdf 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -26,7 +26,7 @@ import com.google.inject.Scopes; import com.google.inject.servlet.ServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; -import org.glyptodon.guacamole.net.basic.rest.auth.LoginRESTService; +import org.glyptodon.guacamole.net.basic.rest.auth.TokenRESTService; import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; import org.glyptodon.guacamole.net.basic.rest.connectiongroup.ConnectionGroupRESTService; @@ -51,7 +51,7 @@ public class RESTServletModule extends ServletModule { bind(PermissionRESTService.class); bind(ProtocolRESTService.class); bind(UserRESTService.class); - bind(LoginRESTService.class); + bind(TokenRESTService.class); // Set up the servlet and JSON mappings bind(GuiceContainer.class); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java index 9e5101449..8f4c4417b 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java @@ -131,6 +131,7 @@ public class BasicTokenSessionMap implements TokenSessionMap { if (age >= sessionTimeout) { logger.debug("Session \"{}\" has timed out.", entry.getKey()); entries.remove(); + session.invalidate(); } // Otherwise, no other sessions can possibly be old enough @@ -162,9 +163,14 @@ public class BasicTokenSessionMap implements TokenSessionMap { sessionMap.put(authToken, session); } + @Override + public GuacamoleSession remove(String authToken) { + return sessionMap.remove(authToken); + } + @Override public void shutdown() { executor.shutdownNow(); } - + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java similarity index 84% rename from guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java rename to guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java index 0463e41f8..95a8e28b7 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/LoginRESTService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenRESTService.java @@ -24,9 +24,11 @@ package org.glyptodon.guacamole.net.basic.rest.auth; import com.google.inject.Inject; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @@ -42,16 +44,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A service for authenticating to the Guacamole REST API. Given valid - * credentials, the service will return an auth token. Invalid credentials will - * result in a permission error. + * A service for managing auth tokens via the Guacamole REST API. * * @author James Muehlner */ - -@Path("/login") +@Path("/token") @Produces(MediaType.APPLICATION_JSON) -public class LoginRESTService { +public class TokenRESTService { /** * The authentication provider used to authenticate this user. @@ -74,7 +73,7 @@ public class LoginRESTService { /** * Logger for this class. */ - private static final Logger logger = LoggerFactory.getLogger(LoginRESTService.class); + private static final Logger logger = LoggerFactory.getLogger(TokenRESTService.class); /** * Authenticates a user, generates an auth token, associates that auth token @@ -88,7 +87,7 @@ public class LoginRESTService { */ @POST @AuthProviderRESTExposure - public APIAuthToken login(@FormParam("username") String username, + public APIAuthToken createToken(@FormParam("username") String username, @FormParam("password") String password, @Context HttpServletRequest request) throws GuacamoleException { @@ -120,5 +119,24 @@ public class LoginRESTService { return new APIAuthToken(authToken, username); } - + + /** + * Invalidates a specific auth token, effectively logging out the associated + * user. + * + * @param authToken The token being invalidated. + */ + @DELETE + @Path("/{token}") + @AuthProviderRESTExposure + public void invalidateToken(@PathParam("token") String authToken) { + + GuacamoleSession session = tokenSessionMap.remove(authToken); + if (session == null) + throw new HTTPException(Status.NOT_FOUND, "No such token."); + + session.invalidate(); + + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java index cd4eb9b91..350988608 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java @@ -51,6 +51,15 @@ public interface TokenSessionMap { */ public GuacamoleSession get(String authToken); + /** + * Removes the GuacamoleSession associated with the given auth token. + * + * @param authToken The token to remove. + * @return The GuacamoleSession for the given auth token, if the auth token + * represents a currently logged in user, null otherwise. + */ + public GuacamoleSession remove(String authToken); + /** * Shuts down this session map, disallowing future sessions and reclaiming * any resources. diff --git a/guacamole/src/main/webapp/app/login/services/authenticationService.js b/guacamole/src/main/webapp/app/auth/authModule.js similarity index 53% rename from guacamole/src/main/webapp/app/login/services/authenticationService.js rename to guacamole/src/main/webapp/app/auth/authModule.js index 319c74c7f..112d34cb0 100644 --- a/guacamole/src/main/webapp/app/login/services/authenticationService.js +++ b/guacamole/src/main/webapp/app/auth/authModule.js @@ -21,33 +21,6 @@ */ /** - * A service for authenticating a user against the REST API. + * The module for authentication and management of tokens. */ -angular.module('index').factory('authenticationService', ['$http', - function authenticationService($http) { - var service = {}; - - /** - * Makes a request to authenticate a user using the login REST API endpoint, - * returning a promise that can be used for processing the results of the call. - * - * @param {string} username The username to log in with. - * @param {string} password The password to log in with. - * @returns {promise} A promise for the HTTP call. - */ - service.login = function login(username, password) { - return $http({ - method: 'POST', - url: 'api/login', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param({ - username: username, - password: password - }) - }); - }; - - return service; -}]); +angular.module('auth', ['util']); diff --git a/guacamole/src/main/webapp/app/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js new file mode 100644 index 000000000..2c2162289 --- /dev/null +++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 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 + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A service for authenticating a user against the REST API. + */ +angular.module('auth').factory('authenticationService', ['$http', '$injector', + function authenticationService($http, $injector) { + + var localStorageUtility = $injector.get("localStorageUtility"); + var service = {}; + + /** + * Makes a request to authenticate a user using the token REST API endpoint, + * returning a promise that can be used for processing the results of the call. + * + * @param {String} username The username to log in with. + * @param {String} password The password to log in with. + * @returns {Promise} A promise for the HTTP call. + */ + service.login = function login(username, password) { + return $http({ + method: 'POST', + url: 'api/token', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data: $.param({ + username: username, + password: password + }) + }).success(function success(data, status, headers, config) { + localStorageUtility.set('authToken', data.authToken); + localStorageUtility.set('userID', data.userID); + }); + }; + + /** + * Makes a request to logout a user using the login REST API endpoint, + * returning a promise that can be used for processing the results of the call. + * + * @returns {Promise} A promise for the HTTP call. + */ + service.logout = function logout() { + return $http({ + method: 'DELETE', + url: 'api/token/' + encodeURIComponent(service.getCurrentToken()) + }); + }; + + /** + * Returns the user ID of the current user. + * + * @returns {String} The user ID of the current user. + */ + service.getCurrentUserID = function getCurrentUserID() { + return localStorageUtility.get('userID'); + }; + + /** + * Returns the auth token associated with the current user. If the current + * user is not logged in, this token may not be valid. + * + * @returns {String} The auth token associated with the current user. + */ + service.getCurrentToken = function getCurrentToken() { + return localStorageUtility.get('authToken'); + }; + + return service; +}]); diff --git a/guacamole/src/main/webapp/app/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html index 22b5b848e..5b0789d66 100644 --- a/guacamole/src/main/webapp/app/home/templates/home.html +++ b/guacamole/src/main/webapp/app/home/templates/home.html @@ -49,7 +49,7 @@
diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index 35101d2bf..642d336b7 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -29,7 +29,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Get the dependencies commonJS style var permissionDAO = $injector.get("permissionDAO"), permissionCheckService = $injector.get("permissionCheckService"), - localStorageUtility = $injector.get("localStorageUtility"), + authenticationService = $injector.get("authenticationService"), $q = $injector.get("$q"), $document = $injector.get("$document"), $window = $injector.get("$window"), @@ -64,7 +64,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', var permissionsLoaded= $q.defer(); $scope.basicPermissionsLoaded = permissionsLoaded.promise; - $scope.currentUserID = localStorageUtility.get('userID'); + $scope.currentUserID = authenticationService.getCurrentUserID(); // If the user is unknown, force a login if(!$scope.currentUserID) @@ -86,6 +86,13 @@ angular.module('index').controller('indexController', ['$scope', '$injector', permissionsLoaded.resolve(); }); }; + + // Provide simple mechanism for logging out the current user + $scope.logout = function logout() { + authenticationService.logout().success(function logoutSuccess() { + $location.path('/login'); + }); + }; // Try to load them now $scope.loadBasicPermissions(); diff --git a/guacamole/src/main/webapp/app/index/indexModule.js b/guacamole/src/main/webapp/app/index/indexModule.js index ce6bd571d..1eb80c9fd 100644 --- a/guacamole/src/main/webapp/app/index/indexModule.js +++ b/guacamole/src/main/webapp/app/index/indexModule.js @@ -23,4 +23,4 @@ /** * The module for the root of the application. */ -angular.module('index', ['ngRoute', 'pascalprecht.translate', 'home', 'manage', 'login', 'client']); +angular.module('index', ['ngRoute', 'pascalprecht.translate', 'auth', 'home', 'manage', 'login', 'client']); diff --git a/guacamole/src/main/webapp/app/login/controllers/loginController.js b/guacamole/src/main/webapp/app/login/controllers/loginController.js index 38a621c76..32860b1c5 100644 --- a/guacamole/src/main/webapp/app/login/controllers/loginController.js +++ b/guacamole/src/main/webapp/app/login/controllers/loginController.js @@ -24,22 +24,14 @@ angular.module('login').controller('loginController', ['$scope', '$injector', function loginController($scope, $injector) { // Get the dependencies commonJS style - var authenticationService = $injector.get("authenticationService"); - var localStorageUtility = $injector.get("localStorageUtility"); - var $location = $injector.get("$location"); + var authenticationService = $injector.get("authenticationService"); + var $location = $injector.get("$location"); - // Clear the auth token and userID to log out the user - localStorageUtility.clear("authToken"); - localStorageUtility.clear("userID"); - $scope.loginError = false; $scope.login = function login() { authenticationService.login($scope.username, $scope.password) .success(function success(data, status, headers, config) { - localStorageUtility.set('authToken', data.authToken); - localStorageUtility.set('userID', data.userID); - // Set up the basic permissions for the user $scope.loadBasicPermissions(); $location.path('/'); @@ -47,4 +39,5 @@ angular.module('login').controller('loginController', ['$scope', '$injector', $scope.loginError = true; }); }; + }]); diff --git a/guacamole/src/main/webapp/app/manage/templates/manage.html b/guacamole/src/main/webapp/app/manage/templates/manage.html index 3d1fed03f..cdbdbea15 100644 --- a/guacamole/src/main/webapp/app/manage/templates/manage.html +++ b/guacamole/src/main/webapp/app/manage/templates/manage.html @@ -44,7 +44,7 @@ THE SOFTWARE.