Skip to content

Instantly share code, notes, and snippets.

@erdum
Last active January 17, 2025 12:36
Show Gist options
  • Save erdum/871658c3283828f5bcb49715f4bc0bbf to your computer and use it in GitHub Desktop.
Save erdum/871658c3283828f5bcb49715f4bc0bbf to your computer and use it in GitHub Desktop.
OTP abuse protection mechanism with versatile delivery option for Laravel
<?php
namespace App\Services;
class OtpService
{
public static function send(
string $identifier,
string $value,
callable $send
): mixed {
$otp = cache($identifier);
// OTP cool downed
if (! $otp) {
cache(
[$identifier => [
'retries' => 0,
'sent_at' => now(),
'expires_at' => now()->addSeconds(config('app.otp.expiry_duration')),
'verified_at' => null,
]],
now()->addSeconds(config('app.otp.retry_duration'))
);
cache(
[$value => $identifier],
now()->addSeconds(config('app.otp.expiry_duration'))
);
return $send($value);
}
if ($otp['retries'] >= config('app.otp.retries')) {
throw new \Exception(
'Too many OTP\'s requested, try again after: '
.$otp['sent_at']->addSeconds(config('app.otp.retry_duration'))->diffInSeconds(now())
.' seconds'
);
}
if ($otp['expires_at']->isFuture()) {
throw new \Exception('Recently OTP requested, try again after: '.$otp['expires_at']->diffInSeconds(now()).' seconds');
}
cache(
[$identifier => [
'retries' => $otp['retries'] + 1,
'sent_at' => now(),
'expires_at' => now()->addSeconds(config('app.otp.expiry_duration')),
'verified_at' => null,
]],
now()->addSeconds(config('app.otp.retry_duration'))
);
cache(
[$value => $identifier],
now()->addSeconds(config('app.otp.expiry_duration'))
);
return $send($value);
}
public static function verify(string $value): bool|string
{
$identifier = cache($value);
if ($identifier) {
$otp = cache($identifier);
if ($otp) {
if (self::is_expired($identifier)) return false;
cache(
[$identifier => [
...$otp,
'verified_at' => now(),
'retries' => 0,
]],
now()->addSeconds(config('app.otp.retry_duration'))
);
return $identifier;
}
}
return false;
}
public static function clear_otp(string $identifier): void
{
cache([$identifier => null]);
}
public static function is_verified(string $identifier): bool
{
$otp = cache($identifier);
return $otp ? $otp['verified_at'] ?? false : false;
}
public static function is_expired(string $identifier): bool
{
$otp = cache($identifier);
return $otp ? $otp['expires_at']->isPast() : false;
}
public static function retries_left(string $identifier): bool|int
{
$otp = cache($identifier);
return $otp ? (config('app.otp.retries') - $otp['retries']) : false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment