WordPress REST API Authentication Using Fetch
Asked Answered
S

3

7

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?

Spatula answered 13/9, 2017 at 18:5 Comment(0)
B
6

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

Brian answered 15/1, 2018 at 15:40 Comment(0)
S
0

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);
Spatula answered 11/3, 2021 at 19:27 Comment(0)
O
0

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.

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

Outbreed answered 13/7, 2021 at 17:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.