Installation:
composer require mkd/laravel-advanced-otp
php artisan vendor:publish --provider="LaravelAdvancedOTP\ServiceProvider" --tag="config"
otp_length, expire_after_minutes).Generate a Custom OTP Method:
php artisan magic-otp:make LoginOTP
app/OtpMethods/LoginOTP.php) with default logic for generation, storage, and validation.First Use Case:
use LaravelAdvancedOTP\Facades\LaravelAdvancedOTP;
$otp = LaravelAdvancedOTP::handle(LoginOTP::class, [
'secret' => 'your_app_secret', // Required for hashing
'email' => 'user@example.com',
]);
$isValid = LaravelAdvancedOTP::verify(LoginOTP::class, [
'secret' => 'your_app_secret',
'email' => 'user@example.com',
'otp' => $userInputOTP,
]);
config/laravel-advanced-otp.php – Adjust OTP length, expiration, and storage drivers (default: cache).app/OtpMethods/ – Extend or override default logic for custom workflows (e.g., SMS, database-backed OTPs).LaravelAdvancedOTP – Centralized API for generation, verification, and cleanup.Email-Based OTP Flow:
$otp = LaravelAdvancedOTP::handle(LoginOTP::class, [
'secret' => 'app_secret',
'email' => $user->email,
'template' => 'emails.otp', // Custom Blade template
]);
LoginOTP method’s generate() and send() logic (default: stores hashed OTP in cache).if (LaravelAdvancedOTP::verify(LoginOTP::class, [
'secret' => 'app_secret',
'email' => $user->email,
'otp' => request('otp'),
])) {
// Authenticate user...
}
Custom Storage/Validation:
generate() in LoginOTP to use a database:
public function generate(array $data): string
{
$otp = Str::random($this->config['otp_length']);
$hashed = hash_hmac('sha256', $otp, $data['secret']);
UserOtp::create([
'user_email' => $data['email'],
'otp' => $hashed,
'expires_at' => now()->addMinutes($this->config['expire_after_minutes']),
]);
return $otp; // Return plain OTP for sending
}
validate() to check against your storage:
public function validate(array $data): bool
{
$hashedInput = hash_hmac('sha256', $data['otp'], $data['secret']);
return UserOtp::where('user_email', $data['email'])
->where('otp', $hashedInput)
->where('expires_at', '>', now())
->exists();
}
Rate Limiting:
throttle middleware or extend LoginOTP to track failed attempts:
public function validate(array $data): bool
{
$attempts = cache()->get("otp_attempts_{$data['email']}", 0);
if ($attempts >= 5) {
throw new \Exception('Too many attempts. Try again later.');
}
// ... rest of validation
}
Integration with Auth:
public function handle(Request $request, Closure $next)
{
if (!$request->has('otp')) {
return redirect()->route('generate.otp');
}
if (!LaravelAdvancedOTP::verify(LoginOTP::class, [
'secret' => config('app.otp_secret'),
'email' => auth()->user()->email,
'otp' => $request->otp,
])) {
return back()->withErrors(['otp' => 'Invalid OTP']);
}
return $next($request);
}
OtpMethod class (e.g., DatabaseOtpMethod) and extend it for consistency across projects.validate() to log failed attempts:
\Log::warning("OTP failure for {$data['email']}", ['otp' => $data['otp']]);
LaravelAdvancedOTP::fake() in tests to mock OTPs:
LaravelAdvancedOTP::fake(LoginOTP::class, '123456');
$this->assertTrue(LaravelAdvancedOTP::verify(...));
Secret Key Management:
secret parameter is required for hashing and must be consistent between generation and verification.config() or environment variables:
'secret' => config('app.otp_secret'),
OTP_SECRET in .env.Cache Expiration:
cache) relies on Laravel’s cache driver. If using file or database, ensure TTL is set:
cache()->put("otp_{$email}", $hashedOtp, now()->addMinutes($this->config['expire_after_minutes']));
LaravelAdvancedOTP::cleanup(LoginOTP::class) periodically (e.g., via a scheduled job).OTP Length vs. Security:
Race Conditions:
public function validate(array $data): bool
{
return cache()->get("otp_{$data['email']}", function () use ($data) {
return false; // Expired or invalid
}) === hash_hmac('sha256', $data['otp'], $data['secret']);
}
Custom Validation Logic:
validate() requires careful handling of edge cases (e.g., empty OTP, expired tokens).Validator for reusable rules:
use Illuminate\Support\Facades\Validator;
public function validate(array $data): bool
{
$validator = Validator::make($data, [
'otp' => ['required', 'string', function ($attribute, $value, $fail) {
if (!$this->isValidOtp($value, $data)) {
$fail('The OTP is invalid.');
}
}],
]);
return !$validator->fails();
}
Verification Fails Silently:
'debug' => env('APP_DEBUG', false),
OTP Not Found in Cache:
otp_{$email}). Override in your OTP method:
protected function getCacheKey(array $data): string
{
return "custom_otp_{$data['email']}_{$data['secret']}";
}
Performance Issues:
// Migration for UserOtp table
$table->string('user_email');
$table->string('otp');
$table->timestamp('expires_at');
$table->index(['user_email', 'expires_at']);
LaravelAdvancedOTP\Contracts\OtpStorageHow can I help you explore Laravel packages today?