Laravel Phone Number Verification with SMS (2026 Guide)

Laravel Phone Number Verification with SMS (2026 Guide)
Laravel SMS Team
Laravel SMS Team
June 24, 2026

Laravel Phone Number Verification with SMS (2026 Guide)

Phone number verification via SMS is a critical security feature for modern web applications. It prevents fake registrations, enables two-factor authentication, and builds trust with users. This guide shows you how to implement a complete phone verification system in Laravel.

Adding the Phone Column to Users Migration

First, add phone number fields to your users table:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('phone', 20)->unique()->nullable()->after('email');
            $table->timestamp('phone_verified_at')->nullable()->after('phone');
            $table->string('verification_code', 6)->nullable();
            $table->timestamp('verification_code_sent_at')->nullable();
            $table->unsignedTinyInteger('verification_attempts')->default(0);
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn([
                'phone',
                'phone_verified_at',
                'verification_code',
                'verification_code_sent_at',
                'verification_attempts',
            ]);
        });
    }
};
---

Create a dedicated verification codes table for better scalability:

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('phone_verifications', function (Blueprint $table) {
            $table->id();
            $table->morphs('verifiable');
            $table->string('phone', 20);
            $table->string('code', 6);
            $table->timestamp('expires_at');
            $table->timestamp('verified_at')->nullable();
            $table->unsignedTinyInteger('attempts')->default(0);
            $table->timestamps();
        });
    }
};
---

## Creating the VerifiedPhone Middleware

Build middleware to protect routes that require a verified phone:

```php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class VerifiedPhone
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();

        if (!$user || !$user->phone_verified_at) {
            return $request->expectsJson()
                ? response()->json(['message' => 'Phone number not verified.'], 403)
                : redirect()->route('verification.phone.notice');
        }

        return $next($request);
    }
}
---

Register it in `bootstrap/app.php`:

```php
<?php

use App\Http\Middleware\VerifiedPhone;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'phone.verified' => VerifiedPhone::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();
---

Apply it to routes:

```php
Route::middleware(['auth', 'phone.verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::post('/orders', [OrderController::class, 'store']);
});
---

## Generating and Sending Verification Codes

Create a service to handle code generation and SMS sending:

```php
<?php

namespace App\Services;

use App\Models\PhoneVerification;
use App\Models\User;
use Illuminate\Support\Str;

class PhoneVerificationService
{
    public function generateCode(): string
    {
        return (string) random_int(100000, 999999);
    }

    public function sendCode(User $user): PhoneVerification
    {
        // Invalidate previous codes
        PhoneVerification::where('verifiable_id', $user->id)
            ->where('verifiable_type', get_class($user))
            ->whereNull('verified_at')
            ->update(['expires_at' => now()]);

        $code = $this->generateCode();

        $verification = PhoneVerification::create([
            'verifiable_id' => $user->id,
            'verifiable_type' => get_class($user),
            'phone' => $user->phone,
            'code' => $code,
            'expires_at' => now()->addMinutes(10),
        ]);

        $user->notify(new PhoneVerificationCode($code));

        $user->update([
            'verification_code' => $code,
            'verification_code_sent_at' => now(),
            'verification_attempts' => 0,
        ]);

        return $verification;
    }

    public function verify(User $user, string $code): bool
    {
        $verification = PhoneVerification::where('verifiable_id', $user->id)
            ->where('verifiable_type', get_class($user))
            ->where('code', $code)
            ->whereNull('verified_at')
            ->where('expires_at', '>', now())
            ->first();

        if (!$verification) {
            $user->increment('verification_attempts');
            return false;
        }

        $verification->update(['verified_at' => now()]);
        $user->update([
            'phone_verified_at' => now(),
            'verification_code' => null,
            'verification_attempts' => 0,
        ]);

        return true;
    }
}
---

The notification that sends the SMS:

```php
<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class PhoneVerificationCode extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public string $code
    ) {}

    public function via(object $notifiable): array
    {
        return ['sms'];
    }

    public function toSms(object $notifiable): string
    {
        return "Your verification code is: {$this->code}. It expires in 10 minutes. Do not share this code.";
    }
}
---

## Verification Endpoint

Create the API endpoint for verification:

```php
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Services\PhoneVerificationService;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class PhoneVerificationController extends Controller
{
    public function __construct(
        protected PhoneVerificationService $verificationService
    ) {}

    public function notice()
    {
        return view('auth.verify-phone');
    }

    public function verify(Request $request)
    {
        $request->validate([
            'code' => 'required|string|size:6',
        ]);

        $user = $request->user();

        if ($user->phone_verified_at) {
            return response()->json(['message' => 'Phone already verified.']);
        }

        $verified = $this->verificationService->verify($user, $request->code);

        if (!$verified) {
            throw ValidationException::withMessages([
                'code' => ['Invalid or expired verification code.'],
            ]);
        }

        return response()->json([
            'message' => 'Phone number verified successfully.',
        ]);
    }
}
---

## Rate Limiting

Protect your verification endpoints from abuse:

```php
<?php

namespace App\Http\Controllers\Auth;

use App\Services\PhoneVerificationService;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\RateLimiter;

class PhoneVerificationController extends Controller
{
    public function send(Request $request)
    {
        $request->validate(['phone' => 'required|phone:mobile']);

        $user = $request->user();
        $key = 'phone-verification:' . $user->id;

        // Max 3 sends per hour
        if (RateLimiter::tooManyAttempts($key, 3)) {
            $seconds = RateLimiter::availableIn($key);
            return response()->json([
                'message' => "Too many attempts. Try again in {$seconds} seconds.",
            ], 429);
        }

        RateLimiter::hit($key, 3600);

        $this->verificationService->sendCode($user);

        return response()->json([
            'message' => 'Verification code sent.',
        ]);
    }

    public function verify(Request $request)
    {
        $request->validate(['code' => 'required|string|size:6']);

        $user = $request->user();
        $key = 'phone-verify:' . $user->id;

        // Max 5 verification attempts per 30 minutes
        if (RateLimiter::tooManyAttempts($key, 5)) {
            $seconds = RateLimiter::availableIn($key);
            return response()->json([
                'message' => "Too many attempts. Try again in {$seconds} seconds.",
            ], 429);
        }

        RateLimiter::hit($key, 1800);

        // ... verification logic
    }
}
---

## Resending Verification Codes

Resend with a cooldown period to prevent abuse:

```php
<?php

namespace App\Http\Controllers\Auth;

use App\Services\PhoneVerificationService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;

class PhoneVerificationController extends Controller
{
    public function resend(Request $request)
    {
        $user = $request->user();

        // Check cooldown
        $lastSent = $user->verification_code_sent_at;
        if ($lastSent && Carbon::parse($lastSent)->addSeconds(60)->isFuture()) {
            $seconds = Carbon::parse($lastSent)->addSeconds(60)->diffInSeconds(now());
            return response()->json([
                'message' => "Please wait {$seconds} seconds before requesting a new code.",
            ], 429);
        }

        $this->verificationService->sendCode($user);

        return response()->json([
            'message' => 'New verification code sent.',
        ]);
    }
}
---

## Integration with Registration

Hook verification into the registration flow:

```php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use App\Services\PhoneVerificationService;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;

class RegisteredUserController extends Controller
{
    public function __construct(
        protected PhoneVerificationService $verificationService
    ) {}

    public function store(Request $request)
    {
        $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'phone' => ['required', 'phone:mobile', 'unique:users'],
            'password' => ['required', 'confirmed', Password::defaults()],
        ]);

        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'phone' => $request->phone,
            'password' => Hash::make($request->password),
        ]);

        event(new Registered($user));

        // Send verification code
        $this->verificationService->sendCode($user);

        // Log the user in
        auth()->login($user);

        // Redirect to verification page instead of dashboard
        return redirect()->route('verification.phone.notice');
    }
}
---

Add a verification notice page in your routes:

```php
<?php

use App\Http\Controllers\Auth\PhoneVerificationController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    Route::get('/verify-phone', [PhoneVerificationController::class, 'notice'])
        ->name('verification.phone.notice');

    Route::post('/verify-phone/send', [PhoneVerificationController::class, 'send'])
        ->name('verification.phone.send');

    Route::post('/verify-phone/resend', [PhoneVerificationController::class, 'resend'])
        ->name('verification.phone.resend');

    Route::post('/verify-phone', [PhoneVerificationController::class, 'verify'])
        ->name('verification.phone.verify');
});
---

## Testing Phone Verification

```php
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use App\Notifications\PhoneVerificationCode;
use App\Services\PhoneVerificationService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class PhoneVerificationTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_can_request_verification_code()
    {
        Notification::fake();

        $user = User::factory()->create(['phone' => '+1234567890']);

        $this->actingAs($user)
            ->postJson('/verify-phone/send')
            ->assertOk();

        Notification::assertSentTo($user, PhoneVerificationCode::class);
    }

    public function test_user_can_verify_with_valid_code()
    {
        $user = User::factory()->create(['phone' => '+1234567890']);
        $service = app(PhoneVerificationService::class);

        $verification = $service->sendCode($user);

        $this->actingAs($user)
            ->postJson('/verify-phone', ['code' => $verification->code])
            ->assertOk();

        $this->assertNotNull($user->fresh()->phone_verified_at);
    }

    public function test_invalid_code_returns_error()
    {
        $user = User::factory()->create(['phone' => '+1234567890']);
        $service = app(PhoneVerificationService::class);
        $service->sendCode($user);

        $this->actingAs($user)
            ->postJson('/verify-phone', ['code' => '000000'])
            ->assertStatus(422);
    }

    public function test_rate_limiting_blocks_excessive_requests()
    {
        $user = User::factory()->create(['phone' => '+1234567890']);

        $this->actingAs($user);

        // Send 3 codes (hits limit)
        for ($i = 0; $i < 3; $i++) {
            $this->postJson('/verify-phone/send')->assertOk();
        }

        // 4th attempt should be blocked
        $this->postJson('/verify-phone/send')->assertStatus(429);
    }
}
---

## Conclusion

Phone number verification is a fundamental security feature that every Laravel app with user accounts should implement. With this guide, you've built a complete system covering code generation, sending, verification, rate limiting, and registration integration.

For SMS delivery, use our [Laravel SMS API](/services/laravel-sms-api) for reliable message delivery. Need help with custom integration? Check out our [custom Laravel SMS development](/services/custom-laravel-sms-development) services or explore our [SMS gateway integration](/services/laravel-sms-gateway-integration-service) expertise.