Secure your PHP REST API with Magic
Tags: php, restapi, magic, authentication
Published: 2020-12-22 19:42:17

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](https://dev.to/shahbaz17/build-a-simple-rest-api-in-php-2edl), I would highly recommend you to check that article first if you want to learn how to build a REST API in core PHP.

{% post shahbaz17/build-a-simple-rest-api-in-php-2edl %}

This guide walks you through to protect the [PHP Rest API](https://github.com/shahbaz17/php-rest-api) endpoints with [Magic](https://magic.link/).

## Why Magic?

[Magic](https://magic.link/) 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](https://medium.com/magiclabs/passwords-suck-6524a8e7aeb5).

## Quickstart

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

{% medium https://medium.com/magiclabs/passwords-suck-6524a8e7aeb5 %}

### Prerequisites

- [PHP 5.6.0 and later](https://www.php.net/downloads.php).
- [MySQL](https://www.mysql.com/downloads/)
- [Composer](http://getcomposer.org/)
- [Postman](https://www.postman.com/downloads/)

## Getting Started

### Clone GitHub Repo

Clone the [PHP Rest API](https://github.com/shahbaz17/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.

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

{% github shahbaz17/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

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](#magic-admin-sdk)

We will use MySQL to power our simple API.

Create a new database and user for your app:

```bash
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.

```php
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:

`.env`

```php
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 <a name="magic-admin-sdk"></a>

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](http://getcomposer.org/).

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

```bash
brew install composer
```

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

```bash
composer require magiclabs/magic-admin-php
```

### Manual Installation

If you do not wish to use Composer, you can download the [latest release](https://github.com/magiclabs/magic-admin-php). Then, to use the bindings, include the `init.php` file.

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

##### Installation Dependency

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

- [`curl`](https://secure.php.net/manual/en/book.curl.php)
- [`gmp`](https://www.php.net/manual/en/book.gmp.php)

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](https://dashboard.magic.link/signup) 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](https://dashboard.magic.link/app/all_apps).

![Magic Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yiehdmd2kpokpeom3rqh.png)

#### Update `.env`

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

Updated `.env`

```php
MAGIC_SECRET_KEY=sk_live_********
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=rest_api_user
DB_PASSWORD=rest_api_password
```

## 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.

```php
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();
    }

    $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();
    }
}
```

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](https://github.com/shahbaz17/php-rest-api).

#### Instantiate Magic

```php
$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](https://docs.magic.link/admin-sdk/php/api-reference).

#### Retrieve `<auth token>` from HTTP Header Request

```php
$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](https://docs.magic.link/decentralized-id#what-is-a-did-token).

```php
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()

```php
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](https://docs.magic.link/decentralized-id#what-is-a-did-token) is generated by a Magic user on the client-side which is passed to your server via Frontend Application.

```php
$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

```php
$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

```php
$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](https://docs.magic.link/decentralized-id). 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.

```php
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

```php
$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.

```php
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

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

#### unauthUpdate()

```php
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.

```php
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

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

#### unauthDelete()

```php
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](https://github.com/shahbaz17/magic-php-rest-api/blob/main/src/Post.php).

## 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](https://magic.link)

- `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](https://www.postman.com/downloads/).

Install dependencies:

```bash
composer install
```

Run Server:

```bash
php -S localhost:8000 -t api
```

To get the DID Token, please follow:

{% github shahbaz17/magic-didtoken %}

Or simply deploy your own on Vercel or Netlfiy.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fshahbaz17%2Fmagic-didtoken&env=NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY,MAGIC_SECRET_KEY) [![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/shahbaz17/magic-didtoken)

Frontend would look like https://magic-didtoken.vercel.app 

## Using your API

## Postman

### `GET /post`

![Magic GET /post](https://dev-to-uploads.s3.amazonaws.com/i/dtt8ojavzm5i5rrxybfr.png)

### `GET /post/{id}`

![Magic GET /post/{id}](https://dev-to-uploads.s3.amazonaws.com/i/rqp3d12k78tk505j8f13.png)

### `POST /post`

- Post Bearer Token
  ![Magic Post Bearer Token](https://dev-to-uploads.s3.amazonaws.com/i/yl3yvi7zevkxj1acahpe.png)
- Post Body
  ![Magic Post Body](https://dev-to-uploads.s3.amazonaws.com/i/yh7l1s0nvclj9582u9rv.png)
- Post Success
  ![Magic Post Success](https://dev-to-uploads.s3.amazonaws.com/i/1mmls9avrbtjuitfg66z.png)
- Post Error: DID Token malformed or missing
  ![Magic Post Error: DID Token malformed or missing](https://dev-to-uploads.s3.amazonaws.com/i/bw7p7onuo15twqg9ntab.png)

### `PUT /post/{id}`

- Post to be updated.
  ![Post](https://dev-to-uploads.s3.amazonaws.com/i/rtsjqkwoakj06d3zgavi.png)
- Un-Authorized Update to Post
  ![Un-Authorized Update to Post](https://dev-to-uploads.s3.amazonaws.com/i/6quqdlgsp7pcsgbb2lnn.png)
- UPDATE Success
  ![UPDATE Success](https://dev-to-uploads.s3.amazonaws.com/i/upjgjgsgw2rtcj7qirbp.png)
- Post after Update
  ![Post after update](https://dev-to-uploads.s3.amazonaws.com/i/4xvdkjlznvjkazkljffc.png)

### `DELETE /post/{id}`

- Un-Auth DELETE
  ![Un-Auth DELETE](https://dev-to-uploads.s3.amazonaws.com/i/l1nqbziu6t3ay51kw55f.png)

- DELETE Success
  ![DELETE Success](https://dev-to-uploads.s3.amazonaws.com/i/lty1lk2p4875e8v6y6hx.png)

## Done

Congratulations!! You have successfully secured your PHP REST API with [Magic](https://magic.link).

## Complete Code

{% github shahbaz17/magic-php-rest-api %}

## What's Next?

Now, as you have secured your PHP REST API with Magic. Let's deploy it on [Heroku](https://heroku.com/) and show the world how awesome REST API you have built which is protected with the best Authentication Layer provided by [Magic](https://magic.link) 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](https://heroku.com/).

{% post shahbaz17/deploy-your-php-api-to-heroku-4lkj %}