Cross site Ajax.Request with cookies and Prototype.js - prototypejs

I need to tell Ajax.Request (from Prototype.js) to set xhr.transport.withCredentials to true (to enable cookies in my cross site requests headers). I failed by trying:
Ajax.Request('http://otherSubdomain.host:port/', {
onCreate: function(request){
request.transport.withCredentials = true;
}
});
Access-Control-Allow-Origin is set and the request is successful, but no cookies were sent.
I hate to point out, but it seems to be much easier with jquery
here is an example solution.

Try to patch Ajax.Request like this:
Ajax.Request.prototype.request = Ajax.Request.prototype.request.wrap(function(request, url) {
if (this.options.withCredentials) {
this.transport.withCredentials = true;
}
request(url);
});
And then you'll have additional option withCredentials:
new Ajax.Request('example.com', {
withCredentials: true
});

Improved Victor's answer a bit.
Fixes in IE where it requires setting withCredentials between 'open' and 'send' and makes it consistent with jQuery ajax options.
Ajax.Request.prototype.setRequestHeaders = Ajax.Request.prototype.setRequestHeaders.wrap(function(setHeaders) {
setHeaders();
if (this.options.xhrFields) Object.extend(this.transport, this.options.xhrFields);
});
And then to use it:
new Ajax.Request('example.com', {
xhrFields: {
withCredentials: true
}
});

Related

Cypress + 2 Super Domains + AntiforgeryToken = Cypress can't be redirect

I am currently working on setting-up a Cypress solution to test a product based on Nuxt and .NET. The product uses an SSO-based login page with a different super domain from the product.
This creates a problem with cookies. Indeed, access to the product generates a redirection to the SSO authentication page. And, following the validation of the authentication form, Chrome driven by Cypress enters a call loop with errors in the attributes of the cookie, in particular the value of the SameSite attribute which is incorrect compared to the expectation SSO side.
Currently, several features are disabled in the browser, namely :
SameSiteByDefaultCookies
CrossSiteDocumentBlockingIfIsolating
CrossSiteDocumentBlockingIfIsolating
IsolateOrigins
site-per-process
Excerpt of cypress/plugins/index.js
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.family === 'chromium') {
launchOptions.args.push(
"--disable-features=SameSiteByDefaultCookies,CrossSiteDocumentBlockingIfIsolating,CrossSiteDocumentBlockingIfIsolating,IsolateOrigins,site-per-process"
);
}
console.log(launchOptions.args);
return launchOptions
});
Even by disabling all the protections on the SSO, no way to make Cypress work.
Here is an Excel file with all of the HTML requests listed via the Chrome console. The file describes the differences between the Chrome driven by Cypress and a "classic" Chrome.
NB: adding "chromeWebSecurity": false, in the configuration of Cypress does not change anything
Not fully knowing what the underlying concerns are, I cannot describe the problem well. All the solutions proposed to similar problems exposed on various forums (StackOverFlow included) were not enough to solve mine. Can you help me please ?
Thanks in advance.
Regards,
Alexander.
This may have been caused by the chrome 94 update, which came out in October 21. Update 94 removed the SameSiteByDefaultCookies flag, so that fix no longer works.
If you're having the same issue I was, I resolved it by intercepting all requests, checking if they had a set-cookie header(s) and rewriting the SameSite attribute. There's probably a neater way to do it, as this does clutter up the cypress dashboard a little. You can add this as a command for easy reuse:
In your commands file:
declare namespace Cypress {
interface Chainable<Subject> {
disableSameSiteCookieRestrictions(): void;
}
}
Cypress.Commands.add('disableSameSiteCookieRestrictions', () => {
cy.intercept('*', (req) => {
req.on('response', (res) => {
if (!res.headers['set-cookie']) {
return;
}
const disableSameSite = (headerContent: string): string => {
return headerContent.replace(/samesite=(lax|strict)/ig, 'samesite=none');
}
if (Array.isArray(res.headers['set-cookie'])) {
res.headers['set-cookie'] = res.headers['set-cookie'].map(disableSameSite);
} else {
res.headers['set-cookie'] = disableSameSite(res.headers['set-cookie']);
}
})
});
});
Usage:
it('should login using third party idp', () => {
cy.disableSameSiteCookieRestrictions();
//add test body here
});
or alteratively, run it before each test:
beforeEach(() => cy.disableSameSiteCookieRestrictions());
I finally found the solution for this SSO authentication.
Here is the code:
Cypress.Commands.add('authentificationSSO', (typedeCompte, login) => {
let loginCompte = 'login';
let motDePasseCompte = 'password';
let baseUrlSSO = Cypress.env('baseUrlSSO') ? Cypress.env('baseUrlSSO') : '';
let baseUrl = Cypress.config('baseUrl') ? Cypress.config('baseUrl') : '';
cy.request(
'GET',
baseUrlSSO +
'/Account/Login?${someParameters}%26redirect_uri%3D' +
baseUrl
).then((response) => {
let requestVerificationToken = '0';
let attributsCookieGroupe = [];
let nomCookie = '';
let tokenCookie = '';
const documentHTML = document.createElement('html');
documentHTML.innerHTML = response.body;
attributsCookieGroupe = response.headers['set-cookie'][0].split(';');
const loginForm = documentHTML.getElementsByTagName('form')[0];
const token = loginForm.querySelector('input[name="__RequestVerificationToken"]')?.getAttribute('value');
requestVerificationToken = token ? token : '';
nomCookie = attributsCookieGroupe[0].split('=')[0];
tokenCookie = attributsCookieGroupe[0].split('=')[1];
cy.setCookie(nomCookie, tokenCookie, { sameSite: 'strict' });
cy.request({
method: 'POST',
url:
baseUrlSSO +
'/Account/Login?{someParameters}%26redirect_uri%3D' +
baseUrl,
followRedirect: false,
form: true,
body: {
Login: loginCompte,
Password: motDePasseCompte,
__RequestVerificationToken: requestVerificationToken,
RememberLogin: false
}
});
});
});

Ajax request with CORS redirect fails in IE11

I'm trying to make an ajax request to a resource on the same domain. Under certain circumstances the request gets redirected(303) to an external resource. The external resource supports CORS.
In browsers like Chrome, Firefox or Safari the request succeeds.
In IE11 the request fails with error:
SCRIPT 7002: XMLHttpRequest: Network Error 0x4c7, The operation was canceled by the user
The ajax request is made with jQuery:
$.ajax({
url: "/data",
type: "POST",
dataType: "json",
contentType: "application/json;charset=UTF-8",
data: JSON.stringify({name: 'John Doe'})
}).done(function () {
console.log('succeeded');
}).fail(function () {
console.log('failed');
});
I've build a little example which demonstrates the problem. You could see the code here.
w/o redirect
w/ redirect
Is there a way to solve this problem? What am I missing?
In the initial definition of the CORS-standard, redirects after a successful CORS-preflight request were not allowed.
IE11 implements this (now outdated) standard.
Since August 2016, this has changed, and all major browsers now support it (Here's the actual pull request).
I'm afraid to support <=IE11 you'll have to modify your server-side code as well to not issue a redirect (at least for <=IE11).
Part 1) Server-side (I'm using node.js express here):
function _isIE (request) {
let userAgent = request.headers['user-agent']
return userAgent.indexOf("MSIE ") > 0 || userAgent.indexOf("Trident/") > 0
}
router.post('data', function (request, response) {
if (_isIE(request)) {
// perform action
res.set('Content-Type', 'text/plain')
return res.status(200).send(`${redirectionTarget}`)
} else {
// perform action
response.redirect(redirectionTarget)
}
})
Part 2 Client-side
Note: This is pure Javascript, but you can easily adapt it to your jQuery/ajax implementation.
var isInternetExplorer = (function () {
var ua = window.navigator.userAgent
return ua.indexOf("MSIE ") > 0 || ua.indexOf("Trident/") > 0
})()
function requestResource (link, successFn, forcedRedirect) {
var http
if (window.XMLHttpRequest) {
http = new XMLHttpRequest()
} else if (window.XDomainRequest) {
http = new XDomainRequest()
} else {
http = new ActiveXObject("Microsoft.XMLHTTP")
}
http.onreadystatechange = function () {
var OK = 200
if (http.readyState === XMLHttpRequest.DONE) {
if (http.status === OK && successFn) {
if (isInternetExplorer && !forcedRedirect) {
return requestResource(http.responseText, successFn, true)
} else {
successFn(http.responseText)
}
}
}
}
http.onerror = http.ontimeout = function () {
console.error('An error occured requesting '+link+' (code: '+http.status+'): '+http.responseText)
}
http.open('GET', link)
http.send(null)
}
its already answered - have a look - https://blogs.msdn.microsoft.com/webdev/2013/10/28/sending-a-cors-request-in-ie/

Detect Ajax and set layout null in SailsJs

I would implement pajax, i need to check server side if a request is ajax and set the layout to null to return the view without layout.
I know that the req object contain a 'xhr' property. How can i the layout to null automatically?
Thanks in advance!
EDIT: i find a solution! See here: https://stackoverflow.com/a/31571250/4870013
You can check req.wantsJSON object:
// True, if the request has a xhr polling origin. (via socket)
req.wantsJSON = req.xhr;
Example:
if (req.wantsJSON) {
res.view("yourView", { layout: null });
} else {
res.view("yourView");
}
you can use do this by detecting request
if(!Request.isAjax)
{
// your code with return Layout
}
else
{
// your code without return Layout
}
You can check request.headers if it contains HTTP_X_REQUESTED_WITH.
If HTTP_X_REQUESTED_WITH has a value of XMLHttpRequest then it is an ajax request.
Example:
if (request.headers["x-requested-with"] == 'XMLHttpRequest') {
//is ajax request
}
I find a solution! I use a policy to accomplish that result.
My policy:
module.exports = function(req, res, next) {
if (req.xhr) {
res.locals.layout = null;
}
return next();
};
And my policy config:
module.exports.policies = {
'*': 'isAjax'
};
Work like a charm!

Clear IE cache when using AJAX without a cache busting querystring, but using http response header

I'm having the classic IE-caches-everything-in-Ajax issue. I have a bit of data that refreshes every minute.
Having researched the forums the solutions boil down to these options (http://stackoverflow.com/questions/5997857/grails-best-way-to-send-cache-headers-with-every-ajax-call):
add a cache-busting token to the query string (like ?time=[timestamp])
send a HTTP response header that specifically forbids IE to cache the request
use an ajax POST instead of a GET
Unfortunately the obvious querysting or "cache: false" setting will not work for me as the updated data file is hosted on Akamai Netstorage and cannot accept querystrings. I don't want to use POST either.
What I want to do is try send an HTTP response header that specifically forbids IE to cache the request or if anyone else knows another cache busting solution??
Does anyone know how this might be done? Any help would be much appreciated.
Here is my code:
(function ($) {
var timer = 0;
var Browser = {
Version: function () {
var version = 999;
if (navigator.appVersion.indexOf("MSIE") != -1) version = parseFloat(navigator.appVersion.split("MSIE")[1]);
return version;
}
}
$.fn.serviceboard = function (options) {
var settings = { "refresh": 60};
return this.each(function () {
if (options) $.extend(settings, options);
var obj = $(this);
GetLatesData(obj, settings.refresh);
if (settings.refresh > 9 && Browser.Version() > 6) {
timer = setInterval(function () { GetLatestData(obj, settings.refresh) }, settings.refresh * 1000);
}
});
};
function GetLatestData(obj, refresh) {
var _url = "/path/updated-data.htm";
$.ajax({
url: _url,
dataType: "html",
complete: function () {},
success: function (data) {
obj.empty().append(data);
}
}
});
}
})(jQuery);
Add a random number to the GET request so that IE will not identify it as "the same" in its cache. This number could be a timestamp:
new Date().getTime()
EDIT perhaps make the requested url:
var _url = "/path/updated-data.htm?" + new Date().getTime()
This shouldn't cause any errors I believe.
EDIT2 Sorry I just read your post a bit better and saw that this is not an option for you.
You say "is hosted on Akamai and cannot accept querystrings" but why not?
I've never heard of a page that won't accept an additional: "?blabla", even when it's html.
This was driving me crazy. I tried many cache busting techniques and setting cache headers. So many of these either did not work or were wild goose chases. The only solution I found which tested to work correctly was setting:
Header Pragma: no-cache
I hope it saves others with IE headaches.

Stop Duplicate Ajax Submisions?

I am wondering what is the best way to stop duplciate submissions when using jquery and ajax?
I come up with 2 possible ways but not sure if these are the only 2.
On Ajax start disable all buttons till request is done. 2 problems I see with this though is I use jquery model dialog so I don't know how easy it would be to disable those button as I not sure if they have id's. Second I if the the request hangs the user has really no way to try again since all the buttons are disabled.
I am looking into something called AjaxQueue at this time I have no clue if it is what I need or how it works since the site where the plugin is apparently down for maintenance.
http://docs.jquery.com/AjaxQueue
Edit
I think this is a spin off of what I was looking at.
http://www.protofunc.com/scripts/jquery/ajaxManager/
The only problem I see with this ajaxManager is that I think I have to change all my $.post, $.get and $.ajax ones to their type.
But what happens if I need a special parameter from $.ajax? Or that fact I like using .post and .get.
Edit 2
I think it can take in all $.ajax options. I am still looking into it. However what I am unsure about now is can I use the same constructor for all requests that will use the same options.
First you have to construct/configure a new Ajaxmanager
//create an ajaxmanager named someAjaxProfileName
var someManagedAjax = $.manageAjax.create('someAjaxProfileName', {
queue: true,
cacheResponse: true
});
Or do I have to make the above every single time?
How about setting a flag when the user clicks the button? You will only clear the flag when the AJAX request completes successfully (in complete, which is called after the success and error callbacks), and you will only send an AJAX request if the flag is not set.
Related to AJAX queuing there is a plugin called jQuery Message Queuing that is very good. I've used it myself.
var requestSent = false;
jQuery("#buttonID").click(function() {
if(!requestSent) {
requestSent = true;
jQuery.ajax({
url: "http://example.com",
....,
timeout: timeoutValue,
complete: function() {
...
requestSent = false;
},
});
}
});
You can set a timeout value for long-running requests (value is in milliseconds) if you think your request has a possibility of hanging. If an timeout occurs, the error callback is called, after which the complete callback gets called.
You could store an active request in a variable, then clear it when there's a response.
var request; // Stores the XMLHTTPRequest object
$('#myButton').click(function() {
if(!request) { // Only send the AJAX request if there's no current request
request = $.ajax({ // Assign the XMLHTTPRequest object to the variable
url:...,
...,
complete: function() { request = null } // Clear variable after response
});
}
});
EDIT:
One nice thing about this, is that you could cancel long running requests using abort().
var request; // Stores the XMLHTTPRequest object
var timeout; // Stores timeout reference for long running requests
$('#myButton').click(function() {
if(!request) { // Only send the AJAX request if there's no current request
request = $.ajax({ // Assign the XMLHTTPRequest object to the variable
url:...,
...,
complete: function() { timeout = request = null } // Clear variables after response
});
timeout = setTimeout( function() {
if(request) request.abort(); // abort request
}, 10000 ); // after 10 seconds
}
});
$.xhrPool = {};
$.xhrPool['hash'] = []
$.ajaxSetup({
beforeSend: function(jqXHR,settings) {
var hash = settings.url+settings.data
if ( $.xhrPool['hash'].indexOf(hash) === -1 ){
jqXHR.url = settings.url;
jqXHR.data = settings.data;
$.xhrPool['hash'].push(hash);
}else{
console.log('Duplicate request cancelled!');
jqXHR.abort();
}
},
complete: function(jqXHR,settings) {
var hash = jqXHR.url+jqXHR.data
if (index > -1) {
$.xhrPool['hash'].splice(index, 1);
}
}
});

Resources