You have probably heard of JSON web token (JWT), and wonder how to implement it into your project. Coming from the old-school PHP session, I was in the same boat, struggling to glue JWT into PHP projects… So here it is, a sharing of my working JWT experiment, minus all the confusing security stuff – Read on!
ⓘ I have included a zip file with all the source code at the start of this tutorial, so you don’t have to copy-paste everything… Or if you just want to dive straight in.
TABLE OF CONTENTS
DOWNLOAD & NOTES
Firstly, here is the download link to the example code as promised.
QUICK NOTES
- Developed and verified on a WAMP8 server, with Google Chrome.
- You will need to install Compser too.
EXAMPLE CODE DOWNLOAD
Click here to download all the example source code, I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
JWT EXAMPLE WITH PHP MYSQL
All right, let us now get into the step-by-step example of using JWT in PHP and MYSQL.
STEP 1) USER DATABASE
CREATE TABLE `users` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `email` (`email`),
ADD KEY `name` (`name`);
ALTER TABLE `users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=1;
INSERT INTO `users` (`id`, `name`, `email`, `password`) VALUES
(1, 'Jon Doe', 'jon@doe.com', '$2y$10$5S0BORM0dC/pVrddltxbg.Fa5EBa5zZDXxNhL5Jt57bCi1aFZpcee');x
First, we are going to need a users database. This is a simple one with only 4 fields:
id
User ID, primary key.name
User’s name.email
User’s email, unique to prevent duplicates.password
User’s password.
The dummy user above is jon@doe.com
, password is 123456
.
STEP 2) CONFIG FILE
<?php
// KEEP CONFIG FILE IN A SECURE FOLDER!
// CHANGE ALL SETTINGS TO YOUR OWN!
// (A) DATABASE SETTINGS
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8");
define("DB_USER", "root");
define("DB_PASSWORD", "");
// (B) JWT STUFF
define("JWT_SECRET", "gHfKxh%zjqC7ZMKAcY@B(fC(aC0Opv9Q");
define("JWT_ISSUER", "YOUR-NAME");
define("JWT_AUD", "site.com");
define("JWT_ALGO", "HS512");
Next, we have a config file to keep the database and JWT settings. Captain Obvious to the rescue, change the database settings to your own, generate your own “very secure secret key”, and DON’T ever disclose it.
P.S. In your project, keep the config in a secure folder. Preferably somewhere not in the public HTTP.
STEP 3) USER LIBRARY
<?php
class User {
// (A) CONNECT TO DATABASE
public $error = "";
private $pdo = null;
private $stmt = null;
function __construct () {
try {
$this->pdo = new PDO(
"mysql:host=".DB_HOST.";dbname=".DB_NAME.";charset=".DB_CHARSET,
DB_USER, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
} catch (Exception $ex) { exit($ex->getMessage()); }
}
// (B) CLOSE CONNECTION
function __destruct () {
if ($this->stmt!==null) { $this->stmt = null; }
if ($this->pdo!==null) { $this->pdo = null; }
}
// (C) RUN SQL QUERY
function query ($sql, $data=null) {
try {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
return true;
} catch (Exception $ex) {
$this->error = $ex->getMessage();
return false;
}
}
// (D) SAVE USER
function save ($name, $email, $password, $id=null) {
if ($id===null) {
$sql = "INSERT INTO `users` (`name`, `email`, `password`) VALUES (?,?,?)";
$data = [$name, $email, password_hash($password, PASSWORD_DEFAULT)];
} else {
$sql = "UPDATE `users` SET `name`=?, `email`=? WHERE `id`=?";
$data = [$name, $email, $id];
}
return $this->query($sql, $data);
}
// (E) GET USER
function get ($id) {
$this->query(sprintf("SELECT * FROM `users` WHERE `%s`=?",
is_numeric($id) ? "id" : "email"
), [$id]);
return $this->stmt->fetch();
}
// (F) VERIFY USER
function verify ($email, $password) {
// (F1) GET USER
$user = $this->get($email);
$valid = is_array($user);
// (F2) CHECK PASSWORD
if ($valid) {
$valid = password_verify($password, $user["password"]);
}
// (F3) RETURN RESULT (FALSE IF INVALID, USER ARRAY IF VALID)
if ($valid) { return $user; }
else {
$this->error = "Invalid user/password";
return false;
}
}
}
The user library looks complicated, but keep calm and look closely.
- (A & B) When a
new User()
is created, the constructor connects to the database. The destructor closes the connection. - (C)
query()
A helper function to run a SQL query. - (D)
save()
Add or update a user. - (E)
get()
Get a user by ID or email. - (F)
verify()
Verifies the given email and password. Returns the user data in an array if valid,false
if invalid.
STEP 4) DOWNLOAD PHP JWT LIBRARY
With the user database and library in place, the next step is to deal with the login itself. But before that, we need to download the PHP-JWT library.
- Open the command line or terminal.
- Navigate to the project folder.
- Run
composer require firebase/php-jwt
.
That’s all. Composer will automatically download PHP-JWT into the vendor/
folder.
STEP 5) LOGIN VERIFICATION
<?php
// (A) NO EMAIL/PASSWORD SENT
if (!isset($_POST["email"]) || !isset($_POST["password"])) { exit("NO"); }
// (B) VALIDATE USER
require "2-config.php";
require "3-lib-user.php";
$USER = new User();
$user = $USER->verify($_POST["email"], $_POST["password"]);
if ($user===false) { exit("NO"); }
// (C) GENERATE JWT TOKEN
require "vendor/autoload.php";
use Firebase\JWT\JWT;
$now = strtotime("now");
echo JWT::encode([
"iat" => $now, // ISSUED AT - TIME WHEN TOKEN IS GENERATED
"nbf" => $now, // NOT BEFORE - WHEN THIS TOKEN IS CONSIDERED VALID
"exp" => $now + 3600, // EXPIRY - 1 HR (3600 SECS) FROM NOW IN THIS EXAMPLE
"jti" => base64_encode(random_bytes(16)), // JSON TOKEN ID
"iss" => JWT_ISSUER, // ISSUER
"aud" => JWT_AUD, // AUDIENCE
// WHATEVER USER DATA YOU WANT TO ADD
"data" => [
"id" => $user["id"],
"name" => $user["name"],
"email" => $user["email"]
]
], JWT_SECRET, JWT_ALGO);
Yep, this pretty much deals with the login sequence. Just POST the email
and password
to this script, it will generate a JWT upon validation. A few quick notes on the JWT “properties” (they call it “claims”):
iat
“Issued at”. When this token is generated.nbf
“Not before”. When this token is effective. Yes, we can issue a token now, but set it to be effective only in the future.exp
“Expiry”. Self-explanatory.jti
“JSON Token ID”. The random token.iss
“Issuer”. Who issued this token. Commonly the domain or company name.aud
“Audience”. Which domain is this token meant for. I.E. If the audience is set forsite-a.com
, the token is only valid onsite-a.com
.data
Whatever else data that you want to add.
That about covers the common ones, but there is a crazy long list of JWT claims – Check out the official IANA website to see all of them.
STEP 6) LOGIN PAGE
THE HTML
<form id="loginForm" onsubmit="return login()">
<h1>LOGIN</h1>
<input type="email" placeholder="Email" name="email" required value="jon@doe.com"/>
<input type="password" placeholder="Password" name="password" required value="123456"/>
<input type="submit" value="Sign In"/>
</form>
Don’t think this needs any explanation.
THE JAVASCRIPT
function login () {
// (A) LOGIN FORM
var data = new FormData(document.getElementById("loginForm"));
// (B) AJAX FETCH
fetch("5-verify.php", { method:"POST", body:data })
.then((res) => {
if (res.status==200) { return res.text(); }
else { alert(`Server error - ${res.status}`); }
})
.then((jwt) => {
if (jwt=="NO") { alert("Invalid user/password"); }
else {
// YOU CAN STORE THE TOKEN IN LOCALSTORAGE
localStorage.setItem("jwt", jwt);
// IN A COOKIE
let expire = new Date();
expire.setTime(expire.getTime() + (3600000)); // 1 HR FROM NOW
document.cookie = `jwt=${jwt};expires=${expire.toUTCString()}`;
/* IN INDEXED DATABSE
IDB.transaction("Settings", "readwrite")
.objectStore("Settings")
.add({"jwt":jwt});*/
/* OR EVEN IN STORAGE CACHE
var jwtBlob = new Blob([jwt], {type: "text/plain"});
var urlBlob = URL.createObjectURL(jwtBlob);
fetch(urlBlob).then((res) => {
caches.open("NAME").then((cache) => {
cache.put("jwt.txt", res);
URL.revokeObjectURL(urlBlob);
});
});*/
alert("Good to go!");
}
});
return false;
}
Now, here comes the confusing part. After user authentication, the above PHP script will return the JWT – It is literally up to you to decide where to store it.
- Store the JWT into
localStorage
. - Store the JWT into a
cookie
. - Create an indexed database and store it inside.
- Or the funky way to create a file in the storage cache.
- Developing a mobile or desktop app? Just create a file and store it inside.
For this example, we will keep it in both the localStorage
and cookie
. Probably not a good idea to have too many copies, but this is just for the sake of demonstration…
STEP 7) PAGE VALIDATION
<?php
// (A) JWT COOKIE NOT SET!
if (!isset($_COOKIE["jwt"])) { exit("NO"); }
// (B) DECODE TOKEN
require "2-config.php";
require "vendor/autoload.php";
use Firebase\JWT\JWT;
try { $jwt = JWT::decode($_COOKIE["jwt"], JWT_SECRET, [JWT_ALGO]); }
catch (Exception $e) { exit("NO"); }
// (C) JWT VALIDATION
// ADD MORE OF YOUR OWN CHECKS HERE IF YOU WANT
// E.G. VALID USER IN DATABASE, YOUR OWN SECURITY HASH, ETC...
$now = strtotime("now");
if ($jwt->iss !== JWT_ISSUER ||
$jwt->nbf > $now || $jwt->exp < $now) { exit("NO"); }
// (D) SHOW THE PAGE ?>
<!DOCTYPE html>
<html>
<head>
<title>It Works!</title>
</head>
<body>Yay!</body>
</html>
Now that the JWT is set in the cookie, we only have to verify it before displaying any protected page. Redirect the user back to the login page if the token is invalid.
STEP 8) API VALIDATION
THE TEST PAGE
<!-- (A) TEST BUTTON -->
<input type="button" value="Test" onclick="test()"/>
<script>
// (B) API REQUEST
function test () {
// (B1) YOU CAN APPEND JWT AS POST DATA
let jwt = localStorage.getItem("jwt"),
data = new FormData();
data.append("KEY", "VALUE");
data.append("jwt", jwt);
// (B2) OR ATTACH IN HTTP AUTH HEADER
fetch("8b-api.php", {
method : "POST",
body : data,
headers: {"Authorization": `Bearer ${jwt}`}
})
.then(res => res.json())
.then((res) => { console.log(res); });
}
</script>
Lastly, how about API calls? It’s the same story again – Up to you to decide.
- Include the JWT in the
POST
. - Attach it in the
COOKIE
. - Or put it into the HTTP
Authorization
header.
PHP API HANDLER
// (A) GET JWT FROM POST
// $jwt = $_POST["jwt"];
// (B) GET JWT FROM COOKIE
// $jwt = $_COOKIE["jwt"];
// (C) GET JWT FROM AUTH HEADER
$jwt = substr(getallheaders()["Authorization"], 7);
// DO THE SAME CHECKS AS 7-PAGES.PHP
// THEN PROCEED WITH API PROCESSING
echo json_encode([
"status" => 1,
"message" => "OK"
]);
Then on the server-side, do the same verification again.
USEFUL BITS & LINKS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
SECURITY NOTES
I have read that keeping the JWT in the localStorage
or cookie
is not safe, but some people just shrug – Where else are you going to keep it then!? Write down on a piece of paper? The safer way seems to be an HTTP Only Cookie, not invulnerable, but at least safer. I am not a security expert by any means, so dear experts – Please share some of your insights with us.
P.S. Also, enforce the use of https://
in your project.
LINKS & REFERENCES
- PHP-JWT – GitHub
- jwt.io
- Set Cookie – PHP
THE END
Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!
Really good example. Thanks.