blob: af5a9ab6c3beb57fccc518d49e89ba5e193a0071 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* A service for authenticating a user against the REST API. Invoking the
* authenticate() or login() functions of this service will automatically
* affect the login dialog, if visible.
*
* This service broadcasts events on $rootScope depending on the status and
* result of authentication operations:
*
* "guacLoginPending"
* An authentication request is being submitted and we are awaiting the
* result. The request may not yet have been submitted if the parameters
* for that request are not ready. This event receives a promise that
* resolves with the HTTP parameters that were ultimately submitted as its
* sole parameter.
*
* "guacLogin"
* Authentication was successful and a new token was created. This event
* receives the authentication token as its sole parameter.
*
* "guacLogout"
* An existing token is being destroyed. This event receives the
* authentication token as its sole parameter. If the existing token for
* the current session is being replaced without destroying that session,
* this event is not fired.
*
* "guacLoginFailed"
* An authentication request has failed for any reason. This event is
* broadcast before any other events that are specific to the nature of
* the failure, and may be used to detect login failures in lieu of those
* events. This event receives two parameters: the HTTP parameters
* submitted and the Error object received from the REST endpoint.
*
* "guacInsufficientCredentials"
* An authentication request failed because additional credentials are
* needed before the request can be processed. This event receives two
* parameters: the HTTP parameters submitted and the Error object received
* from the REST endpoint.
*
* "guacInvalidCredentials"
* An authentication request failed because the credentials provided are
* invalid. This event receives two parameters: the HTTP parameters
* submitted and the Error object received from the REST endpoint.
*/
angular.module('auth').factory('authenticationService', ['$injector',
function authenticationService($injector) {
// Required types
var AuthenticationResult = $injector.get('AuthenticationResult');
var Error = $injector.get('Error');
// Required services
var $q = $injector.get('$q');
var $rootScope = $injector.get('$rootScope');
var localStorageService = $injector.get('localStorageService');
var requestService = $injector.get('requestService');
var service = {};
/**
* The most recent authentication result, or null if no authentication
* result is cached.
*
* @type AuthenticationResult
*/
var cachedResult = null;
/**
* The unique identifier of the local storage key which stores the latest
* authentication token.
*
* @type String
*/
var AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN';
/**
* Retrieves the authentication result cached in memory. If the user has not
* yet authenticated, the user has logged out, or the last authentication
* attempt failed, null is returned.
*
* NOTE: setAuthenticationResult() will be called upon page load, so the
* cache should always be populated after the page has successfully loaded.
*
* @returns {AuthenticationResult}
* The last successful authentication result, or null if the user is not
* currently authenticated.
*/
var getAuthenticationResult = function getAuthenticationResult() {
// Use cached result, if any
if (cachedResult)
return cachedResult;
// Return explicit null if no auth data is currently stored
return null;
};
/**
* Stores the given authentication result for future retrieval. The given
* result MUST be the result of the most recent authentication attempt.
*
* @param {AuthenticationResult} data
* The last successful authentication result, or null if the last
* authentication attempt failed.
*/
var setAuthenticationResult = function setAuthenticationResult(data) {
// Clear the currently-stored result and auth token if the last
// attempt failed
if (!data) {
cachedResult = null;
localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY);
}
// Otherwise, store the authentication attempt directly.
// Note that only the auth token is stored in persistent local storage.
// To re-obtain an autentication result upon a fresh page load,
// reauthenticate with the persistent token, which can be obtained by
// calling getCurrentToken().
else {
// Always store in cache
cachedResult = data;
// Persist only the auth token past tab/window closure, and only
// if not anonymous
if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME)
localStorageService.setItem(
AUTH_TOKEN_STORAGE_KEY, data.authToken);
}
};
/**
* Clears the stored authentication result, if any. If no authentication
* result is currently stored, this function has no effect.
*/
var clearAuthenticationResult = function clearAuthenticationResult() {
setAuthenticationResult(null);
};
/**
* 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
* getCurrentUsername(). Invoking this function will affect the UI,
* including the login screen if visible.
*
* 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|Promise} parameters
* Arbitrary parameters to authenticate with. If a Promise is provided,
* that Promise must resolve with the parameters to be submitted when
* those parameters are available, and any error will be handled as if
* from the authentication endpoint of the REST API itself.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.authenticate = function authenticate(parameters) {
// Coerce received parameters object into a Promise, if it isn't
// already a Promise
parameters = $q.resolve(parameters);
// Notify that a fresh authentication request is underway
$rootScope.$broadcast('guacLoginPending', parameters);
// Attempt authentication after auth parameters are available ...
return parameters.then(function requestParametersReady(requestParams) {
// Strip any properties that are from AngularJS core, such as the
// '$$state' property added by $q. Properties added by AngularJS
// core will have a '$' prefix. The '$$state' property is
// particularly problematic, as it is self-referential and explodes
// the stack when fed to $.param().
requestParams = _.omitBy(requestParams, (value, key) => key.startsWith('$'));
return requestService({
method: 'POST',
url: 'api/tokens',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: $.param(requestParams)
})
// ... if authentication succeeds, handle received auth data ...
.then(function authenticationSuccessful(data) {
var currentToken = service.getCurrentToken();
// If a new token was received, ensure the old token is invalidated,
// if any, and notify listeners of the new token
if (data.authToken !== currentToken) {
// If an old token existed, request that the token be revoked
if (currentToken) {
service.revokeToken(currentToken).catch(angular.noop);
}
// Notify of login and new token
setAuthenticationResult(new AuthenticationResult(data));
$rootScope.$broadcast('guacLogin', data.authToken);
}
// Update cached authentication result, even if the token remains
// the same
else
setAuthenticationResult(new AuthenticationResult(data));
// Authentication was successful
return data;
});
})
// ... if authentication fails, propogate failure to returned promise
['catch'](requestService.createErrorCallback(function authenticationFailed(error) {
// Notify of generic login failure, for any event consumers that
// wish to handle all types of failures at once
$rootScope.$broadcast('guacLoginFailed', parameters, error);
// Request credentials if provided credentials were invalid
if (error.type === Error.Type.INVALID_CREDENTIALS) {
$rootScope.$broadcast('guacInvalidCredentials', parameters, error);
clearAuthenticationResult();
}
// Request more credentials if provided credentials were not enough
else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) {
$rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
clearAuthenticationResult();
}
// Abort rendering of page if an internal error occurs
else if (error.type === Error.Type.INTERNAL_ERROR)
$rootScope.$broadcast('guacFatalPageError', error);
// Authentication failed
throw error;
}));
};
/**
* 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 getCurrentUsername().
*
* 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);
};
/**
* Determines whether the session associated with a particular token is
* still valid, without performing an operation that would result in that
* session being marked as active. If no token is provided, the session of
* the current user is checked.
*
* @param {string} [token]
* The authentication token to pass with the "Guacamole-Token" header.
* If omitted, and the user is logged in, the user's current
* authentication token will be used.
*
* @returns {Promise.<!boolean>}
* A promise that resolves with the boolean value "true" if the session
* is valid, and resolves with the boolean value "false" otherwise,
* including if an error prevents session validity from being
* determined. The promise is never rejected.
*/
service.getValidity = function getValidity(token) {
// NOTE: Because this is a HEAD request, we will not receive a JSON
// response body. We will only have a simple yes/no regarding whether
// the auth token can be expected to be usable.
return service.request({
method: 'HEAD',
url: 'api/session'
}, token)
.then(function sessionIsValid() {
return true;
})
['catch'](function sessionIsNotValid() {
return false;
});
};
/**
* Makes a request to revoke an authentication token using the token REST
* API endpoint, returning a promise that succeeds only if the token was
* successfully revoked.
*
* @param {string} token
* The authentication token to revoke.
*
* @returns {Promise}
* A promise which succeeds only if the token was successfully revoked.
*/
service.revokeToken = function revokeToken(token) {
return service.request({
method: 'DELETE',
url: 'api/session'
}, token);
};
/**
* 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 getCurrentUsername(). Invoking this function
* will affect the UI, including the login screen if visible.
*
* @param {String} username
* The username to log in with.
*
* @param {String} password
* The password to log in with.
*
* @returns {Promise}
* A promise which succeeds only if the login operation was successful.
*/
service.login = function login(username, password) {
return service.authenticate({
username: username,
password: password
});
};
/**
* Makes a request to logout a user using the token REST API endpoint,
* returning a promise that succeeds only if the logout operation was
* successful. Invoking this function will affect the UI, causing the
* visible components of the application to be replaced with a status
* message noting that the user has been logged out.
*
* @returns {Promise}
* A promise which succeeds only if the logout operation was
* successful.
*/
service.logout = function logout() {
// Clear authentication data
var token = service.getCurrentToken();
clearAuthenticationResult();
// Notify listeners that a token is being destroyed
$rootScope.$broadcast('guacLogout', token);
// Delete old token
return service.revokeToken(token);
};
/**
* Returns whether the current user has authenticated anonymously. An
* anonymous user is denoted by the identifier reserved by the Guacamole
* extension API for anonymous users (the empty string).
*
* @returns {Boolean}
* true if the current user has authenticated anonymously, false
* otherwise.
*/
service.isAnonymous = function isAnonymous() {
return service.getCurrentUsername() === '';
};
/**
* Returns the username of the current user. If the current user is not
* logged in, this value may not be valid.
*
* @returns {String}
* The username of the current user, or null if no authentication data
* is present.
*/
service.getCurrentUsername = function getCurrentUsername() {
// Return username, if available
var authData = getAuthenticationResult();
if (authData)
return authData.username;
// No auth data present
return null;
};
/**
* 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, or null if no
* authentication data is present.
*/
service.getCurrentToken = function getCurrentToken() {
// Return cached auth token, if available
var authData = getAuthenticationResult();
if (authData)
return authData.authToken;
// Fall back to the value from local storage if not found in cache
return localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY);
};
/**
* Returns the identifier of the data source that authenticated the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String}
* The identifier of the data source that authenticated the current
* user, or null if no authentication data is present.
*/
service.getDataSource = function getDataSource() {
// Return data source, if available
var authData = getAuthenticationResult();
if (authData)
return authData.dataSource;
// No auth data present
return null;
};
/**
* Returns the identifiers of all data sources available to the current
* user. If the current user is not logged in, this value may not be valid.
*
* @returns {String[]}
* The identifiers of all data sources availble to the current user,
* or an empty array if no authentication data is present.
*/
service.getAvailableDataSources = function getAvailableDataSources() {
// Return data sources, if available
var authData = getAuthenticationResult();
if (authData)
return authData.availableDataSources;
// No auth data present
return [];
};
/**
* Makes an HTTP request leveraging the requestService(), automatically
* including the given authentication token using the "Guacamole-Token"
* header. If no token is provided, the user's current authentication token
* is used instead. If the user is not logged in, the "Guacamole-Token"
* header is simply omitted. The provided configuration object is not
* modified by this function.
*
* @param {Object} object
* A configuration object describing the HTTP request to be made by
* requestService(). As described by requestService(), this object must
* be a configuration object accepted by AngularJS' $http service.
*
* @param {string} [token]
* The authentication token to pass with the "Guacamole-Token" header.
* If omitted, and the user is logged in, the user's current
* authentication token will be used.
*
* @returns {Promise.<Object>}
* A promise that will resolve with the data from the HTTP response for
* the underlying requestService() call if successful, or reject with
* an @link{Error} describing the failure.
*/
service.request = function request(object, token) {
// Attempt to use current token if none is provided
token = token || service.getCurrentToken();
// Add "Guacamole-Token" header if an authentication token is available
if (token) {
object = _.merge({
headers : { 'Guacamole-Token' : token }
}, object);
}
return requestService(object);
};
return service;
}]);