PHP code example of biponix / laravel-secure-otp

1. Go to this page and download the library: Download biponix/laravel-secure-otp library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

biponix / laravel-secure-otp example snippets


return [
    // OTP expiry in minutes (default: 5)
    'expiry_minutes' => env('OTP_EXPIRY_MINUTES', 5),

    // OTP code length (default: 6 digits)
    'length' => env('OTP_LENGTH', 6),

    // Maximum verification attempts (default: 3)
    'max_attempts' => env('OTP_MAX_ATTEMPTS', 3),

    // Hash algorithm and secret for HMAC (prevents rainbow table attacks)
    'hash_algorithm' => env('OTP_HASH_ALGORITHM', 'sha256'),
    'hash_secret' => env('OTP_HASH_SECRET', null), // Falls back to app.key if null

    // Context-aware rate limiting (separate limits for generation vs verification)
    'rate_limits' => [
        // Cache key prefix (prevents collisions in shared cache)
        'prefix' => env('OTP_RATE_LIMIT_PREFIX', 'secure-otp'),

        // Shared defaults (used for both generate and verify if context-specific not set)
        'per_identifier' => [
            'max_attempts' => env('OTP_RATE_LIMIT_IDENTIFIER', 3),
            'decay_seconds' => env('OTP_RATE_LIMIT_IDENTIFIER_DECAY', 3600), // 1 hour
        ],
        'per_ip' => [
            'max_attempts' => env('OTP_RATE_LIMIT_IP', 10),
            'decay_seconds' => env('OTP_RATE_LIMIT_IP_DECAY', 3600), // 1 hour
        ],

        // Optional: Override limits specifically for verification (prevent brute force)
        'verify_per_identifier' => [
            'max_attempts' => env('OTP_VERIFY_RATE_LIMIT_IDENTIFIER', 5),
            'decay_seconds' => env('OTP_VERIFY_RATE_LIMIT_IDENTIFIER_DECAY', 60), // 1 minute
        ],
        'verify_per_ip' => [
            'max_attempts' => env('OTP_VERIFY_RATE_LIMIT_IP', 20),
            'decay_seconds' => env('OTP_VERIFY_RATE_LIMIT_IP_DECAY', 60), // 1 minute
        ],
    ],

    // Custom notification class
    'notification_class' => env('OTP_NOTIFICATION_CLASS', \Biponix\SecureOtp\Notifications\OtpNotification::class),

    // Cleanup after hours (default: 24)
    'cleanup_after_hours' => env('OTP_CLEANUP_AFTER_HOURS', 24),

    // Enable security logging (default: true)
    'enable_logging' => env('OTP_ENABLE_LOGGING', true),
];

use Biponix\SecureOtp\Services\SecureOtpService;

$otp = app(SecureOtpService::class);

// Send OTP to any identifier (no validation)
$otp->send('01700000000');        // Bangladesh phone
$otp->send('[email protected]');   // Email
$otp->send('username123');        // Username
$otp->send('12345');              // User ID

// Verify OTP
$verified = $otp->verify('01700000000', '123456');

use Biponix\SecureOtp\Services\SecureOtpService;
use Biponix\SecureOtp\Types\EmailType;

// Register identifier types in AppServiceProvider::boot()
SecureOtpService::addType('email', new EmailType());

// Now use with type parameter
$otp->send('[email protected]', 'email');     // ✅ Validated & normalized
$otp->verify('[email protected]', '123456', 'email');

use Biponix\SecureOtp\Exceptions\InvalidIdentifierException;
use Biponix\SecureOtp\Exceptions\RateLimitExceededException;
use Biponix\SecureOtp\Exceptions\OtpGenerationException;
use Biponix\SecureOtp\Services\SecureOtpService;

class AuthController extends Controller
{
    public function sendOtp(Request $request, SecureOtpService $otp)
    {
        try {
            // Send OTP (throws on error)
            $otp->send($request->email, 'email');

            return response()->json([
                'message' => 'OTP sent successfully'
            ]);

        } catch (RateLimitExceededException $e) {
            // Rate limit exceeded
            return response()->json([
                'error' => 'Too many requests',
                'retry_after' => $e->getRetryAfter(),
            ], 429);

        } catch (InvalidIdentifierException $e) {
            return response()->json(['error' => 'Invalid email address'], 400);

        } catch (OtpGenerationException $e) {
            return response()->json(['error' => 'Failed to send OTP'], 500);
        }
    }

    public function verifyOtp(Request $request, SecureOtpService $otp)
    {
        // verify() returns bool (doesn't expose why it failed for security)
        $verified = $otp->verify($request->email, $request->code, 'email');

        if ($verified) {
            // OTP verified successfully
            $user = User::where('email', $request->email)->firstOrFail();
            auth()->login($user);

            return response()->json(['message' => 'Login successful']);
        }

        return response()->json(['error' => 'Invalid or expired code'], 422);
    }
}

use Biponix\SecureOtp\Services\SecureOtpService;

public function __construct(
    private SecureOtpService $otp
) {}

public function sendCode(string $identifier): void
{
    // Throws exceptions on error
    $this->otp->send($identifier);
}

// app/Otp/BangladeshSmsType.php
namespace App\Otp;

use Biponix\SecureOtp\Contracts\OtpIdentifierType;

class BangladeshSmsType extends OtpIdentifierType
{
    /**
     * Normalize Bangladesh phone numbers to E.164 format
     */
    public function normalize(string $value): string
    {
        // Remove spaces, dashes, parentheses
        $value = preg_replace('/[\s\-\(\)]/', '', $value);

        // Convert local format (01700000000) to E.164 (+8801700000000)
        if (preg_match('/^0\d{10}$/', $value)) {
            return '+880' . substr($value, 1);
        }

        return $value;
    }

    /**
     * Validate E.164 Bangladesh phone numbers
     */
    public function validate(string $value): bool
    {
        // Must be +880 followed by 10 digits
        return preg_match('/^\+880\d{10}$/', $value) === 1;
    }
}

// app/Providers/AppServiceProvider.php
use App\Otp\BangladeshSmsType;
use Biponix\SecureOtp\Services\SecureOtpService;

public function boot(): void
{
    // Register custom identifier types
    SecureOtpService::addType('sms', new BangladeshSmsType());
}

// Send OTP with validation
$otp->send('01700000000', 'sms');      // ✅ Normalized to +8801700000000
$otp->send('0170-000-0000', 'sms');    // ✅ Normalized to +8801700000000

// Verify with same type
$verified = $otp->verify('01700000000', '123456', 'sms');  // ✅ Works!

// Username type
class UsernameType extends OtpIdentifierType
{
    public function normalize(string $value): string
    {
        return strtolower(trim($value));
    }

    public function validate(string $value): bool
    {
        return preg_match('/^[a-z0-9_]{3,20}$/', $value) === 1;
    }
}

// User ID type
class UserIdType extends OtpIdentifierType
{
    public function normalize(string $value): string
    {
        return trim($value);
    }

    public function validate(string $value): bool
    {
        return ctype_digit($value) && (int)$value > 0;
    }
}

// Register in AppServiceProvider
SecureOtpService::addType('username', new UsernameType());
SecureOtpService::addType('user_id', new UserIdType());

// Usage
$otp->send('john_doe', 'username');
$otp->send('12345', 'user_id');

use Biponix\SecureOtp\Services\SecureOtpService;
use Biponix\SecureOtp\Exceptions\RateLimitExceededException;

public function customDelivery(SecureOtpService $otp)
{
    try {
        // Generate OTP without sending (returns string)
        $code = $otp->generate('[email protected]', 'email');

        // Deliver via your custom method
        $this->sendViaSms($code);

    } catch (RateLimitExceededException $e) {
        // Handle rate limiting
        return response()->json([
            'error' => 'Too many requests',
            'retry_after' => $e->getRetryAfter(),
        ], 429);
    }
}

// Default: queued (non-blocking)
$otp->send('[email protected]');

// Force synchronous sending (blocks until sent)
$otp->sendNow('[email protected]');

use Biponix\SecureOtp\Facades\SecureOtp;

// Send OTP (throws exceptions on error)
SecureOtp::send('[email protected]');

// Verify OTP (returns bool)
$verified = SecureOtp::verify('[email protected]', '123456');

// Generate without sending (returns string, throws on rate limit)
$code = SecureOtp::generate('[email protected]');

// Send synchronously (throws exceptions on error)
SecureOtp::sendNow('[email protected]');

// app/Notifications/MultiChannelOtpNotification.php
namespace App\Notifications;

use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\VonageSmsMessage;
use Illuminate\Notifications\Notification;

class MultiChannelOtpNotification extends Notification
{
    public function __construct(public string $code) {}

    /**
     * Route notification channels based on identifier type
     */
    public function via(object $notifiable): array
    {
        // $notifiable->type comes from SecureOtpService::send($identifier, $type)
        return match ($notifiable->type) {
            'sms' => ['vonage'],           // Phone via SMS
            'email' => ['mail'],           // Email
            'whatsapp' => ['whatsapp'],    // WhatsApp (if configured)
            default => ['mail'],           // Fallback to email
        };
    }

    /**
     * SMS notification
     */
    public function toVonage(object $notifiable): VonageSmsMessage
    {
        return (new VonageSmsMessage)
            ->content("Your verification code is: {$this->code}");
    }

    /**
     * Email notification
     */
    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Your Verification Code')
            ->line("Your verification code is: {$this->code}")
            ->line('This code will expire in ' . config('secure-otp.expiry_minutes', 5) . ' minutes.');
    }
}

// Sends via Vonage SMS
$otp->send('01700000000', 'sms');

// Sends via Email
$otp->send('[email protected]', 'email');

// Sends via WhatsApp (if configured)
$otp->send('+8801700000000', 'whatsapp');

use Illuminate\Support\Facades\Schedule;

Schedule::command('secure-otp:clean --force')
    ->daily()
    ->withoutOverlapping()
    ->onOneServer();

protected function schedule(Schedule $schedule)
{
    $schedule->command('secure-otp:clean --force')
             ->daily()
             ->withoutOverlapping()
             ->onOneServer();
}

use Biponix\SecureOtp\Services\SecureOtpService;

$deleted = app(SecureOtpService::class)->cleanupExpired();
// Returns the number of deleted records

// Configure in .env
OTP_HASH_SECRET=your-secret-key  // Falls back to APP_KEY if not set

try {
    $code = $otp->generate('[email protected]', 'email');  // With validation
    $code = $otp->generate('01700000000');                // Without validation
} catch (RateLimitExceededException $e) {
    // Handle rate limiting: $e->getRetryAfter() gives seconds until retry
}

try {
    $otp->send('[email protected]', 'email');    // Email with validation
    $otp->send('01700000000', 'sms');           // Phone with SMS type
    $otp->send('username123');                  // No validation
} catch (RateLimitExceededException $e) {
    // Return HTTP 429 with retry_after header
}

$verified = $otp->verify('[email protected]', '123456', 'email');
$verified = $otp->verify('01700000000', '123456', 'sms');  // Same type as send()

SecureOtpService::addType('sms', new BangladeshSmsType());
bash
php artisan migrate
bash
php artisan vendor:publish --tag="secure-otp-migrations"
php artisan migrate
bash
php artisan vendor:publish --tag="secure-otp-config"
bash
# In development (prompts for confirmation)
php artisan secure-otp:clean

# In production (bypasses confirmation)
php artisan secure-otp:clean --force