├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── config.php ├── database ├── factories │ └── OTPNotifiableUserFactory.php └── migrations │ ├── 2021_11_02_000000_create_otp_tokens_table.php │ └── 2025_01_28_000000_add_indicator_to_otp_tokens_table.php ├── lang └── en │ └── otp.php └── src ├── Concerns └── HasOTPNotify.php ├── Contracts ├── AbstractTokenRepository.php ├── NotifiableRepositoryInterface.php ├── OTPNotifiable.php ├── SMSClient.php └── TokenRepositoryInterface.php ├── Exceptions ├── OTPException.php └── SMSClientNotFoundException.php ├── NotifiableRepository.php ├── Notifications ├── Channels │ └── OTPSMSChannel.php ├── Messages │ ├── MessagePayload.php │ └── OTPMessage.php └── OTPNotification.php ├── OTPBroker.php ├── ServiceProvider.php ├── Token ├── CacheTokenRepository.php ├── DatabaseTokenRepository.php └── TokenRepositoryManager.php ├── UserProviderResolver.php └── helpers.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.5.0 - 2025-04-16 2 | - Add support Laravel 12 3 | 4 | ## 5.3.0 - 2025-04-14 5 | - Add support template for `OTPMessage` 6 | 7 | ## 5.2.0 - 2025-02-01 8 | - Add support custom indicator for managing multiple OTP tokens 9 | 10 | ## 5.1.0 - 2025-01-27 11 | - Fix exception message handling 12 | 13 | ## 5.0.0 - 2025-01-26 14 | - Add support otp lifetime token functionality with expiration handling 15 | - Replace `InvalidOTPTokenException` and `UserNotFoundByMobileException` with `OTPException` for better error handling 16 | 17 | ## 4.3.0 - 2024-06-21 18 | - Add support only confirm token 19 | 20 | ## 4.2.0 - 2024-04-12 21 | - Add support Laravel 11 22 | - Detracted Laravel 9 23 | 24 | ## 4.0.0 - 2023-03-29 25 | - Add support for Laravel V10 26 | 27 | ## 3.0.1 - 2022-11-10 28 | - Fix database expire token logic 29 | 30 | ## 3.0.0 - 2022-02-16 31 | - Add Support PHP 8.0 and Laravel 9.0 32 | - Deprecated PHP <= 7.* and Laravel <=8.* 33 | 34 | ## 1.0.0 - 2022-02-16 35 | - Init (Support PHP 8.0, 7.4, 7.3 and Laravel 8.*, 7.*, 6.*) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 M.Fouladgar 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel OTP(One-Time Password) 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/fouladgar/laravel-otp.svg)](https://packagist.org/packages/fouladgar/laravel-otp) 4 | ![Test Status](https://img.shields.io/github/actions/workflow/status/mohammad-fouladgar/laravel-otp/run-tests.yml?label=tests) 5 | ![Code Style Status](https://img.shields.io/github/actions/workflow/status/mohammad-fouladgar/laravel-otp/php-cs-fixer.yml?label=code%20style) 6 | ![Total Downloads](https://img.shields.io/packagist/dt/fouladgar/laravel-otp) 7 | 8 | ## Introduction 9 | 10 | Most web applications need an OTP(one-time password) or secure code to validate their users. This package allows you to 11 | send/resend and validate OTP for users authentication with user-friendly methods. 12 | 13 | ## Version Compatibility 14 | 15 | | Laravel | Laravel-OTP | 16 | |:---------------|:------------| 17 | | 12.0.x | 5.5.x | 18 | | 11.0.x | 4.2.x | 19 | | 10.0.x | 4.0.x | 20 | | 9.0.x | 3.0.x | 21 | | 6.0.x to 8.0.x | 1.0.x | 22 | 23 | ## Basic Usage: 24 | 25 | ```php 26 | send('+98900000000'); 34 | // Or 35 | OTP('+98900000000'); 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | Send OTP via channels. 40 | |-------------------------------------------------------------------------- 41 | */ 42 | OTP()->channel(['otp_sms', 'mail', \App\Channels\CustomSMSChannel::class]) 43 | ->send('+98900000000'); 44 | // Or 45 | OTP('+98900000000', ['otp_sms', 'mail', \App\Channels\CustomSMSChannel::class]); 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Send OTP for specific user provider 50 | |-------------------------------------------------------------------------- 51 | */ 52 | OTP()->useProvider('admins') 53 | ->send('+98900000000'); 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Validate OTP 58 | |-------------------------------------------------------------------------- 59 | */ 60 | OTP()->validate('+98900000000', 'token_123'); 61 | // Or 62 | OTP('+98900000000', 'token_123'); 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Validate OTP for specific user provider 67 | |-------------------------------------------------------------------------- 68 | */ 69 | OTP()->useProvider('users') 70 | ->validate('+98900000000', 'token_123'); 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | You may wish to only confirm the token 74 | |-------------------------------------------------------------------------- 75 | */ 76 | OTP()->onlyConfirmToken() 77 | ->validate('+98900000000', 'token_123'); 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | You may wish to use a custom indicator 82 | |-------------------------------------------------------------------------- 83 | */ 84 | OTP()->indicator('custom_indicator') 85 | ->send('+98900000000'); 86 | 87 | OTP()->indicator('custom_indicator') 88 | ->onlyConfirmToken() 89 | ->validate('+98900000000', 'token_123'); 90 | ``` 91 | 92 | ## Installation 93 | 94 | You can install the package via composer: 95 | 96 | ```shell 97 | composer require fouladgar/laravel-otp 98 | ``` 99 | 100 | ## Configuration 101 | 102 | As next step, let's publish config file `config/otp.php` by executing: 103 | 104 | ``` 105 | php artisan vendor:publish --provider="Fouladgar\OTP\ServiceProvider" --tag="config" 106 | ``` 107 | 108 | ### Token Storage 109 | 110 | Package allows you to store the generated one-time password on either `cache` or `database` driver, default is `cache`. 111 | 112 | You can change the preferred driver through config file that we published earlier: 113 | 114 | ```php 115 | // config/otp.php 116 | 117 | 'cache', 124 | ]; 125 | ``` 126 | 127 | ##### Cache 128 | 129 | Note that `Laravel OTP` package uses the already configured `cache` driver to storage token, if you have not configured 130 | one yet or have not planned to do it you can use `database` instead. 131 | 132 | ##### Database 133 | 134 | It means after migrating, a table will be created which your application needs to store verification tokens. 135 | 136 | > If you’re using another column name for `mobile` phone or even `otp_tokens` table, you can customize their values in 137 | > config file: 138 | 139 | ```php 140 | // config/otp.php 141 | 142 | 'mobile', 147 | 148 | 'token_table' => 'otp_token', 149 | 150 | //... 151 | ]; 152 | 153 | ``` 154 | 155 | Depending on the `token_storage` config, the package will create a token table. Also, a `mobile` column will be added to 156 | your `users` ([default provider](#user-providers)) table to show user verification state and store user's mobile phone. 157 | 158 | All right! Now you should migrate the database: 159 | 160 | ``` 161 | php artisan migrate 162 | ``` 163 | 164 | > **Note:** When you are using OTP to login user, consider all columns must be nullable except for the `mobile` column. 165 | > Because, after verifying OTP, a user record will be created if the user does not exist. 166 | 167 | ### Token Life Time 168 | 169 | You can specify an OTP `token_lifetime`, ensuring that once an OTP token is sent to the user, no new OTP token will be 170 | generated or sent until the current token has expired. 171 | 172 | ```php 173 | // config/otp.php 174 | 175 | env('OTP_TOKEN_LIFE_TIME', 5), 181 | ], 182 | 183 | //... 184 | ]; 185 | ``` 186 | 187 | ### User providers 188 | 189 | You may wish to use the OTP for variant users. Laravel OTP allows you to define and manage many user providers that you 190 | need. In order to set up, you should open `config/otp.php` file and define your providers: 191 | 192 | ```php 193 | // config/otp.php 194 | 195 | 'users', 201 | 202 | 'user_providers' => [ 203 | 'users' => [ 204 | 'table' => 'users', 205 | 'model' => \App\Models\User::class, 206 | 'repository' => \Fouladgar\OTP\NotifiableRepository::class, 207 | ], 208 | 209 | // 'admins' => [ 210 | // 'model' => \App\Models\Admin::class, 211 | // 'repository' => \Fouladgar\OTP\NotifiableRepository::class, 212 | // ], 213 | ], 214 | 215 | //... 216 | ]; 217 | ``` 218 | 219 | > **Note:** You may also change the default repository and replace your own repository. however, every repository must 220 | > implement `Fouladgar\OTP\Contracts\NotifiableRepositoryInterface` interface. 221 | 222 | #### Model Preparation 223 | 224 | Every model must implement `Fouladgar\OTP\Contracts\OTPNotifiable` and also use 225 | this `Fouladgar\OTP\Concerns\HasOTPNotify` trait: 226 | 227 | ```php 228 | SMSService->send($payload->to(), $payload->content()); 273 | } 274 | 275 | // ... 276 | } 277 | ``` 278 | 279 | > In above example, `SMSService` can be replaced with your chosen SMS service along with its respective method. 280 | 281 | Next, you should set the client wrapper `SampleSMSClient` class in config file: 282 | 283 | ```php 284 | // config/otp.php 285 | 286 | \App\SampleSMSClient::class, 291 | 292 | //... 293 | ]; 294 | ``` 295 | 296 | ## Practical Example 297 | 298 | Here we have prepared a practical example. Suppose you are going to login/register a user by sending an OTP: 299 | 300 | ```php 301 | OTPService->send($request->get('mobile')); 323 | } catch (Throwable $ex) { 324 | // or prepare and return a view. 325 | return response()->json(['message' => 'An unexpected error occurred.'], 500); 326 | } 327 | 328 | return response()->json(['message' => 'A token has been sent to:'. $user->mobile]); 329 | } 330 | 331 | public function verifyOTPAndLogin(Request $request): JsonResponse 332 | { 333 | try { 334 | /** @var User $user */ 335 | $user = $this->OTPService->validate($request->get('mobile'), $request->get('token')); 336 | 337 | // and do login actions... 338 | 339 | } catch (OTPException $exception){ 340 | return response()->json(['error' => $exception->getMessage()],$exception->getCode()); 341 | } catch (Throwable $ex) { 342 | return response()->json(['message' => 'An unexpected error occurred.'], 500); 343 | } 344 | 345 | return response()->json(['message' => 'Logged in successfully.']); 346 | } 347 | } 348 | 349 | ``` 350 | 351 | ## Customization 352 | 353 | ### Notification Default Channel Customization 354 | 355 | For sending OTP notification there is a default channel. But this package allows you to use your own notification 356 | channel. In order to replace, you should specify channel class here: 357 | 358 | ```php 359 | //config/otp.php 360 | \Fouladgar\OTP\Notifications\Channels\OTPSMSChannel::class, 366 | ]; 367 | ``` 368 | 369 | > **Note:** If you change the default sms channel, the `sms_client` will be an optional config. Otherwise, you must 370 | > define your sms client. 371 | 372 | ### Notification SMS and Email Customization 373 | 374 | OTP notification prepares a default sms and email format that are satisfied for most application. However, you can 375 | customize how the mail/sms message is constructed. 376 | 377 | To get started, pass a closure to the `toSMSUsing/toMailUsing` method provided by 378 | the `Fouladgar\OTP\Notifications\OTPNotification` notification. The closure will receive the notifiable model instance 379 | that is receiving the notification as well as the `token` for validating. Typically, you should call the those methods 380 | from the boot method of your application's `App\Providers\AuthServiceProvider` class: 381 | 382 | ```php 383 | (new OTPMessage()) 395 | ->to($notifiable->mobile) 396 | ->content('Your OTP Token is: '.$token)) 397 | ->template('OTP_TEMPLATE'); 398 | 399 | //Email Customization 400 | OTPNotification::toMailUsing(fn ($notifiable, $token) =>(new MailMessage) 401 | ->subject('OTP Request') 402 | ->line('Your OTP Token is: '.$token)); 403 | } 404 | ``` 405 | 406 | ## Translates 407 | 408 | To publish translation file you may use this command: 409 | 410 | ``` 411 | php artisan vendor:publish --provider="Fouladgar\OTP\ServiceProvider" --tag="lang" 412 | ``` 413 | 414 | you can customize in provided language file: 415 | 416 | ```php 417 | // resources/lang/vendor/OTP/en/otp.php 418 | 419 | 'Your OTP Token is: :token.', 423 | 424 | 'otp_subject' => 'OTP request', 425 | ]; 426 | ``` 427 | 428 | ## Testing 429 | 430 | ```sh 431 | composer test 432 | ``` 433 | 434 | ## Changelog 435 | 436 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 437 | 438 | ## Contributing 439 | 440 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 441 | 442 | ## Security 443 | 444 | If you discover any security related issues, please email fouladgar.dev@gmail.com instead of using the issue tracker. 445 | 446 | ## License 447 | 448 | Laravel-OTP is released under the MIT License. See the bundled 449 | [LICENSE](https://github.com/mohammad-fouladgar/laravel-mobile-verification/blob/master/LICENSE) 450 | file for details. 451 | 452 | Built with :heart: for you. 453 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fouladgar/laravel-otp", 3 | "description": "This package provides convenient methods for sending and validating OTP notifications to users for authentication.", 4 | "keywords": [ 5 | "otp", 6 | "otp-authentication", 7 | "otp-login", 8 | "laravel-otp", 9 | "one-time-password", 10 | "laravel", 11 | "lumen" 12 | ], 13 | "support": { 14 | "issues": "https://github.com/mohammad-fouladgar/laravel-otp/issues", 15 | "source": "https://github.com/mohammad-fouladgar/laravel-otp" 16 | }, 17 | "authors": [ 18 | { 19 | "name": "Mohammad Fouladgar", 20 | "email": "fouladgar.dev@gmail.com", 21 | "role": "Developer" 22 | } 23 | ], 24 | "license": "MIT", 25 | "require": { 26 | "php": "^8.2", 27 | "illuminate/database": "^10.0|^11.0|^12.0", 28 | "illuminate/support": "^10.0|^11.0|^12.0", 29 | "illuminate/notifications": "^10.0|^11.0|^12.0" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^10.5.17|^11.5.3", 33 | "orchestra/testbench": "^7.0|^8.0|^10.0", 34 | "mockery/mockery": "^1.4", 35 | "php-coveralls/php-coveralls": "^2.1" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Fouladgar\\OTP\\": "src/" 40 | }, 41 | "files": [ 42 | "src/helpers.php" 43 | ] 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Fouladgar\\OTP\\Tests\\": "tests/", 48 | "Fouladgar\\OTP\\Database\\Factories\\": "database/factories" 49 | } 50 | }, 51 | "scripts": { 52 | "test": "vendor/bin/phpunit", 53 | "test-coverage": "vendor/bin/php-coveralls -v" 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Fouladgar\\OTP\\ServiceProvider" 59 | ] 60 | } 61 | }, 62 | "config": { 63 | "discard-changes": true 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'users', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | User Providers 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Here you should specify your user providers. This defines how the users are actually retrieved out of your 22 | | database or other storage mechanisms used by this application to persist your user's data. 23 | | 24 | | Keep in mind, every model must implement "Fouladgar\OTP\Contracts\OTPNotifiable" and also 25 | | use this "Fouladgar\OTP\Concerns\HasOTPNotify" trait. 26 | | 27 | | You may also change the default repository and replace your own repository. But every repository must 28 | | implement "Fouladgar\OTP\Contracts\NotifiableRepositoryInterface" interface. 29 | | 30 | */ 31 | 'user_providers' => [ 32 | 'users' => [ 33 | 'table' => 'users', 34 | 'model' => App\Models\User::class, 35 | 'repository' => Fouladgar\OTP\NotifiableRepository::class, 36 | ], 37 | 38 | // 'admins' => [ 39 | // 'model' => \App\Models\Admin::class, 40 | // 'repository' => \Fouladgar\OTP\NotifiableRepository::class, 41 | // ], 42 | ], 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Default Mobile Column 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Here you should specify name of your column (in users table) which user 50 | | mobile number reside in. 51 | | 52 | */ 53 | 'mobile_column' => 'mobile', 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Default OTP Tokens Table Name 58 | |-------------------------------------------------------------------------- 59 | | 60 | | Here you should specify name of your OTP tokens table in database. 61 | | This table will held all information about created OTP tokens for users. 62 | | 63 | */ 64 | 'token_table' => 'otp_tokens', 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Verification Token Length 69 | |-------------------------------------------------------------------------- 70 | | 71 | | Here you can specify length of OTP tokens which will send to users. 72 | | 73 | */ 74 | 'token_length' => env('OTP_TOKEN_LENGTH', 5), 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Verification Token Lifetime 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Here you can specify lifetime of OTP tokens (in minutes) which will send to users. 82 | | 83 | */ 84 | 'token_lifetime' => env('OTP_TOKEN_LIFE_TIME', 5), 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | OTP Prefix 89 | |-------------------------------------------------------------------------- 90 | | 91 | | Here you can specify prefix of OTP tokens for adding to cache. 92 | | 93 | */ 94 | 'prefix' => 'otp_', 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | SMS Client (REQUIRED) 99 | |-------------------------------------------------------------------------- 100 | | 101 | | Here you should specify your implemented "SMS Client" class. This class is responsible 102 | | for sending SMS to users. You may use your own sms channel, so this is not a required option anymore. 103 | | 104 | */ 105 | 'sms_client' => '', 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Token Storage Driver 110 | |-------------------------------------------------------------------------- 111 | | 112 | | Here you may define token "storage" driver. If you choose the "cache", the token will be stored 113 | | in a cache driver configured by your application. Otherwise, a table will be created for storing tokens. 114 | | 115 | | Supported drivers: "cache", "database" 116 | | 117 | */ 118 | 'token_storage' => env('OTP_TOKEN_STORAGE', 'cache'), 119 | 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | Default SMS Notification Channel 123 | |-------------------------------------------------------------------------- 124 | | 125 | | This is an otp default sms channel. But you may specify your own sms channel. 126 | | If you use default channel you must set "sms_client". Otherwise you don't need that. 127 | | 128 | */ 129 | 'channel' => Fouladgar\OTP\Notifications\Channels\OTPSMSChannel::class, 130 | ]; 131 | -------------------------------------------------------------------------------- /database/factories/OTPNotifiableUserFactory.php: -------------------------------------------------------------------------------- 1 | '+989389599530', 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/migrations/2021_11_02_000000_create_otp_tokens_table.php: -------------------------------------------------------------------------------- 1 | userTable = config('otp.user_providers.'.$default.'.table', 'users'); 19 | $this->mobileColumn = config('otp.mobile_column', 'mobile'); 20 | $this->tokenTable = config('otp.token_table', 'otp_tokens'); 21 | } 22 | 23 | public function up(): void 24 | { 25 | if (!Schema::hasColumn($this->userTable, $this->mobileColumn)) { 26 | Schema::table( 27 | $this->userTable, 28 | function (Blueprint $table): void { 29 | $table->string($this->mobileColumn); 30 | } 31 | ); 32 | } 33 | 34 | if (config('otp.token_storage') === 'cache') { 35 | return; 36 | } 37 | 38 | Schema::create( 39 | $this->tokenTable, 40 | static function (Blueprint $table): void { 41 | $table->increments('id'); 42 | $table->string('mobile')->index(); 43 | $table->string('token', 10)->index(); 44 | $table->timestamp('sent_at')->nullable(); 45 | $table->timestamp('expires_at')->nullable(); 46 | 47 | $table->index(['mobile', 'token']); 48 | } 49 | ); 50 | } 51 | 52 | public function down(): void 53 | { 54 | if (config('otp.token_storage') === 'cache') { 55 | return; 56 | } 57 | Schema::drop($this->tokenTable); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /database/migrations/2025_01_28_000000_add_indicator_to_otp_tokens_table.php: -------------------------------------------------------------------------------- 1 | tokenTable = config('otp.token_table', 'otp_tokens'); 16 | $this->defaultIndicator = config('otp.prefix', 'otp_tokens'); 17 | } 18 | 19 | public function up(): void 20 | { 21 | if (Schema::hasTable($this->tokenTable)) { 22 | Schema::table($this->tokenTable, function (Blueprint $table): void { 23 | $table->string('indicator')->default($this->defaultIndicator); 24 | }); 25 | } 26 | } 27 | 28 | public function down(): void 29 | { 30 | if (Schema::hasTable($this->tokenTable)) { 31 | Schema::table($this->tokenTable, static function (Blueprint $table): void { 32 | $table->dropColumn('indicator'); 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lang/en/otp.php: -------------------------------------------------------------------------------- 1 | 'Your OTP Token is: :token.', 14 | 15 | 'otp_subject' => 'OTP request', 16 | 17 | 'token_has_been_expired_or_invalid' => 'the token has been expired or invalid', 18 | 19 | 'user_not_found_by_mobile' => 'user not found by mobile', 20 | 21 | 'otp_has_already_been_sent_for_this_mobile' => 'OTP has already been sent for this mobile', 22 | ]; 23 | -------------------------------------------------------------------------------- /src/Concerns/HasOTPNotify.php: -------------------------------------------------------------------------------- 1 | appendMobileToFillableAttributes(); 12 | 13 | return $this->fillable; 14 | } 15 | 16 | public function sendOTPNotification(string $token, array $channel): void 17 | { 18 | $this->notify(new OTPNotification($token, $channel)); 19 | } 20 | 21 | public function getMobileForOTPNotification(): string 22 | { 23 | return $this->{$this->getOTPMobileField()}; 24 | } 25 | 26 | public function routeNotificationForOTP(): string 27 | { 28 | return $this->{$this->getOTPMobileField()}; 29 | } 30 | 31 | private function appendMobileToFillableAttributes(): void 32 | { 33 | $mobileFiled = $this->getOTPMobileField(); 34 | 35 | if (! in_array($mobileFiled, $this->fillable, true)) { 36 | $this->fillable = array_merge($this->fillable, [$mobileFiled]); 37 | } 38 | } 39 | 40 | private function getOTPMobileField(): string 41 | { 42 | return config('otp.mobile_column', 'mobile'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Contracts/AbstractTokenRepository.php: -------------------------------------------------------------------------------- 1 | getMobileForOTPNotification(); 18 | 19 | $this->deleteExisting($user, $indicator); 20 | 21 | $token = $this->createNewToken(); 22 | 23 | $this->save($mobile, $indicator, $token); 24 | 25 | return $token; 26 | } 27 | 28 | protected function createNewToken(): string 29 | { 30 | return (string) random_int(10 ** ($this->tokenLength - 1), (10 ** $this->tokenLength) - 1); 31 | } 32 | 33 | protected function tokenExpired(string $expiresAt): bool 34 | { 35 | return Carbon::parse($expiresAt)->isPast(); 36 | } 37 | 38 | protected function getPayload(string $mobile, string $indicator, string $token): array 39 | { 40 | return ['mobile' => $mobile, 'indicator' => $indicator, 'token' => $token, 'sent_at' => now()->toDateTimeString()]; 41 | } 42 | 43 | /** 44 | * Insert into token storage. 45 | */ 46 | abstract protected function save(string $mobile, string $indicator, string $token): bool; 47 | } 48 | -------------------------------------------------------------------------------- /src/Contracts/NotifiableRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | mobileColumn = config('otp.mobile_column'); 15 | } 16 | 17 | public function findOrCreateByMobile(string $mobile): OTPNotifiable 18 | { 19 | return $this->model->firstOrCreate([$this->mobileColumn => $mobile]); 20 | } 21 | 22 | public function findByMobile(string $mobile): ?OTPNotifiable 23 | { 24 | return $this->model->where([$this->mobileColumn => $mobile])->first(['id', $this->mobileColumn]); 25 | } 26 | 27 | public function getModel(): OTPNotifiable 28 | { 29 | return $this->model; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Notifications/Channels/OTPSMSChannel.php: -------------------------------------------------------------------------------- 1 | routeNotificationFor('otp', $notification)) { 19 | return null; 20 | } 21 | 22 | /** @var OTPMessage $message */ 23 | $message = $notification->toSMS($notifiable); 24 | 25 | return $this->SMSClient->sendMessage($message->getPayload()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Notifications/Messages/MessagePayload.php: -------------------------------------------------------------------------------- 1 | to; 19 | } 20 | 21 | public function content(): string 22 | { 23 | return $this->content; 24 | } 25 | 26 | public function from(): string 27 | { 28 | return $this->from; 29 | } 30 | 31 | public function template(): mixed 32 | { 33 | return $this->template; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Notifications/Messages/OTPMessage.php: -------------------------------------------------------------------------------- 1 | content = $content; 18 | 19 | return $this; 20 | } 21 | 22 | public function to(string $to): static 23 | { 24 | $this->to = $to; 25 | 26 | return $this; 27 | } 28 | 29 | public function from(string $from): static 30 | { 31 | $this->from = $from; 32 | 33 | return $this; 34 | } 35 | 36 | public function template(mixed $template): static 37 | { 38 | $this->template = $template; 39 | 40 | return $this; 41 | } 42 | 43 | public function getPayload(): MessagePayload 44 | { 45 | return (new MessagePayload($this->to, $this->content, $this->from, $this->template)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Notifications/OTPNotification.php: -------------------------------------------------------------------------------- 1 | channels; 36 | } 37 | 38 | public function toSMS($notifiable) 39 | { 40 | if (static::$toSMSCallback) { 41 | return call_user_func(static::$toSMSCallback, $notifiable, $this->token); 42 | } 43 | 44 | return $this->buildSMSMessage($notifiable); 45 | } 46 | 47 | public function toMail($notifiable) 48 | { 49 | if (static::$toMailCallback) { 50 | return call_user_func(static::$toMailCallback, $notifiable, $this->token); 51 | } 52 | 53 | return $this->buildMailMessage(); 54 | } 55 | 56 | protected function buildSMSMessage($notifiable): OTPMessage 57 | { 58 | return (new OTPMessage()) 59 | ->to($notifiable->getMobileForOTPNotification()) 60 | ->content(Lang::get('OTP::otp.otp_token', ['token' => $this->token])); 61 | } 62 | 63 | protected function buildMailMessage(): MailMessage 64 | { 65 | return (new MailMessage) 66 | ->subject(Lang::get('OTP::otp.otp_subject')) 67 | ->line(Lang::get('OTP::otp.otp_token', ['token' => $this->token])); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/OTPBroker.php: -------------------------------------------------------------------------------- 1 | channel = $this->getDefaultChannel(); 32 | $this->indicator = $this->getDefaultIndicator(); 33 | $this->userRepository = $this->resolveUserRepository(); 34 | } 35 | 36 | /** 37 | * @throws Throwable 38 | */ 39 | public function send(string $mobile, bool $userExists = false): OTPNotifiable 40 | { 41 | $user = $userExists ? $this->findUserByMobile($mobile) : null; 42 | 43 | throw_if(! $user && $userExists, OTPException::whenUserNotFoundByMobile()); 44 | throw_if($this->tokenExists($mobile), OTPException::whenOtpAlreadySent()); 45 | 46 | $notifiable = $user ?? $this->makeNotifiable($mobile); 47 | 48 | $this->token = $this->tokenRepository->create($notifiable, $this->indicator); 49 | 50 | $notifiable->sendOTPNotification( 51 | $this->token, 52 | $this->channel 53 | ); 54 | 55 | return $notifiable; 56 | } 57 | 58 | /** 59 | * @throws OTPException|Throwable 60 | */ 61 | public function validate(string $mobile, string $token, bool $create = true): OTPNotifiable 62 | { 63 | $notifiable = $this->makeNotifiable($mobile); 64 | 65 | throw_unless($this->verifyToken($notifiable, $token), OTPException::whenOtpTokenIsInvalid()); 66 | 67 | if (! $this->onlyConfirm) { 68 | $notifiable = $this->find($mobile, $create); 69 | } 70 | 71 | $this->revoke($notifiable); 72 | 73 | return $notifiable; 74 | } 75 | 76 | public function onlyConfirmToken(bool $confirm = true): static 77 | { 78 | $this->onlyConfirm = $confirm; 79 | 80 | return $this; 81 | } 82 | 83 | public function indicator(string $indicator): static 84 | { 85 | $this->indicator = $indicator; 86 | 87 | return $this; 88 | } 89 | 90 | public function getToken(): ?string 91 | { 92 | return $this->token; 93 | } 94 | 95 | /** 96 | * @throws Exception 97 | */ 98 | public function useProvider(string $name = null): OTPBroker 99 | { 100 | $this->userRepository = $this->resolveUserRepository($name); 101 | 102 | return $this; 103 | } 104 | 105 | public function channel($channel = ['']): static 106 | { 107 | $this->channel = is_array($channel) ? $channel : func_get_args(); 108 | 109 | return $this; 110 | } 111 | 112 | public function revoke(OTPNotifiable $user): bool 113 | { 114 | return $this->tokenRepository->deleteExisting($user, $this->indicator); 115 | } 116 | 117 | /** 118 | * @throws \Exception 119 | */ 120 | protected function resolveUserRepository(string $name = null): NotifiableRepositoryInterface 121 | { 122 | return $this->providerResolver->resolve($name); 123 | } 124 | 125 | private function find(string $mobile, bool $create = true): ?OTPNotifiable 126 | { 127 | return $create ? 128 | $this->findOrCreateUser($mobile) : 129 | $this->findUserByMobile($mobile); 130 | } 131 | 132 | private function findOrCreateUser(string $mobile): OTPNotifiable 133 | { 134 | return $this->userRepository->findOrCreateByMobile($mobile); 135 | } 136 | 137 | private function findUserByMobile(string $mobile): ?OTPNotifiable 138 | { 139 | return $this->userRepository->findByMobile($mobile, $this->indicator); 140 | } 141 | 142 | private function getDefaultChannel(): array 143 | { 144 | $channel = config('otp.channel'); 145 | 146 | return is_array($channel) ? $channel : Arr::wrap($channel); 147 | } 148 | 149 | public function verifyToken(OTPNotifiable $user, string $token): bool 150 | { 151 | return $this->tokenRepository->isTokenMatching($user, $this->indicator, $token); 152 | } 153 | 154 | private function tokenExists(string $mobile): bool 155 | { 156 | return $this->tokenRepository->exists($mobile, $this->indicator); 157 | } 158 | 159 | private function makeNotifiable(string $mobile): OTPNotifiable 160 | { 161 | $mobileColumn = config('otp.mobile_column'); 162 | 163 | return $this->userRepository->getModel()->make([$mobileColumn => $mobile]); 164 | } 165 | 166 | private function getDefaultIndicator() 167 | { 168 | return config('otp.prefix'); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | extend( 24 | 'otp_sms', 25 | fn ($app) => new OTPSMSChannel($app->make(config('otp.sms_client'))) 26 | ); 27 | } 28 | ); 29 | 30 | $this->loadAssetsFrom(); 31 | 32 | $this->registerPublishing(); 33 | } 34 | 35 | public function register(): void 36 | { 37 | $this->mergeConfigFrom($this->getConfig(), 'otp'); 38 | 39 | $this->registerBindings(); 40 | } 41 | 42 | protected function loadAssetsFrom(): void 43 | { 44 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 45 | 46 | $this->loadTranslationsFrom(__DIR__.'/../lang', 'OTP'); 47 | } 48 | 49 | protected function registerPublishing(): void 50 | { 51 | $this->publishes([$this->getConfig() => config_path('otp.php')], 'config'); 52 | 53 | $this->publishes([__DIR__.'/../lang' => app()->langPath().'/vendor/OTP'], 'lang'); 54 | 55 | $this->publishes([__DIR__.'/../database/migrations' => database_path('migrations')], 'migrations'); 56 | } 57 | 58 | protected function getConfig(): string 59 | { 60 | return __DIR__.'/../config/config.php'; 61 | } 62 | 63 | protected function registerBindings(): void 64 | { 65 | $this->app->singleton('token.repository', fn ($app) => new TokenRepositoryManager($app)); 66 | 67 | $this->app->singleton(TokenRepositoryInterface::class, fn ($app) => $app['token.repository']->driver()); 68 | 69 | $this->app->singleton( 70 | SMSClient::class, 71 | static function ($app) { 72 | try { 73 | return $app->make(config('otp.sms_client')); 74 | } catch (Throwable $e) { 75 | throw new SMSClientNotFoundException(); 76 | } 77 | } 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Token/CacheTokenRepository.php: -------------------------------------------------------------------------------- 1 | cache->forget($this->getSignatureKey($user->getMobileForOTPNotification(), $indicator)); 25 | } 26 | 27 | /** 28 | * @throws InvalidArgumentException 29 | */ 30 | public function exists(string $mobile, string $indicator): bool 31 | { 32 | return $this->cache->has($this->getSignatureKey($mobile, $indicator)); 33 | } 34 | 35 | /** 36 | * @throws InvalidArgumentException 37 | */ 38 | public function isTokenMatching(OTPNotifiable $user, string $indicator, string $token): bool 39 | { 40 | $exist = $this->exists($user->getMobileForOTPNotification(), $indicator); 41 | $signature = $this->getSignatureKey($user->getMobileForOTPNotification(), $indicator); 42 | 43 | return $exist && $this->cache->get($signature)['token'] === $token; 44 | } 45 | 46 | protected function save(string $mobile, string $indicator, string $token): bool 47 | { 48 | return $this->cache->add( 49 | $this->getSignatureKey($mobile, $indicator), 50 | $this->getPayload($mobile, $indicator, $token), 51 | now()->addMinutes($this->expires) 52 | ); 53 | } 54 | 55 | protected function getSignatureKey($mobile, string $indicator): string 56 | { 57 | return sprintf('%s%s', $indicator, $mobile); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Token/DatabaseTokenRepository.php: -------------------------------------------------------------------------------- 1 | getTable()->where([ 26 | 'mobile' => $user->getMobileForOTPNotification(), 27 | 'indicator' => $indicator, 28 | ]))->delete(); 29 | } 30 | 31 | protected function getLatestRecord(array $filters): ?array 32 | { 33 | $record = $this->getTable() 34 | ->where($filters) 35 | ->latest() 36 | ->first(); 37 | 38 | return $record ? (array)$record : null; 39 | } 40 | 41 | public function exists(string $mobile, string $indicator): bool 42 | { 43 | $record = $this->getLatestRecord(['mobile' => $mobile, 'indicator' => $indicator]); 44 | 45 | return $record && ! $this->tokenExpired($record['expires_at']); 46 | } 47 | 48 | public function isTokenMatching(OTPNotifiable $user, string $indicator, string $token): bool 49 | { 50 | $record = $this->getLatestRecord([ 51 | 'mobile' => $user->getMobileForOTPNotification(), 52 | 'token' => $token, 53 | 'indicator' => $indicator, 54 | ]); 55 | 56 | return $record && ! $this->tokenExpired($record['expires_at']); 57 | } 58 | 59 | protected function getTable(): Builder 60 | { 61 | return $this->connection->table($this->table); 62 | } 63 | 64 | protected function save(string $mobile, string $indicator, string $token): bool 65 | { 66 | return $this->getTable()->insert($this->getPayload($mobile, $indicator, $token)); 67 | } 68 | 69 | protected function getPayload(string $mobile, string $indicator, string $token): array 70 | { 71 | return parent::getPayload($mobile, $indicator, $token) + 72 | ['expires_at' => now()->addMinutes($this->expires), 'indicator' => $indicator]; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Token/TokenRepositoryManager.php: -------------------------------------------------------------------------------- 1 | config->get('otp.token_storage', 'cache'); 15 | } 16 | 17 | protected function createCacheDriver(): TokenRepositoryInterface 18 | { 19 | return new CacheTokenRepository( 20 | $this->container->make(CacheRepository::class), 21 | $this->config->get('otp.token_lifetime', 5), 22 | $this->config->get('otp.token_length', 5), 23 | ); 24 | } 25 | 26 | protected function createDatabaseDriver(): TokenRepositoryInterface 27 | { 28 | return new DatabaseTokenRepository( 29 | $this->container->make(ConnectionInterface::class), 30 | $this->config->get('otp.token_lifetime'), 31 | $this->config->get('otp.token_length'), 32 | $this->config->get('otp.token_table'), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/UserProviderResolver.php: -------------------------------------------------------------------------------- 1 | getUserProviderConfiguration($name ?: $this->getDefaultProvider()); 23 | 24 | if (empty($config)) { 25 | throw new InvalidArgumentException("User provider [{$name}] is not defined."); 26 | } 27 | 28 | $model = $config['model']; 29 | $repository = $config['repository']; 30 | 31 | if (! is_subclass_of($model, OTPNotifiable::class)) { 32 | throw new Exception('Your model must implement "Fouladgar\OTP\Contracts\OTPNotifiable".'); 33 | } 34 | 35 | if (! is_subclass_of($repository, NotifiableRepositoryInterface::class)) { 36 | throw new Exception('Your repository must implement "Fouladgar\OTP\Contracts\NotifiableRepositoryInterface".'); 37 | } 38 | 39 | return new $repository(new $model()); 40 | } 41 | 42 | protected function getDefaultProvider(): string 43 | { 44 | return $this->config->get('otp.default_provider'); 45 | } 46 | 47 | private function getUserProviderConfiguration(string $userProvider): ?array 48 | { 49 | return $this->config->get('otp.user_providers.'.$userProvider); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | send($mobile); 22 | } 23 | 24 | if (is_array($token)) { 25 | return $OTP->channel($token)->send($mobile); 26 | } 27 | 28 | return $OTP->validate($mobile, $token); 29 | } 30 | } 31 | --------------------------------------------------------------------------------