feat(auth): TOTP 2FA enable/disable with recovery codes

main
boban 2026-05-16 08:08:31 +02:00
parent c193e482ac
commit d7ca84dd2e
2 changed files with 72 additions and 0 deletions

View File

@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use PragmaRX\Google2FA\Google2FA;
class User extends Authenticatable
{
@ -66,4 +67,44 @@ class User extends Authenticatable
{
return $this->hasMany(\App\Domains\Workspace\Models\WorkspaceMember::class);
}
public function enableTwoFactor(string $secret): array
{
$recoveryCodes = collect(range(1, 8))->map(fn() => implode('-', [
strtoupper(substr(bin2hex(random_bytes(5)), 0, 5)),
strtoupper(substr(bin2hex(random_bytes(5)), 0, 5)),
]))->all();
$this->update([
'two_factor_secret' => encrypt($secret),
'two_factor_recovery' => encrypt(json_encode($recoveryCodes)),
]);
return $recoveryCodes;
}
public function disableTwoFactor(): void
{
$this->update([
'two_factor_secret' => null,
'two_factor_recovery' => null,
'two_factor_confirmed_at' => null,
]);
}
public function hasTwoFactorEnabled(): bool
{
return $this->two_factor_confirmed_at !== null;
}
public function verifyTwoFactor(string $code): bool
{
if (! $this->two_factor_secret) {
return false;
}
$secret = decrypt($this->two_factor_secret);
$google2fa = new Google2FA;
return $google2fa->verifyKey($secret, $code) !== false;
}
}

View File

@ -0,0 +1,31 @@
<?php
use App\Models\User;
it('enables 2FA and returns recovery codes', function () {
$user = User::factory()->create();
$codes = $user->enableTwoFactor('JBSWY3DPEHPK3PXP');
expect($codes)->toHaveCount(8)
->and($user->fresh()->two_factor_secret)->not->toBeNull();
});
it('disables 2FA and clears secrets', function () {
$user = User::factory()->create();
$user->enableTwoFactor('JBSWY3DPEHPK3PXP');
$user->disableTwoFactor();
$fresh = $user->fresh();
expect($fresh->two_factor_secret)->toBeNull()
->and($fresh->two_factor_confirmed_at)->toBeNull();
});
it('reports 2FA as disabled before confirmation', function () {
$user = User::factory()->create();
$user->enableTwoFactor('JBSWY3DPEHPK3PXP');
// two_factor_confirmed_at is null → hasTwoFactorEnabled() = false
expect($user->fresh()->hasTwoFactorEnabled())->toBeFalse();
});