Drupal OTP login using Twilio SMS gateway
In this article we are discussing how to replace the default login of a Drupal 9/10 website with OTP login.
Here we are going to implement additional authentication for Drupal login using twilio sms gateway. In this two step verification, upon providing mobile number, we will be sending 4 letter OTP sms to registered mobile number and after providing valid OTP, user redirected to the admin dashboard.
So front login steps include below screens.
Make sure you have at least one trail account step in twilio.
Go to https://www.twilio.com/console
Sign up or login, select only SMS service.
Get a phone number and you can see below account SID, token and phone number in twilio account.
Validate one phone number to which you want to receive OTP, in trial account only validated phone number will receive the OTP.
You can see complete steps to set up trial account and how to send test messages in Twilio documentations.
In order to implement otp login, we are following below steps and developing a custom module say dn_login.
- Alter default Drupal login form and allow only mobile number field for login
- Create service for OTP local storage
- Create service for OTP operations
- Implementation of OTP submission form.
- OTP Expire UI implementation
- Resend OTP implementation
So we are going to implement a custom module dn_login with files as below.
Before starting our development make sure you have field_mobile_number(Machine Name) field added under Configuration -> under People Account settings → select Manage fields.
While installing sample module, for automatically creating this fields, we have included below yml files in sample module.
/config/install/field.field.user.user.field_mobile_number.yml
/config/install/field.storage.user.field_mobile_number.yml
Make sure in form display page, this mobile number field in enabled section.
/admin/config/people/accounts/form-display
Also, we have to create one table in database for storing otp and expiration time. Table structure is as below.
Table name – dn_login_otp
Script to create table included in dn_login.install in sample module.
<?php
/**
* @file
* Install hooks for dn_login module.
*/
/**
* Database schema.
*/
function dn_login_schema() {
$schema['dn_login_otp'] = [
'description' => 'Stores the generated OTP per user.',
'fields' => [
'uid' => [
'description' => 'UID of the User.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'otp' => [
'description' => 'Hashed otp of the User.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
'default' => 0,
],
'expiration' => [
'description' => 'Time when otp will expire.',
'type' => 'varchar_ascii',
'length' => 50,
'not null' => TRUE,
'default' => 0,
],
],
];
return $schema;
}
We need twilio SDK installed in Drupal, you can see documentation how to install in below.
Here I have used command – composer require twilio/sdk
Now we have done all the required activities before starting development.
Next we are discussing each step in details.
1-Alter default Drupal login and allow only mobile number field for login
FIle Path – /dn_login/dn_login.module
In this file we are going to override default Drupal login form.
So we will be removing user name, password fields and submit handlers with mobile number field and our on custom submit handler as below.
function dn_login_form_alter(&$form, FormStateInterface $formState, $form_id) {
if ($form_id == 'user_login_form'){
//$flood_config = \Drupal::service('config.factory')->getEditable('user.flood');
unset($form['#submit'][0]);
unset($form['name']);
unset($form["#validate"]);
unset($form['pass']);
$form['#validate'][] = 'validateMobile';
$form['actions']['submit']['#submit'][] = 'phone_login_callback';
$form['mobilenumber'] = [
'#type' => 'tel',
'#title' => t('Mobile Number'),
'#description' => t('Enter the Mobilenumber with country code, For eg: +91--yournumber for India'),
'#required' => TRUE,
'#maxlength' => 60,
'#attributes' => ['class' => ['mobile-number-login']],
'#size' => 60,
'#weight' => -49,
];
}
}
Here ‘phone_login_callback’ is the submit handler where we are accessing OTP service functions.
You can see more on how to implement phone number login in this article.
Add basic validations for mobile number fields.
function validateMobile(array &$form, FormStateInterface $form_state) {
if($form_state->isValueEmpty('mobilenumber')){
$form_state->setErrorByName('mobilenumber', 'Enter Mobile number');
return;
}
if(!(getNameFromPhone($form_state->getValue('mobilenumber')))){
$form_state->setErrorByName('mobilenumber', 'Mobile number Not Registered');
return;
}
if(getNameFromPhone($form_state->getValue('mobilenumber'))){
if (!$form_state->isValueEmpty('mobilenumber') && user_is_blocked(getNameFromPhone($form_state->getValue('mobilenumber')))) {
// Blocked in user administration.
$form_state->setErrorByName('mobilenumber', 'The user with phone number %mobilenumber has not been activated or is blocked.', ['%mobilenumber' => $form_state->getValue('mobilenumber')]);
}
}
}
In above getNameFromPhone is a custom function retrieving username from mobile number.
Implementation of this function provided below.
function getNameFromPhone($mobileno){
$users = \Drupal::entityTypeManager()->getStorage('user')->loadByProperties(['field_mobile_number' => $mobileno]);
$user = $users ? reset($users) : FALSE;
if ($user) {
return $user->getAccountName();
}else{
return FALSE;
}
}
Submit handler call back function provided below. In this function we are sending Otp.
function phone_login_callback(&$form, FormStateInterface $form_state) {
$mobile = $form_state->getValue('mobilenumber');
if ($form_state->getErrors()) {
unset($form['#prefix']);
unset($form['#suffix']);
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -10,
];
$form_state->setRebuild();
}else{
$users = \Drupal::entityTypeManager()->getStorage('user')->loadByProperties(['field_mobile_number' => $mobile]);
$user = $users ? reset($users) : FALSE;
if ($user) {
$form_state->setValue('name', $user->getAccountName());
$account = user_load_by_name($user->getAccountName());
//=====load otp service
$otp = \Drupal::service('dn_login.otp');
$otp_code = $otp->generate($user->getAccountName());
$callService = \Drupal::service('dn_login.localStorage');
//==============sending SMS
if ($otp_code && $otp->send($otp_code,$mobile)) {
\Drupal::messenger()->addMessage(t('An OTP was sent to your mobile number. Please check your Message.'));
$form_state->setRedirect('dn_login.otp_form');
}
//==============sending SMS
}else{
$form_state->setRebuild();
$form_state->setErrorByName('mobilenumber', 'User doesnot exist');
return;
}
}
}
In above code you can see below two services otp and localStorage that we are using to send otp and otp validations.
$otp = \Drupal::service(‘dn_login.otp’);
$callService = \Drupal::service(‘dn_login.localStorage’);
In the coming steps we will be explaining these service implementations.
2- Create service for OTP local storage
This service for saving user id across the login pages.
Provide the class details in services.yml (Path-/dn_login.services.yml),
services:
dn_login.otp:
class: Drupal\dn_login\Services\Otp
arguments: ['@database', '@password', '@tempstore.private','@session_manager','@current_user']
dn_login.localStorage:
class: Drupal\dn_login\Services\LocalStorage
arguments: ['@tempstore.private','@session_manager','@current_user']
In above first one is the Otp service that we will discuss in detail in next step.
Create the LocalStorage service below path.
/src/Services/LocalStorage.php
Implementation of this file provided below.
<?php
/**
* @file
* Contains \Drupal\demo\Form\MultistepFormBase.
*/
namespace Drupal\dn_login\Services;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionManagerInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
class LocalStorage {
/**
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStoreFactory;
/**
* @var \Drupal\Core\Session\SessionManagerInterface
*/
private $sessionManager;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
private $currentUser;
/**
* @var \Drupal\user\PrivateTempStore
*/
public $store;
/**
* Constructs a \Drupal\dn_login\LocalStorage.
*
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* @param \Drupal\Core\Session\SessionManagerInterface $session_manager
* @param \Drupal\Core\Session\AccountInterface $current_user
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, SessionManagerInterface $session_manager, AccountInterface $current_user) {
$this->tempStoreFactory = $temp_store_factory;
$this->sessionManager = $session_manager;
$this->currentUser = $current_user;
$this->store = $this->tempStoreFactory->get('otp_data');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('tempstore.private'),
$container->get('session_manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}.
*/
public function sessionCheck() {
// Start a manual session for anonymous users.
if ($this->currentUser->isAnonymous() && !isset($_SESSION['otp_holds_session'])) {
$_SESSION['otp_holds_session'] = true;
$this->sessionManager->start();
}
}
/**
* Helper method that removes all the keys from the store collection used for
* the multistep form.
*/
public function deleteStore() {
$keys = ['uid'];
foreach ($keys as $key) {
$this->store->delete($key);
}
}
}
Here we are initializing $this->store variable as temporary storage of otp_data, we can save variables in this using set function, also we retrieve using get function.
In constructor we have to inject below services.
$this->tempStoreFactory = $temp_store_factory;
$this->sessionManager = $session_manager;
$this->currentUser = $current_user;
SessionCheck() function is for initializing session if user is anonymous user. In our case user always will be anonymous, so this session initialization needed for temporary variable storage.
delete() function is for removing all the values stored.
3 – Create service for OTP operations
Here we are creating a service called Otp for below operations,
- generate() function for creating new OTP
- send() function to send OTP to the user mobile.
- check() function to check OTP is valid or not
- expire() function to check OTP of the user expired or not
- getExpirationTime() function returns remaining expiration time
- exists() function checks whether otp already exist in db table
- update() function updates the existing otp
Here we have took this service from module email_login_otp.
We have customized above functions for sending mobile otp.
Create Otp service file in src/services folder as
/src/Services/Otp.php
Include same in module services.yml
/dn_login.services.yml
services:
dn_login.otp:
class: Drupal\dn_login\Services\Otp
arguments: ['@database', '@password', '@tempstore.private','@session_manager','@current_user']
Below provided implementation of /src/Services/Otp.php
<?php
namespace Drupal\dn_login\Services;
use Drupal\Core\Database\Connection;
use Drupal\Core\Password\PasswordInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use \Drupal\Core\Session\SessionManagerInterface;
use \Drupal\Core\Session\AccountInterface;
use Twilio\Rest\Client;
use Twilio\Exceptions\RestException;
use Drupal\dn_login\Services\LocalStorage;
/**
* Otp service class.
*/
class Otp {
/**
* The database object.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The username.
*
* @var string
*/
private $username;
/**
* The tempoStorage object.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
//private $tempStorageFactory;
/**
* @var \Drupal\Core\Session\SessionManagerInterface
*/
//private $sessionManager;
/**
* @var \Drupal\Core\Session\AccountInterface
*/
// protected $current_user;
/**
* @var \Drupal\user\PrivateTempStore
*/
// protected $store;
/**
* Constructor of the class.
*/
//public function __construct(Connection $connection, PasswordInterface $hasher, PrivateTempStoreFactory $tempStoreFactory,SessionManagerInterface $session_manager, AccountInterface $current_user) {
public function __construct(Connection $connection, PasswordInterface $hasher) {
$this->database = $connection;
$this->hasher = $hasher;
}
/**
* Generates a new OTP.
*/
public function generate($username) {
$this->username = $username;
$uid = $this->getUserField('uid');
$callService = \Drupal::service('dn_login.localStorage');
$callService->sessionCheck();
$callService->store->set("uid",$uid);
if ($this->exists($uid)) {
return $this->update($uid);
}
return $this->new($uid);
}
/**
* Sends the OTP to user via mobile.
*/
public function send($otp, $to) {
$account_sid = 'AC2a9d9901280ddd1ec4b46bafb8ea77af';
$auth_token = 'e884ca9372c22fb28e660c0c5d77f95f';
$from_no = '+13512003866';
$client = new Client($account_sid, $auth_token);
$options = [
'from' => $from_no,
'body' => 'Your OTPcode---'.$otp,
];
try {
$message = $client->messages->create($to, $options);
} catch (RestException $e) {
$code = $e->getCode();
$message = $e->getMessage();
}
return $message;
}
/**
* Checks if the given OTP is valid.
*/
public function check($uid, $otp) {
if ($this->exists($uid)) {
$select = $this->database->select('dn_login_otp', 'u')
->fields('u', ['otp', 'expiration'])
->condition('uid', $uid, '=')
->execute()
->fetchAssoc();
if ($select['expiration'] >= time() && $this->hasher->check($otp, $select['otp'])) {
return TRUE;
}
return FALSE;
}
return FALSE;
}
/**
* Checks if the OTP of a user has expired.
*/
public function expire($uid) {
$delete = $this->database->delete('dn_login_otp')
->condition('uid', $uid)
->execute();
return $delete;
}
/**
* Returns the remaining expiration time.
*/
public function getExpirationTime($uid) {
$unixTime = $this->database->select('dn_login_otp', 'o')
->fields('o', ['expiration'])
->condition('uid', $uid, '=')
->condition('otp', '', '!=')
->execute()
->fetchAssoc();
if ($unixTime) {
return $unixTime['expiration'];
}
return FALSE;
}
/**
* Fetches a user value by given field name.
*/
private function getUserField($field) {
$query = $this->database->select('users_field_data', 'u')
->fields('u', [$field])
->condition('name', $this->username, '=')
->execute()
->fetchAssoc();
return $query[$field];
}
/**
* Checks if the OTP already exists for a user.
*/
private function exists($uid) {
$exists = $this->database->select('dn_login_otp', 'u')
->fields('u')
->condition('uid', $uid, '=')
->execute()
->fetchAssoc();
return $exists ?? TRUE;
}
/**
* Generates a new OTP.
*/
private function new($uid) {
$human_readable_otp = rand(100000, 999999);
$this->database->insert('dn_login_otp')->fields([
'uid' => $uid,
'otp' => $this->hasher->hash($human_readable_otp),
'expiration' => strtotime("+5 minutes", time()),
])->execute();
return $human_readable_otp;
}
/**
* Updates the existing OTP.
*/
private function update($uid) {
$human_readable_otp = rand(100000, 999999);
$this->database->update('dn_login_otp')
->fields([
'otp' => $this->hasher->hash($human_readable_otp),
'expiration' => strtotime("+5 minutes", time()),
])
->condition('uid', $uid, '=')
->execute();
return $human_readable_otp;
}
}
As you can see in the generate function we are setting the uid in store variable by calling localStorage service.
$callService = \Drupal::service('dn_login.localStorage');
$callService->sessionCheck();
$callService->store->set("uid",$uid);
Inside send function we have to provide below values, that will be available in your twilio account dashboard.
$account_sid = 'xxxxxxxxxxxxxx';
$auth_token = 'xxxxxxxxxxxxxxx';
$from_no = '+13xxxxxxx';
You can also create a configuration form for saving this value.
Below part where we are sending message using Twilio API.
$client = new Client($account_sid, $auth_token);
$options = [
'from' => $from_no,
'body' => 'Your OTPcode---'.$otp,
];
try {
$message = $client->messages->create($to, $options);
} catch (RestException $e) {
$code = $e->getCode();
$message = $e->getMessage();
}
You can update Body massage as per your requirement.
getExpirationTime function returns the expiry time of the otp from db table, that will be displayed in Otp form.
After sending OTP we have to redirect to otp form, in next step we are implementing this form where validation otp is happening ,
As you saw in first step, we are redirecting to Otp submission form from dn_login.module as provided below.
//==============sending SMS
if ($otp_code && $otp->send($otp_code,$mobile)) {
\Drupal::messenger()->addMessage(t('An OTP was sent to your mobile number. Please check your Message.'));
$form_state->setRedirect('dn_login.otp_form');
}
4. Implementation of OTP submission form
Create Otp form in below path.
/src/Form/OtpForm.php
In this form we will have only otp field will be there as below.
build function provided below.
public function buildForm(array $form, FormStateInterface $form_state) {
$callService = \Drupal::service('dn_login.localStorage');
$expirationTime = $this->otp->getExpirationTime($callService->store->get("uid"));
$form['#cache'] = ['max-age' => 0];
$form['#prefix'] = "<div class='otp-form'>";
$form['#suffix'] = "</div>";
$form['otp'] = [
'#type' => 'textfield',
'#title' => $this->t('OTP'),
'#description' => $this->t('Enter the OTP you received in mobile. Didn\'t receive the OTP? You can resend OTP in: <span id="time">'.$expirationTime.'</span>'),
'#weight' => '0',
'#required' => TRUE,
'#suffix' => '<span class="otp-message"></span>'
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Login'),
'#ajax' => [
'callback' => '::ajaxOtpCallback',
'event' => 'click',
],
];
$form['resend'] = [
'#type' => 'markup',
'#markup' => "<span id='resend-span'>" . $this->t('Resend') . "</span>",
];
$form['#attached']['library'][] = 'dn_login/dn_login.front';
$form['#attached']['drupalSettings']['initial_time'] = date('i:s', (int) $expirationTime - time());
if ((int) $expirationTime > time()) {
$form['otp']['#description'] = $this->t('Enter the OTP you received in Mobile. Didn\'t receive the OTP? You can resend OTP in: <span id="time">@time</span>', ['@time' => date('i:s', (int) $expirationTime - time())]);
}
return $form;
}
Here expiration time first we are getting from db using otp service, then we are updating the span tag (id=time)with with count time use below Jquery code.
/js/otp.js
We will discuss more on this in next step.
Also $form[“resend”] element we are showing only when expiration time become zero, we will be providing resend link in this markup using Jquery.
As you can see you we are using ajax submission of the form and Ajax callback is ajaxOtpCallback.
Before implementing submit call back, we have to validate whether otp is valid or not using below validate function.
public function validateForm(array &$form, FormStateInterface $form_state) {
$callService = \Drupal::service('dn_login.localStorage');
$uid = $callService->store->get('uid');
$value = $form_state->getValue('otp');
if ($this->otp->check($uid, $value) == FALSE) {
$form_state->setErrorByName('otp', 'Invalid or expired OTP.');
}
}
Below provided the Ajax callback function.
public function ajaxOtpCallback(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
$callService = \Drupal::service('dn_login.localStorage');
$uid = $callService->store->get('uid');
if ($form_state->getErrors()) {
//echo "inside errors";exit;
unset($form['#prefix']);
unset($form['#suffix']);
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -10,
];
$form_state->setRebuild();
$response->addCommand(new ReplaceCommand('.otp-form', $form));
return $response;
}
unset($form['#prefix']);
unset($form['#suffix']);
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -10,
];
$response->addCommand(new ReplaceCommand('.otp-form', $form));
$account = $this->enityTypeManager->getStorage('user')->load($uid);
$this->otp->expire($uid);
$callService->deleteStore();
user_login_finalize($account);
$redirect_command = new RedirectCommand(Url::fromRoute('user.page')->toString());
$response->addCommand($redirect_command);
return $response;
}
You can see above after check if any errors, we are making otp as expire and removing from the db. Then we are using function user_login_finalize() to make user as logged in user and redirects to the user home page.
5-OTP Expire UI implementation
In above form, we are displaying expiration time as below in a span tag.
$form['otp']['#description'] = $this->t('Enter the OTP you received in Mobile. Didn\'t receive the OTP? You can resend OTP in: <span id="time">@time</span>', ['@time' => date('i:s', (int) $expirationTime - time())]);
Next step we need to implement count down timer on expiration time variable.
In our Otp form, we have below line in on order to pass initial_time value to drupalSettings.
$form['#attached']['drupalSettings']['initial_time'] = date('i:s', (int) $expirationTime - time());
So here we are implementing using below Jquery code.
/js/otp.js
/**
* @file
* Contains the Js code.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Attaches timer div.
*/
Drupal.behaviors.dn_loginJs = {
attach: function (context, settings) {
//--first hide resend markup which only will display when timer hit
$("#resend-span").hide();
var timer2 = drupalSettings.initial_time;
console.log("timer - initiate-"+timer2);
var interval = setInterval(function() { // time count down-executes this function every second.
// console.log("in intervel");
var timer = timer2.split(':');
//by parsing integer, I avoid all extra string processing
var minutes = parseInt(timer[0], 10);
var seconds = parseInt(timer[1], 10);
--seconds;
minutes = (seconds < 0) ? --minutes : minutes;
if (minutes < 0) clearInterval(interval);
seconds = (seconds < 0) ? 59 : seconds;
seconds = (seconds < 10) ? '0' + seconds : seconds;
if (minutes == 0 || minutes == '00' || minutes == '0') { //====if minutes hits zero show the resend to link to the user.
$("#resend-span").show();
//======inject resend link to resend span element
var link_txt = $("#resend-span").html();
$("#resend-span").html("<a href='javascript:void(0)' id='resend'>"+link_txt+"</a>");
$("#resend-span").click(function(){
window.location = window.location.href+'/resend';
});
}
//console.log("timer---"+minutes + ':' + seconds);
$('#time').html(minutes + ':' + seconds); //====update time in time span element.
timer2 = minutes + ':' + seconds;
}, 1000);
//========handling resend element click
$("#resend-span").click(function(){
window.location = window.location.href+'/resend';
});
}
};
})(jQuery, Drupal, drupalSettings);
As mentioned in above code comments it will do below actions
- Update time in time span in every second.
- Show resend-span element if minutes reaches zero.
6-Resend OTP implementation
As you can see above we are providing /resend for resending OTP, so we need to implement code for resending OTP in a controller.
Update routing file with path and controller mapping as below.
/dn_login.routing.yml
dn_login.resend:
path: '/login-otp/resend'
defaults:
_controller: '\Drupal\dn_login\Controller\ResendController::resend'
_title: 'Resend'
requirements:
_permission: 'access content'
Below provided implementation of resend controller.
<?php
namespace Drupal\dn_login\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
/**
* Class for the general controller.
*/
class ResendController extends ControllerBase {
use StringTranslationTrait;
/**
* Drupal\dn_login\Services\Otp definition.
*
* @var \Drupal\dn_login\Services\Otp
*/
protected $otp;
/**
* Drupal\Core\Session\AccountProxy definition.
*
* @var \Drupal\Core\Session\AccountProxy
*/
protected $currentUser;
/**
* Drupal\Core\Path\CurrentPathStack definition.
*
* @var \Drupal\Core\Path\CurrentPathStack
*/
protected $currentPath;
/**
* Drupal\Core\Entity\EntityTypeManager definition.
*
* @var \Drupal\Core\Entity\EntityTypeManager
*/
protected $enityTypeManager;
/**
* Drupal\Core\Messenger\MessengerInterface definition.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$instance = parent::create($container);
//$instance->tempstorePrivate = $container->get('tempstore.private');
$instance->otp = $container->get('dn_login.otp');
$instance->currentUser = $container->get('current_user');
$instance->currentPath = $container->get('path.current');
$instance->enityTypeManager = $container->get('entity_type.manager');
$instance->messenger = $container->get('messenger');
return $instance;
}
/**
* Resend.
*
* @return string
* Return RedirectResponse.
*/
public function resend() {
$otp = $this->otp;
$callService = \Drupal::service('dn_login.localStorage');
$uid = $callService->store->get('uid');
//$uid = $this->tempstorePrivate->get('dn_login')->get('uid');
$account = $this->enityTypeManager->getStorage('user')->load($uid);
$otp_code = $otp->generate($account->getDisplayname());
$mobile_no = $account->get('field_mobile_number')->value;
//echo $mobile_no;exit;
if ($otp_code && $otp->send($otp_code, $mobile_no)) {
$this->messenger->addMessage($this->t('An OTP was sent to your phone. Please check your message inbox.'));
$redirect = new RedirectResponse(Url::fromRoute('dn_login.otp_form')->toString());
return $redirect->send();
}
return [
'#type' => 'markup',
'#markup' => $this->t('Implement method: resend'),
];
}
Here we are regenerating OTP using Otp service and sending otp again to the phone number.
Clear the cache and try to login. You will receive OTP and can login using it.
You can download this sample code here,
Same module available in drupal.org with configuration form for saving Twilio configurations
Below points need to actioned before/after installation.
- Make sure twillio SDK installed, refer above compressor command to install.
- Make sure you are updating $account_sid, $auth_token and $from_no in send() function in file /src/Services/Otp.php
- Clear all the caches after installation.
- Go to Configurations->account settings -> Manage form display, move mobile number field to enabled section.
/admin/config/people/accounts/form-display