How to Implement OTP-Based Login in Laravel

When I started building secure web applications, I realized how important it is to provide users with a safe and user-friendly login system. One-time password (OTP) authentication is a great way to enhance security by sending a unique code to the user’s email or phone, which they use to log in.

In this article, I’ll share how I implemented OTP-based login in Laravel, a popular PHP framework. This guide is written in simple language, so even if you’re new to Laravel, you can follow along. Let’s dive into the step-by-step process to set up OTP-based login in your Laravel application!

Step-by-Step Guide to Implement OTP-Based Login in Laravel

How to Implement OTP-Based Login in Laravel

Step 1: Set Up Your Laravel Project

First, I make sure I have a fresh Laravel project. If you don’t have one, you can create it using Composer. Open your terminal and run:

composer create-project --prefer-dist laravel/laravel otp-login

This creates a new Laravel project named otp-login. Once the installation is complete, navigate to the project directory and ensure your environment is set up (e.g., database configuration in the .env file).

Step 2: Configure the Database

I use a MySQL database for this project, but you can use any database supported by Laravel. Update your .env file with your database credentials:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=otp_login
DB_USERNAME=your_username
DB_PASSWORD=your_password

Run the migration to create the default users table:

php artisan migrate

Step 3: Install and Configure a Mail Service

Since I’ll send OTPs via email, I configure Laravel’s mail system. For this example, I use Mailtrap, a service for testing emails, but you can use any mail service like SendGrid or SMTP. Update the .env file with your mail configuration:

MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_mailtrap_username
MAIL_PASSWORD=your_mailtrap_password
MAIL_ENCRYPTION=tls
[email protected]
MAIL_FROM_NAME="${APP_NAME}"

Step 4: Create the OTP Model and Migration

I need a table to store OTPs temporarily. I create a new model and migration for OTPs by running:

php artisan make:model OTP -m

This generates an OTP model and a migration file. In the migration file (located in database/migrations), I define the schema for the otps table:

Schema::create('otps', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('user_id');
    $table->string('otp');
    $table->timestamp('expires_at');
    $table->timestamps();
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});

Run the migration:

php artisan migrate

Step 5: Modify the User Model

I update the User model (app/Models/User.php) to establish a relationship with the OTP model:

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    public function otps()
    {
        return $this->hasMany(OTP::class);
    }
}

Step 6: Create the OTP Controller

I create a controller to handle OTP generation and verification:

php artisan make:controller Auth/OTPController

In app/Http/Controllers/Auth/OTPController.php, I add the logic to generate and verify OTPs:

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\OTP;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class OTPController extends Controller
{
    public function showLoginForm()
    {
        return view('auth.otp-login');
    }

    public function sendOTP(Request $request)
    {
        $request->validate(['email' => 'required|email|exists:users,email']);
        $user = User::where('email', $request->email)->first();

        // Generate a 6-digit OTP
        $otp = rand(100000, 999999);
        $expiresAt = now()->addMinutes(10);

        // Save OTP to database
        OTP::create([
            'user_id' => $user->id,
            'otp' => $otp,
            'expires_at' => $expiresAt,
        ]);

        // Send OTP via email
        Mail::raw("Your OTP is: $otp", function ($message) use ($user) {
            $message->to($user->email)->subject('Your OTP Code');
        });

        return redirect()->route('otp.verify')->with('email', $user->email);
    }

    public function showVerifyForm()
    {
        return view('auth.otp-verify');
    }

    public function verifyOTP(Request $request)
    {
        $request->validate([
            'email' => 'required|email|exists:users,email',
            'otp' => 'required|digits:6',
        ]);

        $user = User::where('email', $request->email)->first();
        $otp = OTP::where('user_id', $user->id)
            ->where('otp', $request->otp)
            ->where('expires_at', '>=', now())
            ->first();

        if ($otp) {
            // Log the user in
            auth()->login($user);
            $otp->delete(); // Delete used OTP
            return redirect()->route('home')->with('success', 'Logged in successfully!');
        }

        return back()->withErrors(['otp' => 'Invalid or expired OTP']);
    }
}

Step 7: Create Views

I create two Blade views for the OTP login process.

1. OTP Login Form (resources/views/auth/otp-login.blade.php):

<!DOCTYPE html>
<html>
<head>
    <title>OTP Login</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h2>OTP Login</h2>
        <form method="POST" action="{{ route('otp.send') }}">
            @csrf
            <div>
                <label>Email</label>
                <input type="email" name="email" required>
                @error('email')
                    <span>{{ $message }}</span>
                @enderror
            </div>
            <button type="submit">Send OTP</button>
        </form>
    </div>
</body>
</html>

2. OTP Verification Form (resources/views/auth/otp-verify.blade.php):

<!DOCTYPE html>
<html>
<head>
    <title>Verify OTP</title>
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h2>Verify OTP</h2>
        <form method="POST" action="{{ route('otp.verify') }}">
            @csrf
            <div>
                <label>Email</label>
                <input type="email" name="email" value="{{ session('email') }}" readonly>
            </div>
            <div>
                <label>OTP</label>
                <input type="text" name="otp" required>
                @error('otp')
                    <span>{{ $message }}</span>
                @enderror
            </div>
            <button type="submit">Verify OTP</button>
        </form>
    </div>
</body>
</html>

Step 8: Define Routes

I add routes for OTP login in routes/web.php:

use App\Http\Controllers\Auth\OTPController;

Route::get('/otp/login', [OTPController::class, 'showLoginForm'])->name('otp.login');
Route::post('/otp/send', [OTPController::class, 'sendOTP'])->name('otp.send');
Route::get('/otp/verify', [OTPController::class, 'showVerifyForm'])->name('otp.verify');
Route::post('/otp/verify', [OTPController::class, 'verifyOTP'])->name('otp.verify');
Route::get('/home', function () {
    return view('home');
})->name('home');

Step 9: Test the Application

I start the Laravel server:

php artisan serve

I visit http://localhost:8000/otp/login, enter an email, receive an OTP, and verify it to log in. I ensure a user exists in the users table (you can create one via php artisan tinker or a registration system).

Conclusion

Implementing OTP-based login in Laravel was a rewarding experience for me. It’s a secure and modern way to authenticate users without relying solely on passwords. By following these steps, I was able to set up a fully functional OTP system using Laravel’s built-in features and a mail service. You can extend this further by adding SMS-based OTPs or additional security measures. I hope this guide helps you add OTP authentication to your Laravel project easily!

FAQs

Q: What is OTP-based login?
A: OTP-based login is a secure authentication method where a one-time password is sent to the user’s email or phone, which they use to log in.

Q: Can I use SMS instead of email for OTPs?
A: Yes, you can integrate an SMS service like Twilio or Nexmo to send OTPs via text messages instead of email.

Q: How secure is OTP-based login?
A: OTP-based login is highly secure because the code is temporary and unique. Adding HTTPS and rate-limiting enhances security further.

Q: Can I customize the OTP length?
A: Yes, you can change the OTP length by modifying the rand(100000, 999999) in the controller to generate a different number of digits.

Q: What if the OTP expires?
A: The system checks the expires_at timestamp. If the OTP is expired, the user will need to request a new one.


You might also like :

techsolutionstuff

Techsolutionstuff | The Complete Guide

I'm a software engineer and the founder of techsolutionstuff.com. Hailing from India, I craft articles, tutorials, tricks, and tips to aid developers. Explore Laravel, PHP, MySQL, jQuery, Bootstrap, Node.js, Vue.js, and AngularJS in our tech stack.

RECOMMENDED POSTS

FEATURE POSTS