How to force Authentication on REST API for Password protected page using custom table and fetch() without Plugin

Question

After studying carefully the WordPress Documentation concerning Home / REST API Handbook / Extending the REST API / Adding Custom Endpoints I realized I made a couple of mistakes and I wanted to share with you my findings. Also, at the end I have some additional questions out of curiosity.

1.) Pass object ‘$request’ to the callback functions:

Before:function get_your_data() { ...
After : function get_your_data($request) { ...

2.) Create namespace and route outside of wp/v2 with your own version number:

Before:register_rest_route( 'wp/v2/your_private_page', '/data', array( ...
After : register_rest_route( 'your_private_page/v1', '/data', array( ...

Also adapt http-request URLs in your client side script:

Before:let url="https://oilamerica.com.pa/wp-json/wp/v2/your_private_page/data";
After : let url="https://oilamerica.com.pa/wp-json/your_private_page/v1/data";

3.) Add ‘permission_callback’ to your routes:

Before: I didn’t have it (just had the main callbacks)
After : Added prefix_get_private_data_permissions_check() as Permission Callback function

//  Permission Callback 
// 'ypp' is the Prefix I chose (ypp = Your Private Page)

function ypp_get_private_data_permissions_check() {
    // 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 ) );
    };
    // 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;
};

// And then add the permission_callback to your POST and PUT routes:

add_action('rest_api_init', function() {
    /**
    * Register here your custom routes for your CRUD functions
    */
    register_rest_route( 'your_private_page/v1', '/data', array(
       array(
          'methods'  => WP_REST_Server::READABLE,
          'callback' => 'get_your_data',
          // Always allow.
          'permission_callback' => '__return_true' // <-- good practice, according to docs
       ),
       array(
          'methods'  => WP_REST_Server::CREATABLE,
          'callback' => 'insert_your_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' => 'ypp_get_private_data_permissions_check', // <-- that was the missing part
       ),
       array(
          'methods'  => WP_REST_Server::EDITABLE,
          'callback' => 'update_your_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' => 'ypp_get_private_data_permissions_check', // <-- that was the missing part
       ),
    ));
});

Obviously, that makes a lot of sense, because it’s on the server side (WordPress, php) where the authorization has to take place (dummy me, he he he)


4.) Complete code:

Following code-snippets worked for my selfhosted WordPress installation:

WordPress: version 5.7.2
PHP: version 7.4
host: hostmonster.com
client: Windows 10, tested browsers: Chrome, Firefox, even Edge :-b

Code (HTML with inline <script> ... </script>):

<form id="form1" name="form1">
  <button id="get" onclick="getValues()">GET</button>
  <button id="insert" onclick="insertValues()">CREATE</button>
  <button id="update" onclick="updateValues()">UPDATE</button>
</form>

<script>
let yourData = [];
let yourDataNew = {};
let yourDataUpdated = {};
let token = "";

function getValues() {
  event.preventDefault();
  //READ data
  getYourData();
};
function insertValues() {
  event.preventDefault();
  //CREATE new datarecord
  yourDataNew = {"column_1": "test-1", "column_2": "test-2"};
  insertYourData(yourDataNew);
};
function updateValues() {
  event.preventDefault();
  //UPDATE datarecord
  let idOfLastRecord = yourData[yourData.length-1].id;
  yourDataUpdated = {"id": idOfLastRecord, "column_1": "test-1-modified", "column_2": "test-2-modified"};
  updateYourData(yourDataUpdated);
};

//GET value of Access Cookie wp-postpass_{hash}
token = ("; "+document.cookie).split("; wp-postpass_675xxxxxx   =").pop().split(";").shift();
//token = '24P%24BhPU2oE3ux8v4FFfSFbB9onTPNnglM.'
console.log('TOKEN: ' + token);

// Here comes the REST API part:
// HTTP requests with fetch() promises
function getYourData() {
  let url="https://oilamerica.com.pa/wp-json/your_private_page/v1/data";
  fetch(url, {
    method: 'GET',
    credentials: 'same-origin',
    headers:{
        'Content-Type': 'application/json',
        //credentials: 'same-origin',   <-- no authorization needed
        'Accept': 'application/json',
        //'Authorization': 'Bearer ' + token  <-- no authorization needed
    }
  }).then(res => res.json())
  .then(response => get_success(response))
  .catch(error => failure(error));
};

function insertYourData(data) {
  let url="https://oilamerica.com.pa/wp-json/your_private_page/v1/data";
  fetch(url, {
    method: 'POST',
    credentials: 'same-origin',
    headers:{
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': 'Bearer ' + token
    },
    body: JSON.stringify(data)
  }).then(res => res.json())
  .then(response => create_success(response))
  .catch(error => failure(error));
};

function updateYourData(data) {
  let url="https://oilamerica.com.pa/wp-json/your_private_page/v1/data";
  fetch(url, {
    method: 'PUT',
    credentials: 'same-origin',
    headers:{
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': 'Bearer ' + token
    },
    body: JSON.stringify(data)
  }).then(res => res.json())
  .then(response => update_success(response))
  .catch(error => failure(error));
};

// fetch() promises success functions:
function get_success(json) {
  data = JSON.stringify(json);
  yourData = JSON.parse(data);
  console.log('GET');
  console.log(yourData);
};
function create_success(json) {
  let insertResponse = JSON.stringify(json);
  insertResponse = JSON.parse(insertResponse);
  console.log('CREATE');
  console.log(insertResponse);
};
function update_success(json) {
  let updateResponse = JSON.stringify(json);
  updateResponse = JSON.parse(updateResponse);
  console.log('UPDATE');
  console.log(updateResponse);
};
function failure(error) {
  console.log("Error: " + error);
};

</script>

Code (PHP code in function.php of my installed theme):

/**
 * Add here your custom CRUD functions
 */

/**
  * This is our callback function to return (GET) our data.
  *
  * @param WP_REST_Request $request This function accepts a rest request to process data.
  */
function get_your_data($request) {
    global $wpdb;
    $yourdata = $wpdb->get_results("SELECT * FROM your_custom_table");

    return rest_ensure_response( $yourdata );
};

/**
 * This is our callback function to insert (POST) new data record.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function insert_your_data($request) {
    global $wpdb;
    $contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';

    if ($contentType === "application/json") {
        $content = trim(file_get_contents("php://input"));
        $decoded = json_decode($content, true);
        $newrecord = $wpdb->insert( 'your_custom_table', array( 'column_1' => $decoded['column_1'], 'column_2' => $decoded['column_2']));
    };
    if($newrecord){
        return rest_ensure_response($newrecord);
    }else{
        //something gone wrong
        return rest_ensure_response('failed');
    };

    header("Content-Type: application/json; charset=UTF-8");
};
/**
 * This is our callback function to update (PUT) a data record.
 *
 * @param WP_REST_Request $request This function accepts a rest request to process data.
 */
function update_your_data() {
    global $wpdb;
    $contentType = isset($_SERVER["CONTENT_TYPE"]) ? trim($_SERVER["CONTENT_TYPE"]) : '';

    if ($contentType === "application/json") {
        $content = trim(file_get_contents("php://input"));
        $decoded = json_decode($content, true);
        $updatedrecord = $wpdb->update( 'your_custom_table', array( 'column_1' => $decoded['column_1'], 'column_2' => $decoded['column_2']), array('id' => $decoded['id']), array( '%s' ));
    };

    if($updatedrecord){
        return rest_ensure_response($updatedrecord);
    }else{
        //something gone wrong
        return rest_ensure_response('failed');
    };

    header("Content-Type: application/json; charset=UTF-8");
};

// 'ypp' is the Prefix I chose (ypp = Your Private Page)
function ypp_get_private_data_permissions_check() {
    // 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 ) );
    };
    // 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;
};

add_action('rest_api_init', function() {
    /**
    * Register here your custom routes for your CRUD functions
    */
    register_rest_route( 'your_private_page/v1', '/data', array(
        array(
           'methods'  => WP_REST_Server::READABLE,
           'callback' => 'get_your_data',
           // Always allow.
           'permission_callback' => '__return_true'
        ),
        array(
           'methods'  => WP_REST_Server::CREATABLE,
           'callback' => 'insert_your_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' => 'ypp_get_private_data_permissions_check',
        ),
        array(
           'methods'  => WP_REST_Server::EDITABLE,
           'callback' => 'update_your_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' => 'ypp_get_private_data_permissions_check',
        ),
    ));
});

5.) Testing

Finally got my so desperately anticipated FAILURE  😊

Finally got my so desperately anticipated FAILURE 😊


Final Question, though:

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).

My Questions here:

  • Can I leave out the 'Authorization': 'Bearer ' + token line within my
    HTTP request header ?
  • Hence, do I have to retrieve at all the cookie value (token) or was it not necessary?

I am not sure whether I got it completely right now. Any comments and suggestions are so welcome.

Thank you.

0
Juergen Fink 4 months 2021-07-06T14:54:05-05:00 0 Answers 0 views 0

Leave an answer

Browse
Browse