DEV Community

loading...
Cover image for Secure your PHP REST API with Magic
Magic Labs

Secure your PHP REST API with Magic

shahbaz17 profile image Mohammad Shahbaz Alam Updated on ・9 min read

The Internet is a global public resource that needs to be protected. Let’s start by securing the RESTful API where authenticated users can perform certain actions that unauthenticated users can’t.

This is the second part of the Learn PHP series. If you haven't checked Build a Simple REST API in PHP, I would highly recommend you to check that article first if you want to learn how to build a REST API in core PHP.

This guide walks you through to protect the PHP Rest API endpoints with Magic.

Why Magic?

Magic enables you to ultra-secure your APIs with reliable passwordless logins, such as email magic links, social login, and WebAuthn, with just a few lines of code.

Learn more about Magic and Why Passwords Suck.

Quickstart

Visit https://magic.link/posts/magic-php

Prerequisites

Getting Started

Clone GitHub Repo

Clone the PHP Rest API if you are starting from here, but if you are following the Learn PHP series, you are good to go. You already have all the ingredients needed for a successful recipe. We will just add some Magic touches to it.

git clone https://github.com/shahbaz17/php-rest-api magic-php-rest-api
Enter fullscreen mode Exit fullscreen mode

Build a Simple REST API in PHP

This example shows how to build a simple REST API in core PHP.

Please read https://dev.to/shahbaz17/build-a-simple-rest-api-in-php-2edl to learn more about REST API.

Prerequisites

Getting Started

Clone this project with the following commands:

git clone https://github.com/shahbaz17/php-rest-api.git
cd php-rest-api
Enter fullscreen mode Exit fullscreen mode

Configure the application

Create the database and user for the project.

mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password'
GRANT ALL on blog.* to 'rest_api_user'@'localhost'
quit
Enter fullscreen mode Exit fullscreen mode

Create the post table.

mysql -u rest_api_user -p;
// Enter your password
use blog;

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `author` varchar(255),
  `author_picture` varchar(255),
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);
Enter fullscreen mode Exit fullscreen mode

Copy .env.example to .env file and enter your database deatils.

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Development

Install…

Rest API Endpoints

  • GET /posts: Displays all the posts from post table.
  • GET /post/{id}: Display a single post from post table.
  • POST /post: Create a post and insert into post table.
  • PUT /post/{id}: Update the post in post table.
  • DELETE /post/{id}: Delete a post from post table.

Configure the Database for your PHP REST API

If following the Learn PHP series, you already have what it takes to follow this guide.

Please skip to Install the Magic Admin SDK for PHP

We will use MySQL to power our simple API.

Create a new database and user for your app:

mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
GRANT ALL on blog.* to 'rest_api_user'@'localhost';
quit
Enter fullscreen mode Exit fullscreen mode

The REST API will contain posts for our Blog Application, with the following fields: id, title, body, author, author_picture, created_at. It allows users to post their blog on our Blog application.

Create the database table in MySQL.

mysql -u rest_api_user -p;
// Enter your password
use blog;

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(255) NOT NULL,
  `body` text NOT NULL,
  `author` varchar(255),
  `author_picture` varchar(255),
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

Enter fullscreen mode Exit fullscreen mode

Add the database connection variables to your .env file:

.env

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password
Enter fullscreen mode Exit fullscreen mode

Install the Magic Admin SDK for PHP

The Magic SDK for server-side PHP makes it easy to leverage Decentralized ID Tokens to authenticate the users of your app.

Composer

You can install the bindings via Composer.

For example, to install Composer on Mac OS, run the following command:

brew install composer
Enter fullscreen mode Exit fullscreen mode

Once composer is installed, run the following command to get the latest Magic Admin SDK for PHP:

composer require magiclabs/magic-admin-php
Enter fullscreen mode Exit fullscreen mode

Manual Installation

If you do not wish to use Composer, you can download the latest release. Then, to use the bindings, include the init.php file.

require_once('/path/to/magic-admin-php/init.php');
Enter fullscreen mode Exit fullscreen mode
Installation Dependency

The bindings require the following extensions in order to work properly:

If you use Composer, these dependencies should be handled automatically. If you install manually, you'll want to make sure that these extensions are available.

Get your Magic Secret Key

Sign Up with Magic and get your MAGIC_SECRET_KEY.

Feel free to use the Test Application automatically configured for you, or create a new one from your Dashboard.

Dashboard Image goes here

Update .env

Now, Add one MAGIC_SECRET_KEY variable to the .env file.

Updated .env

MAGIC_SECRET_KEY={SECRET Key}
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

Add Magic to Post.php

Open src\Post.php in your favourite editor.

Add getEmail()

This function is the starting point for our Magic Authentication, it instantiates Magic, validates the token, gets the issuer using the token, and retrieves the user's meta data using the issuer. It also retrieves the token from the HTTP Header.

public function getEmail() {
    $did_token = \MagicAdmin\Util\Http::parse_authorization_header_value(
    getallheaders()['Authorization']
    );

    // DIDT is missing from the original HTTP request header. Returns 404: DID Missing
    if ($did_token == null) {
    return $this->didMissing();
    return $response;
    }

    $magic = new \MagicAdmin\Magic(getenv('MAGIC_SECRET_KEY'));

    try {
    $magic->token->validate($did_token);
    $issuer = $magic->token->get_issuer($did_token);
    $user_meta = $magic->user->get_metadata_by_issuer($issuer);
    return $user_meta->data->email;
    } catch (\MagicAdmin\Exception\DIDTokenException $e) {
    // DIDT is malformed.
    return $this->didMissing();
    return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let me walk you through what this function is doing and how you can configure it for your application if you are not using PHP Rest API.

Instantiate Magic

$magic = new \MagicAdmin\Magic(getenv('MAGIC_SECRET_KEY'));
Enter fullscreen mode Exit fullscreen mode

The constructor allows you to specify your API secret key and HTTP request strategy when your application is interacting with the Magic API.

Read more about Constructor and Arguments on our doc.

Retrieve <auth token> from HTTP Header Request

$did_token = \MagicAdmin\Util\Http::parse_authorization_header_value(getallheaders()['Authorization']);
Enter fullscreen mode Exit fullscreen mode
Authorization: Bearer <auth token>
Enter fullscreen mode Exit fullscreen mode

Include the above code in your existing code, if you're using in your code, to grab <auth token> from HTTP Header Request.

In our case, we call this <auth token> a DID Token.

if ($did_token == null) {
    return $this->didMissing();
    return $response;
}
Enter fullscreen mode Exit fullscreen mode

If DIDT is missing from the original HTTP request header. It returns 404: DID is Malformed or Missing.

didMissing()

private function didMissing() {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body'] = json_encode([
      'error' => 'DID is Malformed or Missing.'
  ]);
    return $response;
}
Enter fullscreen mode Exit fullscreen mode

Validate DID Token <auth token>

The DID Token is generated by a Magic user on the client-side which is passed to your server via Frontend Application.

$magic->token->validate($did_token);
Enter fullscreen mode Exit fullscreen mode

You should always validate the DID Token before proceeding further. It should return nothing if the DID Token is valid, or else it will throw a DIDTokenException if the given DID Token is invalid or malformed.

Get the issuer

$issuer = $magic->token->get_issuer($did_token);
Enter fullscreen mode Exit fullscreen mode

get_issuer returns the Decentralized ID (iss) of the Magic user who generated the DID Token.

Get the User Meta Data

$user_meta = $magic->user->get_metadata_by_issuer($issuer);
Enter fullscreen mode Exit fullscreen mode

get_metadata_by_issuer retrieves information about the user by the supplied iss from the DID Token. This method is useful if you store the iss with your user data, which is recommended.

It returns a MagicResponse

  • The data field contains all of the user meta information.
    • issuer (str): The user's Decentralized ID.
    • email (str): The user's email address.
    • public_address (str): The authenticated user's public address (a.k.a.: public key). Currently, this value is associated with the Ethereum blockchain.

In this guide, we will be using email as the author in the post table.

Update createPost()

This will be the protected route, so let's add Magic to it. It means only the authenticated persons can create a post, where it will use their email as the author field of the post.

private function createPost() {
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
        return $this->unprocessableEntityResponse();
    }

    $query = "
        INSERT INTO posts
            (title, body, author, author_picture)
        VALUES
            (:title, :body, :author, :author_picture);
      ";

    $author = $this->getEmail();

    if(is_string($author)) {
        try {
          $statement = $this->db->prepare($query);
          $statement->execute(array(
            'title' => $input['title'],
            'body'  => $input['body'],
            'author' => $author,
            'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($author)).'.png?s=200',
          ));
          $statement->rowCount();
        } catch (\PDOException $e) {
            exit($e->getMessage());
        }

        $response['status_code_header'] = 'HTTP/1.1 201 Created';
        $response['body'] = json_encode(array('message' => 'Post Created'));
        return $response;
    } else {
      return $this->didMissing();
      return $response;
    }

}
Enter fullscreen mode Exit fullscreen mode

Get Author's email

$author = $this->getEmail();
Enter fullscreen mode Exit fullscreen mode

It returns the email id of the authenticated user.

Author's email and picture

Let's use the email address of the authenticated user to be used as the author of the post and use the email to get the public profile picture set with Gravatar.

    'author' => $author,
    'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($author)).'.png?s=200',
Enter fullscreen mode Exit fullscreen mode

Update updatePost($id)

This route will also be protected, which means the only person who should be able to update the post is the person who wrote it.

private function updatePost($id) {
    $result = $this->find($id);
    if (! $result) {
        return $this->notFoundResponse();
    }
    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
    if (! $this->validatePost($input)) {
        return $this->unprocessableEntityResponse();
    }

    $author = $this->getEmail();

    $query = "
        UPDATE posts
        SET
            title = :title,
            body  = :body,
            author = :author,
            author_picture = :author_picture
        WHERE id = :id AND author = :author;
    ";

    if(is_string($author)) {
      try {
          $statement = $this->db->prepare($query);
          $statement->execute(array(
              'id' => (int) $id,
              'title' => $input['title'],
              'body'  => $input['body'],
              'author' => $author,
              'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($author)).'.png?s=200',
          ));
          if($statement->rowCount()==0) {
            // Different Author trying to update.
            return $this->unauthUpdate();
            return $response;
          }
      } catch (\PDOException $e) {
          exit($e->getMessage());
      }
      $response['status_code_header'] = 'HTTP/1.1 200 OK';
      $response['body'] = json_encode(array('message' => 'Post Updated!'));
      return $response;
    } else {
      return $this->didMissing();
      return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Protect unauthorize update

$query = "
    UPDATE posts
    SET
        title = :title,
        body  = :body,
        author = :author,
        author_picture = :author_picture
    WHERE id = :id AND author = :author;
";
Enter fullscreen mode Exit fullscreen mode

unauthUpdate()

return $this->unauthUpdate();
.
.
.
// unauthUpdate()
private function unauthUpdate() {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body'] = json_encode([
    'error' => 'You are not authorised to delete this post.'
  ]);
    return $response;
}
Enter fullscreen mode Exit fullscreen mode

Update deletePost($id)

This route will also be protected, which means the only person who should be able to delete the post is the person who wrote it.

private function deletePost($id) {
    $author = $this->getEmail();
    if(is_string($author)) {
      $result = $this->find($id);
      if (! $result) {
          return $this->notFoundResponse();
      }

      $query = "
          DELETE FROM posts
          WHERE id = :id AND author = :author;
      ";

      try {
          $statement = $this->db->prepare($query);
          $statement->execute(array('id' => $id, 'author' => $author));
          if($statement->rowCount()==0) {
            // Different Author trying to delete.
            return $this->unauthDelete();
            return $response;
          }
      } catch (\PDOException $e) {
          exit($e->getMessage());
      }
      $response['status_code_header'] = 'HTTP/1.1 200 OK';
      $response['body'] = json_encode(array('message' => 'Post Deleted!'));
      return $response;
    } else {
      // DID Error.
      return $this->didMissing();
      return $response;
    }

}
Enter fullscreen mode Exit fullscreen mode

Protect unauthorize delete

$query = "
  DELETE FROM posts
  WHERE id = :id AND author = :author;
";
Enter fullscreen mode Exit fullscreen mode

unauthDelete()

return $this->unauthDelete();
.
.
.
// unauthDelete()
private function unauthDelete() {
    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
    $response['body'] = json_encode([
      'error' => 'You are not authorised to delete this post.'
  ]);
    return $response;
}
Enter fullscreen mode Exit fullscreen mode

Get the completed Post.php from here.

Endpoints

Available for un-authenticated users:

  • GET /post: Displays all the posts from post table.
  • GET /post/{id}: Displays a single post from post table.

Available for authenticated users: Protected with Magic

  • POST /post: Creates a post and inserts into post table.
  • PUT /post/{id}: Updates the post in post table. Also, ensures a user cannot update someone else's post.
  • DELETE /post/{id}: Deletes the post from post table. Also, ensures a user cannot delete someone else's post.

Development

Let's install the dependencies, start the PHP Server and test the APIs with a tool like Postman.

Install dependencies:

composer install
Enter fullscreen mode Exit fullscreen mode

Run Server:

php -S localhost:8000 -t api
Enter fullscreen mode Exit fullscreen mode

Start the Frontend Application to get the DID token for testing. If you are following https://github.com/shahbaz17/magic-php-rest-api

php -S localhost:8002 -t public
Enter fullscreen mode Exit fullscreen mode

Or, get it from https://b8e51.csb.app/

Using your API

Postman

GET /post

Magic GET /post

GET /post/{id}

Magic GET /post/{id}

POST /post

  • Post Bearer Token Magic Post Bearer Token
  • Post Body Magic Post Body
  • Post Success Magic Post Success
  • Post Error: DID Token malformed or missing Magic Post Error: DID Token malformed or missing

PUT /post/{id}

  • Post to be updated. Post
  • Un-Authorized Update to Post Un-Authorized Update to Post
  • UPDATE Success UPDATE Success
  • Post after Update Post after update

DELETE /post/{id}

  • Un-Auth DELETE
    Un-Auth DELETE

  • DELETE Success
    DELETE Success

Done

Congratulations!! You have successfully secured your PHP REST API with Magic.

Complete Code

GitHub logo shahbaz17 / magic-php-rest-api

Secure your PHP Rest API with Magic.

Secure your PHP Rest API with Magic!

This is a simple PHP REST API protected with Magic.

We will be using Magic Admin PHP SDK in this sample code to protect the PHP REST API.

The Magic Admin PHP SDK provides convenient ways for developers to interact with Magic API endpoints and an array of utilities to handle DID Token.

Please read https://dev.to/shahbaz17/secure-your-php-rest-api-with-magic-82k to learn more about securing the PHP REST API with Magic.

Prerequisites

Getting Started

Clone this project with the following commands:

git clone https://github.com/shahbaz17/magic-php-rest-api.git
cd magic-php-rest-api
Enter fullscreen mode Exit fullscreen mode

Configure the application

Create the database and user for the project.

mysql -u root -p
CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password'
GRANT ALL on blog.* to 'rest_api_user'@'localhost'
quit
Enter fullscreen mode Exit fullscreen mode

Create the post table.

mysql -u rest_api_user -p
// Enter
Enter fullscreen mode Exit fullscreen mode

What's Next?

Now, as you have secured your PHP REST API with Magic. Let's deploy it on Heroku and show the world how awesome REST API you have built which is protected with the best Authentication Layer provided by Magic with just a few lines of modification to a core PHP application.

In my next article, I will cover how you can deploy your REST API application to Heroku.

Discussion (3)

pic
Editor guide
Collapse
0yorick0 profile image
Yorick

Thank you for this great tutorial.

I have one question though. It's about the DID token that you use with Postman to test your API. I've spend a lot of time reading the Magic doc and exploring the dashboard, but it seems to me that at this time there's no way to get a fake DID token from the dashboard (I mean, like I was a simple user).

The only way seems to build a client-side app and use it to test our API. Am I wright ?

If this is the case, you may want to mention it in your tutorial, to avoid us to look for something that doesn't exists^^

And if I'm wrong, could you please show me where to find on the dashboard this fake DID token, so that I can test the API ?

Thank you in advance :)

Collapse
shahbaz17 profile image
Mohammad Shahbaz Alam Author

Hello Yorick,

Thanks for asking this question.

As of now, there is no fake DID token. One has to build a frontend application to get a DID token to test.

For now, run:
php -S localhost:8002 -t public

If you are following github.com/shahbaz17/magic-php-res...

This will start frontend application, where you will get the DID token.

Thank you for mentioning, I will add this line to the post.

Collapse
shahbaz17 profile image
Mohammad Shahbaz Alam Author

Or get a DID token from b8e51.csb.app/ for testing.