Welcome to the Building Your Startup With PHP series, which is guiding readers through the launch of an actual startup, Meeting Planner. Each episode details different coding and business challenges, with detailed examples you can use to learn.
Introduction
Recently, I introduced you to Yii’s simple REST API generation and Meeting Planner’s new “RESTful” service API. At that time, I mentioned that these APIs were only loosely secured. Sure, there was a shared secret between the client and the server, but there were a couple of problems.
First, the secret key and user tokens were repeatedly transmitted in query parameters of SSL calls. And there was no other authenticity check for the data, allowing a middle-person attack.
In today’s episode, I’ll guide you through how I secured the API against these weaknesses for a more robust API.
If you’ve been reading our startup series, you’ve probably already tried Meeting Planner and Simple Planner, but if not, please do. Scheduling a meeting is easy:
As usual, I’ll be participating in the comments below, so please offer your thoughts. You can also reach me on Twitter @lookahead_io. I’m always especially intrigued if you want to suggest new features or topics for future tutorials.
As a reminder, all of the code for Meeting Planner is written in the Yii2 Framework for PHP. If you’d like to learn more about Yii2, check out our parallel series Programming With Yii2.
The Initial API Security
Let’s begin by taking a look at the early API security I coded. We’ll presume there’s a mobile app that I’ve shared an $app_id
and $app_secret
with. Only API callers with these keys are accepted.
For example, the app tries to register its owner, likely a new Meeting Planner user:
public function actionRegister($app_id='', $app_secret='', $source='',$firstname ='',$lastname='', $email = '',$oauth_token='') { Yii::$app->response->format = Response::FORMAT_JSON; // verify app_id and app_key if (!Service::verifyAccess($app_id,$app_secret)) { // to do - error msg return false; }
The app calls the above actionRegister
via https://api.meetingplanner.io/user-token/register/ with arguments as follows:
-
$app_id
and$app_secret
for authentication -
$source = 'facebook'
for the OAuth service we’re using, and accompanying$oauth_token
from that service -
$email
,$firstname
, and$lastname
provided via OAuth
All of those are query arguments such as:
https://api.meetingplanner.io/user-token/register/?app_id=777&app_secret=imwithher&source=facebook&oauth_token=zuckerburger&email=tom@macfarlins.com&firstname=thomas&lastname=macfarlins
Service::verifyAccess($app_id,$app_secret)
looks up the keys to authenticate the call as shown below:
class Service extends Model { public static function verifyAccess($app_id,$app_secret) { if ($app_id == Yii::$app->params['app_id'] && $app_secret == Yii::$app->params['app_secret']) { Yii::$app->params['site']['id']=SiteHelper::SITE_SP; return true; } else { return false; } }
Because the keys and the data were sent via SSL, they’re pretty secure but not invincible. Neither is the secret key safe on users’ iPhones for certain.
How can we make this more secure? Here are a few ideas:
- Don’t ever transmit the secret key over the Internet.
- Don’t transmit any of the data as URL parameters which might show up in server logs.
- Sign all the data to verify its accuracy.
These are actually standard practices used for securing APIs.
Note: An example of the risk of transmitting data that could be exposed in server logs would be the email and the Facebook OAuth token. If found in logs, these could be used with the Facebook API to access someone’s Facebook account.
Implementing Better API Security
Using Hash Signatures
First, I’m going to stop transmitting the $app_secret
. Instead, we’ll sign the outgoing data with it before making an API call.
So we’ll alphabetize the variables and concatenate them into a string, like this:
$data = $email.$firstname.$lastname.$oauth_token.$source;
Resulting in:
$data = '[email protected]'
Then, we’ll hash the data with PHP’s hash_hmac and the sha256
algorithm using our secret key.
$signature = hash_hmac('sha256', $data,Yii::$app->params['app_secret']);
This creates a unique hash code based on the arguments of the API call and our shared secret key:
$signature => 9f6d2f7dd7d674e85eff51f40f5f830787c37d84d4993ac9ccfea2800285bd02
Now, we can make a call on the API without transmitting the secret key. Instead, we transmit the signature of the hashed data above.
I’ve been using Postman to test the API, but you can also use cURL:
Here’s the receiving API code that responded to the call above:
public function actionRegister($app_id='', $source='',$firstname ='',$lastname='',$email = '',$oauth_token='',$sig='') { Yii::$app->response->format = Response::FORMAT_JSON; $sig_target = hash_hmac('sha256',$email.$firstname.$lastname.$oauth_token.$source,Yii::$app->params['app_secret']); if ($app_id != Yii::$app->params['app_id'] && $sig==$sig_target) { return 'it worked!'; } else { return 'failed!'; }
Furthermore, as I reviewed last time, each user receives their own token when they access Meeting Planner through the API, e.g. via their mobile phone. So, subsequent to registration, we can sign calls with their individual token and don’t need to transmit either the application’s secret key or the user’s individual token.
Sending Data in the HTTPS Headers
Next, we’ll migrate sending data in the headers. You can do this easily with Postman or cURL. Here’s Postman:
And here’s cURL:
public function actionCurl($sig) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL,"http://localhost:8888/mp-api/user-token/register?sig=".$sig); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $headers = [ 'app_id: '.'imwithher', 'email: '.'[email protected]', 'firstname: '.'thomas', 'lastname: '.'macfarlins', 'oauth_token: '.'zuckerburger', 'source: '.'facebook', ]; curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $server_output = curl_exec ($ch); var_dump($server_output); curl_close ($ch); }
Here’s the receiving code which gets the API data from HTTPS Headers:
public function actionRegister($sig='') { Yii::$app->response->format = Response::FORMAT_JSON; $headers = Yii::$app->request->headers; $email= $headers->get('email'); $firstname= $headers->get('firstname'); $lastname= $headers->get('lastname'); $oauth_token= $headers->get('oauth_token'); $source = $headers->get('source'); if ($headers->has('app_id')) { $app_id = $headers->get('app_id'); } $sig_target = hash_hmac('sha256',$email.$firstname.$lastname.$oauth_token.$source,Yii::$app->params['app_secret']); if ($app_id != Yii::$app->params['app_id'] && $sig==$sig_target) { return 'it worked!'; } else { return 'failed!'; }
In Closing
We began today with the following goals:
- Don’t ever transmit the secret key over the Internet.
- Don’t transmit any of the data as URL parameters which might show up in server logs.
- Sign all the data to verify its accuracy.
And we accomplished all of these goals with only modest changes to our API code. It was fun making these changes and seeing how easily we can better secure an API. I hope you enjoyed following along with today’s episode.
I regularly monitor the comments, so please join in the discussion. You can also reach me on Twitter @lookahead_io directly. And, of course, watch for upcoming tutorials here in the Building Your Startup With PHP series.
If you didn’t earlier, try scheduling a meeting at Meeting Planner and let me know what you think. I especially appreciate feature requests.
Related Links
- Simple Planner or Meeting Planner
- How to Program With Yii2 (Envato Tuts+)
- Building Your Startup: Designing a RESTful API (Envato Tuts+)
- Programming With Yii: Building a RESTful API (Envato Tuts+)