Skip to content

Instantly share code, notes, and snippets.

@kavhad
Last active January 28, 2016 08:36
Show Gist options
  • Save kavhad/bb0d8e4a446496a6c05a to your computer and use it in GitHub Desktop.
Save kavhad/bb0d8e4a446496a6c05a to your computer and use it in GitHub Desktop.
passive SAML token (ADFS) re-authentication on client during jquery ajax loads
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8"/>
<link type="text/css" rel="stylesheet" href="https://code.jquery.com/ui/1.11.4/themes/black-tie/jquery-ui.css" />
</head>
<body>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
<script type="text/javascript" src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script type="text/javascript">
//This script is for adding functioanlity to a page that can
//automatically handle passive SAML-P (ADFS) authentication when doing ajax calls with jQuery
//this can occur if the SAML-P token has been invalidated and the browser does an ajax call to the server
//the ajax calls will be sent again in the order they were called after authentication finishes successfully
//if passive authentication requires user to enter credentials then user is presented with dialog
//with possibly links that open in a new tab, this is because in the project we use ADFS and ADFS does not allow
//login view to show inside an iframe. The dialog contains text describing how to finish authentication and it also contains
//a Ok-button and an Cancel-button. When Ok-button is clicked it's presumed the user has finished authentication in another window and
//all ajax request which are in queue is resent. When Cancel-button is clicked all waiting ajax requests are
//failed explicitly.
//Requirements: jquery and jquery ui dialog
function makeCall() { //test function for initiating an xhr which responds with 401 (substitute with your own)
return $.get('/some-resource') //
.then(function (data) {
},
function (error) {
});
}
//below is url to a login page which is used for passive authentication
var loginPageUrl = '/login';
//requires jquery and jquery ui dialog
//before usage must initiate using
//myModule.initiateHandleAjaxPassiveSam(loginUrlPath)
(function (module) {
//just to keep track if the functionality has already been added
var alreadyInitiated = false;
module.initiateHandleAjaxPassiveSam = function (loginUrlPath, onAuthenticatingCallback, onAuthenticationFinishedCallback) {
//initates the functionality needed to hijack ajax (xhr) calls
//and do authentication
//loginPath, url to a path that causes passive authentication
if (alreadyInitiated)
return;
alreadyInitiated = true;
if (loginUrlPath.substring(0, 1) !== "/")
loginUrlPath = '/' + loginUrlPath;
var renewAuthenticationTokenUrl = location.protocol + "//" + location.host + loginUrlPath; //
var authenticationTryDate = new Date();
var authenticationTimeout = 10000; //10 seconds
//A queue of deferred ajax call that will be stored during authentication phase
//and resolved after user have been authenticated, or rejected if user has cancelled
var deferrQueue = [];
var addToDeferrQueue = function (deferred) {
deferrQueue.push(deferred);
}
//will resolve all actions waiting for user finishing authentication
var flushAndResolveQueue = function () {
var deferrQueueCopy = deferrQueue;
deferrQueue = [];
for (var i = 0; i < deferrQueueCopy.length; i++) {
deferrQueueCopy[i].resolve('assume_authenticated');
}
}
//will reject all deferred actions because user has not managed to successfully login
var flushAndRejectQueue = function () {
var deferrQueueCopy = deferrQueue;
deferrQueue = [];
for (var i = 0; i < deferrQueueCopy.length; i++) {
deferrQueueCopy[i].reject('user_cancelled_authentication.');
}
}
var dialogOpen = false;
var openAuthenticateDeferredDialog = function () {
//in the case passive authentication is not successful
//then user needs to login actively, since at least the ADFS version we use won't
//allow the login screen to load into iframe, I've opted with a more manual approach.
//where the user is prompted to authenticate in another browser window and then click an OK-button
//which will cause all "deferred" ajax request to be called
//only one dialog is needed for all request, we don't open any more dialogs
if (dialogOpen)
return;
//if user presses cancel in dialog all "deferred" ajax calls will be rejected which will lead
//them to fail with the original authorization error.
var shouldrejectonclose = true; //defaults to true when pressing 'X' in corner as well as 'Cancel' button
var $dialog = $('<div></div>').appendTo('body');
//add information for user so they know what's going on how to proceed by login in another tab
$dialog.append("You have been logged out, (instructions and links)...");
$dialog.dialog({
title: 'You have been logged out due to inactivity',
autoOpen: true,
buttons: {
'OK': function () {
flushAndResolveQueue();
shouldrejectonclose = false;
$dialog.dialog('close');
},
'Cancel': function () {
shouldrejectonclose = true;
$dialog.dialog('close');
}
},
close: function () {
dialogOpen = false;
$dialog.dialog('destroy');
$dialog.remove();
if (shouldrejectonclose)
flushAndRejectQueue();
}
});
dialogOpen = true;
}
function authenticate() {
//will try to do a passive authenticate attempt through a hidden iFrame
//if this fail because user needs to actively login
//will prompt the dialog that instructs the user how to login manually
if (onAuthenticatingCallback) //in my case I needed callbacks to disable som other global ajax handlers
onAuthenticatingCallback();
return $.Deferred(function (d) {
authenticationTryDate = new Date();
//this iframe is used to cause passive silent authentication through ADFS redirect
var iFrame = $("<iframe></iframe>");
iFrame.hide();
iFrame.appendTo(document.body);
iFrame.attr('src', renewAuthenticationTokenUrl);
//set a timeout for how long we will wait for server to respond for a passive authentication attempt
var failAuthenticationTimeout = setTimeout(function () {
d.reject('authentication_request_timeout');
iFrame.remove();
}, authenticationTimeout);
iFrame.load(function () {
//when iframe has been loaded
//there are in my case two things that could have happened
//the user have been successfully authenticated passively and
//then the url of the host should be the same in this case I
//simply resolve the deferred action which will cause the original ajax call to be sent again
//and this time the token is refreshed.
//if the user has been redirected to another domain for active authentication by the user
//this is easly identified when I try to access location object which will cause
//browser security exception to be thrown which occurs because the same origin policy required by
//the browsers.
try {
if (!this.contentWindow.location.hostname) {
//we probably won't be able to branch into this branch because of browser same origin policy
throw new Error('hostname is emty, undefined or null which indicates cannot access iframe so authentication has led to redirect to proxy page');
} else { //authentication succedded silently resolve deferred action immiediatly (resend ajax requests)
d.resolve();
}
} catch (error) {
//if access to window location path name is causing security breach exception then that indicates
//might be good idea to check for error type here, but this does the trick for my usage.
//add incoming deferred to queue to be handled after manual login attempt by user
addToDeferrQueue(d);
//open dialog for user if not already open
openAuthenticateDeferredDialog();
//d.reject('dialog_opened');
} finally { //remove iframe since it won't be needed more for this authentication attempt
iFrame.remove();
clearTimeout(failAuthenticationTimeout);
}
});
});
};
var callOriginalError = function(originalOptions, jqXHR, textStatus, errorThrown) {
if (originalOptions._error) originalOptions._error(jqXHR, textStatus, errorThrown);
};
//set up prefilter to replace original error handler to handle cases of authentication errors
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
originalOptions._error = originalOptions.error;
//create copy of original options with the url added if this wasn't added in the original options
var optionsCopy = $.extend({url:options.url}, originalOptions);
//flag that is used to determine how the deferred authentication was resolved
var assume_authenticated = false;
if (deferrQueue.length > 0) {
//there are items waiting to be sent
//we abort current xhr and immidatly and add the current request to the deferrQueue,
//This is useful in case of authentication have been done after some requests have been queued up
//and the current request have come afterwards which could cause it to be sent before the first ones.
//This makes sure that the current request is aborted and a new one is added at the end of the queue
console.log('waiting until authentication actions has finished before finalizing request to:'+ options.url);
jqXHR.abort();
//a deferr object is used to signal when authentication process (either automatically or by user) has finished so
//that the request can be resent.
var deferr = $.Deferred();
$.when(deferr).then(function (d) {
//if user has clicked OK or authentication have succedded automatically
//we'll come to this branch
if (onAuthenticationFinishedCallback) //if there are callbacks when authentication is finished
onAuthenticationFinishedCallback();
assume_authenticated = d === "assume_authenticated"; //if
console.log('retrying ajax to:' + optionsCopy.url);
return $.ajax(optionsCopy);
})
.fail(function (d) {
if (!assume_authenticated) {
console.log(d);
callOriginalError(originalOptions, jqXHR, 'aborted', d);
}
});
//add to deferr queue or when user have clicked OK or Cancel in dialog
addToDeferrQueue(deferr);
//open dialog
openAuthenticateDeferredDialog();
return;
}
// overwrite error handler for current request
options.error = function (_jqXHR, _textStatus, _errorThrown) {
//if (console && console.log)
// console.log(_errorThrown);
if (_jqXHR.status === 401) {
return $.when(authenticate())
.then(function (d) { //authentication seems to have succeded resending ajax
if (onAuthenticationFinishedCallback)
onAuthenticationFinishedCallback();
assume_authenticated = d === "assume_authenticated";
console.log('retrying ajax to:' + optionsCopy.url);
return $.ajax(optionsCopy); //resend request, but use copy since we want the error handler not to be nested
}).fail(function (d) { //three cases: user cancelled authentication, authentication failed somehow or resend ajax failed with other errors
if (!assume_authenticated) { //if this caused of user cancallation or other reason than retry failed
console.log(d);
callOriginalError(originalOptions, _jqXHR, _textStatus, _errorThrown);
}
});
//} else {
// return $.ajax(originalOptions);
//}
} else { //other errors except 401 immidiatly fail
return $.Deferred(function (d) {
callOriginalError(originalOptions, _jqXHR, _textStatus, _errorThrown);
d.reject(_errorThrown);
});
}
};
});
(function () {
//This block overwrites jQuery original load function to make it work
//with deferred, otherwise deferred ajax actions which are called again after successful authentication
//won't load into the dom element.
//The original code is from the jQuery version which I'm using in a project.
//You can remove this block if you don't use the load method.
var origLoad = jQuery.fn.load;
jQuery.fn.load = function (url, params, callback) {
if (typeof url !== "string" && origLoad) {
return origLoad.apply(this, arguments);
}
var selector, response, type,
self = this,
off = url.indexOf(" ");
if (off >= 0) {
selector = url.slice(off, url.length);
url = url.slice(0, off);
}
// If it's a function
if (jQuery.isFunction(params)) {
// We assume that it's the callback
callback = params;
params = undefined;
// Otherwise, build a param string
} else if (params && typeof params === "object") {
type = "POST";
}
var responseHandler = function (responseText, status, jqXHR) {
// Save response for use in complete callback
response = arguments;
self.html(selector ?
// If a selector was specified, locate the right elements in a dummy div
// Exclude scripts to avoid IE 'Permission Denied' errors
jQuery("<div>").append(jQuery.parseHTML(responseText)).find(selector) :
// Otherwise use the full result
responseText);
if (callback)
self.each(callback, response || [jqXHR.responseText, status, jqXHR]);
};
// If we have elements to modify, make the request
if (self.length > 0) {
jQuery.ajax({
url: url,
// if "type" variable is undefined, then "GET" method will be used
type: type,
dataType: "html",
data: params,
success: responseHandler,
fail: responseHandler
});
}
return this;
};
}());
};
}(window.myModule = window.myModule || {}));
myModule.initiateHandleAjaxPassiveSam(loginPageUrl);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment