feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
Three verification methods (TOTP authenticator, email code, backup codes), trusted device management with 30-day expiry, role-based enforcement for super_admin and org_admin, admin reset capability, and full test coverage (46 tests). Modifies login flow to support MFA challenge/response with temporary session tokens stored in cache. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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->boolean('mfa_enabled')->default(false)->after('email');
|
||||
$table->string('mfa_method', 20)->nullable()->after('mfa_enabled');
|
||||
$table->text('mfa_secret')->nullable()->after('mfa_method');
|
||||
$table->timestamp('mfa_confirmed_at')->nullable()->after('mfa_secret');
|
||||
$table->boolean('mfa_enforced')->default(false)->after('mfa_confirmed_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'mfa_enabled',
|
||||
'mfa_method',
|
||||
'mfa_secret',
|
||||
'mfa_confirmed_at',
|
||||
'mfa_enforced',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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('mfa_backup_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->ulid('user_id');
|
||||
$table->string('code_hash', 64);
|
||||
$table->boolean('used')->default(false);
|
||||
$table->timestamp('used_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')
|
||||
->cascadeOnDelete();
|
||||
$table->index(['user_id', 'used']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mfa_backup_codes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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('trusted_devices', function (Blueprint $table) {
|
||||
$table->ulid('id')->primary();
|
||||
$table->ulid('user_id');
|
||||
$table->string('device_hash', 64);
|
||||
$table->string('device_name')->nullable();
|
||||
$table->string('ip_address', 45);
|
||||
$table->timestamp('trusted_until');
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')
|
||||
->cascadeOnDelete();
|
||||
$table->index(['user_id', 'device_hash', 'trusted_until']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('trusted_devices');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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('mfa_email_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->ulid('user_id');
|
||||
$table->string('code', 6);
|
||||
$table->timestamp('expires_at');
|
||||
$table->boolean('used')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')
|
||||
->cascadeOnDelete();
|
||||
$table->index(['user_id', 'code', 'used', 'expires_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('mfa_email_codes');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user