├── .gitignore ├── src ├── OtpAuthServiceProvider.php ├── stubs │ ├── has-otp-auth-trait.stub │ └── otp-auth.stub └── Commands │ └── MakeOtpAuthCommand.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | yarn-error.log 5 | 6 | # Laravel 4 specific 7 | bootstrap/compiled.php 8 | app/storage/ 9 | 10 | # Laravel 5 & Lumen specific 11 | public/storage 12 | public/hot 13 | 14 | # Laravel 5 & Lumen specific with changed public path 15 | public_html/storage 16 | public_html/hot 17 | 18 | storage/*.key 19 | .env 20 | Homestead.yaml 21 | Homestead.json 22 | /.vagrant 23 | .phpunit.result.cache 24 | -------------------------------------------------------------------------------- /src/OtpAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 26 | \Cuongnd88\OtpAuth\Commands\MakeOtpAuthCommand::class, 27 | ]); 28 | } 29 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cuongnd88/otp-auth", 3 | "description": "Laravel OTP Authentication (One Time Passwords)", 4 | "homepage": "https://github.com/cuongnd88/otp-auth", 5 | "keywords": ["laravel", "otp", "one time passwords", "authentication"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Ngo Dinh Cuong", 10 | "email": "dinhcuongngo@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.2.5" 15 | }, 16 | "require-dev": { 17 | "mockery/mockery": "~1.3.1", 18 | "phpunit/phpunit": "7.*" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Cuongnd88\\OtpAuth\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Cuongnd88\\OtpAuth\\Test\\": "tests" 28 | } 29 | }, 30 | "extra": { 31 | "laravel": { 32 | "providers": [ 33 | "Cuongnd88\\OtpAuth\\OtpAuthServiceProvider" 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ngo Dinh Cuong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stubs/has-otp-auth-trait.stub: -------------------------------------------------------------------------------- 1 | otp(); 15 | return $this->validateOtp($authenticator, $otp); 16 | } 17 | 18 | /** 19 | * Get OTP data 20 | * 21 | * @return \Illuminate\Notifications\DatabaseNotification 22 | */ 23 | public function otp() 24 | { 25 | return $this->notifications() 26 | ->where('type', 'LIKE', '%DummyOtpAuthClass%') 27 | ->whereNull('read_at') 28 | ->first(); 29 | } 30 | 31 | /** 32 | * Validate OTP 33 | * 34 | * @param \Illuminate\Notifications\DatabaseNotification $authenticator 35 | * @param mixed $otp 36 | * 37 | * @return void 38 | */ 39 | public function validateOtp($authenticator, $otp) 40 | { 41 | $result = false; 42 | if (is_null($authenticator)) { 43 | return response()->json($result,200); 44 | } 45 | if ($authenticator 46 | && now()->lte($authenticator->data['expired_at']) 47 | && $authenticator->data['otp'] == $otp 48 | ) { 49 | $result = true; 50 | } 51 | $authenticator->markAsRead(); 52 | return response()->json($result,200); 53 | } 54 | 55 | /** 56 | * Authenticate by OTP 57 | * 58 | * @param string $otp 59 | * @param string $credentialValue 60 | * @return void 61 | */ 62 | public static function authByOtp($otp, $credentialValue) 63 | { 64 | $model = new static; 65 | $credentialName = property_exists($model,'credential') ? $model->credential : 'email'; 66 | 67 | $authenticator = $model->where($credentialName, '=', $credentialValue)->first(); 68 | if (is_null($authenticator)) { 69 | return response()->json(false,200); 70 | } 71 | 72 | $authenticator = $authenticator->notifications() 73 | ->where('type', 'LIKE', '%DummyOtpAuthClass%') 74 | ->whereNull('read_at') 75 | ->first(); 76 | 77 | return $model->validateOtp($authenticator, $otp); 78 | } 79 | } -------------------------------------------------------------------------------- /src/stubs/otp-auth.stub: -------------------------------------------------------------------------------- 1 | otp = $this->generateOtp($otpLength ?? self::OPT_LENGTH); 34 | $this->lifeTime = $lifeTime ?? self::OPT_LIFETIME; 35 | $this->defaultChannels = $this->verifyChannels($channels); 36 | } 37 | 38 | /** 39 | * Get the notification's delivery channels. 40 | * 41 | * @param mixed $notifiable 42 | * @return array 43 | */ 44 | public function via($notifiable) 45 | { 46 | return $this->defaultChannels; 47 | } 48 | 49 | /** 50 | * Get the mail representation of the notification. 51 | * 52 | * @param mixed $notifiable 53 | * @return \Illuminate\Notifications\Messages\MailMessage 54 | */ 55 | public function toMail($notifiable) 56 | { 57 | return (new MailMessage) 58 | ->line('Your OTP is '.$this->otp) 59 | ->line('Thank you for using our application!'); 60 | } 61 | 62 | /** 63 | * Get the array representation of the notification. 64 | * 65 | * @param mixed $notifiable 66 | * @return array 67 | */ 68 | public function toArray($notifiable) 69 | { 70 | return [ 71 | 'otp' => $this->otp, 72 | 'expired_at' => now()->addMinutes($this->lifeTime)->toDateTimeString(), 73 | ]; 74 | } 75 | 76 | /** 77 | * Generate OTP 78 | * 79 | * @param integer|string $n 80 | * 81 | * @return string 82 | */ 83 | public function generateOtp($n) 84 | { 85 | $generator = "0987654321"; 86 | $result = ""; 87 | 88 | for ($i = 1; $i <= $n; $i++) { 89 | $result .= substr($generator, (rand()%(strlen($generator))), 1); 90 | } 91 | return $result; 92 | } 93 | 94 | /** 95 | * Verify channels 96 | * 97 | * @param string|array $channels 98 | * 99 | * @return array 100 | */ 101 | public function verifyChannels($channels) 102 | { 103 | if ($channels && is_array($channels)) { 104 | return array_merge($this->defaultChannels, $channels); 105 | } 106 | if ($channels && is_string($channels)) { 107 | array_push($this->defaultChannels, $channels); 108 | } 109 | return $this->defaultChannels; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Commands/MakeOtpAuthCommand.php: -------------------------------------------------------------------------------- 1 | qualifyClass($this->getNameInput()); 51 | $path = $this->getPath($name); 52 | $this->makeDirectory($path); 53 | 54 | $this->files->put($path, $this->sortImports($this->buildClass($name))); 55 | $this->generateOtpAuthTrait($path, explode('/', $this->getNameInput())); 56 | 57 | $this->migrateNotificationTable(); 58 | 59 | $this->info($this->type.' created successfully.'); 60 | } 61 | 62 | /** 63 | * Migrate Notification table 64 | * 65 | * @return void 66 | */ 67 | public function migrateNotificationTable() 68 | { 69 | if (empty(glob(base_path().'/database/migrations/*_create_notifications_table.php'))) { 70 | $this->call('notifications:table'); 71 | } 72 | } 73 | 74 | /** 75 | * Generate Otp Auth Trait 76 | * 77 | * @param string $path 78 | * @param string $nameInput 79 | * 80 | * @return void 81 | */ 82 | public function generateOtpAuthTrait($path, $nameInput) 83 | { 84 | list($path, $name, $otpAuthClass) = $this->qualifyTrait($path, $nameInput); 85 | $this->files->put($path, $this->sortImports($this->buildTrait($name, $otpAuthClass))); 86 | } 87 | 88 | /** 89 | * To qualify Trait 90 | * 91 | * @param string $path 92 | * @param string $nameInput 93 | * 94 | * @return array 95 | */ 96 | public function qualifyTrait($path, $nameInput) 97 | { 98 | $otpAuthClass = array_pop($nameInput); 99 | $name = $this->qualifyClass(implode($nameInput).'/'.$this->traitName); 100 | $path = str_replace($otpAuthClass, $this->traitName, $path); 101 | return [$path, $name, $otpAuthClass]; 102 | } 103 | 104 | /** 105 | * Build Trait 106 | * 107 | * @param string $name 108 | * @param string $otpAuthClass 109 | * 110 | * @return string 111 | */ 112 | protected function buildTrait($name, $otpAuthClass) 113 | { 114 | $stub = $this->files->get($this->getTraitStub()); 115 | 116 | return $this->replaceNamespaceTrait($stub, $name, $otpAuthClass); 117 | } 118 | 119 | /** 120 | * Replace Namespace Trait 121 | * 122 | * @param string $stub 123 | * @param string $name 124 | * @param string $otpAuthClass 125 | * 126 | * @return string 127 | */ 128 | protected function replaceNamespaceTrait(&$stub, $name, $otpAuthClass) 129 | { 130 | return str_replace( 131 | ['DummyNamespace', 'DummyOtpAuthClass'], 132 | [$this->getNamespace($name), $otpAuthClass], 133 | $stub 134 | ); 135 | } 136 | 137 | /** 138 | * Get Trait Stub 139 | * 140 | * @return string 141 | */ 142 | protected function getTraitStub() 143 | { 144 | return __DIR__.'/../stubs/has-otp-auth-trait.stub'; 145 | } 146 | 147 | /** 148 | * Get the stub file for the generator. 149 | * 150 | * @return string 151 | */ 152 | protected function getStub() 153 | { 154 | return __DIR__.'/../stubs/otp-auth.stub'; 155 | } 156 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel OTP AUTH 2 | 3 | This package allows you to authenticate with one time password access (OTP). 4 | 5 | Example Usage: 6 | 7 | ```php 8 | Route::get("/notify", function(){ 9 | return App\Models\User::find(1)->notify(new App\Authentication\SendOtp('mail', 4, 10)); 10 | }); 11 | 12 | Route::get("/auth-otp/{otp}", function(){ 13 | return App\Models\User::authByOtp(request()->otp, '84905.......'); 14 | }); 15 | 16 | Route::get("/check-otp/{otp}", function(){ 17 | return App\Models\User::find(1)->checkOtp(request()->otp); 18 | }); 19 | ``` 20 | 21 | ## Contents 22 | 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Generate OTP](#generate-otp) 26 | - [Verify OTP](#verify-otp) 27 | - [Basic identification](#basic-identification) 28 | - [Demo](#demo) 29 | - [Credits](#credits) 30 | 31 | 32 | ## Installation 33 | 34 | 1- Add the package to your dependencies. 35 | 36 | ``` 37 | $ composer require cuongdinhngo/otp-auth 38 | ``` 39 | 40 | 2- Run the command: 41 | 42 | ```php 43 | php artisan auth:otp {ClassName} 44 | ``` 45 | 46 | Example: 47 | 48 | ```php 49 | php artisan auth:otp Authentication/SendOtp 50 | ``` 51 | 52 | _`SendOtp` class and `HasOtpAuth` trait are auto-generated at `app/Authentication` directory._ 53 | 54 | _`CreateNotificationsTable` class is alseo auto-generated at `app/database/migrations`._ 55 | 56 | 57 | 3- Apply the migrations: 58 | 59 | _It will create a table called `notifications` to store generated OTP information._ 60 | 61 | ``` 62 | $ php artisan migrate 63 | ``` 64 | 65 | 66 | ## Usage 67 | 68 | ### Generate OTP 69 | 70 | You can generate OTP via email or SMS 71 | 72 | ```php 73 | Route::get("/notify", function(){ 74 | return App\Models\User::find(1)->notify(new App\Authentication\SendOtp(['mail', 'nexmo'])); 75 | }); 76 | ``` 77 | 78 | This package allows you to alter OTP length and lifetime 79 | 80 | ```php 81 | Route::get("/notify", function(){ 82 | $length = 4; 83 | $liftime = 10; //minutes 84 | return App\Models\User::find(1)->notify(new App\Authentication\SendOtp(['mail', 'nexmo']), $length, $liftime); 85 | }); 86 | ``` 87 | 88 | 89 | **OTP default length**: The default length is `6`. 90 | 91 | **OTP default lifetime**: The default lifetime is `1` minute. 92 | 93 | There is the detail of auto-generate `SentOTP` class: 94 | 95 | ```php 96 | otp = $this->generateOtp($otpLength ?? self::OPT_LENGTH); 130 | $this->lifeTime = $lifeTime ?? self::OPT_LIFETIME; 131 | $this->defaultChannels = $this->verifyChannels($channels); 132 | } 133 | 134 | /** 135 | * Get the notification's delivery channels. 136 | * 137 | * @param mixed $notifiable 138 | * @return array 139 | */ 140 | public function via($notifiable) 141 | { 142 | return $this->defaultChannels; 143 | } 144 | 145 | /** 146 | * Get the mail representation of the notification. 147 | * 148 | * @param mixed $notifiable 149 | * @return \Illuminate\Notifications\Messages\MailMessage 150 | */ 151 | public function toMail($notifiable) 152 | { 153 | return (new MailMessage) 154 | ->line('Your OTP is '.$this->otp) 155 | ->line('Thank you for using our application!'); 156 | } 157 | 158 | /** 159 | * Get the array representation of the notification. 160 | * 161 | * @param mixed $notifiable 162 | * @return array 163 | */ 164 | public function toArray($notifiable) 165 | { 166 | return [ 167 | 'otp' => $this->otp, 168 | 'expired_at' => now()->addMinutes($this->lifeTime)->toDateTimeString(), 169 | ]; 170 | } 171 | 172 | /** 173 | * Get the Nexmo / SMS representation of the notification. 174 | * 175 | * @param mixed $notifiable 176 | * 177 | * @return mixed 178 | */ 179 | public function toTwilio($notifiable) 180 | { 181 | return (new TwilioMessage) 182 | ->to("+8439xxxxxxx") 183 | ->from("+xxxxxxxxxx") 184 | ->body('OTP AUTH is '.$this->otp); 185 | } 186 | 187 | /** 188 | * Generate OTP 189 | * 190 | * @param integer|string $n 191 | * 192 | * @return string 193 | */ 194 | public function generateOtp($n) 195 | { 196 | $generator = "09xxxxxxx"; 197 | $result = ""; 198 | 199 | for ($i = 1; $i <= $n; $i++) { 200 | $result .= substr($generator, (rand()%(strlen($generator))), 1); 201 | } 202 | return $result; 203 | } 204 | 205 | /** 206 | * Verify channels 207 | * 208 | * @param string|array $channels 209 | * 210 | * @return array 211 | */ 212 | public function verifyChannels($channels) 213 | { 214 | if ($channels && is_array($channels)) { 215 | return array_merge($this->defaultChannels, $channels); 216 | } 217 | if ($channels && is_string($channels)) { 218 | array_push($this->defaultChannels, $channels); 219 | } 220 | return $this->defaultChannels; 221 | } 222 | } 223 | 224 | ``` 225 | 226 | **toTwilio**: This method is implemented by importing [delivery-channels](https://github.com/cuongdinhngo/delivery-channels). 227 | 228 | ### Verify OTP 229 | 230 | After sent OTP via your configed methods, you call `authByOtp` to authenticate 231 | 232 | ```php 233 | Route::get("/auth-otp/{otp}", function(){ 234 | return App\Models\User::authByOtp(request()->otp, '84905123456'); 235 | }); 236 | ``` 237 | Based on your credentials, you might authenticate with email or phone number 238 | 239 | #### Set up the credentials: 240 | 241 | In this case, you can apply `User` model which must use `HasOtpAuth` trait 242 | 243 | ```php 244 | . . . . 245 | use App\Authentication\HasOtpAuth; 246 | 247 | class User extends Authenticatable 248 | { 249 | use Notifiable; 250 | use HasOtpAuth; 251 | 252 | protected $credential = 'mobile'; 253 | 254 | . . . . 255 | ``` 256 | 257 | Let see more detail `HasOtpAuth` trait 258 | 259 | ```php 260 | otp(); 274 | return $this->validateOtp($authenticator, $otp); 275 | } 276 | 277 | /** 278 | * Get OTP data 279 | * 280 | * @return \Illuminate\Notifications\DatabaseNotification 281 | */ 282 | public function otp() 283 | { 284 | return $this->notifications() 285 | ->where('type', 'LIKE', '%SendOtp%') 286 | ->whereNull('read_at') 287 | ->first(); 288 | } 289 | 290 | /** 291 | * Validate OTP 292 | * 293 | * @param \Illuminate\Notifications\DatabaseNotification $authenticator 294 | * @param mixed $otp 295 | * 296 | * @return void 297 | */ 298 | public function validateOtp($authenticator, $otp) 299 | { 300 | $result = false; 301 | if (is_null($authenticator)) { 302 | return response()->json($result,200); 303 | } 304 | if ($authenticator 305 | && now()->lte($authenticator->data['expired_at']) 306 | && $authenticator->data['otp'] == $otp 307 | ) { 308 | $result = true; 309 | } 310 | $authenticator->markAsRead(); 311 | return response()->json($result,200); 312 | } 313 | 314 | /** 315 | * Authenticate by OTP 316 | * 317 | * @param string $otp 318 | * @param string $credentialValue 319 | * @return void 320 | */ 321 | public static function authByOtp($otp, $credentialValue) 322 | { 323 | $model = new static; 324 | $credentialName = property_exists($model,'credential') ? $model->credential : 'email'; 325 | 326 | $authenticator = $model->where($credentialName, '=', $credentialValue)->first(); 327 | if (is_null($authenticator)) { 328 | return response()->json(false,200); 329 | } 330 | 331 | $authenticator = $authenticator->notifications() 332 | ->where('type', 'LIKE', '%SendOtp%') 333 | ->whereNull('read_at') 334 | ->first(); 335 | 336 | return $model->validateOtp($authenticator, $otp); 337 | } 338 | } 339 | ``` 340 | 341 | ### Basic identification 342 | 343 | In some cases, you just need to identify the right access, you might need to execute `checkOtp` method 344 | 345 | ```php 346 | Route::get("/check-otp/{otp}", function(){ 347 | return auth()->user->checkOtp(request()->otp); 348 | }); 349 | ``` 350 | 351 | ### Demo 352 | 353 | This is demo soure code. 354 | [Laravel Colab](https://github.com/cuongdinhngo/lara-colab/blob/master/alpha/routes/web.php) 355 | 356 | ## Credits 357 | 358 | - Ngo Dinh Cuong 359 | 360 | [LinkedIn](https://www.linkedin.com/in/ngodinhcuong/) 361 | --------------------------------------------------------------------------------