OTP stands for ‘One Time Password’, usually seen in most apps or websites for Two Factor Authentication. It mostly involves with database where you temporarily store the data to verify. However, in some cases, it is better and very economical if we can avoid the database and that’s what we will be doing in this tutorial.
How it Works
I saw a post by Anam Ahmed about this topic way back in 2019 and found it really interesting. It was based on Javascript. At that time, I did not get a chance to implement this in one of my projects, until very recently. My project is based on PHP (WordPress). You can get the detailed explanation in the original post here. Unlike a database tied verification, this method verifies OTP using cryptography.
Our Project
To make things easier to understand, I am going to explain the process with a sample project built with WordPress so we can test the OTP without database. This can be replicated in other CMS running on PHP or Vanilla PHP applications but will require tweaking.
We are going to build a form that will collect user’s anonymous feedback while keeping the feedbacks genuine as we will be verifying the submissions with OTP that the user will receive via SMS. You can easily make it into an email-based verification as well.
The Setup
First of all, we are going to set up a fresh, up to date WordPress installation. To create things easier to understand, let’s create a folder inside your theme folder called “otp-wo-database“.
Next, we will create a custom post type for the reviews. Each submission of the review form will create a new post in our custom post type. Let’s create a file “otp-post-type.php” inside the folder we just created earlier and put the following code on it.
<?php
function register_review_post_type() {
$labels = [
"name" => __( "Reviews", "textdomain" ),
"singular_name" => __( "Review", "textdomain" ),
];
$args = [
"label" => __( "Reviews", "textdomain" ),
"labels" => $labels,
"description" => "",
"public" => true,
"publicly_queryable" => true,
"show_ui" => true,
"show_in_rest" => true,
"rest_base" => "",
"rest_controller_class" => "WP_REST_Posts_Controller",
"has_archive" => false,
"show_in_menu" => true,
"show_in_nav_menus" => true,
"delete_with_user" => false,
"exclude_from_search" => false,
"capability_type" => "post",
"map_meta_cap" => true,
"hierarchical" => false,
"rewrite" => [ "slug" => "review", "with_front" => true ],
"query_var" => true,
"supports" => [ "title", "editor", "author", "custom-fields" ],
];
register_post_type( "review", $args );
}
add_action( 'init', 'register_review_post_type' );
Code language: PHP (php)
The code is yet to be recognized by WordPress, to make that, we add the following code in our theme’s functions.php file. We will be doing this for other files we will be adding soon as well.
require_once __DIR__ . '/otp-wo-database/otp-post-type.php';
Code language: PHP (php)
The Shortcode
Next, we are going to create a shortcode that will show the review submission form and submitted reviews. At the bottom, we are also adding a function that will load the javascript file and WordPress Ajax URL. This code will be on a new file named “otp-shortcode.php” inside our “otp-wo-database” folder.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit( 'Direct script access denied.' );
}
function otp_reviews_shortcode() {
otp_reviews_scripts();
ob_start(); ?>
<h3>Submit Review Anonymously</h3>
<form action="" id="otp-review-form">
<div id="review-submission">
<input type="text" id="review-title" name="review-title" value="" placeholder="Title" />
<textarea name="review-comment" id="review-comment" cols="30" rows="2" placeholder="Comment"></textarea>
<div class="rating">
<input id="star5" name="star" type="radio" value="5" class="radio-btn hide" />
<label for="star5" >☆</label>
<input id="star4" name="star" type="radio" value="4" class="radio-btn hide" />
<label for="star4" >☆</label>
<input id="star3" name="star" type="radio" value="3" class="radio-btn hide" />
<label for="star3" >☆</label>
<input id="star2" name="star" type="radio" value="2" class="radio-btn hide" />
<label for="star2" >☆</label>
<input id="star1" name="star" type="radio" value="1" class="radio-btn hide" />
<label for="star1" >☆</label>
<div class="clear"></div>
</div>
<a href="" id="goto-otp">Proceed</a>
</div>
<div id="otp-generate">
<input type="text" id="otp-phone" name="otp-phone" value="" placeholder="Your Phone" />
<a href="" id="send-otp">Send OTP</a>
</div>
<div id="otp-verify">
<input type="password" id="otp-code" name="otp-code" value="" placeholder="OTP Code" />
<a href="" id="verfiy-otp">Verify</a>
</div>
<div id="finish">
<div class="message"></div>
</div>
</form>
<div id="otp-reviews">
<?php
$args = array(
'post_type' => 'review',
'posts_per_page' => -1,
'order' => 'DESC'
);
$reviews_query = new WP_Query( $args );
if ( $reviews_query->have_posts() ) { ?>
<h3 class="submitted-reviews">Submitted Reviews</h3>
<?php while ( $reviews_query->have_posts() ) {
$reviews_query->the_post(); ?>
<div class="otp-review">
<h3><?php the_title(); ?></h3>
<p class="rating"><?php echo get_post_meta( get_the_ID(), 'review_rating', true ); ?> ☆</p>
<p class="review"><?php the_content(); ?></p>
</div>
<?php }
wp_reset_postdata();
} ?>
</div>
<?php
return ob_get_clean();
}
add_shortcode( 'otp_reviews', 'otp_reviews_shortcode' );
function otp_reviews_scripts() {
wp_enqueue_script( 'otp-review', get_stylesheet_directory_uri(). '/otp-wo-database/otp-review.js', array('jquery'), '1.0.8', true);
wp_localize_script( 'otp-review', 'ajax_url', admin_url('admin-ajax.php') );
}
Code language: HTML, XML (xml)
And just like earlier, let’s add the following in functions.php file
require_once __DIR__ . '/otp-wo-database/otp-shortcode.php';
Code language: PHP (php)
OTP Functions
Now we are going to add a new file inside our “otp-wo-database” folder called “otp-functions.php” and put the following code on it.
<?php
function create_hash( $phone, $otp, $ttl ) {
$expires = time() + $ttl * 60 * 1000; // Expires after in Minutes
$data = $phone.$otp.$expires;
$algorithm = "sha256";
$key = "__^%&Q@$&*!@#$%^&*^__"; // Secret key
$hash = hash_hmac( $algorithm, $data, $key ); // Create SHA256 hash of the data with key
return $hash.".".$expires;
}
function verify_otp( $phone, $otp, $hash ) {
// Seperate Hash value and expires from the hash returned from the user
if ( strpos( $hash, '.' ) !== false ) {
$hashdata = explode ( ".", $hash );
// Check if expiry time has passed
if( $hashdata[1] < time() ) {
return false;
}
// Calculate new hash with the same key and the same algorithm
$data = $phone.$otp.$hashdata[1];
$algorithm = "sha256";
$key = "__^%&Q@$&*!@#$%^&*^__"; // Secret key
$newHash = hash_hmac($algorithm, $data, $key);
// Match the hashes
if( $newHash == $hashdata[0] ) {
return true;
}
} else {
return false;
}
}
function generate_otp( $length ) {
$chars = '0123456789';
$charLength = strlen($chars);
$otpString = '';
for ($i = 0; $i < $length; $i++) {
$otpString .= $chars[ rand( 0, $charLength - 1 ) ];
}
return $otpString;
}
Code language: HTML, XML (xml)
We have 3 functions on this file that we will use later for creating a hash, verifying OTP, and generating OTP code. Then, making sure our new file is loaded.
require_once __DIR__ . '/otp-wo-database/otp-functions.php';
Code language: PHP (php)
Ajax Requests
Notice that we have enqueued a javascript file in our shortcode with “otp_reviews_scripts” function at the end. Now, we are going to add that js file.
Create “otp-review.js” file inside the “otp-wo-database” folder and put the following code.
;(function($){
$(document).ready(function(){
$('#goto-otp').click( function(e) {
e.preventDefault();
if ( $( 'input:radio[name=star]' ).is(':checked') && $('#review-title').val().length !== 0 && $('#review-comment').val().length !== 0 ) {
$('#review-submission').hide();
$('#otp-generate').show();
} else {
$('.error').remove();
$('<span class="error">Please fill all fields</span>').insertBefore('#review-title');
}
});
$('#send-otp').click( function(e) {
e.preventDefault();
if ( $( '#otp-phone' ).val() ) {
if ( $( '#otp-phone' ).val().length == 10 && $('#otp-phone')[0].checkValidity() == true ) {
var data = { action: 'request_otp', phone: $( '#otp-phone' ).val() }
$.ajax({
url: ajax_url,
data: data,
success : function( response ) {
if ( response ) {
$('#otp-generate').hide();
$('#otp-verify').show();
} else {
$('.error').remove();
$('<span class="error">System error. Please contact admin.</span>').insertBefore('#otp-phone');
}
}
});
} else {
$('.error').remove();
$('<span class="error">Please enter a valid phone number</span>').insertBefore('#otp-phone');
}
} else {
$('.error').remove();
$('<span class="error">Please enter a valid phone number</span>').insertBefore('#otp-phone');
}
});
$('#verfiy-otp').click( function(e) {
e.preventDefault();
if ( $( '#otp-code' ).val() ) {
if ( $( '#otp-code' ).val().length == 6 && $('#otp-code')[0].checkValidity() == true ) {
var data = { action: 'verify_otp', otp: $( '#otp-code' ).val() }
$.ajax({
url: ajax_url,
data: data,
success : function( response ) {
if ( response ) {
processReview();
$('#otp-verify').hide();
$('#finish').show();
window.setTimeout(function(){location.reload()},3000)
} else {
$('.error').remove();
$('<span class="error">Invalid OTP entered.</span>').insertBefore('#otp-code');
}
}
});
} else {
$('.error').remove();
$('<span class="error">Please enter correct OTP</span>').insertBefore('#otp-code');
}
} else {
$('.error').remove();
$('<span class="error">Please enter correct OTP</span>').insertBefore('#otp-code');
}
});
function processReview() {
var title = $('#review-title').val();
var comment = $('#review-comment').val();
var rating = $('input[name="star"]:checked').val();
var data = {
action: 'submit_review',
title : title,
comment : comment,
rating : rating
}
$.ajax({
url : ajax_url,
data : data,
success : function( response ) {
if( response ) {
$("#finish .message").append( response );
}
}
});
}
});
})(jQuery);
Code language: JavaScript (javascript)
We have 3 Ajax actions in our javascript code. The first Ajax action will request for OTP code from the data (phone number). Then the next one will verify the OTP code user submits. The last one is for submitting the review. The last two actions could have been combined but I wanted to make it as simple as possible for everyone.
Callback Functions
We have our Ajax requests ready now we will create the callback functions for them. As mentioned earlier, we have 3 of them.
First we are going to add “otp-cb-request-otp.php” file inside our “otp-wo-database” folder and put the following code.
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit( 'Direct script access denied.' );
}
/**
* We are using Twilio for the SMS gateway
* https://www.twilio.com/docs/libraries/php
*/
// Load the Twilio PHP library
require_once __DIR__ . '/Twilio/autoload.php';
// Use the REST API Client to make requests to the Twilio REST API
use Twilio\Rest\Client;
/**
* Callback when a otp is requested
*/
add_action('wp_ajax_request_otp', 'request_otp_callback');
add_action('wp_ajax_nopriv_request_otp', 'request_otp_callback');
function request_otp_callback() {
if ( isset( $_GET[ 'phone' ] ) && ! empty( $_GET[ 'phone' ] ) ) {
$phone = sanitize_text_field( $_GET['phone'] );
session_start();
$ttl = 2;
$otp = generate_otp(6);
$hash = create_hash( $phone, $otp, $ttl );
$_SESSION['user_phone'] = $phone;
$_SESSION['user_hash'] = $hash;
// Your Account SID and Auth Token from twilio.com/console
$sid = 'xxxxxxxxxxx'; // from Twilio account
$token = 'xxxxxxxxxxx'; // from Twilio account
$client = new Client($sid, $token);
// Use the client to do fun stuff like send text messages!
$client->messages->create(
// the number you'd like to send the message to
'+1'.$phone, // assuming user is from USA
[
// A Twilio phone number you purchased at twilio.com/console
'from' => '+11234567890', // from Twilio account
// the body of the text message you'd like to send
'body' => "Your OTP is ".$otp." . It will expire in 2 mins"
]
);
echo 'OTP Sent';
}
wp_die();
}
Code language: HTML, XML (xml)
As mentioned in the code, I am using Twilio for this project. You will require to download the PHP SDK provided by them here and unzip to our “otp-wo-database” folder.
You can signup for free (like I did for this tutorial) and try out their service. If you sign up with my referral link, you and I both will get a $10 credit. You can get more information about the PHP SDK from Twilio here.
The next file will contain the OTP verifying callback function. Let’s create “otp-cb-verify.php” file inside, of course “otp-wo-database” folder.
<?php
// Do not allow directly accessing this file.
if ( ! defined( 'ABSPATH' ) ) {
exit( 'Direct script access denied.' );
}
/**
* Callback when a otp is verified
*/
add_action('wp_ajax_verify_otp', 'verify_otp_callback');
add_action('wp_ajax_nopriv_verify_otp', 'verify_otp_callback');
function verify_otp_callback() {
session_start();
if ( ! empty( $_SESSION['user_hash'] ) ) {
if ( isset( $_GET[ 'otp' ] ) && ! empty( $_GET[ 'otp' ] ) ) {
$otp = sanitize_text_field( $_GET['otp'] );
$phone = $_SESSION['user_phone'];
$hash = $_SESSION['user_hash'];
$verify = verify_otp( $phone, $otp, $hash );
if ( $verify == TRUE ) {
$success = true;
session_destroy();
echo 'OTP is valid';
}
}
}
wp_die();
}
Code language: HTML, XML (xml)
Lastly, we will add another file for submitting the callback function that submits the review after the OTP verification is done. Let’s create “otp-cb-submit.php” file inside “otp-wo-database” folder and put the following code.
<?php
// Do not allow directly accessing this file.
if ( ! defined( 'ABSPATH' ) ) {
exit( 'Direct script access denied.' );
}
/**
* Callback when a review is submitted
*/
add_action('wp_ajax_submit_review', 'submit_review_callback');
add_action('wp_ajax_nopriv_submit_review', 'submit_review_callback');
function submit_review_callback() {
if ( isset( $_GET['title'] ) &&
isset( $_GET['comment'] ) &&
isset( $_GET['rating'] ) ) {
$title = sanitize_text_field( $_GET['title'] );
$comment = sanitize_text_field( $_GET['comment'] );
$rating = sanitize_text_field( $_GET['rating'] );
$review_args = array(
'post_type' => 'review',
'post_title' => $title,
'post_content' => $comment,
'post_status' => 'publish',
);
$review_id = wp_insert_post( $review_args );
if ( !is_wp_error( $review_id ) ) {
update_post_meta( $review_id, 'review_rating', $rating );
echo 'Submission Successful!';
} else {
echo 'Submission Failed!';
}
}
wp_die();
}
Code language: HTML, XML (xml)
Let’s not forget to require or callback functions and add the following to our theme’s functions.php file.
require_once __DIR__ . '/otp-wo-database/otp-cb-request-otp.php';
require_once __DIR__ . '/otp-wo-database/otp-cb-verify.php';
require_once __DIR__ . '/otp-wo-database/otp-cb-submit.php';
Code language: PHP (php)
CSS Styles
I am using the Twenty Twenty-One default theme, the following styles will most likely work with it to match what I have as my output. You can change it anyway you like.
#otp-review-form {
background: #acdaff;
padding: 30px;
}
#otp-review-form #otp-generate,
#otp-review-form #otp-verify,
#otp-review-form #finish {
display: none;
}
#otp-review-form input,
#otp-review-form textarea {
margin-bottom: 15px;
width: 100%;
border: none;
}
#otp-review-form a {
display: inline-block;
text-decoration: none;
background: #009688;
color: #fff;
padding: 10px 40px;
margin-top: 20px;
}
#otp-reviews h3.submitted-reviews {
margin-bottom: 30px;
}
#otp-reviews .otp-review {
padding: 20px;
margin-bottom: 20px;
background: #fff9c6;
}
/* Star rating CSS inspired from https://jsfiddle.net/0kwnb7ph/ */
#otp-review-form .txt-center {
text-align: center;
}
#otp-review-form .hide {
display: none;
}
#otp-review-form .clear {
float: none;
clear: both;
}
#otp-review-form .rating {
width: 165px;
unicode-bidi: bidi-override;
direction: rtl;
text-align: center;
position: relative;
}
#otp-review-form .rating > label {
float: right;
display: inline;
padding: 0;
margin: 0;
position: relative;
width: 1.1em;
cursor: pointer;
color: #000;
font-size: 30px !important;
}
#otp-review-form .rating > label:hover,
#otp-review-form .rating > label:hover ~ label,
#otp-review-form .rating > input.radio-btn:checked ~ label {
color: transparent;
}
#otp-review-form .rating > label:hover:before,
#otp-review-form .rating > label:hover ~ label:before,
#otp-review-form .rating > input.radio-btn:checked ~ label:before,
#otp-review-form .rating > input.radio-btn:checked ~ label:before {
content: "\2605";
position: absolute;
left: 0;
color: #ff8c00;
}
.error {
display: block;
color: #e91e63;
margin: 10px 0;
}
Code language: CSS (css)
Final Output
Let me know how it worked out for you, in the comments. I have more exciting tutorials on the line and possibly new features to this site as well so be sure to check around soon. Thanks!