PHP - Need API w/ Authentication

JayC

Always Learning
Aug 8, 2013
5,493
1,398
Hey Guys,

So I am working on my own custom CMS for a client, who has requested that I give them an API that another system will be able to make simple calls to validate or pull information. So for example, they need to know if a phone number exists in my system.

Here is how I would like to do it:

{domain}/api/get/{command}/params
{domain}/api/post/{command}/params

So for example:
{domain}/api/get/phone_exists&phone=5555555555

Here is what I was thinking:

API would be a PHPpage that has a connection to the database / main system (Would not require them to be LOGGED IN for the SESSION, but I will need to somehow validate their authentication <-- this is also where I need some help)

get/post is a parameter I can read from the URL rewrite, that I would call an appropriate function, which would have a switch statement for the commands available.

Each command would have its own php page that would perform the required items, and return a result.



  • Is my structure OK?
  • How do I proceed with Authentication?
  • Other helpful tips?
 

RastaLulz

fight teh power
Staff member
May 3, 2010
3,926
3,921
Quite the broad question you have there.

I guess it really depends on what your desired outcome is. If it's a one off thing, and you don't really care (as long it's secure), then it really doesn't matter how you achieve it.

However, if your goal is to create an API that is , and would be more familiar to your average developer, then there's room for improvement.

Pre-requisites:

Additionally, I'd recommend installing a client like , that will allow you to interact with your API, if you haven't already.

Anyways, I assume the phone number comes from some table in the database? Let's assume it's the users table.

Here's what your HTTP request might look like:
Code:
GET /users?phone_number=8675309
Host: example.com
Authorization: YOUR_AUTH_TOKEN

Which would return something like this:
JSON:
{
  "meta": {
    "total": 1
  },
  "items": [
    {
      "id": 1,
      "name": "John Doe",
      "phone_number": "8675309"
    },
  ],
}

So what this allows you to do is query for all users with the specific phone number you're looking for, and can validate whether it has been used or not based on meta.total in the response. Then if you have any other need for accessing users, you already have an endpoint setup. Also, you'll see that Authorization would be sent as a header in the HTTP request, which you validate server side, and if it passes, you'd serve the content. If the Authorization header if invalid, you'd respond with a 401 status code.

Hopefully that's inline with what you're attempting to do, and gives you some direction.
 

JayC

Always Learning
Aug 8, 2013
5,493
1,398
Quite the broad question you have there.

I guess it really depends on what your desired outcome is. If it's a one off thing, and you don't really care (as long it's secure), then it really doesn't matter how you achieve it.

However, if your goal is to create an API that is , and would be more familiar to your average developer, then there's room for improvement.

Pre-requisites:

Additionally, I'd recommend installing a client like , that will allow you to interact with your API, if you haven't already.

Anyways, I assume the phone number comes from some table in the database? Let's assume it's the users table.

Here's what your HTTP request might look like:
Code:
GET /users?phone_number=8675309
Host: example.com
Authorization: YOUR_AUTH_TOKEN

Which would return something like this:
JSON:
{
  "meta": {
    "total": 1
  },
  "items": [
    {
      "id": 1,
      "name": "John Doe",
      "phone_number": "8675309"
    },
  ],
}

So what this allows you to do is query for all users with the specific phone number you're looking for, and can validate whether it has been used or not based on meta.total in the response. Then if you have any other need for accessing users, you already have an endpoint setup. Also, you'll see that Authorization would be sent as a header in the HTTP request, which you validate server side, and if it passes, you'd serve the content. If the Authorization header if invalid, you'd respond with a 401 status code.

Hopefully that's inline with what you're attempting to do, and gives you some direction.
Thanks for this. I'll read up those documentation, I do want to implement the restful api method.
 

BIOS

ಠ‿ಠ
Apr 25, 2012
906
247
Thanks for this. I'll read up those documentation, I do want to implement the restful api method.
If you want to go more towards a RESTful design, URLs are generally used a bit differently.

You want to view them as single entities, rather than the methods you have in the routes (/api/get, /api/post). Don't include the actual HTTP method in the URL, as that's supposed to be implied by the method of the actual HTTP request.

So in your example:
Code:
/api/get/phone_exists&phone=5555555555

This could be implemented as:
Code:
GET /api/phone_numbers
This might return a list of phone numbers, given a specific auth header token.

Code:
GET /api/phone_numbers/5555555555
Would show you details on a specific phone number

Whereas
Code:
POST /api/phone_numbers

{"phone":"5555555555"}
Would allow you to create a new phone number.

Similarly, you could also allow updating a result like so:
Code:
PUT /api/phone_numbers/5555555555

{"new_phone":"1111111111"}
would allow you to update the old phone number value.

DELETE might delete a phone number from the system, and so on.. Query strings can be used but it's generally cleaner to sparsely use them, e.g. GET /api/phone_numbers?limit=15 to limit the number of items in the response. They're often used for pagination too.

I wouldn't necessarily create a new PHP file for every command, instead follow a design pattern; a common one which many use is MVC but there's others too. In Rasta's example you might have a User controller which interacts with a User model/repository (essentially any query/database logic), and views which would be your response.

You also don't have to do any nasty rewrites yourself, you can use an application router to handle all that for you, e.g, see: .

For the actual auth you'll probably want to use the Authorization header as Rasta pointed out, but how you build that system is entirely up to you. Though I'd recommend standards like . Here's some libraries to look at for PHP:

Further reading:
 
Last edited:

JayC

Always Learning
Aug 8, 2013
5,493
1,398
Okay,

I am ready for some criticism and feedback -

Here is my overall structure:

/ api / api.php
/ api / models / volunteer.php
/ api / security / authentication.php

Here is my Authentication File:
Code:
$realm = 'API';
    
    $admins = array('REDACTED' => 'REDACTED', 'REDACTED2' => 'REDACTED2');

    // function to parse the http auth header
    function http_digest_parse($txt)
    {
        // protect against missing data
        $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
        $data = array();
        $keys = implode('|', array_keys($needed_parts));

        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

        foreach ($matches as $m) {
            $data[$m[1]] = $m[3] ? $m[3] : $m[4];
            unset($needed_parts[$m[1]]);
        }

        return $needed_parts ? false : $data;
    }

    if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$realm.
               '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');

        print "Sorry, you are not authorized to access this area";
        exit;
    }
    
    // analyze the PHP_AUTH_DIGEST variable
    if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($admins[$data['username']]))
    {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$realm.
               '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
    
    // generate the valid response
    $A1 = md5($data['username'] . ':' . $realm . ':' . $admins[$data['username']]);
    $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
    $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
    
    if ($data['response'] != $valid_response){
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$realm.
               '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }

The authentication works great, I am utilizing Postman as @RastaLulz mentioned and it really helps to test the system!

Now for the API.php file:
Code:
<?php
    //Require Authentication
    require_once 'security/authentication.php';
    
    header('Content-Type: application/json'); //Set return type to JSON
    define('IN_INDEX', 1); //Define access to allow global.php to be referenced
    
    //Include Global File
    include_once '../../global.php'; //Access global.php, which will give access to the engine which has the MySQL connection
    
    //Declare Global Variables
    global $engine; //Define engine from the global file
    
    //Include Model Files
    include_once 'models/volunteer.php'; //Include our volunteer model, which will give the blueprint / class for what a volunteer is
    
    //Declare Model Variables
    $volunteer = new Volunteer($engine); //Define an instance of our volunteer
    
    //Get the type of request, current working on just GET requests
    $reqType = '';

    if(isset($_GET['type'])){
        $reqType = $_GET['type'];
    }else{
        header('HTTP/1.1 400 Bad Request');
        print "Must Define Type Parameter";
        exit;
    }

    switch(strtoupper($reqType)){
        case "GET":
        {
            HandleGET(); //We established we want to GET, we can call this function to handle our GET options
        }
        break;
    
        case "POST":
        {
            HandlePOST();
        }
        break;
        
        default:
        {
            header('HTTP/1.1 404 Not Found');
            print "The requested Type is not supported";
            exit;
        }
        break;
    }
    
    function HandleGET(){
        global $volunteer; //Get access to our blueprint class
        
       //What is the action we want to GET
        $action = '';
        
        if(isset($_GET['action'])){
            $action = $_GET['action'];
        }else{
            header('HTTP/1.1 400 Bad Request');
            print "Must Define Action Parameter";
            exit;
        }
        
        switch(strtoupper($action)){
            case "VOLUNTEERS": //Sample for returning all of the volunteers in the system
            {
                //Volunteer Query
                $result = $volunteer->read();
                
                $num = mysqli_num_rows($result);
                
                //Check Volunteers Exist
                if($num > 0){
                    // Volunteer Array
                    $volunteers_arr = array();
                    $volunteers_arr['data'] = array();
                    
                    while($row = mysqli_fetch_array($result)){
                        extract($row);
                        $volunteer_item = array(
                            'id' => $ID,
                            'fname' => $fname,
                            'lname' => $lname,
                            'email' => $email,
                            'phone' => $phone,
                            'created' => $created
                        );
                        
                        //Push to "data" results
                        array_push($volunteers_arr['data'], $volunteer_item);
                    }
                    
                    //Turn result array to JSON
                    echo json_encode($volunteers_arr);
                }else{
                    //No Posts
                    echo json_encode(
                        array('message' => 'No Volunteers Found')
                    );
                }
            }
            break;
            default:
            {
                header('HTTP/1.1 404 Not Found');
                print "The requested Action is not supported";
                exit;
            }
            break;
        }
    }
    
    function HandlePOST(){
        global $volunteer;
        
    }
?>


And here is my basic volunteer model class:
Code:
<?php
class Volunteer{
    private $engine;
    private $table = 'volunteers';
    
    //Post Properties
    public $ID;
    public $firstname;
    public $lastname;
    public $email;
    public $phone;
    public $account_create;
    
    // Constructor with DB
    public function __construct($dbEngine){
        $this->engine = $dbEngine;
    }
    
    // Get Volunteers
    public function read(){
        //Create Query
        $query = 'SELECT
              vol.ID as ID,
              vol.firstname as fname,
              vol.lastname as lname,
              vol.email as email,
              vol.phone as phone,
              vol.account_created as created
            FROM
              ' . $this->table . ' vol
            ORDER BY
              vol.account_created DESC';
        
        //Fetch Results
        $results = $this->engine->query($query);
        
        return $results;
    }
}
?>
Post automatically merged:

Just going to bump this, as I am on a time frame and want to know if this is following best practices or if I should change how I am implementing this...
 
Last edited:

RastaLulz

fight teh power
Staff member
May 3, 2010
3,926
3,921
Like @BIOS mentioned, you'd be doing your self a service to use a framework like or (I'd argue that you should just use instead of Lumen). Or even using to pull in widely used , instead of trying to reinvent the wheel. I'd also recommend following 's, especially and , so that when you do share code, it's formatted in a standard way.

As for the code you provided:
  • I'd be more interested in how you interact with the API. Explain what the endpoints will be, and how you'll handle auth (as if you're providing documentation to your client).
  • An API should be consistent in what content-type it returns. By that I mean, it should always return JSON (or XML, CSV, etc, but be consistent), so that the API consumer has a general expectation of how to parse the response. So when you return an error, that should also be consistent; it appears that you've done this to extent, but not when there's a >400 status code.
  • Instead of returning No Volunteers Found, simply return an empty array.
 
Last edited:

JayC

Always Learning
Aug 8, 2013
5,493
1,398
Like @BIOS mentioned, you'd be doing your self a service to use a framework like or (I'd argue that you should just use instead of Lumen). Or even using to pull in widely used , instead of trying to reinvent the wheel. I'd also recommend following 's, especially and , so that when you do share code, it's formatted in a standard way.

As for the code you provided:
  • I'd be more interested in how you interact with the API. Explain what the endpoints will be, and how you'll handle auth (as if you're providing documentation to your client).
  • An API should be consistent in what content-type it returns. By that I mean, it should always return JSON (or XML, CSV, etc, but be consistent), so that the API consumer has a general expectation of how to parse the response. So when you return an error, that should also be consistent; it appears that you've done this to extent, but not when there's a >400 status code.
  • Instead of returning No Volunteers Found, simply return an empty array.
To answer your points on the code:

First -
- The API is a JQuery Request, and in the code, above I was using Digest PHP Authentication, which I realize now is not easy to implement since you have to return a response, then reply again to gain access. So I am going to use basic authentication so now my code is the following:
Code:
<?php
    $realm = 'API';
   
    $admins = array('REDACTEDUSER' => 'REDACTEDPASS', 'USER2' => 'PASS2');

    if (empty($_SERVER['PHP_AUTH_USER'])) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
   
    // Check Login Credentials
    if (!isset($admins[$_SERVER['PHP_AUTH_USER']]))
    {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
   
    if($admins[$_SERVER['PHP_AUTH_USER']] != $_SERVER['PHP_AUTH_PW']){
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
?>

This works and prints the message if it fails to authenticate.
You must be registered for see images attach



In response to the second and third suggestion -
- I will update all returns so it will have the following consistency:

[Array]
"data" --> Returned Array/Data
"message" --> Returned Response

I will set "data" to be blank if it fails, with the message being unable to authorize, or whatever the issue is.

I will look into those API elements, but I would honestly rather re-invent the wheel for this project. Let me explain why -

By utilizing these services, I will not have as good of an understanding of how the base level works. With me creating my own Api and handling everything from authentication, to organization, to even routing, I am going to take a lot more away from this. In the future I will be able to use these services, to make programming these apis faster and not having to do it all myself again. The take-away will be more valuable the first time around, in my opinion.
Post automatically merged:

Update -

I can get data from the page using PostMan but I am unable to return data using Ajax.

Could someone tell me what I am doing wrong here:

Code:
var username = 'User';
var password = 'Pass';
var url = 'http://localhost/app/api/api.php?type=GET&action=phone_exists&phone=5555555555'
var postData = {
"type" : 'GET',
"action" : 'phone_exists',
"phone" : input.value,
};

$.ajax({
url: url,
type: 'GET',
dataType: 'json',
data: postData,
contentType: 'application/json',
beforeSend: function(xhr) {
xhr.setRequestHeader("Authorization", "Basic "+btoa(username+':'+password));
},
success: function(json){
alert(json);
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
return true;
}
});

You must be registered for see images attach
 

Attachments

  • 1607819175042.png
    1607819175042.png
    12.5 KB · Views: 4
Last edited:

Hahn

Member
Jan 7, 2017
44
7
Hi @JayCustom , I'm sorry for my bad English, I'm Brazilian and I was here reading your topic. Look, last week I was in the same situation as you to implement security even if it is minimal in the company's system in the API part of the system. I did it, I implemented JWT, where a Bearer token is passed in the header Authorization. I recommend reading the JWT on the JWT website, Google's first page. It is not at all complex, since the idea of JWT is for you to receive the Token and decode it, so you can verify the key delivered by it. Yes it works with a key that you must keep it very well, because only your server-side should contain it. Read about it, it's really cool, I implemented and liked it. Be well and I hope everything goes well!
 

Berk

berkibap#4233
Developer
Oct 17, 2015
863
190
To answer your points on the code:

First -
- The API is a JQuery Request, and in the code, above I was using Digest PHP Authentication, which I realize now is not easy to implement since you have to return a response, then reply again to gain access. So I am going to use basic authentication so now my code is the following:
Code:
<?php
    $realm = 'API';
  
    $admins = array('REDACTEDUSER' => 'REDACTEDPASS', 'USER2' => 'PASS2');

    if (empty($_SERVER['PHP_AUTH_USER'])) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
  
    // Check Login Credentials
    if (!isset($admins[$_SERVER['PHP_AUTH_USER']]))
    {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
  
    if($admins[$_SERVER['PHP_AUTH_USER']] != $_SERVER['PHP_AUTH_PW']){
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="'.$realm.'"');
        print "Sorry, you are not authorized to access this area";
        exit;
    }
?>

This works and prints the message if it fails to authenticate.
You must be registered for see images attach



In response to the second and third suggestion -
- I will update all returns so it will have the following consistency:

[Array]
"data" --> Returned Array/Data
"message" --> Returned Response

I will set "data" to be blank if it fails, with the message being unable to authorize, or whatever the issue is.

I will look into those API elements, but I would honestly rather re-invent the wheel for this project. Let me explain why -

By utilizing these services, I will not have as good of an understanding of how the base level works. With me creating my own Api and handling everything from authentication, to organization, to even routing, I am going to take a lot more away from this. In the future I will be able to use these services, to make programming these apis faster and not having to do it all myself again. The take-away will be more valuable the first time around, in my opinion.
Post automatically merged:

Update -

I can get data from the page using PostMan but I am unable to return data using Ajax.

Could someone tell me what I am doing wrong here:

Code:
var username = 'User';
var password = 'Pass';
var url = 'http://localhost/app/api/api.php?type=GET&action=phone_exists&phone=5555555555'
var postData = {
"type" : 'GET',
"action" : 'phone_exists',
"phone" : input.value,
};

$.ajax({
url: url,
type: 'GET',
dataType: 'json',
data: postData,
contentType: 'application/json',
beforeSend: function(xhr) {
xhr.setRequestHeader("Authorization", "Basic "+btoa(username+':'+password));
},
success: function(json){
alert(json);
},
error: function(xhr, status, error) {
var err = eval("(" + xhr.responseText + ")");
alert(err.Message);
return true;
}
});

You must be registered for see images attach
For your issue, add this to your PHP page before authentication:
PHP:
header("Access-Control-Allow-Origin: your-website.com");
 

JayC

Always Learning
Aug 8, 2013
5,493
1,398
For your issue, add this to your PHP page before authentication:
PHP:
header("Access-Control-Allow-Origin: your-website.com");
Hey, I have that already allowing all domains


Code:
header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST");
    header("Access-Control-Allow-Headers: Origin, Methods, Content-Type");
 

Users who are viewing this thread

Top