DEV Community

Cover image for Handle OTP Auth via your own source code
Ngo Dinh Cuong
Ngo Dinh Cuong

Posted on

Handle OTP Auth via your own source code

Laravel OTP AUTH

This package allows you to authenticate with one time password access (OTP).

https://github.com/cuongnd88/otp-auth

Example Usage:

Route::get("/notify", function(){
    return App\Models\User::find(1)->notify(new App\Authentication\SendOtp('mail', 4, 10));
});

Route::get("/auth-otp/{otp}", function(){
    return App\Models\User::authByOtp(request()->otp, '84905279285');
});

Route::get("/check-otp/{otp}", function(){
    return App\Models\User::find(1)->checkOtp(request()->otp);
});
Enter fullscreen mode Exit fullscreen mode

Contents

Installation

1- Add the package to your dependencies.

$ composer require cuongnd88/otp-auth
Enter fullscreen mode Exit fullscreen mode

2- Run the command:

php artisan auth:otp {ClassName}
Enter fullscreen mode Exit fullscreen mode

Example:

php artisan auth:otp Authentication/SendOtp
Enter fullscreen mode Exit fullscreen mode

SendOtp class and HasOtpAuth trait are auto-generated at app/Authentication directory.

CreateNotificationsTable class is alseo auto-generated at app/database/migrations.

3- Apply the migrations:

It will create a table called notifications to store generated OTP information.

$ php artisan migrate
Enter fullscreen mode Exit fullscreen mode

Usage

Generate OTP

You can generate OTP via email or SMS

Route::get("/notify", function(){
    return App\Models\User::find(1)->notify(new App\Authentication\SendOtp(['mail', 'nexmo']));
});
Enter fullscreen mode Exit fullscreen mode

This package allows you to alter OTP length and lifetime

Route::get("/notify", function(){
    $length = 4;
    $liftime = 10; //minutes
    return App\Models\User::find(1)->notify(new App\Authentication\SendOtp(['mail', 'nexmo']), $length, $liftime);
});
Enter fullscreen mode Exit fullscreen mode

OTP default length: The default length is 6.

OTP default lifetime: The default lifetime is 1 minute.

There is the detail of auto-generate SentOTP class:

<?php

namespace App\Authentication;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Cuongnd88\DeliveryChannel\Messages\TwilioMessage;

class SendOtp extends Notification
{
    use Queueable;

    protected $defaultChannels = ['database'];

    protected $otp;

    protected $lifeTime;

    const OPT_LIFETIME = 1;

    const OPT_LENGTH = 6;

    /**
     * Construct
     *
     * @param array|string $channels
     * @param integer|string $otpLength
     * @param integer|string $lifeTime
     */
    public function __construct($channels = null, $otpLength = null, $lifeTime = null)
    {
        $this->otp = $this->generateOtp($otpLength ?? self::OPT_LENGTH);
        $this->lifeTime = $lifeTime ?? self::OPT_LIFETIME;
        $this->defaultChannels = $this->verifyChannels($channels);
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return $this->defaultChannels;
    }

    /**
     * Get the mail representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return \Illuminate\Notifications\Messages\MailMessage
     */
    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('Your OTP is '.$this->otp)
                    ->line('Thank you for using our application!');
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            'otp' => $this->otp,
            'expired_at' => now()->addMinutes($this->lifeTime)->toDateTimeString(),
        ];
    }

    /**
     * Get the Nexmo / SMS representation of the notification.
     *
     * @param  mixed  $notifiable
     *
     * @return mixed
     */
    public function toTwilio($notifiable)
    {
        return (new TwilioMessage)
                    ->to("+8439xxxxxxx")
                    ->from("+xxxxxxxxxx")
                    ->body('OTP AUTH is '.$this->otp);
    }

    /**
     * Generate OTP
     *
     * @param integer|string $n
     *
     * @return string
     */
    public function generateOtp($n)
    {
        $generator = "0987654321";
        $result = "";

        for ($i = 1; $i <= $n; $i++) {
            $result .= substr($generator, (rand()%(strlen($generator))), 1);
        }
        return $result;
    }

    /**
     * Verify channels
     *
     * @param string|array $channels
     *
     * @return array
     */
    public function verifyChannels($channels)
    {
        if ($channels && is_array($channels)) {
            return array_merge($this->defaultChannels, $channels);
        }
        if ($channels && is_string($channels)) {
            array_push($this->defaultChannels, $channels);
        }
        return $this->defaultChannels;
    }
}

Enter fullscreen mode Exit fullscreen mode

toTwilio: This method is implemented by importing delivery-channels.

Verify OTP

After sent OTP via your configed methods, you call authByOtp to authenticate

Route::get("/auth-otp/{otp}", function(){
    return App\Models\User::authByOtp(request()->otp, '84905123456');
});
Enter fullscreen mode Exit fullscreen mode

Based on your credentials, you might authenticate with email or phone number

Set up the credentials:

In this case, you can apply User model which must use HasOtpAuth trait

. . . .
use App\Authentication\HasOtpAuth;

class User extends Authenticatable
{
    use Notifiable;
    use HasOtpAuth;

    protected $credential = 'mobile';

. . . .
Enter fullscreen mode Exit fullscreen mode

Let see more detail HasOtpAuth trait

<?php

namespace App\Authentication;

trait HasOtpAuth
{
    /**
     * Check OTP
     *
     * @return bool
     */
    public function checkOtp($otp)
    {
        $authenticator = $this->otp();
        return $this->validateOtp($authenticator, $otp);
    }

    /**
     * Get OTP data
     *
     * @return \Illuminate\Notifications\DatabaseNotification
     */
    public function otp()
    {
        return $this->notifications()
                ->where('type', 'LIKE', '%SendOtp%')
                ->whereNull('read_at')
                ->first();
    }

    /**
     * Validate OTP
     *
     * @param \Illuminate\Notifications\DatabaseNotification $authenticator
     * @param mixed $otp
     *
     * @return void
     */
    public function validateOtp($authenticator, $otp)
    {
        $result = false;
        if (is_null($authenticator)) {
            return response()->json($result,200);
        }
        if ($authenticator
            && now()->lte($authenticator->data['expired_at'])
            && $authenticator->data['otp'] == $otp
        ) {
            $result = true;
        }
        $authenticator->markAsRead();
        return response()->json($result,200);
    }

    /**
     * Authenticate by OTP
     *
     * @param string $otp
     * @param string $credentialValue
     * @return void
     */
    public static function authByOtp($otp, $credentialValue)
    {
        $model = new static;
        $credentialName = property_exists($model,'credential') ? $model->credential : 'email';

        $authenticator = $model->where($credentialName, '=', $credentialValue)->first();
        if (is_null($authenticator)) {
            return response()->json(false,200);
        }

        $authenticator = $authenticator->notifications()
                    ->where('type', 'LIKE', '%SendOtp%')
                    ->whereNull('read_at')
                    ->first();

        return $model->validateOtp($authenticator, $otp);
    }
}
Enter fullscreen mode Exit fullscreen mode

Basic identification

In some cases, you just need to identify the right access, you might need to execute checkOtp method

Route::get("/check-otp/{otp}", function(){
    return auth()->user->checkOtp(request()->otp);
});
Enter fullscreen mode Exit fullscreen mode

Demo

This is demo soure code.
Laravel Colab

Credits

  • Ngo Dinh Cuong

LinkedIn

Discussion (0)