Guide

How to secure PHP REST API with Magic

How to secure PHP REST API with Magic

11 January 2021

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 tutorial shows how to protect PHP REST API endpoints with Magic. We will be building a RESTful API (Post API) where authenticated users can perform certain actions that unauthenticated users can’t.

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.

Prerequisites

Live Demo

Frontend: Visit https://0720t.csb.app to get a DIDT for testing the REST API with Postman.

Backend: Make a GET request to https://magic-php.herokuapp.com to get the list of Posts.

GET is un-protected and doesn't require any DIDT, for POST, PUT, and DELETE you have to use Postman and pass the DIDT received above as Bearer + DIDT in the Authorization header.

Quick Start

Get the code

Clone this project with the following commands:

git clone https://github.com/shahbaz17/magic-php-rest-api.git
cd magic-php-rest-api

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

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`)
);

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

cp .env.example .env

.env

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password

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

.env complete

MAGIC_SECRET_KEY=sk_test_01234567890 // Paste SECRET KEY
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password

Development

Install the project dependencies and start the PHP server:

composer install
php -S localhost:8008 -t api

Start the Frontend Application:

php -S localhost:8002 -t public

Visit http://localhost:8002 to get the token for testing with Postman.

Endpoints Protected by Magic

  • GET /post: Available for un-authenticated users. Display all the posts from posts table.
  • GET /post/{id}: Available for un-authenticated users. Display a single post from posts table.
  • POST /post: Available for authenticated users. Create a post and insert into posts table.
  • PUT /post/{id}: Available for authenticated users. Update the post in posts table. Also, ensures a user cannot update someone else's post.
  • DELETE /post/{id}: Available for authenticated users. Delete a post from posts table. Also, ensures a user cannot delete someone else's post.

Using API

Postman

Getting Started

What is a REST API?

“REpresentational State Transfer (REST) is a software architectural style that defines a set of constraints to be used for creating Web services. Web services that conform to the REST architectural style, called RESTful Web services, provide interoperability between computer systems on the internet. RESTful Web services allow the requesting systems to access and manipulate textual representations of Web resources by using a uniform and predefined set of stateless operations. Other kinds of Web services, such as SOAP Web services, expose their own arbitrary sets of operations.” - Wikipedia

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

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

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

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`)
);

Add the database connection variables to your .env file:

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password

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

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

composer require magiclabs/magic-admin-php

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');

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.

MAGIC_SECRET_KEY={SECRET Key}
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

For added security, in Magic's dashboard settings (https://dashboard.magic.link), you can specify the URLs that are allowed to use your live API keys. Test API keys are always allowed to be used on localhost, however it will block your Live API keys from working anywhere except the URLs specifically added to your allow list.

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;
    }
}

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'));

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']);
Authorization: Bearer <auth token>

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;
}

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;
}

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);

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);

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);

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;
    }

}

Get Author's email

$author = $this->getEmail();

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',

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;
    }
}

Protect unauthorize update

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

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;
}

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;
    }

}

Protect unauthorize delete

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

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;
}

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

Run Server:

php -S localhost:8000 -t api

Start the Frontend Application:

php -S localhost:8002 -t public

Visit http://localhost:8002 to get the token for testing with Postman.

Using your API with 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.

Get the Complete Code

https://github.com/shahbaz17/magic-php-rest-api

What's Next?

Learn about our Laravel SDK

The Magic Laravel SDK makes it easy to leverage Decentralized ID Tokens to authenticate your users for your app. This guide will cover some important topics for getting started with server-side APIs and to make the most of Magic's features.

Use Magic with existing tools

Customize your Magic flow

You can customize the login experience using your own UI instead of Magic's default one and/or customize the magic link email with your brand. Learn how to customize.

Let's make some magic!