JWT Login & Authentication With PHP MYSQL (Step By Step Example)

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 JWT PHP MYSQL Useful Bits & Links
The End

 

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.
If you spot a bug, feel free to comment below. I try to answer short questions too, but it is one person versus the entire world… If you need answers urgently, please check out my list of websites to get help with programming.

 

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

1-users.sql
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

2-config.php
<?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

3-lib-user.php
<?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

5-verify.php
<?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 for site-a.com, the token is only valid on site-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

6a-login.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

6b-login.js
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

7-pages.php
<?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

8a-api.html
<!-- (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

8b-api.php
// (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

 

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!

2 thoughts on “JWT Login & Authentication With PHP MYSQL (Step By Step Example)”

Leave a Comment

Your email address will not be published.