From 8710a83937895a41f04096a1f6d4b79504ac28c6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Jan 2025 13:52:28 +0330 Subject: [PATCH] add support otp lifetime token functionality with expiration handling --- .github/workflows/php-cs-fixer.yml | 30 ++--- CHANGELOG.md | 3 + README.md | 116 +++++++++++------- VERSION | 2 +- src/Contracts/TokenRepositoryInterface.php | 7 +- src/Exceptions/InvalidOTPTokenException.php | 13 -- src/Exceptions/OTPException.php | 23 ++++ .../UserNotFoundByMobileException.php | 13 -- src/OTPBroker.php | 19 +-- src/Token/CacheTokenRepository.php | 11 +- src/Token/DatabaseTokenRepository.php | 29 ++++- src/helpers.php | 4 +- tests/CacheTokenRepositoryTest.php | 2 +- tests/DatabaseTokenRepositoryTest.php | 2 +- tests/OTPBrokerTest.php | 31 ++++- 15 files changed, 193 insertions(+), 112 deletions(-) delete mode 100644 src/Exceptions/InvalidOTPTokenException.php create mode 100644 src/Exceptions/OTPException.php delete mode 100644 src/Exceptions/UserNotFoundByMobileException.php diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index c19e1f5..00e8e06 100755 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -1,23 +1,19 @@ name: Check & fix styling -on: [push, pull_request] +on: [push,pull_request] jobs: php-cs-fixer: - runs-on: ubuntu-latest + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php-cs-fixer.dist --allow-risky=yes - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Run PHP CS Fixer - uses: docker://oskarstark/php-cs-fixer-ga - with: - args: --config=.php-cs-fixer.dist --allow-risky=yes - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: Fix styling \ No newline at end of file + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index e07478d..8069293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 4.3.0 - 2024-06-21 + - Add support only confirm token + ## 4.2.0 - 2024-04-12 - Add support Laravel 11 - Detracted Laravel 9 diff --git a/README.md b/README.md index 27c7c34..a77ffde 100755 --- a/README.md +++ b/README.md @@ -19,46 +19,60 @@ Laravel | Laravel-OTP 6.0.x to 8.0.x | 1.0.x ## Basic Usage: - ```php send('+989389599530'); -// or -OTP('+989389599530'); - -/** - * Send OTP via channels. - */ +/* +|-------------------------------------------------------------------------- +| Send OTP via SMS. +|-------------------------------------------------------------------------- +*/ +OTP()->send('+98900000000'); +// Or +OTP('+98900000000'); + +/* +|-------------------------------------------------------------------------- +| Send OTP via channels. +|-------------------------------------------------------------------------- +*/ OTP()->channel(['otp_sms', 'mail', \App\Channels\CustomSMSChannel::class]) - ->send('+989389599530'); -// or -OTP('+989389599530', ['otp_sms', 'mail', \App\Channels\CustomSMSChannel::class]); - -/** - * Send OTP for specific user provider - */ + ->send('+98900000000'); +// Or +OTP('+98900000000', ['otp_sms', 'mail', \App\Channels\CustomSMSChannel::class]); + +/* +|-------------------------------------------------------------------------- +| Send OTP for specific user provider +|-------------------------------------------------------------------------- +*/ OTP()->useProvider('admins') - ->send('+989389599530'); - -/** - * Validate OTP - */ -OTP()->validate('+989389599530', 'token_123'); -// or -OTP('+989389599530', 'token_123'); -// or + ->send('+98900000000'); + +/* +|-------------------------------------------------------------------------- +| Validate OTP +|-------------------------------------------------------------------------- +*/ +OTP()->validate('+98900000000', 'token_123'); +// Or +OTP('+98900000000', 'token_123'); + +/* +|-------------------------------------------------------------------------- +| Validate OTP for specific user provider +|-------------------------------------------------------------------------- +*/ OTP()->useProvider('users') - ->validate('+989389599530', 'token_123'); -// or -OTP()->useProvider('users') - ->onlyConfirmToken() - ->validate('+989389599530', 'token_123'); + ->validate('+98900000000', 'token_123'); +/* +|-------------------------------------------------------------------------- +| You may wish to only confirm the token +|-------------------------------------------------------------------------- +*/ +OTP()->onlyConfirmToken() + ->validate('+98900000000', 'token_123'); ``` - ## Installation You can install the package via composer: @@ -132,7 +146,24 @@ php artisan migrate > **Note:** When you are using OTP to login user, consider all columns must be nullable except for the `mobile` column. Because, after verifying OTP, a user record will be created if the user does not exist. -## User providers +### Token Life Time +You can specify an OTP `token_lifetime`, ensuring that once an OTP token is sent to the user, no new OTP token will be generated or sent until the current token has expired. + +```php +// config/otp.php + + env('OTP_TOKEN_LIFE_TIME', 5), + ], + + //... +]; +``` +### User providers You may wish to use the OTP for variant users. Laravel OTP allows you to define and manage many user providers that you need. In order to set up, you should open `config/otp.php` file and define your providers: @@ -242,7 +273,7 @@ return [ ## Practical Example -Here we have prepared a practical example. Suppose you are going to login/register a customer by sending an OTP: +Here we have prepared a practical example. Suppose you are going to login/register a user by sending an OTP: ```php OTPService->send($request->get('mobile')); } catch (Throwable $ex) { // or prepare and return a view. - return response()->json(['message'=>'An unexpected error occurred.'], 500); + return response()->json(['message' => 'An unexpected error occurred.'], 500); } - return response()->json(['message'=>'A token has been sent to:'. $user->mobile]); + return response()->json(['message' => 'A token has been sent to:'. $user->mobile]); } public function verifyOTPAndLogin(Request $request): JsonResponse @@ -283,13 +314,13 @@ class AuthController // and do login actions... - } catch (InvalidOTPTokenException $exception){ - return response()->json(['error'=>$exception->getMessage()],$exception->getCode()); + } catch (OTPException $exception){ + return response()->json(['error' => $exception->getMessage()],$exception->getCode()); } catch (Throwable $ex) { - return response()->json(['message'=>'An unexpected error occurred.'], 500); + return response()->json(['message' => 'An unexpected error occurred.'], 500); } - return response()->json(['message'=>'Logged in successfully.']); + return response()->json(['message' => 'Logged in successfully.']); } } @@ -305,6 +336,7 @@ channel. In order to replace, you should specify channel class here: ```php //config/otp.php findUserByMobile($mobile) : null; - throw_if(! $user && $userExists, UserNotFoundByMobileException::class); + throw_if(! $user && $userExists, OTPException::whenUserNotFoundByMobile()); + throw_if($this->tokenExists($mobile), OTPException::whenOtpAlreadySent()); $notifiable = $user ?? $this->makeNotifiable($mobile); @@ -53,13 +53,13 @@ public function send(string $mobile, bool $userExists = false): OTPNotifiable } /** - * @throws InvalidOTPTokenException|Throwable + * @throws OTPException|Throwable */ public function validate(string $mobile, string $token, bool $create = true): OTPNotifiable { $notifiable = $this->makeNotifiable($mobile); - throw_unless($this->tokenExists($notifiable, $token), InvalidOTPTokenException::class); + throw_unless($this->verifyToken($notifiable, $token), OTPException::whenOtpTokenIsInvalid()); if(!$this->onlyConfirm){ $notifiable = $this->find($mobile, $create); @@ -136,9 +136,14 @@ private function getDefaultChannel(): array return is_array($channel) ? $channel : Arr::wrap($channel); } - private function tokenExists(OTPNotifiable $user, string $token): bool + public function verifyToken(OTPNotifiable $user, string $token): bool { - return $this->tokenRepository->exists($user, $token); + return $this->tokenRepository->isTokenMatching($user, $token); + } + + private function tokenExists(string $mobile): bool + { + return $this->tokenRepository->exists($mobile); } private function makeNotifiable(string $mobile): OTPNotifiable diff --git a/src/Token/CacheTokenRepository.php b/src/Token/CacheTokenRepository.php index da5ae79..113e494 100644 --- a/src/Token/CacheTokenRepository.php +++ b/src/Token/CacheTokenRepository.php @@ -24,12 +24,17 @@ public function deleteExisting(OTPNotifiable $user): bool return $this->cache->forget($this->getSignatureKey($user->getMobileForOTPNotification())); } - public function exists(OTPNotifiable $user, string $token): bool + public function exists(string $mobile): bool { + return $this->cache->has($this->getSignatureKey($mobile)); + } + + public function isTokenMatching(OTPNotifiable $user, string $token): bool + { + $exist = $this->exists($user->getMobileForOTPNotification()); $signature = $this->getSignatureKey($user->getMobileForOTPNotification()); - return $this->cache->has($signature) && - $this->cache->get($signature)['token'] === $token; + return $exist && $this->cache->get($signature)['token'] === $token; } protected function save(string $mobile, string $token): bool diff --git a/src/Token/DatabaseTokenRepository.php b/src/Token/DatabaseTokenRepository.php index 7f50145..52df879 100755 --- a/src/Token/DatabaseTokenRepository.php +++ b/src/Token/DatabaseTokenRepository.php @@ -25,14 +25,31 @@ public function deleteExisting(OTPNotifiable $user): bool return (bool) optional($this->getTable()->where('mobile', $user->getMobileForOTPNotification()))->delete(); } - public function exists(OTPNotifiable $user, string $token): bool + protected function getLatestRecord(array $filters): ?array { - $record = (array) $this->getTable() - ->where('mobile', $user->getMobileForOTPNotification()) - ->where('token', $token) - ->first(); + $record = $this->getTable() + ->where($filters) + ->latest() + ->first(); - return $record && ! $this->tokenExpired($record['expires_at']); + return $record ? (array) $record : null; + } + + public function exists(string $mobile): bool + { + $record = $this->getLatestRecord(['mobile' => $mobile]); + + return $record && !$this->tokenExpired($record['expires_at']); + } + + public function isTokenMatching(OTPNotifiable $user, string $token): bool + { + $record = $this->getLatestRecord([ + 'mobile' => $user->getMobileForOTPNotification(), + 'token' => $token, + ]); + + return $record && !$this->tokenExpired($record['expires_at']); } protected function getTable(): Builder diff --git a/src/helpers.php b/src/helpers.php index 05dd1b6..645c6db 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,12 +1,12 @@ repository->create($this->user); - $this->assertTrue($this->repository->exists($this->user, $token)); + $this->assertTrue($this->repository->isTokenMatching($this->user, $token)); } /** diff --git a/tests/DatabaseTokenRepositoryTest.php b/tests/DatabaseTokenRepositoryTest.php index fb3dea6..00c4e33 100644 --- a/tests/DatabaseTokenRepositoryTest.php +++ b/tests/DatabaseTokenRepositoryTest.php @@ -63,7 +63,7 @@ public function it_can_find_existing_and_not_expired_token_successfully(): void { $token = $this->repository->create($this->user); - $this->assertTrue($this->repository->exists($this->user, $token)); + $this->assertTrue($this->repository->isTokenMatching($this->user, $token)); } /** diff --git a/tests/OTPBrokerTest.php b/tests/OTPBrokerTest.php index 50941a9..afb527e 100644 --- a/tests/OTPBrokerTest.php +++ b/tests/OTPBrokerTest.php @@ -3,8 +3,7 @@ namespace Fouladgar\OTP\Tests; use Fouladgar\OTP\Contracts\OTPNotifiable; -use Fouladgar\OTP\Exceptions\InvalidOTPTokenException; -use Fouladgar\OTP\Exceptions\UserNotFoundByMobileException; +use Fouladgar\OTP\Exceptions\OTPException; use Fouladgar\OTP\Notifications\Channels\OTPSMSChannel; use Fouladgar\OTP\Notifications\OTPNotification; use Fouladgar\OTP\Tests\Models\OTPNotifiableUser; @@ -43,7 +42,8 @@ public function it_can_send_token_to_an_exist_user(): void */ public function it_can_throw_not_found_if_user_exists_is_true(): void { - $this->expectException(UserNotFoundByMobileException::class); + $this->expectException(OTPException::class); + OTP()->send(self::MOBILE, true); } @@ -135,7 +135,7 @@ public function it_can_not_validate_a_token_when_token_is_expired_or_invalid(): { $user = OTPNotifiableUser::factory()->create(); - $this->expectException(InvalidOTPTokenException::class); + $this->expectException(OTPException::class); OTP()->validate($user->mobile, '12345'); } @@ -190,8 +190,10 @@ public function it_can_revoke_a_token_successfully(): void /** * @test */ - public function it_can_send_by_using_provider(): void + public function it_can_not_send_otp_when_already_sent(): void { + $this->expectException(OTPException::class); + Notification::fake(); $user = OTP(self::MOBILE); @@ -204,6 +206,25 @@ public function it_can_send_by_using_provider(): void ); } + /** + * @test + */ + public function it_can_send_by_using_provider(): void + { + Notification::fake(); + + $otp = OTP(); + + $user = $otp->send(self::MOBILE, false); + + $this->assertInstanceOf(OTPNotifiable::class, $user); + + Notification::assertSentTo( + $user, + OTPNotification::class + ); + } + /** * @test */