Simple One Time Password (OTP) With PHP MySQL – Free Source Code

Welcome to a quick tutorial on how to create a one-time password (OTP) in PHP and MySQL. Need some extra security for resetting a user account, confirming an email address, payments, or securing transactions?

Creating and verifying an OTP in PHP is actually a straightforward 2-steps process:

  1. Generate a random one-time password on request. Save it into the database, and send it to the user via email or SMS.
  2. The user accesses a challenge page and enters the OTP. We verify if the given OTP is valid – Proceed with the transaction if it is valid.

But just how exactly are one-time passwords done in PHP? Let us walk you through an example in this guide – 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 OTP Database PHP Core Library
Request & Verify Useful Bits & Links The End

 

DOWNLOAD & NOTES

Firstly, here is the download link to the example code as promised.

 

QUICK NOTES

  • Create a test database and import the 1-database.sql file.
  • Change the database settings in 2-otp.php to your own, you may also want to fill up all the @TODO gaps too.
  • Launch 3a-request.php to generate an OTP request, 3b-challenge.php for the challenge.
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 the 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.

 

 

PART 1) OTP DATABASE

All right, let us now start by creating a simple database table to store the generated OTP, timestamp, and stuff.

 

OTP TABLE

1-database.sql
CREATE TABLE `otp` (
  `user_email` varchar(255) NOT NULL,
  `otp_pass` varchar(255) NOT NULL,
  `otp_timestamp` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `otp_tries` tinyint(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
 
ALTER TABLE `otp`
  ADD PRIMARY KEY (`user_email`);
Field Description
user_email The primary key. This can actually be the user ID, transaction ID, order ID – Whatever you want to track.
otp_pass The one-time password.
otp_timestamp Time at which the OTP is generated, used to calculate the expiry time.
otp_tries The number of challenge attempts. Used to stop brute force attacks.

That is the gist of it, please feel free to make changes to fit your own project.

 

 

PART 2) PHP LIBRARY FILE

Next, we will need to create a PHP library to deal with the OTP processes.

 

PHP OTP LIBRARY

2-otp.php
<?php
class OTP {
  // (A) CONSTRUCTOR - CONNECT TO DATABASE
  protected $pdo = null;
  protected $stmt = null;
  public $error = "";
  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) DESTRUCTOR - CLOSE CONNECTION
  function __destruct() {
    if ($this->stmt !== null) { $this->stmt = null; }
    if ($this->pdo !== null) { $this->pdo = null; }
  }

  // (C) GENERATE OTP
  function generate ($email) {
    // @TODO - 
    // YOU SHOULD CHECK IF THE PROVIDED EMAIL IS A VALID USER IN YOUR SYSTEM
    
    // (C1) CHECK IF USER ALREADY HAS EXISTING OTP REQUEST
    $this->stmt = $this->pdo->prepare(
      "SELECT * FROM `otp` WHERE `user_email`=?"
    );
    $this->stmt->execute([$email]);
    $otp = $this->stmt->fetch();

    // (C2) ALREADY HAS OTP REQUEST
    if (is_array($otp)) {
      // @TODO - 
      // ADD YOUR OWN RULES HERE - ALLOW NEW REQUEST IF EXIPRED?
      // $validTill = strtotime($otp["otp_timestamp"]) + (OTP_VALID * 60);
      // if (strtotime("now") > $validTill) { DELETE OLD REQUEST }
      // else { ERROR }
      $this->error = "You already have a pending OTP.";
      return false;
    }

    // (C3) CREATE RANDOM PASSWORD
    $alphabets = "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789";
    $count = strlen($alphabets) - 1;
    $pass = "";
    for ($i=0; $i<OTP_LEN; $i++) { $pass .= $alphabets[rand(0, $count)]; } 
 
    // (C4) DATABASE ENTRY 
    try { 
      $this->stmt = $this->pdo->prepare(
        "REPLACE INTO `otp` (`user_email`, `otp_pass`) VALUES (?,?)"
      );
      $this->stmt->execute([$email, password_hash($pass, PASSWORD_DEFAULT)]);
    } catch (Exception $ex) {
      $this->error = $ex->getMessage();
      return false;
    }

    // (C5) SEND VIA EMAIL
    // @TODO - FORMAT YOUR OWN "NICE" EMAIL OR SEND VIA SMS.
    $mailSubject = "Your OTP";
    $mailBody = "Your OTP is $pass. Enter it at 3b-challenge.php within 15 minutes.";
    if (@mail($email, $mailSubject, $mailBody)) { 
      return true; 
    } else {
      $this->error = "Failed to send OTP email.";
      return false;
    }
  }

  // (D) CHALLENGE OTP
  function challenge ($email, $pass) {
    // (D1) GET THE OTP ENTRY
    $this->stmt = $this->pdo->prepare(
      "SELECT * FROM `otp` WHERE `user_email`=?"
    );
    $this->stmt->execute([$email]);
    $otp = $this->stmt->fetch();

    // (D2) OTP ENTRY NOT FOUND
    if (!is_array($otp)) {
      $this->error = "The specified OTP request is not found.";
      return false;
    }

    // (D3) TOO MANY TRIES
    if ($otp["otp_tries"] >= OTP_TRIES) {
      $this->error = "Too many tries for OTP.";
      return false;
    }

    // (D4) EXPIRED
    $validTill = strtotime($otp["otp_timestamp"]) + (OTP_VALID * 60);
    if (strtotime("now") > $validTill) {
      $this->error = "OTP has expired.";
      return false;
    }

    // (D5) INCORRECT PASSWORD - ADD STRIKE
    if (!password_verify($pass, $otp["otp_pass"])) {
      $strikes = $otp["otp_tries"] + 1;
      $this->stmt = $this->pdo->prepare(
        "UPDATE `otp` SET `otp_tries`=? WHERE `user_email`=?"
      );
      $this->stmt->execute([$strikes, $email]);

      // @TODO - TOO MANY STRIKES 
      // LOCK ACCOUNT? REQUIRE MANUAL VERIFICATION? SUSPEND FOR 24 HOURS?
      // if ($strikes >= OTP_TRIES) { DO SOMETHING }

      $this->error = "Incorrect OTP.";
      return false;
    }

    // (D6) ALL OK - DELETE OTP
    $this->stmt = $this->pdo->prepare(
      "DELETE FROM `otp` WHERE `user_email`=?"
    );
    $this->stmt->execute([$email]);
    return true;
  }
}

// (E) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8");
define("DB_USER", "root");
define("DB_PASSWORD", "");

// (F) ONE-TIME PASSWORD SETTINGS
define("OTP_VALID", "15"); // VALID FOR X MINUTES
define("OTP_TRIES", "3"); // MAX TRIES
define("OTP_LEN", "8"); // PASSWORD LENGTH

// (G) NEW OTP OBJECT
$_OTP = new OTP();

 

 

THE EXPLANATION

Yikes! This library looks complicated at first, but keep calm and look carefully.

  • (A & B) When $_OTP = new OTP() is created, the constructor will automatically connect to the database; The destructor will disconnect when done.
  • (C & D) There are only 2 functions! Not going to explain line-by-line, but in general:
    • function generate() is step 1 of the process. This generates a random OTP, saves it into the database, and sends it to the users via email.
    • function verify() is step 2 of the process. Basically, a lot of checks, and verify a given password against the OTP in the database.
  • (E to G) Self-explanatory…

That’s all to the “complicated code”.

 

PART 3) OTP REQUEST & VERIFICATION PAGES

With the foundations now established, the last step is to use the OTP library in your own project. As everyone has a different use for OTP, this will only be a raw basic “request and verify” demo pages.

 

REQUEST FOR OTP

3a-request.php
<!-- (A) OTP REQUEST FORM -->
<h1>OTP REQUEST</h1>
<form method="post" target="_self">
  <input type="email" name="email" required value="john@doe.com"/><br>
  <input type="submit" value="Go"/>
</form>
 
<?php
// (B) PROCESS OTP REQUEST
if (isset($_POST["email"])) {
  require "2-otp.php";
  $pass = $_OTP->generate($_POST["email"]);
  echo $pass ? "<div>OTP SENT.</div>" : "<div>".$_OTP->error."</div>" ;
}
?>

That’s right, just use $_OTP->generate(EMAIL) to create an OTP for the user. This will send the OTP and link via email (or you can use an SMS gateway in your own project).

 

OTP CHALLENGE PAGE

3b-challenge.php
<!-- (A) OTP CHALLENGE FORM -->
<h1>OTP CHALLENGE</h1>
<form method="post" target="_self">
  <input type="email" name="email" required value="john@doe.com"/><br>
  <input type="text" name="otp" required/><br>
  <input type="submit" value="Go"/>
</form>
 
<?php
// (B) PROCESS OTP CHALLENGE
if (isset($_POST["email"])) {
  require "2-otp.php";
  $pass = $_OTP->challenge($_POST["email"], $_POST["otp"]);
  echo $pass ? "<div>OTP VERIFIED.</div>" : "<div>".$_OTP->error."</div>" ;
}
?>

This shouldn’t be a mystery anymore – We use $_OTP->challenge(EMAIL, OTP) to verify the entered password against the database.

 

 

USEFUL BITS & LINKS

That’s all for this guide, and here is a small section on some extras and links that may be useful to you.

 

LINKS & REFERENCES

Need even more security? Consider the following.

 

THE END

Thank you for reading, and we have come to the end of this guide. I hope that it has helped you with your project, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!

5 thoughts on “Simple One Time Password (OTP) With PHP MySQL – Free Source Code”

  1. EDIT – SUMMARY
    This miserable twat copy-and-pasted code from above, then insisted “your code has errors” and “my Dreamweaver is not broken”. Could not even give a single runtime error message, don’t even know what “runtime” means. Took offense and resolved to lowly personal attacks when proven wrong.

    Wake up your bl**dy idea – Dreamweaver only supports up to PHP 7.1 at the time of writing. That is already way past its end of life. Go figure it out yourself.

    https://code-boxx.com/faq/#nobad
    https://code-boxx.com/faq/#nolike
    https://code-boxx.com/faq/#badatti

  2. Hi, This post was very useful
    Please can you post some thing similar but with a file that lets the otp be sent to a client email
    For example, a client signs up on my website as a user and next time he wants to login he is requested to check is email for an OTP before he can proceed.. i did try but i keep getting an error saying email not found on database… when i make a test run. Please help
    Thank you

Leave a Comment

Your email address will not be published.