GUACAMOLE-839: Merge add webapp SSO support for certificates / smart cards.

This commit is contained in:
Virtually Nick
2023-03-07 20:59:57 -05:00
committed by GitHub
42 changed files with 2241 additions and 242 deletions

View File

@@ -18,25 +18,47 @@
*/
/**
* A service for authenticating a user against the REST API.
* 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 two events on $rootScope depending on the result of
* authentication operations: 'guacLogin' if authentication was successful and
* a new token was created, and 'guacLogout' if an existing token is being
* destroyed or replaced. Both events will be passed the related token as their
* sole parameter.
* This service broadcasts events on $rootScope depending on the status and
* result of authentication operations:
*
* If a login attempt results in an existing token being replaced, 'guacLogout'
* will be broadcast first for the token being replaced, followed by
* 'guacLogin' for the new token.
*
* Failed logins may also result in guacInsufficientCredentials or
* guacInvalidCredentials events, if the provided credentials were rejected for
* being insufficient or invalid respectively. Both events will be provided
* the set of parameters originally given to authenticate() and the error that
* rejected the credentials. The Error object provided will contain set of
* expected credentials returned by the REST endpoint. This set of credentials
* will be in the form of a Field array.
* "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) {
@@ -46,6 +68,7 @@ angular.module('auth').factory('authenticationService', ['$injector',
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');
@@ -141,7 +164,8 @@ angular.module('auth').factory('authenticationService', ['$injector',
* 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().
* 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.
@@ -151,57 +175,75 @@ angular.module('auth').factory('authenticationService', ['$injector',
*
* If a token is provided, it will be reused if possible.
*
* @param {Object} parameters
* Arbitrary parameters to authenticate with.
* @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) {
// Attempt authentication
return requestService({
method: 'POST',
url: 'api/tokens',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: $.param(parameters)
})
// Coerce received parameters object into a Promise, if it isn't
// already a Promise
parameters = $q.resolve(parameters);
// If authentication succeeds, handle received auth data
.then(function authenticationSuccessful(data) {
// Notify that a fresh authentication request is underway
$rootScope.$broadcast('guacLoginPending', parameters);
var currentToken = service.getCurrentToken();
// Attempt authentication after auth parameters are available ...
return parameters.then(function requestParametersReady(requestParams) {
// 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) {
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);
// 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;
// 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
// ... 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);
@@ -321,7 +363,8 @@ angular.module('auth').factory('authenticationService', ['$injector',
* 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().
* 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.
@@ -342,7 +385,9 @@ angular.module('auth').factory('authenticationService', ['$injector',
/**
* 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.
* 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

View File

@@ -177,51 +177,7 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
* authentication service, redirecting to the main view if successful.
*/
$scope.login = function login() {
// Authentication is now in progress
$scope.submitted = true;
// Start with cleared status
$scope.loginError = null;
// Attempt login once existing session is destroyed
authenticationService.authenticate($scope.enteredValues)
// Retry route upon success (entered values will be cleared only
// after route change has succeeded as this can take time)
.then(function loginSuccessful() {
$route.reload();
})
// Reset upon failure
['catch'](requestService.createErrorCallback(function loginFailed(error) {
// Initial submission is complete and has failed
$scope.submitted = false;
// Clear out passwords if the credentials were rejected for any reason
if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
// Flag generic error for invalid login
if (error.type === Error.Type.INVALID_CREDENTIALS)
$scope.loginError = {
'key' : 'LOGIN.ERROR_INVALID_LOGIN'
};
// Display error if anything else goes wrong
else
$scope.loginError = error.translatableMessage;
// Reset all remaining fields to default values, but
// preserve any usernames
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {
if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues)
$scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE;
});
}
}));
authenticationService.authenticate($scope.enteredValues)['catch'](requestService.IGNORE);
};
/**
@@ -244,6 +200,48 @@ angular.module('login').directive('guacLogin', [function guacLogin() {
};
// Update UI to reflect in-progress auth status (clear any previous
// errors, flag as pending)
$rootScope.$on('guacLoginPending', function loginSuccessful() {
$scope.submitted = true;
$scope.loginError = null;
});
// Retry route upon success (entered values will be cleared only
// after route change has succeeded as this can take time)
$rootScope.$on('guacLogin', function loginSuccessful() {
$route.reload();
});
// Reset upon failure
$rootScope.$on('guacLoginFailed', function loginFailed(event, parameters, error) {
// Initial submission is complete and has failed
$scope.submitted = false;
// Clear out passwords if the credentials were rejected for any reason
if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
// Flag generic error for invalid login
if (error.type === Error.Type.INVALID_CREDENTIALS)
$scope.loginError = {
'key' : 'LOGIN.ERROR_INVALID_LOGIN'
};
// Display error if anything else goes wrong
else
$scope.loginError = error.translatableMessage;
// Reset all remaining fields to default values, but
// preserve any usernames
angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) {
if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues)
$scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE;
});
}
});
// Reset state after authentication and routing have succeeded
$rootScope.$on('$routeChangeSuccess', function routeChanged() {
$scope.enteredValues = {};