Free·

Multi-Login Authentication - Email, Username, or SSN

How to implement flexible user authentication allowing login with email, username, or social security number.

The Problem

In many applications, users need flexibility in how they authenticate. Some users remember their email, others prefer their username, and in certain contexts (like government or enterprise systems), users may need to log in with identifiers like a social security number, or another, like Tax numbers / CPF / NIF, etc...

The default authentication systems typically only support email-based login, forcing users into a single authentication method. This creates friction in the user experience and may not meet business requirements that demand multiple authentication methods.

The Solution

The solution involves three key changes:

  1. Add username and ss_number columns to the users table with unique constraints.
  2. Replace the standard email field with a generic "credential" field.
  3. Override the authentication logic to detect which field type the user entered and authenticate against the correct column.

Here's how it works:

1. Update the User Migration

Add two new unique columns to your users table:

  • username - for username-based login
  • ss_number - for social security number or tax ID based login
database/migrations/0001_01_01_000000_create_users_table.php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('username')->unique();
            $table->string('email')->unique();
            $table->string('ss_number')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });

        Schema::create('password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });

        Schema::create('sessions', function (Blueprint $table) {
            $table->string('id')->primary();
            $table->foreignId('user_id')->nullable()->index();
            $table->string('ip_address', 45)->nullable();
            $table->text('user_agent')->nullable();
            $table->longText('payload');
            $table->integer('last_activity')->index();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
        Schema::dropIfExists('password_reset_tokens');
        Schema::dropIfExists('sessions');
    }
};

2. Customize the Login Page

In app/Filament/Pages/Auth/Login.php, override three key methods:

  • form() - Replace the standard email field with a generic "credential" TextInput
  • getCredentialsFromFormData() - Query the database to find which field (email, username, or ss_number) matches the user's input, then return credentials with the correct field name
  • throwFailureValidationException() - Update error messages to reference the "credential" field instead of "email"
app/Filament/Pages/Auth/Login.php
<?php

namespace App\Filament\Pages\Auth;

use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Filament\Auth\Http\Responses\Contracts\LoginResponse;
use App\Models\User;
use Illuminate\Validation\ValidationException;
use SensitiveParameter;

class Login extends \Filament\Auth\Pages\Login
{
    public function mount(): void
    {
        parent::mount();

        if (app()->environment('local')) {
            $this->form->fill([
                'credential' => '[email protected]',
//                'credential' => '123456789',
//                'credential' => 'test-user',
                'password' => 'password',
                'remember' => true,
            ]);
        }
    }

    public function form(Schema $schema): Schema
    {
        return $schema
            ->components([
                TextInput::make('credential')
                    ->label('Credential')
                    ->required()
                    ->autocomplete()
                    ->autofocus()
                    ->extraInputAttributes(['tabindex' => 1]),
                $this->getPasswordFormComponent(),
                $this->getRememberFormComponent(),
            ]);
    }

    protected function getCredentialsFromFormData(#[SensitiveParameter] array $data): array
    {
        $credential = $data['credential'];

        // Try to find user by email, username, or ss_number
        $user = User::where('email', $credential)
            ->orWhere('username', $credential)
            ->orWhere('ss_number', $credential)
            ->first();

        if (!$user) {
            return [
                'email' => $credential, // Fallback to email for validation error
                'password' => $data['password'],
            ];
        }

        // Return the actual field that matched
        $field = match(true) {
            $user->email === $credential => 'email',
            $user->username === $credential => 'username',
            $user->ss_number === $credential => 'ss_number',
            default => 'email',
        };

        return [
            $field => $credential,
            'password' => $data['password'],
        ];
    }

    protected function throwFailureValidationException(): never
    {
        throw ValidationException::withMessages([
            'data.credential' => __('filament-panels::auth/pages/login.messages.failed'),
        ]);
    }
}

3. Update Factory and Seeder for Development

For testing in your local environment, update the UserFactory and DatabaseSeeder to generate the new fields:

UserFactory - Add fake data generators:

  • username - uses fake()->userName() to generate realistic usernames
  • ss_number - uses a truncated IMEI number to simulate a 9-digit ID

DatabaseSeeder - Add test credentials:

  • Create a test user with all three credential types for easy local testing
  • Example: email [email protected], username test-user, ss_number 123456789
database/factories/UserFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * The current password being used by the factory.
     */
    protected static ?string $password;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'username' => fake()->userName(),
            'ss_number' => str(fake()->imei())->limit(9, end: '')->value(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => static::$password ??= Hash::make('password'),
            'remember_token' => Str::random(10),
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}
database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        // User::factory(10)->create();

        User::factory()->create([
            'name' => 'Test User',
            'email' => '[email protected]',
            'username' => 'test-user',
            'ss_number' => '123456789',
        ]);
    }
}

Now users can log in with any of their credentials all through a single, intuitive input field.