Mysterious 401 challenge when using AJAX - ajax

I have a .NET Core web app hosted on the net.
I'm using claims based auth via cookies:
When login success...
var principal = new ClaimsPrincipal();
var id = new ClaimsIdentity(user);
id.AddClaim(new Claim("ViewData", "Allowed"));
id.AddClaim(new Claim("TenantId", user.TenantId));
principal.AddIdentity(id);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
This all works fine for every user apart from one - however, this one particular user (a 3rd party) is running into an auth popup in their browser (they've tried a few) when they interact with a particular page - every other page works fine.
This leads me to believe the issue is environmental, but I want to understand what could be happening here.
The only difference between the page in question and every other page is that this one does an AJAX post to a controller in order to save some data. The Home controller requires auth to view (or edit) the data.
[Authorize(Policy = "ViewData")]
The Ajax is your standard stuff
Razor:
$.ajax({
type: 'POST',
url: '#Url.Action("_Save", "Home")',
dataType: 'json',
contentType: 'application/json',
data: ko.toJSON(viewModel.model()),
success: function (result) {
//... callback code etc
Checking the rendered JS shows that the AJAX call is relative to the current page and therefore isn't going to some strange URL
Raw JS:
$.ajax({
type: 'POST',
url: '/Home/_Save',
dataType: 'json',
contentType: 'application/json',
data: ko.toJSON(viewModel.model()), // ... etc
I can see the cookie being included in the headers when I look in my browser:
accept: application/json, text/javascript, */*; q=0.01
accept-encoding: gzip, deflate, br
accept-language: en-GB,en-US;q=0.9,en;q=0.8
cache-control: no-cache
content-length: 4775
content-type: application/json
cookie: <cookie details here>
Unfortunately, it being a 3rd party I can't really connect to the machine to view the debug console for the browser.
The question I have really is a longshot - it sounds like it could be a proxy issue but I don't understand why making an AJAX call is any different to making the login POST request, unless of course my AJAX setup is missing some auth data that is required - maybe a header of some sort?
Has anyone seen something like this before?

I have seen issues somewhat like this, and they have generally been related to security settings. You might try looking into CSRF headers. It could be several things. This could include a local antivirus, on-machine firewall, anti-spyware, or other privacy / protection application. Since it is a single user, debugging it will be extremely difficult, and I would recommend you figure out how that user's security settings / applications vary from their colleagues'.
Adding your site to the trusted sites list in specific browsers may resolve the single issue. It's almost certainly something hidden in there.

Well, this sorted itself out after a while - I believe it was probably network related.
The 3rd party tried again a week later and had no issues this time.

Related

Ajax saving but not sending CORS cookies from 127.0.0.1 to service

So I've been having issues sending cookies with a cross-domain request to a service. I've gotten to make it work in our CI environment, but not locally. Basically, I have an API at api.service.com, and it's accessed via AJAX calls run from clients at webapp.service.com. The API sets a cookie for .service.com via set-cookie. Then all subsequent calls to the API should include this cookie. This works as intended, when running from webapp.service.com. This will work in prod just fine. However, for obvious reasons, I'd like be able to develop the webapp locally, and run API calls against api.service.com from either local files or localhost service.
I understand that Chrome is a little iffy regarding saving cookies for local files, but I've addressed that, and it is not the issue. The cookie is, in fact, saving. It's just not sending that cookie with subsequent API calls. Here's the workflow I've got going on (with some genericized/censored product names):
An AJAX call POSTS to our API:
$.ajax({
method: "POST",
crossDomain: true,
xhrFields: {
withCredentials: true,
},
url: 'https://api.service.com/login',
data: data,
contentType:"text/plain",
dataType: "json",
success: function(data){
...
}
});
The CORS stuff is set up to allow credentials and this origin (the allowed origin updates dynamically, doesn't use *. So we get this cookie back: cookie:service-token=7f7d251ebeec37f7c0815....; SameSite=lax;Max-Age=2629744; domain=.service.com; path=/;
It shows up in Chrome like this:
Request cookie
I know for a fact that this actually works to save the cookie. However, perhaps not how I want. I go into Chrome's cookies, and it updates properly as seen:
Chrome saved cookie
The problem I'm seeing there is that its "send for" value is "same-site connections only". I have no idea how to originally set that for Chrome to treat it as "any kind of connection." I think this is the reason that, when I send another AJAX call, that cookie is not included in the request.
I've seen other posts like this that were resolved by adding crossDomain and/or withCredentials to the AJAX call. This did not resolve it for me. This is a subsequent call to the API:
$.ajax({
url: 'https://api.service.com/getTheThing',
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function(data){...}
});
The cookie is not included in this request, and thus fails.
It turns out, setting SameSite=lax was doing the opposite of what I thought it would. Removing that solved this issue.

read ALL response headers of ajax GET request of S3 objects

Question Overview:
I am accessing a list of files stored in my AWS S3 bucket through a CORS request of presigned files. This basically works fine. However, the objects have some custom METADATA attached to them, which I can't access. I understood, that I can access this metadata only when I add the header key (e.g. "x-amz-meta-1234", where 1234 is the key of my metadata) to the Expose-Headers of the target-bucket's CORS config. While this works so far for me, I can't set the expose-header with a wildcard (e.g. "x-amz-meta-*"), which would solve my problem, but AWS doesn't support wildcards for the expose-header entries.
However, when I look in the NETWORK tab of my Chrome Dev Tools, all desired metadata is showing up in the headers during the GET/HEAD request (note the entries on the lower part, x-amz-meta-4021 and -template_id):
This is my HEAD call:
$.ajax({
url: url,
dataType: 'json',
crossDomain: true,
type: 'HEAD',
success: function(data, status, jqXHR) {
console.log('got some response ..?');
console.log(data);
console.log(jqXHR);
console.log('responseHeader template_id: ' + jqXHR.getResponseHeader('x-amz-meta-template_id'));
console.log('responseHeader meta-4021: ' + jqXHR.getResponseHeader('x-amz-meta-4021'));
console.log(jqXHR.getAllResponseHeaders());
},
error: function(error, xhr, data) {
console.log('in error..');
console.log(error);
console.log(xhr);
console.log(data);
}
});
});
And this is the console output:
Object {readyState: 4, getResponseHeader: function, getAllResponseHeaders:
function, setRequestHeader: function, overrideMimeType: function…}
responseHeader template_id: 813
responseHeader meta-4021: null
x-amz-meta-template_id: 813
Last-Modified: Fri, 09 Jun 2017 13:05:33 GMT
Content-Type: video/mp4
I set expose-header for the metadata-entry 'template_id' explicitly and therefore the header-data is returned correctly for this entry. However, for the entry '4021' I didn't set the expose-header. The problem is, that this metadata (and the keys) are produced by our (android/ios) apps, and I can't really control the keys of that metadata that easily.
Whats puzzling me: why am I able to see the whole response in the chrome network tab, but can't access this data from a client-side script? There are many possible workarounds and solutions, but I basically want to understand, why my browser can display me data, which can't be accessed by jQuery.
PS: in case you want to see the CORS config or the full script, please let me know. I tried to be as precise as possible. Thanks in advance!
I basically want to understand, why my browser can display me data, which can't be accessed by jQuery.
To understand this, you need to understand the purpose of CORS.
CORS isn't really about access control, and CORS isn't really working on your site's behalf. CORS is working on behalf of the user and the browser, to prevent the browser from becoming a confused deputy and doing something the user would not have wanted. This usually coincides with something the site would also not have wanted, but that's secondary.
The browser's default behavior is to assume that programmatic access to cross-origin requests is bad, which is why they are denied when no Access-Control-Allow-Origin header is present. Your bank would not want internetbadguys.com to make ajax requests to the bank web site, and if that site tried, the browser would block it unless the bank's web server foolishly allowed it with a CORS response.
CORS is a mechanism for your site to tell the brower, "yes, the cross-origin request you are making is not unexpected, it's allowed... and from this response, the browser is allowed to engage in certain behaviors, such as exposing the following response headers to the code making the request."
In that light, the behavior you observe is correct. Exposing headers (or not) doesn't mean include them in the HTTP response (or not) -- exposing headers gives the browser permission to expose what it knows to the ajax caller. If the cross-origin origin wants them exposed, it has to be explicit.

Cross-domain AJAX POST withCredentials and IE8+ compatibility

I have a login setup for one of my sites where the user types their information into a login popup on the home page, which then submits the information back to a servlet and then receives a response back via JSON. The home page then proceeds to send the user to their profile page or alternatively displays an error (e.g., if username and password do not match).
$.ajax({
dataType: 'jsonp',
async: false,
url: loginLocation,
type: 'GET',
crossDomain: true,
cache: false,
xhrFields: crossDomain ? {
withCredentials: true
} : {},
data: ({'key1': value1, 'key2': value2, ..., 'keyN':'valueN'}),
success: function(data){
if (data && data.status && data.status == "success") {
window.location = profileLocation;
} else {
errorHandler();
}
},
error: errorHandler
});
I am looking to change this from a GET request to a POST in order to prevent arbitrary query strings being sent into the servlet. However, it appears that there are several considerations at play here with regards to how the solution ought to be laid out. It must:
use POST instead of GET
be a cross-domain request (the login page and the servlet are on different domains over both of which I have access/control)
use the withCredentials parameter (the login functionality relies on the JSESSIONID cookie so this parameter is required)
be compatible with IE8 and above
I have tried looking into cross-domain ajax requests that fit the above criteria, but the major sticking point seems to be the IE8/IE9 compatibility. Approaches such as easyXDM appear to be ambiguous as to support for these browsers (I have seen conflicting reports online as to how it works in IE8) and I don't want to run into the danger of realizing it won't work halfway through implementation.
So in short, is there a way to do cross-domain ajax requests using POST and with the withCredentials parameter, that is also compatible with IE8+? Is easyXDM an appropriate solution to this?
I was able to determine the solution to the above question by using the xdomain library (found at https://github.com/jpillora/xdomain) which overrides the request behavior to allow cross-domain ajax in IE8 and IE9. This involved setting up the proxy.html as shown in the example on the xdomain site as well as adding Access-Control-Allow-Origin and other related headers to the server response. This allows cross-domain ajax JSON POST requests using withCredentials in IE8+ per the criteria listed in the original post. It also allows cross-domain requests between HTTP and HTTPS.

CORS $.ajax session cookies (access-control-allow-credentials & withCredentials=true)

I realize this question has been asked a dozen or more times and each response given indicates I am doing it right but perhaps I am missing something.
AJAX serves up CORS request like so...
$.ajax({
url: 'someotherdomain.com',
type: 'post',
data: {key: 'value'},
dataType: 'json',
async: false,
crossDomain: true,
beforeSend: function(xhr){
xhr.withCredentials = true;
},
success: function(x, status, xhr){
},
error: function(xhr, status, error){
}
});
PHP serves up CORS requests like so...
header('Access-Control-Max-Age: 1728000');
header('Access-Control-Allow-Origin: http://someotherdomain.com');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-MD5, X-Alt-Referer');
header('Access-Control-Allow-Credentials: true');
header("Content-Type: application/json; charset=utf-8");
According to all documentation as long as the 'Access-Control-Allow-Credentials' server side header, and the 'withCredentials=true' client side header is set session cookie handling between the domains should be transparent. Am I missing something?
async: false
was preventing the session cookie from being sent back to the server on each request. The following fixed it.
async: true
Although this does allow for the session cookie to get set by the browser when making a cross origin request sharing call, I am now experiencing problems regarding the following scenario:
Server A sends response to client
Client using CORS makes request of server B
XMLHttpRequest -> PHP -> Session handler -> MySQL -> Stored Procedure
Due to the MUTEX locks in the PHP session management the asynchronous nature and apparently, requirement may force a work around of manually setting the cookie with a different header option such as XCookie or something similar to keep the servers session and client requests synchronized.
This particular work around does not sit well with me as I believe it would open up an easy lane of travel for session hijacking and session replay attack vectors.
Using an SSL/TLS wrapped connection may assist in preventing the above scenario but in terms of independently providing security measures for the client I do not believe this should suffice.
Anyone with any thoughts on this?
In your example above, you are setting the Access-Control-Allow-Origin header to 'http://someotherdomain.com', which is the same as the url you are requesting from JQuery. The Access-Control-Allow-Origin header should be the value of the domain the request is coming from. As a quick, test, try setting the value of this header to '*' (without the quotes) and see if it works ('*' means all domains are allowed).

if ios6 safari is caching ajax calls, is it caching passwords? security risk?

Following on from the thread
Is Safari on iOS 6 caching $.ajax results?
If io6 safari is caching the results from non unique ajax calls then it must also be caching the call itself. Would this then mean it is caching usernames and passwords in a login situation thereby posing a security risk?
Short version: If you're sending usernames and passwords over the wire in plaintext, you've already opened a huge security hole.
Long version: Browsers will cache based on URI, so if you're sending user/pass as GET variables, then yes it will cache and yes it is a security risk. However, even if the browser didn't cache this, you're still doing something wrong. A third party need only look at the HTTP header to see what the user/pass is.
If you are sending this as POST, it is a bit harder to find the username/password. The browser will not cache the request as the URL is always the same. However, it is still possible to read the content of the request and find the user/pass.
To be the most secure, use HTTPS and pass the values via POST. The entire HTTP request is encrypted, including the headers. However, the browser will still cache the URL, so using GET variables is still a bad idea.
Example from the jQuery documentation on using POST with ajax:
$.ajax({
type: "POST",
url: "some.php",
data: { name: "John", location: "Boston" }
}).done(function( msg ) {
alert( "Data Saved: " + msg );
});

Resources