Opencart API session, api_id and permission error

in Development


The official Opencart API is poorly documented, with typos and omissions: docs.opencart.com/en-gb/system/users/api. Provided you have your API key, making a simple external API request using cURL or Postman to have e.g. order status updated seems impossible and overcomplicated: you'd have to use that API key to request a token first, and then make requests using that token. The token by itself is not enough, as it gets cross-validated with api_id inside $this->session->data. Or, how Daniel puts it:

ok the system is a little different compared to editing orders on the same server. you have to login then get a session id or token returned. you need to include that in each request.

Due to what seems like a bug, that token doesn't get linked to the required api_id properly (despite Daniel claiming it got fixed in 2015 and then later in 2016), so the requests result in a permission error -- either error_permission, or Warning: You do not have permission to access the API, depending on translation availability.

I was dealing with the code version after 46b7132 from 29 Jun 2016, and despite the commit message saying literally ok the API should be working again for now, it did not.

The advice that's the closest to a real fix is to manually copy system/library/session.php and catalog/controller/startup/session.php from the latest version, but it doesn't sound like a good idea because:

  • this doesn't answer what exactly was the problem;
  • there is too much code to copy, and the more code you copy, the higher are the chances of getting something off or messed up.

So lets take a close look at those two. The catalog/controller/startup folder shows the most recent change in session.php is a 4 year old commit b390799Additional fixed (requires verification) to fix issues with the API login.php and also session start-up, which looks very relevant, yet doesn't fix the issue. But going back in that file's history reveals b358841more token changes right after the aforementioned should be working for now, which introduces the following change. Changing those exact two lines in your copy of session.php and fixes the external API that you login to using api_token but at the same time it breaks those admin calls to the API, such as the one that updates the order status.

So the solution is to combine those two: keep the original if looking for $this->request->get['token'], and add an else if from the above commit that looks for $this->request->get['api_token'], with the corresponding body. That should satisfy both cases and your session.php should look like this:

if (isset($this->request->get['token']) && isset($this->request->get['route']) && substr($this->request->get['route'], 0, 4) == 'api/') {
    $this->db->query("DELETE FROM " . DB_PREFIX . "api_session WHERE TIMESTAMPADD(HOUR, 1, date_modified) < NOW()");

    $query = $this->db->query("SELECT DISTINCT * FROM " . DB_PREFIX . "api a LEFT JOIN " . DB_PREFIX . "api_session as ON (a.api_id = as.api_id) LEFT JOIN " . DB_PREFIX . "api_ip ai ON (as.api_id = ai.api_id) WHERE a.status = '1' AND as.token = '" . $this->db->escape($this->request->get['token']) . "' AND ai.ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "'");

    if ($query->num_rows) {
        $this->session->start('api', $query->row['session_id']);

        // keep the session alive
        $this->db->query("UPDATE " . DB_PREFIX . "api_session SET date_modified = NOW() WHERE api_session_id = '" . (int)$query->row['api_session_id'] . "'");
    }
} elseif (isset($this->request->get['api_token']) && isset($this->request->get['route']) && substr($this->request->get['route'], 0, 4) == 'api/') {
    $this->db->query("DELETE FROM " . DB_PREFIX . "api_session WHERE TIMESTAMPADD(HOUR, 1, date_modified) < NOW()");

    $query = $this->db->query("SELECT DISTINCT * FROM " . DB_PREFIX . "api a LEFT JOIN " . DB_PREFIX . "api_session as ON (a.api_id = as.api_id) LEFT JOIN " . DB_PREFIX . "api_ip ai ON (as.api_id = ai.api_id) WHERE a.status = '1' AND as.token = '" . $this->db->escape($this->request->get['api_token']) . "' AND ai.ip = '" . $this->db->escape($this->request->server['REMOTE_ADDR']) . "'");

    if ($query->num_rows) {
        $this->session->start('api', $query->row['session_id']);

        // keep the session alive
        $this->db->query("UPDATE " . DB_PREFIX . "api_session SET date_modified = NOW() WHERE api_session_id = '" . (int)$query->row['api_session_id'] . "'");
    }
} else {
    $this->session->start();
}

Existing issues, for reference:

#api #opencart