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:
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.
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.
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:
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 theposts
table.GET /post/{id}
: Available for un-authenticated users. Display a single post from theposts
table.POST /post
: Available for authenticated users. Create a post and insert it into theposts
table.PUT /post/{id}
: Available for authenticated users. Update the post in theposts
table. Also, it ensures a user cannot update someone else's post.DELETE /post/{id}
: Available for authenticated users. Delete a post from theposts
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 frompost
table.GET /post/{id}
: Display a single post frompost
table.POST /post
: Create a post and insert intopost
table.PUT /post/{id}
: Update the post inpost
table.DELETE /post/{id}
: Delete a post frompost
table.
#Configure the Database for your PHP REST API
Create a new database and user for your app:
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.
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.
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.
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.
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.
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 thepost
table.GET /post/{id}
: Displays a single post frompost
table.
Available for authenticated
users: Protected with Magic
POST /post
: Creates a post and inserts it into thepost
table.PUT /post/{id}
: Updates the post inpost
table. Also, it ensures a user cannot update someone else's post.DELETE /post/{id}
: Deletes the post frompost
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
Laravel – A tutorial to demonstrate how to add authorization to a Laravel API with Magic's Laravel Plugin.
Next.js (Vercel) – Learn how to use the popular Next.js framework to build a React app and deploy it with Vercel.
#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.