12

I want to make GET, POST & PUT calls to a 3rd party API and display the response on the client side via AJAX. The API calls require a token, but I need to keep that token secret / not in the client-side JS code.

I've seen a few suggestions like this one to have server-side code in the middle that would be queried by the AJAX, and would handle the actual API call. I'm OK working directly with the API from AJAX, but I'm unsure of how to work with a two-step process in order to hide the token from users. My Googling hasn't turned up any pointers on a best-practice method of achieving this.

In my case the server in the middle would be running PHP, so I assume cURL / Guzzle is the straightforward option to make the API calls with the token. The API responses will be JSON.

Can anyone please give me a rough example of how this would be achieved using jQuery.ajax(), to PHP, to the 3rd party API?

Alternatively if there are any quality resources that cover this method in detail, I'd appreciate a link. Equally, if this is a terrible method to use it'd be great to know why.

Edit
Probably worth noting that I want as much flexibility in deploying this as possible; it would be used on multiple sites with unique configurations, so ideally this would be implemented without altering server or hosting account configuration.

t-jam
  • 811
  • 1
  • 6
  • 21
  • I'm sure you already know this, but you need to be more specific and upload the work you have done so far. You are on the right track going with curl on php side and implementing custom api for js side – georoot Feb 23 '18 at 12:14
  • 1
    @georoot At this point I don't have any code samples (other than a messy test that fails) as I'm really just looking for an overview of how to achieve this, and whether it's a sensible approach. If I can see how to trigger a cURL request in another PHP file with AJAX then get the response back, hopefully I can work out the rest of the details. – t-jam Feb 23 '18 at 12:21
  • Two ways to go around, one is the curl if you want to write a program for it , or other way write an nginx proxy for api calls and just add http param to that :) , let me just write that down for you ;) – georoot Feb 23 '18 at 12:23
  • 1
    @georoot I'll generally be working with Apache so I figure cURL is the most straightforward option? – t-jam Feb 23 '18 at 12:25
  • you can pretty much do the same in apache also .. i haven't worked a lot with that, but i will be glad to find a solution for that. Give me a minute to edit my answer – georoot Feb 23 '18 at 12:27
  • removed comment in wrong place – scytale Mar 08 '18 at 17:32

6 Answers6

7

Because all you want is to add token to http headers, which i am assuming is Authorization a simple way would be to implement a proxy server that makes calls to your api endpoint after adding up those. A sample file for nginx would be

location /apiProxy {
    proxy_pass http://www.apiendPoint.com/;
    proxy_set_header Authorization <secret token>;
}

This is a much more smarter approach rather than writing a program and gets you off with 4 lines of code. Make sure to change your parameters accordingly and add other parameters as needed by api client you are using. The only difference on javascript side would be to use the location url rather than one provided by service which acts as a proxy.

Edit

The configuration for apache would be

NameVirtualHost *
<VirtualHost *>
   <LocationMatch "/apiProxy">
      ProxyPass http://www.apiendPoint.com/
      ProxyPassReverse http://www.apiendPoint.com/
      Header add Authorization "<secret token>"
      RequestHeader set Authorization "<secret token>"   
   </LocationMatch>
</VirtualHost>
georoot
  • 3,557
  • 1
  • 30
  • 59
  • Thanks for the feedback. I'm unfamiliar with writing / configuring proxies, but the eventual end-result may need to be set up on servers where I only have account-level access, e.g. a cPanel account, or even implementing as a WordPress plugin or similar. I'm assuming this would be an issue for this method? – t-jam Feb 23 '18 at 12:31
  • i am not familiar with cpanel hosting, but i am sure they give you some method to configure server. Writing php code would take you a couple of days at minimum depending on your proficiency but using the apache/nginx proxy gets your work done in a minute :) – georoot Feb 23 '18 at 12:33
  • Much appreciated. I'll have more of a look into this method and see how it'll work for me and what I'm hoping to achieve... I'm still quite keen on seeing how this would be achieved with PHP though. – t-jam Feb 23 '18 at 12:36
  • for php, although a little longer route you need to implement a basic http proxy :) – georoot Feb 23 '18 at 12:36
  • Neat solution and new to me. However I can confirm this option is not available for my sites on shared hosting and I suspect it is the same for the majority of sites that use this type of hosting. – scytale Mar 07 '18 at 15:44
  • @scytale just checked on, most hosting provides use apache server and allow configuration using .htaccess file. I am not on apache background but let me do some research on same and add that option also for people who don't have server of there own :) – georoot Mar 07 '18 at 16:20
  • @georoot You are giving me an education! Annoyed at my ignorance I did a few Googles. As I now understand it you can use htaccess however mod_proxy module is required. This shared host does not allow it https://www.hostinger.com/how-to/do-you-support-mod-proxy (I am not sure if they are typical). – scytale Mar 07 '18 at 18:57
  • Not a criticism (just trying to understand answer) I assume in the sites JS you effectively replace `"apiendPoint.com/?somevars=somevalues&authentication=apikey"` with `"mysite.com/apiProxy/?somevars=somevalues"` and use proxy server settings to add the API key? An edge case, but I assume this won't prevent Pirate site using the transaction allowance for my API key if they similarly use "mysite.com/apiProxy/" in their JS? – scytale Mar 07 '18 at 19:39
  • @scytale i actually didn't mention that assuming you knew but there is a policy called *same-origin policy* which would allow ajax only from your hostname. Now that is not foolproof because some smart people can still use your api using curl but that would prevent abuse to certain limit. If you are paying a lot for api, you can implement some sort of custom token mechanism generated for your hostname. – georoot Mar 08 '18 at 09:40
  • It used to work for me, I may be out of date but I think you can still use JSONP with callback to bypass SOP. This is/was done legitimately by many sites using 3rd party APIs. – scytale Mar 08 '18 at 12:01
  • @scytale see the best solution would be to write a proxy on PHP with curl, but that is just too much writing code and not such a smart move. I prefer to keep stuff simpler on my end and that's why configure server to get away with couple of lines of code when they already support proxy. Pretty sure most websites do the same and should work with json api as well because that is just a data format and has nothing to do with proxy :) – georoot Mar 08 '18 at 12:24
  • @georoot Thanks, very useful, upvoted. My comment qns were to ensure I understood your (new to me) solution (I currently use php/curl). Now I'm aware of proxy config (and it was available on my server) it would be the solution I'd use with most APIs. My qn re piggybacking via the proxy was related to edge cases that t-jam is interested in. – scytale Mar 08 '18 at 17:33
  • The webserver's proxy are actually better to use cause they are developed specifically for that purpose. Using php curl although nice idea if you don't have access to server configuration takes up time and maintenance and obviously more complicated than adding a couple of lines straight. That was the simplest way i would have used to solve the problem :) – georoot Mar 08 '18 at 17:37
7

It is bit hard without sample code. But As per I understood you can follow this,

AJAX CALL

$.ajax({
        type: "POST",
        data: {YOU DATA},
        url: "yourUrl/anyFile.php",
        success: function(data){
           // do what you need to 

            }
        });

In PHP

Collect your posted data and handle API, Something like this

$data = $_POST['data']; 
// lets say your data something like this
$data =array("line1" => "line1", "line2"=>"line1", "line3" =>"line1");


 $api = new Api();
 $api->PostMyData($data );

Example API Class

class Api
{
const apiUrl         = "https://YourURL/ ";
const targetEndPoint = self::apiUrl. "someOtherPartOFurl/";

const key       = "someKey819f053bb08b795343e0b2ebc75fb66f";
const secret    ="someSecretef8725578667351c9048162810c65d17";

private $autho="";



public function PostMyData($data){      
  $createOrder = $this->callApi("POST", self::targetEndPoint, $data, true);
  return $createOrder;
 }

private function callApi($method, $url, $data=null, $authoRequire = false){
    $curl = curl_init();

    switch ($method)
    {
        case "POST":
            curl_setopt($curl, CURLOPT_POST, 1);

            if ($data)               
                curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
                break;
        case "PUT":
            curl_setopt($curl, CURLOPT_PUT, 1);
            break;
        default:
            if ($data)
                $url = sprintf("%s?%s", $url, http_build_query($data));
    }

    if($authoRequire){
        $this->autho = self::key.":".self::secret;
        // Optional Authentication:
        curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
        curl_setopt($curl, CURLOPT_USERPWD, $this->autho);
    }

    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

    $result = curl_exec($curl);

    curl_close($curl);


    return $result;

 }
}
MMRahman
  • 319
  • 4
  • 15
  • Thanks, this looks like quite a clean approach, though I'd be concerned about potential unwanted use of this relay by other people / scripts? – t-jam Mar 08 '18 at 01:52
  • Not sure what are you referring to? if it is about security, -- $this->autho = self::key.":".self::secret; -- this is basically based on key and secret policy. Which you do when target api required Autho. [ -- if($authoRequire){ $this->autho = self::key.":".self::secret; // Optional Authentication: curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($curl, CURLOPT_USERPWD, $this->autho); } --- ] If your target required only pass or token you just send token or whatever required. – MMRahman Mar 08 '18 at 07:52
  • Maybe I'm missing something, but this looks like authentication between the PHP script and the API, not any authentication between AJAX and the PHP script. My concern is if there is only hidden authentication between PHP and the API, what in this prevents anyone else from sending their AJAX calls to this PHP script? – t-jam Mar 08 '18 at 08:00
  • 1
    In this scenario, You can use your own security keys. Not sure how is your Application designed, If there is something session included, I will prefer to use that. And post this session key from AJAX along with DATA. Then in php compare php and incoming session key. once success, then do next.. – MMRahman Mar 08 '18 at 08:05
  • 2
    This approach will work, put in additional ip and hostname check to make sure that it’s the server hitting itself. I have a similar but more complex solution using this approach on multiple apis that needed something like this. I would also recommend not putting sensitive key and token info in the codebase, use DotEnv package and use a .env file that is ignored in the gitigonore and put the different environment keys in there. – jeremykenedy Mar 08 '18 at 14:26
5

From your requirements it looks like "server-side code in the middle" relay(proxy) script is the best option.

PHP example here. N.B. to handle CURL errors it returns a new "object" comprising ['status'] ('OK' or info on CURL failure) and ['msg'] containing the actual response from the API provider. In your JS the original API "object" would now require extracting one level down under 'msg'.

Basic Relays/Proxies can be circumvented

If you use a relay script then someone looking for an API key will probably try elsewhere. However; the pirate could simply replace his call to the API provider using your API key, with a call to your script (and your API key will still be used).

Running of your AJAX/relay script by search engine bots

Google bots (others?) execute AJAX. I assume (relay or not) if your AJAX does not need user input then bot visits will result in API key usage. Bots are "improving". In future (now?) they might emulate user input e.g. if selecting a city from a dropdown results in API request then Google might cycle thro dropdown options.

If of concern you could include a check in your relay script e.g.

  $bots = array('bot','slurp','crawl','spider','curl','facebook','fetch','mediapartners','scan','google'); // add your own
  foreach ($bots as $bot) :
    if (strpos( strtolower($_SERVER['HTTP_USER_AGENT']), $bot) !== FALSE):  // its a BOT
      // exit error msg or default content for search indexing (in a format expected by your JS)  
      exit (json_encode(array('status'=>"bot")));
    endif;
  endforeach;

Relay script and additional code to cater for above issues

Do not overdo pirate protection; relays should be fast and delay unnoticeable by visitors. Possible solutions (no expert and rusty with sessions):

1: PHP sessions solution

Checks whether relay is called by someone who visited your AJAX page in last 15 mins, has provided a valid token, and has the same User Agent and IP Address.

  Your Ajax Pages add the following snippets to your PHP & JS:

  ini_set('session.cookie_httponly', 1 );
  session_start();
  // if expired or a "new" visitor
  if (empty($_SESSION['expire']) || $_SESSION['expire'] < time()) $_SESSION['token'] = md5('xyz' . uniqid(microtime())); // create token (fast/sufficient) 

  $_SESSION['expire'] = time() + 900; // make session valid for next 15 mins
  $_SESSION['visitid'] = $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'];
  ...
  // remove API key from your AJAX and add token value to JS e.g.
  $.ajax({type:"POST", url:"/path/relay.php",data: yourQueryParams + "&token=<?php echo $_SESSION['token']; ?>", success: function(data){doResult(data);} });

  The relay/proxy Script (session version):

  Use an existing example relay script and before the CURL block add:

  session_start();  // CHECK REQUEST IS FROM YOU AJAX PAGE
  if (empty($_SESSION['token']) ||  $_SESSION['token'] != $_POST['token'] || $_SESSION['expire'] < time()
        || $_SESSION['visitid'] != $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']  ) {
    session_destroy();  // (invalid) clear session's variables, you could also kill session/cookie
    exit (json_encode(array('status'=>'blocked'))); // exit an object that can be understood by your JS
  }

  Assumes standard session ini settings. Cookies required and page/relay on same domain (workround possible). Sessions might impact performance. If site already uses Sessions, code will need to take this into account.

2: Sessionless/Cookieless option

  Uses a token associated with specific IP Address and User Agent, valid for a maximum of 2 hours.

  Functions used by both page and relay e.g. "site-functions.inc":

<?php
function getToken($thisHour = TRUE) {  // provides token to insert on page or to compare with the one from page
  if ($thisHour) $theHour = date("jH"); else $theHour = date("jH", time() -3600); // token for current or previous hour
  return hash('sha256', 'salt' . $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT'] .  $theHour); 
}

function isValidToken($token) {  // is token valid for current or previous hour
  return (getToken() == $token || getToken(FALSE) == $token);
}
?>

  Relay Script Use an existing example and before the CURL block add:

// assign post variable 'token' to $token 
include '/pathTo/' . 'site-functions.inc';
$result = array('status'=>'timed out (try reloading) or invalid request');
    if ( ! isValidToken($token)) exit(json_encode(array('msg'=>'invalid/timeout'))); // in format for handling by your JS

  Pages needing the API (or your javascript include file):

<?php include '/pathTo/' . 'site-functions.inc'; ?>
...
// example Javascript with PHP insertion of token value
var dataString = existingDataString + "&token=" + "<?php echo getToken(); ?>"
jQuery.ajax({type:"POST", url:"/whatever/myrelay.php",data: dataString, success: function(data){myOutput(data);} });

Note: User Agent is spoofable. IP (REMOTE_ADDR) "cannot" be faked but setup on a minority of sites can cause issues e.g. if you are behind NGINX you may find REMOTE_ADDR always contains the NGINX server IP.

If you are using a typical 3rd party API that will provide NON sensitive information until you reach the usage cap for your API Key then (I think) above solutions should be sufficient.

scytale
  • 1,339
  • 1
  • 11
  • 14
  • Thanks for these code samples, especially including comment around attempting to keep the relay script itself safe from unintended use. I've seen a couple of scripts rely only on restricting referrer, though that doesn't seem very secure on its own. This site token approach would be helpful in conjunction. – t-jam Mar 08 '18 at 01:39
  • What are your thoughts around using different methods (GET / PUT / POST / DELETE) and passing these through the relay? Are you aware of a way to catch the AJAX method in the relay script and re-use that in cURL? – t-jam Mar 08 '18 at 01:44
  • Not sure what you're getting at re HTTP methods. All the script needs from AJAX are USER specified "variables". Let your script identify how to make the request to the 3rd party API. e.g. city weather: all you'd need from AJAX is the city name. Your could then use ONE CURL "request" to API provider using its required method and city argument name and if that fails (e.g. usage limit reached) your script could then makes a SECOND CURL request to ANOTHER API provider using their completely DIFFERENT method and different argument name for city. – scytale Mar 08 '18 at 11:40
  • The token code in answer has a comment re combining time with UA to create token - for clarity (just in case) this means User Agent. Answer now mentions PHP session variables; I'm rusty but if you want example I'll try and provide one. – scytale Mar 08 '18 at 11:42
  • Thanks for all the detail - I've awarded you the bounty because you've gone into the most depth around the security issues and potential ways around them. While I may not use this approach exactly, it's steered me in the right direction - cheers. – t-jam Mar 09 '18 at 07:02
  • Thanks t-jam. Re-read answer today; its untidy and although PHP session code works its a poor example. So if I get chance I may at some point edit answer (yet again!) for future visitors. – scytale Mar 09 '18 at 18:03
2

As people pointed out, you want a proxy method on your server to hide the API-key.

To avoid misuse of your method on the server, protect the call with an one time token (like you usually use for forms) - generated from your server (not in javascript..).

I am not a fan of the coded pasted above which checks for known http-user agents... or site tokens ... this is not secure.

michael - mlc
  • 376
  • 5
  • 5
1

If you use cUrl that you must to protect is your server. The way I personally use is the Google reCaptcha that is sure made for arranging problems like yours. Very well explained the integration in client and server sides step by step here. https://webdesign.tutsplus.com/tutorials/how-to-integrate-no-captcha-recaptcha-in-your-website--cms-23024 With this way you don't need to change anything in your virtualhost files and any apache configurations.

Luis Gar
  • 457
  • 1
  • 4
  • 18
1

I would use the solutiuon @MMRahman published, if you want to add a security layer between your backend and your frontend what you could do it is when the user make login generate a unique ID, store it in the server session and in a cookie or local/session store of the browser, this way when you call your backend with ajax you can get the value from the place where you stored in in the browser, and check if the values are the same, if yes you call the external api and return the values if not just ignore the request.

So summaring: User login -> generate unique ID -> store it in server session and browser session -> make call from ajax passing as parameter the value from browser session-> check if it matchs with server session stored value -> if yes call external api using the token stored in your backend / db / file / whatever you want