├── .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 | 
3 | [](https://opensource.org/licenses/MIT)
4 | 
5 | 
6 | 
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 | [](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 |
--------------------------------------------------------------------------------