guides
Guide

How to secure PHP REST API with Magic

Magic Staff · January 11, 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 specific 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 specific 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

#Quick Start

#Get the code

Clone this project with the following commands:

Bash
01
02git clone https://github.com/shahbaz17/magic-php-rest-api.git
03cd magic-php-rest-api

#Configure the application

Create the database and user for the project.

Bash
01
02mysql -u root -p
03CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
04CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
05GRANT ALL on blog.* to 'rest_api_user'@'localhost';
06quit

Create the post table.

Bash
01
02mysql -u rest_api_user -p;
03// Enter your password `rest_api_password`
04use blog;
05
06CREATE TABLE `post` (
07  `id` int(11) NOT NULL AUTO_INCREMENT,
08  `title` varchar(255) NOT NULL,
09  `body` text NOT NULL,
10  `author` varchar(255),
11  `author_picture` varchar(255),
12  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
13  PRIMARY KEY (`id`)
14);

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

01cp .env.example .env

.env

01DB_HOST=localhost
02DB_PORT=3306
03DB_DATABASE=blog
04DB_USERNAME=rest_api_user
05DB_PASSWORD=rest_api_password

#Get your Magic Secret Key

Sign Up with Magic and get your MAGIC_SECRET_KEY.

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

.env complete

01MAGIC_SECRET_KEY=sk_live_01234567890 // Paste SECRET KEY
02DB_HOST=localhost
03DB_PORT=3306
04DB_DATABASE=blog
05DB_USERNAME=rest_api_user
06DB_PASSWORD=rest_api_password

#Development

Install the project dependencies and start the PHP server:

01composer install
01php -S localhost:8000 -t api

Start the Frontend Application:

Bash
01
02$ git clone https://github.com/shahbaz17/magic-didtoken
03$ cd magic-didtoken
04$ cp .env.example .env
05# enter your Magic API keys in your .env file
06$ yarn install
07$ yarn dev
08# starts app at http://localhost:3000

Visit http://localhost:3000 to get the DID Token for testing with Postman.

#Endpoints Protected by Magic

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

#Using API

Use Postman to test your API routes.

#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 add some Magic touches to it.

01git 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:

Bash
01
02mysql -u root -p
03
04CREATE DATABASE blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
05
06CREATE USER 'rest_api_user'@'localhost' identified by 'rest_api_password';
07
08GRANT ALL on blog.* to 'rest_api_user'@'localhost';
09
10quit

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.

Bash
01
02mysql -u rest_api_user -p;
03// Enter your password
04use blog;
05
06CREATE TABLE `post` (
07  `id` int(11) NOT NULL AUTO_INCREMENT,
08  `title` varchar(255) NOT NULL,
09  `body` text NOT NULL,
10  `author` varchar(255),
11  `author_picture` varchar(255),
12  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
13  PRIMARY KEY (`id`)
14);

Add the database connection variables to your .env file:

01DB_HOST=localhost
02DB_PORT=3306
03DB_DATABASE=blog
04DB_USERNAME=rest_api_user
05DB_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:

01brew install composer

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

01composer 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.

01require_once('/path/to/magic-admin-php/init.php');

Installation Dependency

The bindings require the following extensions 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 for a free Magic account and get your MAGIC_SECRET_KEY.

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

Update .env

Now, Add MAGIC_SECRET_KEY variable to the .env file.

01MAGIC_SECRET_KEY=sk_live_************
02DB_HOST=localhost
03DB_PORT=3306
04DB_DATABASE=blog
05DB_USERNAME=rest_api_user
06DB_PASSWORD=rest_api_password

For added security, in Magic's dashboard settings (https://dashboard.magic.link), you can specify the URLs that are allowed to use your API keys. It will block your API keys from working anywhere except the URLs specifically added to your allowed list.

#Add Magic to Post.php

Open src\Post.php in your favorite 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 metadata using the issuer. It also retrieves the token from the HTTP Header.

PHP
01
02public function getEmail() {
03    $did_token = \MagicAdmin\Util\Http::parse_authorization_header_value(
04    getallheaders()['Authorization']
05    );
06
07    // DIDT is missing from the original HTTP request header. Returns 404: DID Missing
08    if ($did_token == null) {
09    return $this->didMissing();
10    }
11
12    $magic = new \MagicAdmin\Magic(getenv('MAGIC_SECRET_KEY'));
13
14    try {
15    $magic->token->validate($did_token);
16    $issuer = $magic->token->get_issuer($did_token);
17    $user_meta = $magic->user->get_metadata_by_issuer($issuer);
18    return $user_meta->data->email;
19    } catch (\MagicAdmin\Exception\DIDTokenException $e) {
20    // DIDT is malformed.
21    return $this->didMissing();
22    }
23}

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

#Instantiate Magic

01$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

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

Include the above code in your existing code. If you're using it in your code, grabbing <auth token> from HTTP Header Request.

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

01if ($did_token == null) {
02    return $this->didMissing();
03}

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

#didMissing()

01private function didMissing() {
02    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
03    $response['body'] = json_encode([
04      'error' => 'DID is Malformed or Missing.'
05  ]);
06    return $response;
07}

#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 the Frontend Application.

01$magic->token->validate($did_token);

It would be best if you always validated 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

01$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

01$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, using their email as the author field of the post.

PHP
01
02private function createPost() {
03    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
04    if (! $this->validatePost($input)) {
05        return $this->unprocessableEntityResponse();
06    }
07
08    $query = "
09        INSERT INTO posts
10            (title, body, author, author_picture)
11        VALUES
12            (:title, :body, :author, :author_picture);
13      ";
14
15    $author = $this->getEmail();
16
17    if(is_string($author)) {
18        try {
19          $statement = $this->db->prepare($query);
20          $statement->execute(array(
21            'title' => $input['title'],
22            'body'  => $input['body'],
23            'author' => $author,
24            'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($author)).'.png?s=200',
25          ));
26          $statement->rowCount();
27        } catch (\PDOException $e) {
28            exit($e->getMessage());
29        }
30
31        $response['status_code_header'] = 'HTTP/1.1 201 Created';
32        $response['body'] = json_encode(array('message' => 'Post Created'));
33        return $response;
34    } else {
35      return $this->didMissing();
36    }
37
38}

#Get Author's email

01$author = $this->getEmail();

It returns the email id of the authenticated user.

#Author's email and picture

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

01'author' => $author,
02'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 update the post is the person who wrote it.

PHP
01
02private function updatePost($id) {
03    $result = $this->find($id);
04    if (! $result) {
05        return $this->notFoundResponse();
06    }
07    $input = (array) json_decode(file_get_contents('php://input'), TRUE);
08    if (! $this->validatePost($input)) {
09        return $this->unprocessableEntityResponse();
10    }
11
12    $author = $this->getEmail();
13
14    $query = "
15        UPDATE posts
16        SET
17            title = :title,
18            body  = :body,
19            author = :author,
20            author_picture = :author_picture
21        WHERE id = :id AND author = :author;
22    ";
23
24    if(is_string($author)) {
25      try {
26          $statement = $this->db->prepare($query);
27          $statement->execute(array(
28              'id' => (int) $id,
29              'title' => $input['title'],
30              'body'  => $input['body'],
31              'author' => $author,
32              'author_picture' => 'https://secure.gravatar.com/avatar/'.md5(strtolower($author)).'.png?s=200',
33          ));
34          if($statement->rowCount()==0) {
35            // Different Author trying to update.
36            return $this->unauthUpdate();
37          }
38      } catch (\PDOException $e) {
39          exit($e->getMessage());
40      }
41      $response['status_code_header'] = 'HTTP/1.1 200 OK';
42      $response['body'] = json_encode(array('message' => 'Post Updated!'));
43      return $response;
44    } else {
45      return $this->didMissing();
46    }
47}

#Protect unauthorize update

01$query = "
02    UPDATE posts
03    SET
04        title = :title,
05        body  = :body,
06        author = :author,
07        author_picture = :author_picture
08    WHERE id = :id AND author = :author;
09";

#unauthUpdate()

01return $this->unauthUpdate();
02// .
03// .
04// .
05// unauthUpdate()
06private function unauthUpdate() {
07  $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
08  $response['body'] = json_encode([
09    'error' => 'You are not authorized to delete this post.'
10  ]);
11  return $response;
12}

#Update deletePost($id)

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

PHP
01
02private function deletePost($id) {
03    $author = $this->getEmail();
04    if(is_string($author)) {
05      $result = $this->find($id);
06      if (! $result) {
07          return $this->notFoundResponse();
08      }
09
10      $query = "
11          DELETE FROM posts
12          WHERE id = :id AND author = :author;
13      ";
14
15      try {
16          $statement = $this->db->prepare($query);
17          $statement->execute(array('id' => $id, 'author' => $author));
18          if($statement->rowCount()==0) {
19            // Different Author trying to delete.
20            return $this->unauthDelete();
21
22          }
23      } catch (\PDOException $e) {
24          exit($e->getMessage());
25      }
26      $response['status_code_header'] = 'HTTP/1.1 200 OK';
27      $response['body'] = json_encode(array('message' => 'Post Deleted!'));
28      return $response;
29    } else {
30      // DID Error.
31      return $this->didMissing();
32    }
33}

#Protect unauthorize delete

01$query = "
02  DELETE FROM posts
03  WHERE id = :id AND author = :author;
04";

#unauthDelete()

01return $this->unauthDelete();
02// .
03// .
04// .
05// unauthDelete()
06private function unauthDelete() {
07    $response['status_code_header'] = 'HTTP/1.1 404 Not Found';
08    $response['body'] = json_encode([
09      'error' => 'You are not authorized to delete this post.'
10  ]);
11    return $response;
12}

Get the completed Post.php from here.

#Endpoints

Available for un-authenticated users:

  • GET /post: Displays all the posts from the 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 it into the post table.
  • PUT /post/{id}: Updates the post in post table. Also, it ensures a user cannot update someone else's post.
  • DELETE /post/{id}: Deletes the post from post table. Also, it 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:

01composer install

Run Server:

01php -S localhost:8000 -t api

Start the Frontend Application to get the DID Token:

01$ git clone https://github.com/shahbaz17/magic-didtoken
02$ cd magic-didtoken
03$ cp .env.example .env
04# enter your Magic API keys in your .env file
05$ yarn install
06$ yarn dev
07# starts app at http://localhost:3000

Visit http://localhost:3000 to get the DID Token for testing with Postman.

#Using your API with Postman

#GET /post

#GET /post/{id}

#POST /post

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

#PUT /post/{id}

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

#DELETE /post/{id}

  • Un-Auth DELETE ⁠
  • 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 making the most out of Magic's features.

#Use Magic with existing tools

#Customize your Magic flow

You can customize the login experience using your 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!