├── .gitignore ├── composer.json ├── config └── otp.php ├── phpunit.xml ├── readme.md ├── src ├── Constants │ ├── DBStates.php │ ├── StatusCodes.php │ └── StatusMessages.php ├── Exceptions │ └── InvalidMethodException.php ├── Models │ └── Otps.php ├── Object │ ├── OtpRequestObject.php │ └── OtpValidateRequestObject.php ├── OtpValidator.php ├── OtpValidatorFacade.php ├── OtpValidatorServiceProvider.php ├── Services │ ├── DatabaseServices.php │ ├── EmailTransportService.php │ ├── OtpMailable.php │ ├── OtpService.php │ ├── Responder.php │ ├── SMSTransportService.php │ ├── SNSTransportService.php │ ├── TransportServiceInterface.php │ └── Transporter.php └── database │ └── migrations │ └── 2020_07_12_000000_create_otps_table.php ├── template-otp ├── email.blade.php └── sms.blade.php └── tests └── Feature └── OTPTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | ### IDE ### 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | *.code-workspace 8 | /.idea 9 | /.php_cs.cache 10 | 11 | ### Packages ### 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | npm-debug.log 16 | yarn-error.log 17 | /storage/*.key 18 | /vendor 19 | /node_modules 20 | composer.lock 21 | composer.phar 22 | 23 | ### Storages ### 24 | /public/storage 25 | .env 26 | .env.backup 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ferdous/laravel-otp-validate", 3 | "description": "Laravel package for OTP validation with built-in features like retry and resend mechanism. Built in max retry and max resend blocking. OTP/Security Code can be send over SMS or Email of your choice with user-defined template.", 4 | "type": "library", 5 | "homepage": "https://github.com/ferdousulhaque/laravel-otp-validate", 6 | "keywords": [ 7 | "php", 8 | "laravel", 9 | "otp", 10 | "security-code", 11 | "email", 12 | "sms", 13 | "otp-validation", 14 | "retry", 15 | "resend" 16 | ], 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "A S Md Ferdousul Haque", 21 | "email": "ferdousul.haque@gmail.com", 22 | "homepage": "http://ferdousulhaque.66ghz.com" 23 | } 24 | ], 25 | "minimum-stability": "dev", 26 | "prefer-stable": true, 27 | "autoload": { 28 | "psr-4": { 29 | "Ferdous\\OtpValidator\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Ferdous\\OtpValidator\\Tests\\": "tests/" 35 | } 36 | }, 37 | "require": { 38 | "php": "^7.4|^8.0", 39 | "guzzlehttp/guzzle": "^7.0.1", 40 | "nesbot/carbon": "^2.22", 41 | "aws/aws-sdk-php": "^3.179" 42 | }, 43 | "require-dev": { 44 | "orchestra/testbench": "^6.0@dev" 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Ferdous\\OtpValidator\\OtpValidatorServiceProvider" 50 | ], 51 | "aliases": { 52 | "OtpVerifier": "Ferdous\\OtpValidator\\OtpValidatorFacade" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /config/otp.php: -------------------------------------------------------------------------------- 1 | env('OTP_SERVICE','enabled'), 5 | 'table-name' => env('OTP_TABLE_NAME','otps'), 6 | 'timeout' => env('OTP_TIMEOUT', 120), 7 | 'digit' => env('OTP_DIGIT', 4), 8 | 'resend' => env('OTP_RESEND_SERVICE', 'enabled'), 9 | 'max-retry' => env('OTP_MAX_RETRY',2), 10 | 'max-resend' => env('OTP_MAX_RESEND',1), 11 | 'service-name' => env('OTP_SERVICE_NAME','OTP Service'), 12 | 'company-name' => env('OTP_COMPANY_NAME','Test Company'), 13 | 'send-by' => [ 14 | 'email' => env('OTP_SEND_BY_EMAIL',0), 15 | 'sms' => env('OTP_SEND_BY_SMS',1), 16 | 'aws-sns' => env('OTP_SEND_BY_AWS_SNS',0), 17 | ], 18 | 'email' => [ 19 | 'from' => env('OTP_EMAIL_FROM','example@mail.com'), 20 | // Class string specifying the `Mailable` class to use when sending e-mail OTPs. 21 | // The constructor of the class will be called with a single string argument (the OTP 22 | // value). 23 | // If this config is left unspecified a default mailable class will be used. 24 | 'mailable-class' => Ferdous\OtpValidator\Services\OtpMailable::class, 25 | 'name' => env('OTP_EMAIL_FROM_NAME','Example'), 26 | 'subject' => env('OTP_EMAIL_SUBJECT','Security Code') 27 | ], 28 | 'smsc' => [ 29 | 'url' => env('OTP_SMSC_URL'), 30 | 'method' => env('OTP_SMSC_METHOD', 'GET'), 31 | 'add_code' => env('OTP_COUNTRY_CODE',null), 32 | 'json' => env('OTP_SMSC_OVER_JSON',1), 33 | 'headers' => [ 34 | 'header1' => '', 35 | 'header2' => '', 36 | 'authKey' => '' 37 | // Add the required headers 38 | ], 39 | 'params' => [ 40 | 'send_to_param_name' => env('OTP_SMSC_PARAM_TO_NAME','number'), 41 | 'msg_param_name' => env('OTP_SMSC_PARAM_MSG_NAME','msg'), 42 | 'others' => [ 43 | 'username' => env('OTP_SMSC_USER'), 44 | 'password' => env('OTP_SMSC_PASS'), 45 | 'param1' => '', 46 | 'param2' => '' 47 | // Add other params to send over request body/query 48 | ], 49 | ] 50 | ], 51 | 'aws' => [ 52 | 'sns' => [ 53 | 'version' => env('AWS_SNS_VERSION','2010-03-31'), 54 | 'credentials' => [ 55 | 'key' => env('AWS_SNS_KEY',null), 56 | 'secret' => env('AWS_SNS_SECRET',null), 57 | ], 58 | 'region' => env('AWS_SNS_REGION','us-east-1'), 59 | 'profile' => env('AWS_SNS_PROFILE',null) 60 | ] 61 | ] 62 | ]; 63 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OTP Validate Package in Laravel 2 | ![Packagist Downloads](https://img.shields.io/packagist/dt/ferdous/laravel-otp-validate) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 4 | ![Packagist Version](https://img.shields.io/packagist/v/ferdous/laravel-otp-validate) 5 | ![Packagist PHP Version Support](https://img.shields.io/packagist/php-v/ferdous/laravel-otp-validate) 6 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/ferdousulhaque/laravel-otp-validate) 7 | 8 | This package is for easy setup for OTP validation process. No hassle, just plug and play. Following the steps mentioned below and you will be able to get a fully working OTP Validation system. You can use this later for authentication or e-commerce production selling, order confirmation. 9 | 10 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/ferdousulhaque) 11 | 12 | ## Major change from version v2.0.1 13 | Only Supports 7.4 and above & 8.0 and above, so any PHP versions below users will need to upgrade to use the latest version onwards. 14 | * PHP version 7.4 and above 15 | * PHP version 8.0 and above 16 | 17 | If you are using PHP 7.4 below, please use version v1.4.0. 18 | 19 | ## Installation 20 | 21 | ### Install Package 22 | Require this package with composer: 23 | ``` 24 | composer require ferdous/laravel-otp-validate 25 | ``` 26 | ### Add Service Provider & Facade 27 | 28 | #### For Laravel 5.5+ 29 | Once the package is added, the service provider and facade will be auto discovered. 30 | 31 | #### For Older versions of Laravel 32 | Add the ServiceProvider to the providers array in `config/app.php`: 33 | ```php 34 | Ferdous\OtpValidator\OtpValidatorServiceProvider::class 35 | ``` 36 | 37 | Add the Facade to the aliases array in `config/app.php`: 38 | ```php 39 | 'OtpValidator' => Ferdous\OtpValidator\OtpValidatorServiceProvider::class 40 | ``` 41 | 42 | ## Publish Config 43 | Once done, publish the config to your config folder using: 44 | ``` 45 | php artisan vendor:publish --provider="Ferdous\OtpValidator\OtpValidatorServiceProvider" 46 | ``` 47 | This command will create a `config/otp.php` file. 48 | 49 | ### Email Configs 50 | From the `.env` file the email configs are setup. No other changes required. 51 | 52 | See the `otp.php` file for optional customizations such as the ability to specify a custom 53 | `Mailable` class. 54 | 55 | ### SMS Configs 56 | As the SMS Gateways use different methods and also extra headers and params, you may need to update the sms configs in the `otp.php` file. 57 | 58 | ## Migrate Database 59 | Run the following command to create the otps table. 60 | ``` 61 | php artisan migrate 62 | ``` 63 | It will create a otps table with the required columns. 64 | 65 | ## Environment 66 | Add the following Key-Value pair to the `.env` file in the Laravel application 67 | 68 | ``` 69 | # Basic OTP Configs 70 | OTP_SERVICE='enabled' 71 | OTP_TABLE_NAME='otps' 72 | OTP_TIMEOUT=120 73 | OTP_DIGIT=5 74 | OTP_RESEND_SERVICE='enabled' 75 | OTP_MAX_RETRY=2 76 | OTP_MAX_RESEND=1 77 | # Company and Service 78 | OTP_SERVICE_NAME= 79 | OTP_COMPANY_NAME= 80 | # OTP via Email / SMS 81 | OTP_SEND_BY_EMAIL=1 82 | OTP_SEND_BY_SMS=1 83 | # Email Configurations 84 | OTP_EMAIL_FROM= 85 | OTP_EMAIL_FROM_NAME= 86 | OTP_EMAIL_SUBJECT= 87 | # SMS Configurations 88 | OTP_SMSC_URL='https://sms' 89 | OTP_SMSC_METHOD= 90 | OTP_COUNTRY_CODE= 91 | OTP_SMSC_OVER_JSON= 92 | OTP_SMSC_PARAM_TO_NAME= 93 | OTP_SMSC_PARAM_MSG_NAME= 94 | OTP_SMSC_USER= 95 | OTP_SMSC_PASS= 96 | AWS_SNS_VERSION= 97 | AWS_SNS_KEY= 98 | AWS_SNS_SECRET= 99 | AWS_SNS_REGION= 100 | ``` 101 | 102 | ## Definitions 103 | Definition of the features in config are: 104 | 105 | - service : enable/disable OTP Service 106 | - timeout: timeout for OTP 107 | - digit: OTP Digit 108 | - resend-service: enable/disable resend Service 109 | - max-retry: max retry for a single request 110 | - max-resend: max resend for a single request 111 | - service-name: for which the service is used 112 | - company-name: for which company 113 | - send-by: there are 3 ways to share otp (Email/SMS/AWS SNS) 114 | - email: this key specifies the required information for email (e.g. from, name, subject etc.) 115 | - sms: configure with SMS gateway to send SMS. 116 | (Universal Configurator) 117 | 118 | ## Defining Send By on Runtime 119 | 120 | The config method can be used to set send-by [ SMS / Email / SNS ] at runtime. 121 | 122 | config('otp.send-by.email', 1); 123 | config('otp.send-by.sms', 0); 124 | 125 | ## OTP Request Templates 126 | Once the template files are published, open `resources/views/vendor/template-otp/` 127 | 128 | ## Sample Controller 129 | Run the following command to create a controller. 130 | 131 | `php artisan make:controller OtpController` 132 | 133 | Below is a sample for calling the OTP Validator in OtpController. 134 | 135 | ```php 136 | namespace App\Http\Controllers; 137 | 138 | use Ferdous\OtpValidator\Object\OtpRequestObject; 139 | use Ferdous\OtpValidator\OtpValidator; 140 | use Ferdous\OtpValidator\Object\OtpValidateRequestObject; 141 | 142 | class OtpController extends Controller 143 | { 144 | /** 145 | * @return array 146 | */ 147 | public function requestForOtp() 148 | { 149 | return OtpValidator::requestOtp( 150 | new OtpRequestObject('1432', 'buy-shirt', '01711084714', 'ferdousul.haque@gmail.com') 151 | ); 152 | } 153 | 154 | /** 155 | * @param Request $request 156 | * @return array 157 | */ 158 | public function validateOtp(Request $request) 159 | { 160 | $uniqId = $request->input('uniqueId'); 161 | $otp = $request->input('otp'); 162 | return OtpValidator::validateOtp( 163 | new OtpValidateRequestObject($uniqId,$otp) 164 | ); 165 | } 166 | 167 | /** 168 | * @param Request $request 169 | * @return array 170 | */ 171 | public function resendOtp(Request $request) 172 | { 173 | $uniqueId = $request->input('uniqueId'); 174 | return OtpValidator::resendOtp($uniqueId); 175 | } 176 | 177 | } 178 | ``` 179 | 180 | Add the following to the `routes/web.php` file. 181 | 182 | ``` 183 | Route::get('/test/otp-request', [\App\Http\Controllers\OtpController::class, 'requestForOtp']); 184 | Route::get('/test/otp-validate', [\App\Http\Controllers\OtpController::class, 'validateOtp']); 185 | Route::get('/test/otp-resend', [\App\Http\Controllers\OtpController::class, 'resendOtp']); 186 | ``` 187 | 188 | ## Response/Error Descriptions 189 | The below table describes the error codes generated in the response and their corresponding meanings. 190 | 191 | ```json 192 | { 193 | "code": 201, 194 | "message": "OTP Sent to the recipient", 195 | "requestId": 1432, 196 | "type": "buy-shirt" 197 | } 198 | ``` 199 | 200 | #### Request OTP Response Codes 201 | 202 | | Code | Meanings 203 | |--------|------------------------------------------ 204 | | 201 | Successfully Generated OTP and shared. 205 | | 400 | Bad request. 206 | | 501 | Resend Service Disabled. 207 | | 503 | Service Unavailable. 208 | 209 | #### OTP Validate Response Codes 210 | 211 | | Code | Meanings 212 | |--------|------------------------------------------ 213 | | 200 | Correct OTP. 214 | | 400 | Invalid OTP. 215 | | 404 | OTP Expired/Not Found. 216 | | 413 | Max Retry Exceeded. 217 | 218 | ## License 219 | MIT 220 | 221 | ## Special Thanks 222 | - [Nahid Bin Azhar](https://github.com/nahid) For the Feedback. 223 | 224 | ## Support 225 | - For any bugs, please help to create an issue. 226 | - For any problem installing or configurations, feel free to knock me. 227 | [ferdousul.haque@gmail.com](mailto:ferdousul.haque@gmail.com) 228 | 229 | ## Featured Article 230 | - [How to create a laravel OTP/Security code verification for e-commerce website](https://medium.com/@ferdousul.haque/how-to-create-a-laravel-otp-security-code-verification-for-e-commerce-website-55de8161cfb8) 231 | 232 | ## Example SMS Gateways Configuration 233 | 234 | ### [Muthofun](https://www.muthofun.com/) 235 | If you are trying to integrate one of most popular SMS gateway of Bangladesh, muthofun is a popular Bulk SMS Gateway in our country. Here is a sample configuration for the Muthofun SMS Gateway 236 | 237 | ```php 238 | 'smsc' => [ 239 | 'url' => env('OTP_SMSC_URL'), 240 | 'method' => env('OTP_SMSC_METHOD', 'GET'), 241 | 'add_code' => env('OTP_COUNTRY_CODE',null), 242 | 'json' => env('OTP_SMSC_OVER_JSON',1), 243 | 'headers' => [], 244 | 'params' => [ 245 | 'send_to_param_name' => env('OTP_SMSC_PARAM_TO_NAME','number'), 246 | 'msg_param_name' => env('OTP_SMSC_PARAM_MSG_NAME','msg'), 247 | 'others' => [ 248 | 'user' => env('OTP_SMSC_USER'), 249 | 'password' => env('OTP_SMSC_PASS'), 250 | 'unicode' => 1 251 | ], 252 | ] 253 | ]; 254 | ``` 255 | 256 | .env file will be as the following 257 | 258 | ``` 259 | OTP_SMSC_URL='http://clients.muthofun.com:8901/esmsgw/sendsms.jsp?' 260 | OTP_SMSC_METHOD='GET' 261 | OTP_COUNTRY_CODE='88' 262 | OTP_SMSC_OVER_JSON=0 263 | OTP_SMSC_PARAM_TO_NAME='mobiles' 264 | OTP_SMSC_PARAM_MSG_NAME='sms' 265 | OTP_SMSC_USER='YourUserName' 266 | OTP_SMSC_PASS='YourPassWord' 267 | ``` 268 | 269 | ### [Infobip](https://www.infobip.com/) 270 | Example for integrating with the infobip SMS platform, renowned SMS Gateway. 271 | 272 | using GET method 273 | 274 | ```php 275 | 'smsc' => [ 276 | 'url' => env('OTP_SMSC_URL'), 277 | 'method' => env('OTP_SMSC_METHOD', 'GET'), 278 | 'add_code' => env('OTP_COUNTRY_CODE',null), 279 | 'json' => env('OTP_SMSC_OVER_JSON',1), 280 | 'headers' => [], 281 | 'params' => [ 282 | 'send_to_param_name' => env('OTP_SMSC_PARAM_TO_NAME','number'), 283 | 'msg_param_name' => env('OTP_SMSC_PARAM_MSG_NAME','msg'), 284 | 'others' => [ 285 | 'username' => env('OTP_SMSC_USER'), 286 | 'password' => env('OTP_SMSC_PASS'), 287 | 'from' => 'InfoSMS', 288 | 'flash' => true 289 | ], 290 | ] 291 | ]; 292 | ``` 293 | 294 | .env file will be as the following 295 | 296 | ``` 297 | OTP_SMSC_URL='https://{baseUrl}/sms/1/text/query?' 298 | OTP_SMSC_METHOD='GET' 299 | OTP_COUNTRY_CODE='88' 300 | OTP_SMSC_OVER_JSON=0 301 | OTP_SMSC_PARAM_TO_NAME='to' 302 | OTP_SMSC_PARAM_MSG_NAME='text' 303 | OTP_SMSC_USER='YourUserName' 304 | OTP_SMSC_PASS='YourPassWord' 305 | ``` 306 | 307 | ### [msg91](https://msg91.com) 308 | Sample for integrating with the msg91 SMS gateway. 309 | 310 | using GET method 311 | 312 | ```php 313 | 'smsc' => [ 314 | 'url' => env('OTP_SMSC_URL'), 315 | 'method' => env('OTP_SMSC_METHOD', 'GET'), 316 | 'add_code' => env('OTP_COUNTRY_CODE',null), 317 | 'json' => env('OTP_SMSC_OVER_JSON',1), 318 | 'headers' => [], 319 | 'params' => [ 320 | 'send_to_param_name' => env('OTP_SMSC_PARAM_TO_NAME','number'), 321 | 'msg_param_name' => env('OTP_SMSC_PARAM_MSG_NAME','msg'), 322 | 'others' => [ 323 | 'authkey' => 'YourAuthKey', 324 | 'sender' => 'YourSenderId', 325 | 'route' => '1', 326 | 'country' => '88', 327 | ], 328 | ], 329 | 'wrapper' => 'sms', 330 | ]; 331 | ``` 332 | 333 | .env file will be as the following 334 | 335 | ``` 336 | OTP_SMSC_URL='https://control.msg91.com/api/v2/sendsms?' 337 | OTP_SMSC_METHOD='POST' 338 | OTP_COUNTRY_CODE='88' 339 | OTP_SMSC_OVER_JSON=1 340 | OTP_SMSC_PARAM_TO_NAME='to' 341 | OTP_SMSC_PARAM_MSG_NAME='text' 342 | OTP_SMSC_USER='YourUserName' 343 | OTP_SMSC_PASS='YourPassWord' 344 | ``` 345 | 346 | ### [Using AWS Simple Notification Service (SNS)](https://aws.amazon.com/sns/) 347 | Sample steps for integrating with the AWS SNS. 348 | 349 | Create a IAM user with the appropriate policy permissions. Go to the IAM service and create your application’s user; be sure to capture its AWS Key and AWS Secret values and put this into your environment file. From there add the following policy to the user or its group. 350 | 351 | ``` 352 | { 353 | "Version": "2012-10-17", 354 | "Statement": [ 355 | { 356 | "Sid": "AllowSendingSMSMessages", 357 | "Effect": "Allow", 358 | "Action": [ 359 | "sns:Publish", 360 | "sns:CheckIfPhoneNumberIsOptedOut" 361 | ], 362 | "Resource": [ 363 | "*" 364 | ] 365 | } 366 | ] 367 | } 368 | ``` 369 | 370 | Here we set the ability to publish, check for opt-outs, and apply this across a wildcard resource instead of a specific topic as we will be sending notifications directly to phone numbers and not an SNS topic. 371 | -------------------------------------------------------------------------------- /src/Constants/DBStates.php: -------------------------------------------------------------------------------- 1 | setTable(config('otp.table-name')); 14 | parent::__construct($attributes); 15 | } 16 | 17 | public function setTable($table) 18 | { 19 | $this->table = $table; 20 | return $this; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Object/OtpRequestObject.php: -------------------------------------------------------------------------------- 1 | client_req_id = $client_req_id; 28 | $this->number = $number; 29 | $this->email = $email; 30 | $this->type = $type; 31 | $this->resend = $resend; 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Object/OtpValidateRequestObject.php: -------------------------------------------------------------------------------- 1 | unique_id = $unique_id; 21 | $this->otp = $otp; 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/OtpValidator.php: -------------------------------------------------------------------------------- 1 | 1, 21 | 'disabled' => 0 22 | ]; 23 | 24 | const ENABLED = 1; 25 | const DISABLED = 0; 26 | 27 | /** 28 | * @param OtpRequestObject $request 29 | * @return array 30 | */ 31 | public static function requestOtp(OtpRequestObject $request) 32 | { 33 | if (self::$switch[config('otp.service')] !== self::ENABLED) { 34 | return Responder::formatter([ 35 | 'code' => StatusCodes::SERVICE_UNAVAILABLE, 36 | 'message' => StatusMessages::SERVICE_UNAVAILABLE 37 | ]); 38 | } 39 | 40 | if (empty($request)) { 41 | return Responder::formatter([ 42 | 'code' => StatusCodes::BAD_REQUEST, 43 | 'message' => StatusMessages::BAD_REQUEST 44 | ]); 45 | } 46 | 47 | try { 48 | $getId = self::getUuidId($request); 49 | } catch (\Exception $ex) { 50 | return Responder::formatter([ 51 | 'code' => StatusCodes::BAD_REQUEST, 52 | 'message' => StatusMessages::BAD_REQUEST 53 | ]); 54 | } 55 | 56 | if(empty($getId)) 57 | return Responder::formatter([ 58 | 'code' => StatusCodes::RESEND_SERVICE_DISABLED, 59 | 'message' => StatusMessages::RESEND_SERVICE_DISABLED 60 | ]); 61 | 62 | return Responder::formatter([ 63 | 'code' => StatusCodes::SUCCESSFULLY_SENT_OTP, 64 | 'message' => StatusMessages::SUCCESSFULLY_SENT_OTP, 65 | 'uniqueId' => $getId, 66 | 'type' => $request->type 67 | ]); 68 | } 69 | 70 | /** 71 | * @param OtpRequestObject $request 72 | * @return string 73 | */ 74 | private static function getUuidId(OtpRequestObject $request): string 75 | { 76 | try { 77 | $data = OtpService::expireOldOtpRequests($request); 78 | 79 | if(self::$switch[config('otp.resend')] === self::DISABLED && !empty($data)) return ""; 80 | 81 | // Resend Exceed 82 | if(self::$switch[config('otp.resend')] === self::ENABLED && OtpService::countResend($request) > config('otp.max-resend')) return ""; 83 | 84 | // OTP Generation and Persistence 85 | $getOtp = OtpService::otpGenerator(); 86 | $uuid = md5($request->client_req_id.time()); 87 | OtpService::createOtpRecord($request, $getOtp, $uuid); 88 | 89 | // Send OTP 90 | Transporter::sendCode($request, $getOtp); 91 | 92 | return $uuid; 93 | } catch (\Throwable $th) { 94 | throw $th; 95 | } 96 | } 97 | 98 | /** 99 | * @param OtpValidateRequestObject $request 100 | * @return array 101 | */ 102 | public static function validateOtp(OtpValidateRequestObject $request): array 103 | { 104 | if(empty($request)){ 105 | return Responder::formatter([ 106 | 'code' => StatusCodes::BAD_REQUEST, 107 | 'message' => StatusMessages::BAD_REQUEST 108 | ]); 109 | } 110 | $getData = OtpService::findRequest($request->unique_id); 111 | 112 | if (!empty($getData)) { 113 | if ($getData->otp == $request->otp) { 114 | OtpService::updateTo($request, DBStates::USED); 115 | return Responder::formatter([ 116 | 'code' => StatusCodes::OTP_VERIFIED, 117 | 'message' => StatusMessages::VERIFIED_OTP, 118 | 'requestId' => $getData->client_req_id, 119 | 'type' => $getData->type 120 | ]); 121 | } else { 122 | if ($getData->retry > config('otp.max-retry')) { 123 | OtpService::updateTo($request, DBStates::EXPIRED); 124 | return Responder::formatter([ 125 | 'code' => StatusCodes::TOO_MANY_WRONG_RETRY, 126 | 'message' => StatusMessages::TOO_MANY_WRONG_RETRY, 127 | 'resendId' => $request->unique_id 128 | ]); 129 | } else { 130 | OtpService::updateRetry($request); 131 | return Responder::formatter([ 132 | 'code' => StatusCodes::INVALID_OTP_GIVEN, 133 | 'message' => StatusMessages::INVALID_OTP_GIVEN, 134 | 'resendId' => $request->unique_id 135 | ]); 136 | } 137 | } 138 | } else { 139 | return Responder::formatter([ 140 | 'code' => StatusCodes::OTP_TIMEOUT, 141 | 'message' => StatusMessages::OTP_TIMEOUT, 142 | 'resendId' => $request->unique_id 143 | ]); 144 | } 145 | } 146 | 147 | /** 148 | * @param $uniqueId 149 | * @return array 150 | */ 151 | public static function resendOtp($uniqueId) 152 | { 153 | try { 154 | $request_data = OtpService::findRequest($uniqueId, 1); 155 | 156 | if (!empty($request_data) && self::$switch[config('otp.resend')] === self::ENABLED) { 157 | return self::requestOtp( 158 | new OtpRequestObject( 159 | $request_data->client_req_id, 160 | $request_data->type, 161 | $request_data->number, 162 | $request_data->email 163 | ) 164 | ); 165 | } 166 | return []; 167 | } catch (\Exception $ex) { 168 | return []; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/OtpValidatorFacade.php: -------------------------------------------------------------------------------- 1 | app instanceof LaravelApplication && $this->app->runningInConsole()) { 25 | $this->publishes([ 26 | $source => config_path('otp.php'), 27 | $templates => base_path('resources/views/vendor/template-otp') 28 | ]); 29 | } 30 | 31 | // Lumen Package Configuration 32 | if ($this->app instanceof LumenApplication) { 33 | $this->app->configure('otp'); 34 | } 35 | 36 | $this->mergeConfigFrom($source, 'otp'); 37 | 38 | // Migrate the OTPs tables 39 | $this->migrateTables(); 40 | } 41 | 42 | /** 43 | * Register the application services. 44 | * 45 | * @return void 46 | */ 47 | public function register() 48 | { 49 | $this->app->singleton(OtpValidator::class, function () { 50 | return new OtpValidator(); 51 | }); 52 | } 53 | 54 | /** 55 | * Add tables for migration. 56 | * 57 | * @return void 58 | */ 59 | private function migrateTables() 60 | { 61 | $this->loadMigrationsFrom(__DIR__.'/database/migrations'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Services/DatabaseServices.php: -------------------------------------------------------------------------------- 1 | $request->client_req_id, 24 | 'number' => $request->number, 25 | 'email' => $request->email, 26 | 'type' => $request->type, 27 | 'otp' => $otp, 28 | 'uuid' => $uuid, 29 | 'retry' => 0, 30 | 'status' => DBStates::NEW 31 | ]); 32 | } 33 | 34 | /** 35 | * @param $uuid 36 | * @param $resend 37 | * @return mixed 38 | */ 39 | public static function findUuidAvailable($uuid, $resend){ 40 | if($resend == 0){ 41 | return Otps::where('status', DBStates::NEW) 42 | ->where('uuid', $uuid) 43 | ->where('created_at', '>', Carbon::now(config('app.timezone'))->subSeconds(config('otp.timeout'))) 44 | ->first(); 45 | } 46 | return Otps::where('status', DBStates::NEW) 47 | ->where('uuid', $uuid) 48 | ->first(); 49 | 50 | } 51 | 52 | /** 53 | * @param OtpRequestObject $request 54 | * @return mixed 55 | */ 56 | public static function expireOld(OtpRequestObject $request){ 57 | return Otps::where('client_req_id', $request->client_req_id) 58 | ->where('type', $request->type) 59 | ->where('status', 'new') 60 | ->update(['status' => 'expired']); 61 | } 62 | 63 | /** 64 | * @param OtpValidateRequestObject $request 65 | * @param $state 66 | * @return mixed 67 | */ 68 | public static function updateTo(OtpValidateRequestObject $request, $state){ 69 | return Otps::where('uuid', $request->unique_id) 70 | ->update(['status' => $state]); 71 | } 72 | 73 | /** 74 | * @param OtpValidateRequestObject $request 75 | * @return mixed 76 | */ 77 | public static function updateRetry(OtpValidateRequestObject $request){ 78 | return Otps::where('uuid', $request->unique_id) 79 | ->increment('retry'); 80 | } 81 | 82 | public static function countResend(OtpRequestObject $request){ 83 | return Otps::where('client_req_id', $request->client_req_id) 84 | ->where('type', $request->type)->count(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Services/EmailTransportService.php: -------------------------------------------------------------------------------- 1 | email = $email; 15 | $this->otp = $otp; 16 | } 17 | 18 | public function send() 19 | { 20 | $mailableClass = config('otp.email.mailable-class', OtpMailable::class); 21 | if (!empty($this->email) && !empty($this->otp)) { 22 | Mail::to($this->email)->send(new $mailableClass($this->otp)); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/OtpMailable.php: -------------------------------------------------------------------------------- 1 | otp = $otp; 19 | } 20 | 21 | public function build() 22 | { 23 | return $this->from(config('otp.email.from'),config('otp.email.name')) 24 | ->view('vendor.template-otp.email') 25 | ->subject(config('otp.email.subject')); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/OtpService.php: -------------------------------------------------------------------------------- 1 | number = $number; 31 | $this->otp = $otp; 32 | $this->service = config('otp.service-name'); 33 | $this->company = config('otp.company-name'); 34 | $this->createClient(); 35 | } 36 | 37 | /** 38 | * @return $this 39 | */ 40 | protected function createClient() 41 | { 42 | if (!self::$client) { 43 | self::$client = new Client; 44 | } 45 | return $this; 46 | } 47 | 48 | /** 49 | * @return GuzzleHttp\Client | null 50 | */ 51 | public function getClient() 52 | { 53 | return self::$client; 54 | } 55 | 56 | /** 57 | * @param string $otp 58 | * @param string $service 59 | * @param string $company 60 | * @return array|string 61 | */ 62 | private function replaceOtpInTheTemplate(string $otp, string $service, string $company) 63 | { 64 | try { 65 | return view('vendor.template-otp.sms') 66 | ->with(['otp' => $otp, 'company' => $company, 'service' => $service]) 67 | ->render(); 68 | } catch (\Throwable $e) { 69 | echo $e->getMessage(); 70 | } 71 | } 72 | 73 | /** 74 | * 75 | */ 76 | public function send(): void 77 | { 78 | try { 79 | $this->sendMessage( 80 | $this->number, 81 | $this->replaceOtpInTheTemplate($this->otp, $this->service, $this->company)); 82 | } catch (Exception $e) { 83 | // dd($e->getMessage()); 84 | throw $e; 85 | } 86 | } 87 | 88 | /** 89 | * @param $to 90 | * @param $message 91 | * @param null $extra_params 92 | * @param array $extra_headers 93 | * @return $this 94 | * @throws InvalidMethodException 95 | */ 96 | public function sendMessage($to, $message, $extra_params = null, $extra_headers = []) 97 | { 98 | $config = config('otp.smsc'); 99 | $headers = $config['headers'] ?? []; 100 | $number = isset($config['add_code']) ? $config['add_code'] . $to : $to; 101 | $send_to_param_name = $config['params']['send_to_param_name']; 102 | $msg_param_name = $config['params']['msg_param_name']; 103 | $params = $config['params']['others']; 104 | 105 | if ($extra_params) { 106 | $params = array_merge($params, $extra_params); 107 | } 108 | 109 | if ($extra_headers) { 110 | $headers = array_merge($headers, $extra_headers); 111 | } 112 | 113 | // wrapper 114 | $wrapper = $config['wrapper'] ?? NULL; 115 | $wrapperParams = $config['wrapperParams'] ?? []; 116 | $send_vars = []; 117 | 118 | if ($wrapper) { 119 | $send_vars[$send_to_param_name] = $number; 120 | $send_vars[$msg_param_name] = $message; 121 | } else { 122 | $params[$send_to_param_name] = $number; 123 | $params[$msg_param_name] = $message; 124 | } 125 | 126 | if ($wrapper && $wrapperParams) { 127 | $send_vars = array_merge($send_vars, $wrapperParams); 128 | } 129 | 130 | try { 131 | //Build Request 132 | $request = new Request($config['method'], $config['url']); 133 | if ($config['method'] == "GET") { 134 | $promise = $this->getClient()->sendAsync( 135 | $request, 136 | [ 137 | 'query' => $params, 138 | 'headers' => $headers 139 | ] 140 | ); 141 | } elseif ($config['method'] == "POST") { 142 | $payload = $wrapper ? array_merge(array($wrapper => array($send_vars)), $params) : $params; 143 | if ((isset($config['json']) && $config['json'])) { 144 | $promise = $this->getClient()->sendAsync( 145 | $request, 146 | [ 147 | 'json' => $payload, 148 | 'headers' => $headers 149 | ] 150 | ); 151 | } else { 152 | $promise = $this->getClient()->sendAsync( 153 | $request, 154 | [ 155 | 'query' => $params, 156 | 'headers' => $headers 157 | ] 158 | ); 159 | } 160 | } else { 161 | throw new InvalidMethodException( 162 | sprintf("Only GET and POST methods allowed.") 163 | ); 164 | } 165 | 166 | $response = $promise->wait(); 167 | $this->response = $response->getBody()->getContents(); 168 | $this->responseCode = $response->getStatusCode(); 169 | Log::info("OTP Validator: Number: {$number} SMS Gateway Response Code: {$this->responseCode}"); 170 | Log::info("OTP Validator: Number: {$number} SMS Gateway Response Body: \n {$this->response}"); 171 | } catch (RequestException $e) { 172 | if ($e->hasResponse()) { 173 | //dd($e); 174 | $response = $e->getResponse(); 175 | $this->response = $response->getBody()->getContents(); 176 | $this->responseCode = $response->getStatusCode(); 177 | 178 | Log::error("OTP Validator: Number:{$number} SMS Gateway Response Code: {$this->responseCode}"); 179 | Log::error("OTP Validator: Number:{$number} SMS Gateway Response Body: \n { $this->response}"); 180 | 181 | // $this->response = $e->getResponseBodySummary($e->getResponse()); 182 | } 183 | } 184 | return $this; 185 | 186 | 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /src/Services/SNSTransportService.php: -------------------------------------------------------------------------------- 1 | client) { 39 | // Configuration for the SNS 40 | $this->client = new SnsClient([ 41 | 'version' => config('otp.aws.sns.version'), 42 | 'credentials' => new Credentials( 43 | config('otp.aws.sns.credentials.key'), 44 | config('otp.aws.sns.credentials.secret') 45 | ), 46 | 'region' => config('otp.aws.sns.region'), 47 | ]); 48 | } 49 | 50 | $this->number = $number; 51 | $this->otp = $otp; 52 | $this->service = config('otp.service-name'); 53 | $this->company = config('otp.company-name'); 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param string $otp 59 | * @param string $service 60 | * @param string $company 61 | * @return array|string 62 | */ 63 | private function replaceOtpInTheTemplate(string $otp, string $service, string $company) 64 | { 65 | try { 66 | return view('vendor.template-otp.sms') 67 | ->with(['otp' => $otp, 'company' => $company, 'service' => $service]) 68 | ->render(); 69 | } catch (\Throwable $e) { 70 | echo $e->getMessage(); 71 | } 72 | } 73 | 74 | /** 75 | * 76 | */ 77 | public function send() 78 | { 79 | try { 80 | $this->sendMessage( 81 | $this->number, 82 | $this->replaceOtpInTheTemplate($this->otp, $this->service, $this->company)); 83 | } catch (Exception $e) { 84 | dd($e->getMessage()); 85 | } 86 | } 87 | 88 | /** 89 | * @param $to 90 | * @param $message 91 | */ 92 | private function sendMessage($to, $message){ 93 | try { 94 | $config = config('otp.smsc'); 95 | $number = isset($config['add_code']) ? $config['add_code'] . $to : $to; 96 | 97 | $result = $this->client->publish([ 98 | 'Message' => $message, 99 | 'MessageAttributes' => [ 100 | // For SMS Sending Reliability 101 | 'AWS.SNS.SMS.SMSType' => [ 102 | 'DataType' => 'String', 103 | 'StringValue' => 'Transactional', 104 | ], 105 | ], 106 | 'PhoneNumber' => $number, 107 | ]); 108 | 109 | Log::info("OTP Validator: Number: {$number} AWS SNS Gateway Response Code: ".json_encode($result)); 110 | } catch (AwsException $e) { 111 | // output error message if fails 112 | if (!empty($e->getMessage())) { 113 | $response = $e->getMessage(); 114 | Log::error("OTP Validator: Number:{$number} AWS SNS Gateway Response Code: {$response}"); 115 | } 116 | } 117 | return $this; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Services/TransportServiceInterface.php: -------------------------------------------------------------------------------- 1 | email)) { 30 | self::sendOver(new EmailTransportService($request->email, $otp)); 31 | } 32 | } catch (\Exception $ex) { 33 | return false; 34 | //dd($ex->getMessage()); 35 | throw $ex; 36 | } 37 | } 38 | 39 | public static function sendOverSMS(OtpRequestObject $request, string $otp) 40 | { 41 | try { 42 | if (intval(config('otp.send-by.sms')) === 1 && !empty($request->number)) { 43 | self::sendOver(new SMSTransportService($request->number, $otp)); 44 | } 45 | } catch (\Exception $ex) { 46 | return false; 47 | //dd($ex->getMessage()); 48 | throw $ex; 49 | } 50 | } 51 | 52 | public static function sendOverAwsSns(OtpRequestObject $request, string $otp) 53 | { 54 | try { 55 | if (intval(config('otp.send-by.aws-sns')) === 1 && !empty($request->number)) { 56 | self::sendOver(new SNSTransportService($request->number, $otp)); 57 | } 58 | } catch (\Exception $ex) { 59 | return false; 60 | //dd($ex->getMessage()); 61 | throw $ex; 62 | } 63 | } 64 | 65 | public static function sendOver(TransportServiceInterface $service) 66 | { 67 | try { 68 | $service->send(); 69 | } catch (\Throwable $th) { 70 | throw $th; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/database/migrations/2020_07_12_000000_create_otps_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('client_req_id'); 19 | $table->string('number')->nullable(); 20 | $table->string('email')->nullable(); 21 | $table->string('type'); 22 | $table->string('otp'); 23 | $table->string('uuid'); 24 | $table->tinyInteger('retry'); 25 | $table->enum('status',['new','used', 'expired']); 26 | $table->timestamps(); 27 | $table->index(['client_req_id', 'uuid', 'status', 'type']); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists(config('otp.table-name')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /template-otp/email.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | Dear Subscriber,
9 | Your Security Code is {{$otp}}. 10 |

11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /template-otp/sms.blade.php: -------------------------------------------------------------------------------- 1 | Your OTP for {{$service}} service is {{$otp}}. Please don't share with anyone. Thank you. {{$company}} 2 | -------------------------------------------------------------------------------- /tests/Feature/OTPTest.php: -------------------------------------------------------------------------------- 1 | otpService = new OtpService(); 16 | } 17 | 18 | /** 19 | * 20 | * @dataProvider dataProviderForOtpGenerate 21 | */ 22 | public function testOtpGenerate($digit, $expected): void 23 | { 24 | $random = $this->otpService->otpGenerator($digit); 25 | $this->assertTrue(strlen((string)$random) == $expected); 26 | } 27 | 28 | /** 29 | * @return int[] 30 | */ 31 | public function dataProviderForOtpGenerate(): array 32 | { 33 | return [ 34 | [10, 10], 35 | [100, 100], 36 | [0,0], 37 | [-1,0] 38 | ]; 39 | } 40 | } 41 | --------------------------------------------------------------------------------