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 process:
- Generate a random one-time password on request. Save it into the database, and send it to the user via email (or SMS).
- The user accesses a challenge page and enters the OTP. Verify and proceed if the given OTP 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!
TABLE OF CONTENTS
PHP MYSQL ONE-TIME PASSWORD
All right, let us now get into the details of building an OTP system in PHP and MYSQL.
PART 1) DATABASE TABLE
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=utf8mb4;
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 OTP LIBRARY
2A) LIBRARY INIT
<?php
class OTP {
// (A) CONSTRUCTOR - CONNECT TO DATABASE
protected $pdo = null;
protected $stmt = null;
public $error = "";
function __construct() {
$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
]);
}
// (B) DESTRUCTOR - CLOSE CONNECTION
function __destruct() {
if ($this->stmt !== null) { $this->stmt = null; }
if ($this->pdo !== null) { $this->pdo = null; }
}
// (C) HELPER - RUN SQL QUERY
function query ($sql, $data=null) : void {
$this->stmt = $this->pdo->prepare($sql);
$this->stmt->execute($data);
}
// ...
}
// (F) DATABASE SETTINGS - CHANGE TO YOUR OWN!
define("DB_HOST", "localhost");
define("DB_NAME", "test");
define("DB_CHARSET", "utf8mb4");
define("DB_USER", "root");
define("DB_PASSWORD", "");
// (G) ONE-TIME PASSWORD SETTINGS
define("OTP_VALID", "15"); // valid for x minutes
define("OTP_TRIES", "3"); // max tries
define("OTP_LEN", "8"); // password length
// (H) NEW OTP OBJECT
$_OTP = new OTP();
Before the newbies foam in the mouth, keep calm, and let’s go through the library section by section.
- (A, B, H) When
$_OTP = new OTP()
is created, the constructor will automatically connect to the database; The destructor closes the connection when done. - (C)
query()
A simple helper function to run an SQL query. - (F) Database settings – Remember to change to your own.
- (G) A bunch of OTP settings, change these if you want.
2B) OTP REQUEST
// (D) GENERATE OTP
function generate ($email) {
/* (D1) @TODO - CHECK IF EMAIL IS VALID USER?
$this->query("SELECT * FROM `users` WHERE `user_email`=?", [$email]);
if (!is_array($this->stmt->fetch())) {
$this->error = "$email is not a valid user";
return false;
} */
// (D2) CHECK FOR EXISTING OTP REQUEST
$this->query("SELECT * FROM `otp` WHERE `user_email`=?", [$email]);
if (is_array($this->stmt->fetch())) {
// @TODO - 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;
}
// (D3) CREATE RANDOM PASSWORD
$alphabets = "abcdefghijklmnopqrstuwxyzABCDEFGHIJKLMNOPQRSTUWXYZ0123456789";
$count = strlen($alphabets) - 1;
$pass = "";
for ($i=0; $i<OTP_LEN; $i++) { $pass .= $alphabets[rand(0, $count)]; }
$this->query(
"REPLACE INTO `otp` (`user_email`, `otp_pass`) VALUES (?,?)",
[$email, password_hash($pass, PASSWORD_DEFAULT)]
);
// (D4) SEND VIA EMAIL
// @TODO - FORMAT YOUR OWN "NICE EMAIL" OR SEND VIA SMS.
$subject = "Your OTP";
$body = "Your OTP is $pass. Enter it at http://site.com/3b-challenge.php within ".OTP_VALID." minutes.";
if (@mail($email, $subject, $body)) { return true; }
else {
$this->error = "Failed to send OTP email.";
return false;
}
}
As in the introduction above, “the first step” involves the user requesting an OTP – This function deals with that. The core parts are pretty much:
- (D3) Generate a random password and save it into the database.
- (D4) Send the random password to the user.
That’s all. But you will want to complete this function on your own:
- (D1) If the OTP is open to registered users only – Do your own check here.
- (D2) Allow the user to request another OTP when a previous one has expired? Or does the user have to manually contact an admin?
- (D4) Create your own “nice email”, or integrate your own SMS gateway here.
2C) OTP CHALLENGE & VERIFY
// (E) CHALLENGE OTP
function challenge ($email, $pass) {
// (E1) GET THE OTP ENTRY
$this->query("SELECT * FROM `otp` WHERE `user_email`=?", [$email]);
$otp = $this->stmt->fetch();
// (E2) OTP ENTRY NOT FOUND
if (!is_array($otp)) {
$this->error = "The specified OTP request is not found.";
return false;
}
// (E3) TOO MANY TRIES
if ($otp["otp_tries"] >= OTP_TRIES) {
$this->error = "Too many tries for OTP.";
return false;
}
// (E4) EXPIRED
$validTill = strtotime($otp["otp_timestamp"]) + (OTP_VALID * 60);
if (strtotime("now") > $validTill) {
$this->error = "OTP has expired.";
return false;
}
// (E5) INCORRECT PASSWORD - ADD STRIKE
if (!password_verify($pass, $otp["otp_pass"])) {
$strikes = $otp["otp_tries"] + 1;
$this->query("UPDATE `otp` SET `otp_tries`=? WHERE `user_email`=?", [$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;
}
// (E6) ALL OK - DELETE OTP
$this->query("DELETE FROM `otp` WHERE `user_email`=?", [$email]);
return true;
}
After the user receives the OTP and clicks on the link, this function will deal with “the second step” – Which is to verify the OTP. Don’t think this needs much explanation… Trace through on your own, it is pretty much a whole load of checks.
P.S. You will want to complete (E5) on your own. What happens when the user enters the wrong OTP too many times? Lock their account? Suspend for a period of time?
PART 3) OTP HTML PAGES
3A) REQUEST FOR OTP
<!-- (A) OTP REQUEST FORM -->
<form method="post" target="_self">
<label>Email</label>
<input type="email" name="email" required value="jon@doe.com">
<input type="submit" value="Request OTP">
</form>
<?php
// (B) PROCESS OTP REQUEST
if (isset($_POST["email"])) {
require "2-otp.php";
$pass = $_OTP->generate($_POST["email"]);
echo $pass ? "<div class='note'>OTP SENT.</div>" : "<div class='note'>".$_OTP->error."</div>" ;
}
?>
- Just a regular HTML form to request for the OTP.
- When the form is submitted, we use
$_OTP->generate(EMAIL)
to create and send the OTP to the user.
3B) OTP CHALLENGE PAGE
<form method="post" target="_self">
<label>Email</label>
<input type="email" name="email" required value="jon@doe.com">
<label>OTP</label>
<input type="password" name="otp" required>
<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"]);
// @TODO - DO SOMETHING ON VERIFIED
echo $pass ? "<div class='note'>OTP VERIFIED.</div>" : "<div class='note'>".$_OTP->error."</div>" ;
}
?>
- The user clicks on the link in the email, lands on this page – Enters the OTP into this form.
- This shouldn’t be a mystery anymore. Use
$_OTP->challenge(EMAIL, OTP)
to verify the entered password against the database. Complete this on your own – Proceed to do whatever is required on verification.
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
SUPPORT
600+ free tutorials & projects on Code Boxx and still growing. I insist on not turning Code Boxx into a "paid scripts and courses" business, so every little bit of support helps.
Buy Me A Meal Code Boxx eBooks
EXAMPLE CODE DOWNLOAD
Click here for the source code on GitHub gist, just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
EXTRA 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
- PHP Send Email With HTML Template – Code Boxx
- Simple User Login System With PHP MySQL – Code Boxx
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!
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
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
https://code-boxx.com/php-user-registration-form-email-verification/
How do you get a new otp
$_OTP->generate(EMAIL);