Multi-Login Authentication - Email, Username, or SSN

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:
- Add
usernameandss_numbercolumns to the users table with unique constraints. - Replace the standard email field with a generic "credential" field.
- 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 loginss_number- for social security number or tax ID based login
<?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" TextInputgetCredentialsFromFormData()- Query the database to find which field (email, username, or ss_number) matches the user's input, then return credentials with the correct field namethrowFailureValidationException()- Update error messages to reference the "credential" field instead of "email"
<?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- usesfake()->userName()to generate realistic usernamesss_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], usernametest-user, ss_number123456789
<?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,
]);
}
}
<?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.