Guides
Guide

How to secure PHP REST API with Magic

How to secure PHP REST API with Magic

11 January 2021
Banner for How to secure PHP REST API with Magic

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:

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 `rest_api_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 First Application automatically configured for you, or create a new one from your Dashboard.

Magic Dashboard

.env complete

MAGIC_SECRET_KEY=sk_live_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:8000 -t api

Start the Frontend Application:

$ git clone https://github.com/shahbaz17/magic-didtoken $ cd magic-didtoken $ cp .env.example .env # enter your Magic API keys in your .env file $ yarn install $ yarn dev # 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.

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

Magic Dashboard for SECRET KEY

Update .env

Now, Add MAGIC_SECRET_KEY variable to the .env file.

MAGIC_SECRET_KEY=sk_live_************ DB_HOST=localhost DB_PORT=3306 DB_DATABASE=blog DB_USERNAME=rest_api_user DB_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.

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 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 it in your code, grabbing <auth token> from HTTP Header Request.

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

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

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

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

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

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 authenticated user's email address 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 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(); } } 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(); } }

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 authorized to delete this post.' ]); return $response; }

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.

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

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 authorized 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 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:

composer install

Run Server:

php -S localhost:8000 -t api

Start the Frontend Application to get the DID Token:

$ git clone https://github.com/shahbaz17/magic-didtoken $ cd magic-didtoken $ cp .env.example .env # enter your Magic API keys in your .env file $ yarn install $ yarn dev # 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

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 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!