I am trying out the new Fetch API but is having trouble with Cookies. Specifically, after a successful login, there is a Cookie header in future requests, but Fetch seems to ignore that headers, and all my requests made with Fetch is unauthorized.
Is it because Fetch is still not ready or Fetch does not work with Cookies?
I build my app with Webpack. I also use Fetch in React Native, which does not have the same issue.
Fetch does not use cookie by default. To enable cookie, do this:
fetch(url, {
credentials: "same-origin"
}).then(...).catch(...);
In addition to #Khanetor's answer, for those who are working with cross-origin requests: credentials: 'include'
Sample JSON fetch request:
fetch(url, {
method: 'GET',
credentials: 'include'
})
.then((response) => response.json())
.then((json) => {
console.log('Gotcha');
}).catch((err) => {
console.log(err);
});
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
Have just solved. Just two f. days of brutforce
For me the secret was in following:
I called POST /api/auth and see that cookies were successfully received.
Then calling GET /api/users/ with credentials: 'include' and got 401 unauth, because of no cookies were sent with the request.
The KEY is to set credentials: 'include' for the first /api/auth call too.
If you are reading this in 2019, credentials: "same-origin" is the default value.
fetch(url).then
Programmatically overwriting Cookie header in browser side won't work.
In fetch documentation, Note that some names are forbidden. is mentioned. And Cookie happens to be one of the forbidden header names, which cannot be modified programmatically. Take the following code for example:
Executed in the Chrome DevTools console of page https://httpbin.org/, Cookie: 'xxx=yyy' will be ignored, and the browser will always send the value of document.cookie as the cookie if there is one.
If executed on a different origin, no cookie is sent.
fetch('https://httpbin.org/cookies', {
headers: {
Cookie: 'xxx=yyy'
}
}).then(response => response.json())
.then(data => console.log(JSON.stringify(data, null, 2)));
P.S. You can create a sample cookie foo=bar by opening https://httpbin.org/cookies/set/foo/bar in the chrome browser.
See Forbidden header name for details.
Just adding to the correct answers here for .net webapi2 users.
If you are using cors because your client site is served from a different address as your webapi then you need to also include SupportsCredentials=true on the server side configuration.
// Access-Control-Allow-Origin
// https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api
var cors = new EnableCorsAttribute(Settings.CORSSites,"*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
This works for me:
import Cookies from 'universal-cookie';
const cookies = new Cookies();
function headers(set_cookie=false) {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
};
if (set_cookie) {
headers['Authorization'] = "Bearer " + cookies.get('remember_user_token');
}
return headers;
}
Then build your call:
export function fetchTests(user_id) {
return function (dispatch) {
let data = {
method: 'POST',
credentials: 'same-origin',
mode: 'same-origin',
body: JSON.stringify({
user_id: user_id
}),
headers: headers(true)
};
return fetch('/api/v1/tests/listing/', data)
.then(response => response.json())
.then(json => dispatch(receiveTests(json)));
};
}
My issue was my cookie was set on a specific URL path (e.g., /auth), but I was fetching to a different path. I needed to set my cookie's path to /.
If it still doesn't work for you after fixing the credentials.
I also was using the :
credentials: "same-origin"
and it used to work, then it didn't anymore suddenly, after digging much I realized that I had change my website url to http://192.168.1.100 to test it in LAN, and that was the url which was being used to send the request, even though I was on http://localhost:3000.
So in conclusion, be sure that the domain of the page matches the domain of the fetch url.
Related
I am trying out the new Fetch API but is having trouble with Cookies. Specifically, after a successful login, there is a Cookie header in future requests, but Fetch seems to ignore that headers, and all my requests made with Fetch is unauthorized.
Is it because Fetch is still not ready or Fetch does not work with Cookies?
I build my app with Webpack. I also use Fetch in React Native, which does not have the same issue.
Fetch does not use cookie by default. To enable cookie, do this:
fetch(url, {
credentials: "same-origin"
}).then(...).catch(...);
In addition to #Khanetor's answer, for those who are working with cross-origin requests: credentials: 'include'
Sample JSON fetch request:
fetch(url, {
method: 'GET',
credentials: 'include'
})
.then((response) => response.json())
.then((json) => {
console.log('Gotcha');
}).catch((err) => {
console.log(err);
});
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
Have just solved. Just two f. days of brutforce
For me the secret was in following:
I called POST /api/auth and see that cookies were successfully received.
Then calling GET /api/users/ with credentials: 'include' and got 401 unauth, because of no cookies were sent with the request.
The KEY is to set credentials: 'include' for the first /api/auth call too.
If you are reading this in 2019, credentials: "same-origin" is the default value.
fetch(url).then
Programmatically overwriting Cookie header in browser side won't work.
In fetch documentation, Note that some names are forbidden. is mentioned. And Cookie happens to be one of the forbidden header names, which cannot be modified programmatically. Take the following code for example:
Executed in the Chrome DevTools console of page https://httpbin.org/, Cookie: 'xxx=yyy' will be ignored, and the browser will always send the value of document.cookie as the cookie if there is one.
If executed on a different origin, no cookie is sent.
fetch('https://httpbin.org/cookies', {
headers: {
Cookie: 'xxx=yyy'
}
}).then(response => response.json())
.then(data => console.log(JSON.stringify(data, null, 2)));
P.S. You can create a sample cookie foo=bar by opening https://httpbin.org/cookies/set/foo/bar in the chrome browser.
See Forbidden header name for details.
Just adding to the correct answers here for .net webapi2 users.
If you are using cors because your client site is served from a different address as your webapi then you need to also include SupportsCredentials=true on the server side configuration.
// Access-Control-Allow-Origin
// https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api
var cors = new EnableCorsAttribute(Settings.CORSSites,"*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
This works for me:
import Cookies from 'universal-cookie';
const cookies = new Cookies();
function headers(set_cookie=false) {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
};
if (set_cookie) {
headers['Authorization'] = "Bearer " + cookies.get('remember_user_token');
}
return headers;
}
Then build your call:
export function fetchTests(user_id) {
return function (dispatch) {
let data = {
method: 'POST',
credentials: 'same-origin',
mode: 'same-origin',
body: JSON.stringify({
user_id: user_id
}),
headers: headers(true)
};
return fetch('/api/v1/tests/listing/', data)
.then(response => response.json())
.then(json => dispatch(receiveTests(json)));
};
}
My issue was my cookie was set on a specific URL path (e.g., /auth), but I was fetching to a different path. I needed to set my cookie's path to /.
If it still doesn't work for you after fixing the credentials.
I also was using the :
credentials: "same-origin"
and it used to work, then it didn't anymore suddenly, after digging much I realized that I had change my website url to http://192.168.1.100 to test it in LAN, and that was the url which was being used to send the request, even though I was on http://localhost:3000.
So in conclusion, be sure that the domain of the page matches the domain of the fetch url.
I am trying out the new Fetch API but is having trouble with Cookies. Specifically, after a successful login, there is a Cookie header in future requests, but Fetch seems to ignore that headers, and all my requests made with Fetch is unauthorized.
Is it because Fetch is still not ready or Fetch does not work with Cookies?
I build my app with Webpack. I also use Fetch in React Native, which does not have the same issue.
Fetch does not use cookie by default. To enable cookie, do this:
fetch(url, {
credentials: "same-origin"
}).then(...).catch(...);
In addition to #Khanetor's answer, for those who are working with cross-origin requests: credentials: 'include'
Sample JSON fetch request:
fetch(url, {
method: 'GET',
credentials: 'include'
})
.then((response) => response.json())
.then((json) => {
console.log('Gotcha');
}).catch((err) => {
console.log(err);
});
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
Have just solved. Just two f. days of brutforce
For me the secret was in following:
I called POST /api/auth and see that cookies were successfully received.
Then calling GET /api/users/ with credentials: 'include' and got 401 unauth, because of no cookies were sent with the request.
The KEY is to set credentials: 'include' for the first /api/auth call too.
If you are reading this in 2019, credentials: "same-origin" is the default value.
fetch(url).then
Programmatically overwriting Cookie header in browser side won't work.
In fetch documentation, Note that some names are forbidden. is mentioned. And Cookie happens to be one of the forbidden header names, which cannot be modified programmatically. Take the following code for example:
Executed in the Chrome DevTools console of page https://httpbin.org/, Cookie: 'xxx=yyy' will be ignored, and the browser will always send the value of document.cookie as the cookie if there is one.
If executed on a different origin, no cookie is sent.
fetch('https://httpbin.org/cookies', {
headers: {
Cookie: 'xxx=yyy'
}
}).then(response => response.json())
.then(data => console.log(JSON.stringify(data, null, 2)));
P.S. You can create a sample cookie foo=bar by opening https://httpbin.org/cookies/set/foo/bar in the chrome browser.
See Forbidden header name for details.
Just adding to the correct answers here for .net webapi2 users.
If you are using cors because your client site is served from a different address as your webapi then you need to also include SupportsCredentials=true on the server side configuration.
// Access-Control-Allow-Origin
// https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api
var cors = new EnableCorsAttribute(Settings.CORSSites,"*", "*");
cors.SupportsCredentials = true;
config.EnableCors(cors);
This works for me:
import Cookies from 'universal-cookie';
const cookies = new Cookies();
function headers(set_cookie=false) {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content')
};
if (set_cookie) {
headers['Authorization'] = "Bearer " + cookies.get('remember_user_token');
}
return headers;
}
Then build your call:
export function fetchTests(user_id) {
return function (dispatch) {
let data = {
method: 'POST',
credentials: 'same-origin',
mode: 'same-origin',
body: JSON.stringify({
user_id: user_id
}),
headers: headers(true)
};
return fetch('/api/v1/tests/listing/', data)
.then(response => response.json())
.then(json => dispatch(receiveTests(json)));
};
}
My issue was my cookie was set on a specific URL path (e.g., /auth), but I was fetching to a different path. I needed to set my cookie's path to /.
If it still doesn't work for you after fixing the credentials.
I also was using the :
credentials: "same-origin"
and it used to work, then it didn't anymore suddenly, after digging much I realized that I had change my website url to http://192.168.1.100 to test it in LAN, and that was the url which was being used to send the request, even though I was on http://localhost:3000.
So in conclusion, be sure that the domain of the page matches the domain of the fetch url.
I'm having problems with cookie authentication between an expressJS server and a VueJS font-end.
When logging in through the site, I successfully get a HTTPOnly Cookie in the set-cookie header:
Screenshot (Ignore the Auth header, using it for testing only)
I also see the cookie in the devTools, and everything looks right too me, I'm not an expert on cookies though so it may not be correct
The problem is when I request the user's settings on another endpoint, the cookie is not sent to the server. The req.cookie object is empty when the this request is handled on the server side.
Here is my fetch code:
const loginOptions = {
method: 'POST',
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password,
}),
credentials: 'same-origin',
};
const settingsOptions = {
method: 'GET',
mode: 'cors',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
};
const loginResponse = await fetch(baseUrl + '/login', loginOptions);
const userSettings = await fetch(baseUrl + '/settings', settingsOptions);
I've tried using credentials: "include", without success.
On the express server I'm using cors like this:
app.use(cors({
origin: '*',
credentials: true,
}));
Here is also an example of the second request, the 403 status is set by the server when no cookie is attached to the request.
I've tried setting the domain of the cookie to both localhost and 127.0.0.1 as suggested in another thread. I have left it on localhost for now.
Solved
I had read somewhere that you should add a specific domain value to the cookie when creating it. If I just removed that setting, it sets it automatically I'm guessing, and then it worked! So my guess is that I had set the domain value to the wrong value for what I was trying to do
Your response has access-control-allow-origin: http://localhost:8080 which implies you are making a cross-origin request.
You said:
credentials: 'same-origin',
… which tells your client-side code to only include credentials for same-origin requests.
I read somewhere that Chrome wasn't friendly with cookies and localhost env, maybe it could be that.
https://bugs.chromium.org/p/chromium/issues/detail?id=56211
Furthermore, I had some problems with cookies, express and vueJS some times ago.
Maybe it can help you: SetCookie header not stored
I had read somewhere that you should add a specific domain value to the cookie when creating it. If I just removed that setting, it sets it automatically I'm guessing, and then it worked! So my guess is that I had set the domain value to the wrong value for what I was trying to do
I'm making a POST request to a node.js server and I'm having trouble getting it to work. Here's my request:
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'this-can-be-anything',
};
export const postVote = (id, vote) =>
fetch(`${uri}/posts/${id}`, {
method: 'POST',
headers,
body: JSON.stringify({options: vote}),
}).then(response => response.json())
.then(data => data)
.catch(err => console.log(err));
The function accepts an 'id' and a 'vote', both strings. The id is being used as part of the URI in the request, and the vote is being supplied as options so the API knows what to do with it. Both of the arguments are being passed correctly:
id = '8xf0y6ziyjabvozdd253nd'
vote = 'upVote'
Here's a link to the GitHub repository for the server/API:
Udacity Readable API
and a screenshot of the network when firing the request:
UPDATE: Added the second screenshot which shows status 200. Though it shows this and appears to have been successful, it still doesn't post to the server and the information stays the same.
What you are looking at is the OPTIONS request in the network tab. For cross origin requests, every request if preceeded by an OPTIONS request which tells the calling client (browser, for example) if those HTTP methods are supported by the remote server for use in crosss origin context.
Check the other requests out. If the OPTIONS request was responded to correctly by the server, the browser must automatically follow up with your POST request
EDIT:
Also, the docs specify the param name to be option whereas in your screenshot it is coming up as options.
Further reading: CORS
Try declaring the headers as such:
var headers = new Headers({
'Content-Type': 'application/json',
'Authorization': 'this-can-be-anything',
})
I'm attempting to use cookie authentication for WordPress REST API access using the Fetch API, however the auth is failing with the following error.
403: Cookie Nonce is Invalid
I'm using the following script to connect to the API.
const headers = new Headers({
'Content-Type': 'application/json',
'X-WP-Nonce': WPAPI.nonce
});
fetch(WPAPI.root + 'my-endpoint/upload/', {
method: 'POST',
headers: headers,
body: JSON.stringify(data)
})
When I switch from using Fetch to XMLHttpRequest it works as expected.
let request = new XMLHttpRequest();
request.open('POST', WPAPI.root + 'my-endpoint/upload/', true);
request.setRequestHeader('X-WP-Nonce', WPAPI.nonce);
request.setRequestHeader('Content-Type', 'application/json');
request.send(JSON.stringify(data));
Is it possible there an issue with the way headers are being sent in the Fetch method?
WordPress nonce authentication requires the use of cookies and by default Fetch doesn't send those along. You can use the credentials option to make this work:
fetch(endpoint, {
credentials: 'same-origin'
})
https://github.com/github/fetch#sending-cookies
Came across my post from 4 years ago looking for the same issue :)
This solves the problem.
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce' : my_var.nonce
},
body: JSON.stringify(data),
});
const content = await response.json();
console.log(content);
Late, but maybe helpful for other readers as I added code specifically for fetch() promise according to this question.
WordPress uses nonce automatically within their cookies, as I found out.
Solution: Permission Callback function checking for cookie
WordPress: version 5.7.2
PHP: version 7.4
host: hostmonster.com
client: Windows 10
browsers: tested on Chrome, Firefox, even Edge 😜 worked
Code (PHP code in function.php of your installed theme):
add_action('rest_api_init', function() {
/**
* Register here your custom routes for your CRUD functions
*/
register_rest_route( 'my-endpoint/v1', '/upload/', array(
array(
'methods' => WP_REST_Server::READABLE, // = 'GET'
'callback' => 'get_data',
// Always allow, as an example
'permission_callback' => '__return_true'
),
array(
'methods' => WP_REST_Server::CREATABLE, // = 'POST'
'callback' => 'create_data',
// Here we register our permissions callback
// The callback is fired before the main callback to check if the current user can access the endpoint
'permission_callback' => 'prefix_get_private_data_permissions_check',
),
));
});
// The missing part:
// Add your Permission Callback function here, that checks for the cookie
// You should define your own 'prefix_' name, though
function prefix_get_private_data_permissions_check() {
// Option 1: Password Protected post or page:
// Restrict endpoint to browsers that have the wp-postpass_ cookie.
if ( !isset($_COOKIE['wp-postpass_'. COOKIEHASH] )) {
return new WP_Error( 'rest_forbidden', esc_html__( 'OMG you can not create or edit private data.', 'my-text-domain' ), array( 'status' => 401 ) );
};
// Option 2: Authentication based on logged-in user:
// Restrict endpoint to only users who have the edit_posts capability.
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error( 'rest_forbidden', esc_html__( 'OMG you can not create or edit private data.', 'my-text-domain' ), array( 'status' => 401 ) );
};
// This is a black-listing approach. You could alternatively do this via white-listing, by returning false here and changing the permissions check.
return true;
};
function create_data() {
global $wpdb;
$result = $wpdb->query(...);
return $result;
}
function get_data() {
global $wpdb;
$data = $wpdb->get_results('SELECT * from `data`');
return $data;
}
Make sure to include in your HTML page credentials: 'same-origin' in your HTTP request, as stated correctly in previous answers and comments above.
Code (HTML with inline <script> ... </script>):
<script>
// Here comes the REST API part:
// HTTP requests with fetch() promises
function getYourData() {
let url = 'https://example.com/wp-json/my-endpoint/v1/upload/';
fetch(url, {
method: 'GET',
credentials: 'same-origin', // <-- make sure to include credentials
headers:{
'Content-Type': 'application/json',
'Accept': 'application/json',
//'Authorization': 'Bearer ' + token <-- not needed, WP does not check for it
}
}).then(res => res.json())
.then(response => get_success(response))
.catch(error => failure(error));
};
function insertYourData(data) {
let url = 'https://example.com/wp-json/my-endpoint/v1/upload/';
fetch(url, {
method: 'POST',
credentials: 'same-origin', // <-- make sure to include credentials
headers:{
'Content-Type': 'application/json',
'Accept': 'application/json',
//'Authorization': 'Bearer ' + token <-- not needed, WP does not check for it
},
body: JSON.stringify(data)
}).then(res => res.json())
.then(response => create_success(response))
.catch(error => failure(error));
};
// your Success and Failure-functions:
function get_success(json) {
// do something here with your returned data ....
console.log(json);
};
function create_success(json) {
// do something here with your returned data ....
console.log(json);
};
function failure(error) {
// do something here ....
console.log("Error: " + error);
};
</script>
Final thoughts:
Is 'Authorization': 'Bearer ' + token necessary in header of HTTP request?
After some testing, I realized that if ( !isset($_COOKIE['wp-postpass_'. COOKIEHASH] )) { ... within the Permission Callback not only checks if the Cookie is set on client browser, but it seems also to check its value (the JWT token).
Because I dobble checked as with my initial code, passing a false token, eliminating the cookie, or leaving session open but changing in the back-end the password of site (hence WordPress would create a new token, hence value of set wp_postpass_ cookie would change) and all test went correctly - REST API blocked, not only verifying presence of cookie, but also its value (which is good - thank you WordPress team).
Sources:
I found following resource concerning above thoughts in the FAQ section:
Why is the REST API not verifying the incoming Origin header? Does this expose my site to CSRF attacks?
Because the WordPress REST API does not verify the Origin header of
incoming requests, public REST API endpoints may therefore be accessed
from any site. This is an intentional design decision.
However, WordPress has an existing CSRF protection mechanism which
uses nonces.
And according to my testing so far, the WP-way of authentication works perfectly well.
Thumbs up 👍 for the WordPress team
Additional 2 sources from the WordPress REST API Handbook:
REST API Handbook / Extending the REST API / Routes and Endpoints
REST API Handbook / Extending the REST API / Adding Custom Endpoints
And 1 source form WordPress Code Reference concerning rest_cookie_check_errors() function:
Reference / Functions / rest_cookie_check_errors()
For those interested in full story of my findings, following link to my thread with answers, code snippets and additional findings.
How to force Authentication on REST API for Password protected page using custom table and fetch() without Plugin