CSRF mismatch when POSTing to sails backend from Angular JS - angularjs

In angular, I obtain a CSRF token like this:
// Get CSRF token and set as header
var csrfRequest = Auth.getCSRF().then(function(data){
console.log(data.data._csrf);
$rootScope.csrf = data.data._csrf;
});
Which logs the new token to the console (this works fine).
Then, I try to login to a sails.js api. Here's the request:
POST /auth/login HTTP/1.1
Host: localhost:1337
Connection: keep-alive
Content-Length: 108
Pragma: no-cache
Cache-Control: no-cache
Accept: application/json, text/plain, */*
Origin: http://localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36
Content-Type: application/json;charset=UTF-8
Referer: http://localhost/sails-front/src/login/
Accept-Encoding: gzip, deflate
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
Request Payloadview parsed
{"email":"myemail#email.com","password":"mypass","_csrf":"PIlVO7S362OroPGBSG0X1vW2FydkP9VhK8cMk="}
The _csrf field is the same as that which was received in the getCSRF call.
Yet I get the response 'CSRF mismatch'. However, when I try the exact same thing with Postman, it works fine (so I don't think it's a problem with the sails server).
Here's the login code in angular. In the controller:
$scope.login = function() {
Auth.login({
email: $scope.email,
password: $scope.password,
_csrf: $rootScope.csrf
},
function(res) {
$state.go('app.home');
},
function(err) {
$rootScope.error = "Failed to login";
});
};
And the Auth service:
/*********************************
* login
******************************** */
function login(user, success, error) {
$http.post(API_BASE_URL + 'auth/login', user).success(function(user){
changeUser(user);
success(user);
}).error(error);
}

You posted your full request, and the answer is hidden there in plain sight--not by looking at what is being sent, but what is being omitted: namely, the cookie. The CSRF token is valid for a single Sails session, but you are neglecting to send the cookie with your AJAX request, so Sails has no idea which session the CSRF token you're sending is for.
To tell Angular to send cookies with your request, use the withCredentials setting:
$http.post(API_BASE_URL + 'auth/login', user, {withCredentials: true})

Related

React not sending back httpOnly & CSRF cookies back to Flask REST API, even with options included (fetch API)

I'm building a REST API with Flask (using Flask-RESTful and Flask-JWT-Extended) and a front end React application that consumes its data.
To log users in, after getting their credentials through the login endpoint, the server sends back an access JWT and a refresh JWT (both in the form of httpOnly cookies) and a CSRF cookie for each JWT, the idea being that for further requests to any API endpoint the client has to send the access JWT and the CSRF cookies.
As I understand, those cookies should be sent automatically, but in reality they're not being sent. The login works fine (I receive the 4 cookies I expect), but on further requests my React front end is not sending the cookies back, even including the authorization: 'include' and credentials: 'include' options in the request header.
I think it's worth to note that the React server runs on port 3000 and my Flask API on port 5000. I think it may cause a problem, but its just a hunch as I don't know how it might affect the communication between the two.
These are the response headers for the login endpoint (which works fine):
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 29
Set-Cookie: access_token_cookie=<very_long_access_JWT>; HttpOnly; Path=/
Set-Cookie: csrf_access_token=<csrf_access_token>; Path=/
Set-Cookie: refresh_token_cookie= <very_long_refresh_JWT>; HttpOnly; Path=/
Set-Cookie: csrf_refresh_token= <csrf_refresh_token>; Path=/
Access-Control-Allow-Origin: http://localhost:3000
Vary: Origin
Server: Werkzeug/2.0.2 Python/3.10.2
Date: Fri, 18 Mar 2022 21:07:03 GMT
These are the request headers for trying to send a POST request to the test endpoint:
POST /api/test HTTP/1.1
Host: 127.0.0.1:5000
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
sec-ch-ua: "Chromium";v="98", " Not A;Brand";v="99"
authorization: include
DNT: 1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.136 Safari/537.36
credentials: include
sec-ch-ua-platform: "Linux"
Accept: */*
Origin: http://localhost:3000
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,es;q=0.8
And this is a "condensed" version of the fetch function that I use on every View on my React application.In reality, the fetchResource() function is saved in its own module and is imported and called by differente Views as needed. I have tested this function before to fetch data without login in and it works.
/**
* Multipurpose fetch function
* #param {string} queryURL URL string to fetch from.
* #param {Object} options request options. Defaults to null
* #returns {Object} returns jsonified data if fetch was successfull, error object if not
*/
const fetchResource = async (queryURL, options = null) => {
try {
const response = await fetch(queryURL, options);
const data = await response.json();
if (!response.ok) {
return { code: response.status, data };
}
return data;
} catch (error) {
return `Looks like there was a problem: ${error}`;
}
};
fetchResource('http://127.0.0.1:5000/api/test', {
method: 'POST',
headers: {
authorization: 'include',
credentials: 'include'
}
});
What could be causing this issue? I'm completely new to cookies and I'm at a loss here.

NodeJS proxy not working as intended

The current route of the request originates on localhost:3001, goes through a proxy running on that same localhost at localhost:3001/proxy, where the request is then routed to the Salesforce instance. The proxy is made using ExpressJS and the client side app is made using AngularJS. Note: I did remember to tag my security token at the end of my password (Salesforce API requirement), although when using cURL, this doesn't seem to be necessary. Here are a series of HTTP traces that will hopefully provide some clues:
HTTP Request Log from Angular.JS App:
POST /proxy HTTP/1.1
Host: localhost:3001
Connection: keep-alive
Content-Length: 234
Pragma: no-cache
Cache-Control: no-cache
X-User-Agent: salesforce-toolkit-rest-javascript/v29.0
Origin: http://localhost:3001
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: application/json, text/plain, */*
SalesforceProxy-Endpoint: https://uniquename.salesforce.com/services/oauth2/token
Referer: http://localhost:3001/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
Cookie: liveagent_oref=; liveagent_ptid=3c69c2f9-139d-4439-ba6c-fd8d9dcae101; liveagent_vc=5
grant_type=password&client_id=3MVGxyzxyzxyzxyz&client_secret=99889988&username=first.last%40email.com&password=pswdwACYodaYfHs
400 Bad Request
Object {error_description: "grant type not supported", error: "unsupported_grant_type"}
Relevant Express.JS code used for proxy routing:
app.all('/proxy', function(req, res) {
var url = req.header('SalesforceProxy-Endpoint');
console.log(req.body); //prints all form data variables in JSON format
console.log(res.body); //undefined
request({url: url}).pipe(res).on('error', function(error){
//I think I may need to pipe more information using request?
console.log(error);
});
});
Request details using cURL:
curl -v https://uniquename.salesforce.com/services/oauth2/token
-d "grant_type=password" -d "client_id=3MVGxyzxyzxyzxyz"
-d "client_secret=99889988" -d "username=jfirst.last#email.com" -d "password=pswd"
> POST /services/oauth2/token HTTP/1.1
> User-Agent: curl/7.41.0
> Host: uniquename.salesforce.com
> Accept: */*
> Content-Length: 207
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 207 out of 207 bytes
< HTTP/1.1 200 OK
< Date: Wed, 29 Jul 2015 06:04:55 GMT
< Set-Cookie: BrowserId=auu1mgvHSMS1EedDEduz8Q;Path=/;Domain=.salesforce.com;Exp
ires=Sun, 27-Sep-2015 06:04:55 GMT
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Pragma: no-cache
< Cache-Control: no-cache, no-store
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
<
{
"id":"https://test.salesforce.com/id/05390530530",
"issued_at":"1438132525896197",
"token_type":"Bearer",
"instance_url":"https://uniquename.salesforce.com",
"signature":"blahblah",
"access_token":"XXXXXXXXXXXXXXXXX"
}
* Connection #0 to
host uniquename.salesforce.com left intact
As you can see, I get back a valid response from the cURL request. I suspect something is wrong with the proxy, as it may not be forwarding all the form data to Salesforce, but I'm not sure how to debug that in Express.JS. The reason I suspect this is because if I try curl https://uniquename.salesforce.com/services/oauth2/token it returns the same unsupported_grant_type error.
I finally got this working by switching to use the cors-anywhere proxy. I deploy my AngularJS app on port 8080, and the proxy on port 3001. My packages are managed using npm and grunt. Here is the code for the proxy:
var host = 'localhost';
var port = 3001;
var cors_proxy = require('cors-anywhere');
cors_proxy.createServer().listen(port, host, function() {
console.log('CORS proxy running on ' + host + ':' + port);
});
And here is how I'm making the HTTP request in AngularJS (you have to fill in your own credentials in the data object):
var login = {
method: 'POST',
url: 'http://localhost:3001/https://login.salesforce.com/services/oauth2/token',
data: 'grant_type=password&client_id='+encodeURIComponent(CLIENT_ID)+'&client_secret='+encodeURIComponent(CLIENT_SECRET)+'&username='+encodeURIComponent(EMAIL)+'&password='+encodeURIComponent(PASSWORD+USER_SECURITY_TOKEN),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*'
}
};
$http(login).success(function(response){
console.log(response);
})
.error( function(response, status){
console.log(response);
console.log("Status: " + status);
});
You can run the proxy with the node server.js command, and run the AngularJS app with grunt. I hope this helps someone out, this was a tough problem to solve.
I don't think you're proxying everything in the HTTP request. I've included some code below that will pass the request method and the request headers to your endpoint.. Also try using the 'request-debug' library so you can compare the proxy HTTP request vs your curl request and look for differences
var express = require('express');
var app = express();
var request = require('request');
require('request-debug')(request);
app.all('/proxy', function(req, res) {
var url = req.header('SalesforceProxy-Endpoint');
var options = {
url: url,
method: req.method,
headers: req.headers
}
request(options).pipe(res).on('error', function (error, response, body) {
if (!error && response.statusCode == 200) {
//TODO: do something useful with the response
} else {
console.log('ERROR: ' + error);
}
});
});

AngularJS WebApi Authorization header does not appear to be getting passed

I am trying to create a call with AngularJS v1.3.15 into ASP.NET WebAPI (latest from Nuget) to get a list of customers. I can successfully authenticate and get back a token. I add the token to an Authentication header but when I make the call it gets kicked back saying the Authentication header is missing.
The Angular call after I get the token looks like this
$scope.baseUrl = "http://localhost:7800/";
$http({
method: 'GET',
url: $scope.baseUrl + 'customer',
headers: {
'Authorization': $scope.token
}})
I have also tried to utilize the angularjs $resourse
return $resource($scope.baseUrl + 'customer', { }, { 'get': { method: 'GET', isArray: true, headers: { 'Authorization': $scope.token } } });
In the WebApiConfig Register method I have the following setup
var cors = new EnableCorsAttribute("*","*","*");
config.EnableCors(cors);
in a DelegatingHandler I check for the Authorization header like so
var authHeader = request.Headers.Authorization;
From the Angular app it is always null. If I run a check from Fiddler and PostMan I get the Authorization header just fine.
When I press F12 from Chrome and look at the request header these are the results
OPTIONS /customer HTTP/1.1
Host: localhost:7800
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: http://localhost:63342
User-Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2376.0 Safari/537.36
Access-Control-Request-Headers: accept, authorization
Accept: */*
Referer: http://localhost:63342/Test/index.html
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
Fixed my issue, after some searches I found that OPTIONS does not seem to be supported out of the box. I found that if I add NuGet package Microsoft.Owin.Cors and then add
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
to the startup.cs file

AngularJS $http seems to not respond correctly when I am using CORS

I have an AngularJS application. It sends out requests to another server for data and so there's an OPTIONS request goes out with every $HTTP call.
When I check with fiddler there are two calls. The Options that always returns a 200 OK and then the data call.
However when I check the $HTTP it seems that it's getting the first request ( the options request ) and not getting the second request the one with real data.
Can someone point me in the right direction with this?
Here's one example of the code that is not responding correctly:
.factory('isUsernameAvailable', function (appConstant, $q, $http) {
return function (username) {
var deferred = $q.defer();
// if (!angular.isDefined(username) || username == null || username == "" || username.length < 6 ) return deferred.resolve();
var url = appConstant.baseUrl + '/api/user/existsByName';
$http({
url: url,
method: "PUT",
data: {
userName: username
}
}).then(function (data) {
// Found the user, therefore not unique.
deferred.reject("User name is taken");
}, function (data) {
// User not found, therefore unique!
deferred.resolve();
});
return deferred.promise;
}
})
I expect it to be returning as success or failure depending on if it finds the username. But in this case it always responds as a fail/error.
Here are the requests being made:
OPTIONS http://localhost:3048/api/user/existsByName HTTP/1.1
Host: localhost:3048
Connection: keep-alive
Access-Control-Request-Method: PUT
Origin: http://localhost:2757
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Access-Control-Request-Headers: accept, authorization, content-type
Accept: */*
Referer: http://localhost:2757/Auth/register
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
giving:
HTTP/1.1 200 OK
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:2757
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: content-type
X-SourceFiles: =?UTF-8?B?QzpcR1xhYmlsaXRlc3Qtc2VydmVyXFdlYlJvbGVcYXBpXHVzZXJcZXhpc3RzQnlOYW1l?=
X-Powered-By: ASP.NET
Date: Mon, 12 Jan 2015 17:52:12 GMT
Content-Length: 0
Then:
PUT http://localhost:3048/api/user/existsByName HTTP/1.1
Host: localhost:3048
Connection: keep-alive
Content-Length: 35
Accept: application/json, text/plain, */*
Origin: http://localhost:2757
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Authorization: null
Content-Type: application/json;charset=UTF-8
Referer: http://localhost:2757/Auth/register
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
{"userName":"abdddcdefgg#live.com"}
giving:
HTTP/1.1 404 Not Found
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:2757
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: X-Custom-Header
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcR1xhYmlsaXRlc3Qtc2VydmVyXFdlYlJvbGVcYXBpXHVzZXJcZXhpc3RzQnlOYW1l?=
X-Powered-By: ASP.NET
Date: Mon, 12 Jan 2015 17:52:12 GMT
Content-Length: 0
The problem is even if the second request returns a 200 when I debug the success and error functions it still goes to the error function all of the time.
You should use JSONP to do cross domain JSON calls. Look at the documentation here: https://docs.angularjs.org/api/ng/service/$http#jsonp. Also, your referring page and the response from the OPTIONS request must have the appropriate CORS headers set or else the browser will refuse to send the request here are the header settings that I use.
Access-Control-Allow-Headers:Content-Type, Authorization, Content-Length, X-Requested-With, Accept, x-csrf-token, origin
Access-Control-Allow-Methods:GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Origin:*
To call $http.jsonp with a PUT request, you would set up a configuration such as
var config = {
method: 'POST',
data: { test: 'test' }
};
and then pass that into the $http.jsonp call
$http.jsonp('http://example.com', config);
Here is more documentation https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS and http://en.wikipedia.org/wiki/JSONP

AngularJS http request doesn't send session cookie for CORS

My script is:
<script>
function eventController($scope, $http) {
$http.get("http://example.com/api/Event", {withCredentials: true})
.success(function (response) {
$scope.events = response;
});
}
</script>
Request header from Fiddler:
GET http://example.com/api/Event HTTP/1.1
Host: example.com
Connection: keep-alive
Cache-Control: max-age=0
Accept: application/json, text/plain, */*
Origin: http://www.example.com
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Referer: http://www.example.com/Home/Event
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8
I need a authentication session cookie to be sent with this request.
It only sends cookie when the url of the webpage has no www in the front.
I have found a similar question in this site and the answer is:
.config(function ($routeProvider, $httpProvider) {
$httpProvider.defaults.withCredentials = true;
//rest of route code
But where do I put this code in my script?
The web application I'm developing is an ASP.NET MVC5 app with Web Api 2.
That configuration needs to be injected into the angular module, like this:
// get an existing module by name
var app = angular.module('app');
// inject $httpProvider configuration
app.config(function ($httpProvider) {
$httpProvider.defaults.withCredentials = true;
});
It turns out that my AngularJS script works fine (Just pass {withCredentials: true} as a parameter in the $http.get method). What went wrong was that my MVC application doesn't store session cookie in the subdomain. So I fixed it by adding CookieDomain in the CookieAuthenticationOptions.

Resources